@@ -0,0 +1,79 @@
|
||||
// The handlers of the module's procedures: each takes the service its kind may use and calls one use
|
||||
// case. No rule lives here.
|
||||
|
||||
import { implement } from "@conusai-kit/core"
|
||||
import {
|
||||
itemAvailability,
|
||||
itemCreate,
|
||||
itemList,
|
||||
reservationList,
|
||||
stockRelease,
|
||||
stockReserve,
|
||||
} from "../contract/index.js"
|
||||
import {
|
||||
itemAvailability as availabilityOf,
|
||||
createItem,
|
||||
listItems,
|
||||
listProjectReservations,
|
||||
releaseStock,
|
||||
reserveStock,
|
||||
} from "../core/inventory.js"
|
||||
import { inventoryServiceOf } from "./service.js"
|
||||
|
||||
const cursorOf = (input: { after?: string | undefined; limit?: number | undefined }) => ({
|
||||
...(input.after === undefined ? {} : { after: input.after }),
|
||||
...(input.limit === undefined ? {} : { limit: input.limit }),
|
||||
})
|
||||
|
||||
export const itemListImpl = implement(itemList, ({ input, ctx }) =>
|
||||
listItems(inventoryServiceOf(ctx.container, ctx.kind), {
|
||||
...(input.q === undefined ? {} : { q: input.q }),
|
||||
...cursorOf(input),
|
||||
}),
|
||||
)
|
||||
|
||||
export const itemCreateImpl = implement(itemCreate, ({ input, ctx }) =>
|
||||
createItem(inventoryServiceOf(ctx.container, "effect"), {
|
||||
sku: input.sku,
|
||||
name: input.name,
|
||||
unit: input.unit,
|
||||
...(input.description === undefined ? {} : { description: input.description }),
|
||||
}),
|
||||
)
|
||||
|
||||
export const itemAvailabilityImpl = implement(itemAvailability, ({ input, ctx }) =>
|
||||
availabilityOf(inventoryServiceOf(ctx.container, ctx.kind), input.itemId),
|
||||
)
|
||||
|
||||
export const reservationListImpl = implement(reservationList, ({ input, ctx }) =>
|
||||
listProjectReservations(inventoryServiceOf(ctx.container, ctx.kind), ctx.scope, {
|
||||
...(input.status === undefined ? {} : { status: input.status }),
|
||||
...cursorOf(input),
|
||||
}),
|
||||
)
|
||||
|
||||
export const stockReserveImpl = implement(stockReserve, ({ input, ctx }) =>
|
||||
reserveStock(inventoryServiceOf(ctx.container, "effect"), ctx.scope, {
|
||||
itemId: input.itemId,
|
||||
locationId: input.locationId,
|
||||
quantity: input.quantity,
|
||||
requestId: input.requestId,
|
||||
}),
|
||||
)
|
||||
|
||||
export const stockReleaseImpl = implement(stockRelease, ({ input, ctx }) =>
|
||||
releaseStock(inventoryServiceOf(ctx.container, "effect"), ctx.scope, {
|
||||
reservationId: input.reservationId,
|
||||
...(input.quantity === undefined ? {} : { quantity: input.quantity }),
|
||||
requestId: input.requestId,
|
||||
}),
|
||||
)
|
||||
|
||||
export const INVENTORY_PROCEDURES = [
|
||||
itemListImpl,
|
||||
itemCreateImpl,
|
||||
itemAvailabilityImpl,
|
||||
reservationListImpl,
|
||||
stockReserveImpl,
|
||||
stockReleaseImpl,
|
||||
]
|
||||
@@ -0,0 +1,170 @@
|
||||
// Generated SDK adapter. Production dispatch requires a server-prepared request binding.
|
||||
|
||||
import { businessAppError, type Client, requestBusinessBinding, toBusinessError } from "@conusai-kit/business"
|
||||
import type { Container, ProcedureKind } from "@conusai-kit/core"
|
||||
import {
|
||||
type Availability,
|
||||
type Item,
|
||||
inventoryCreateItem,
|
||||
inventoryCreateReservation,
|
||||
inventoryGetItem,
|
||||
inventoryGetItemAvailability,
|
||||
inventoryGetReservation,
|
||||
inventoryListItems,
|
||||
inventoryListReservations,
|
||||
inventoryReleaseReservation,
|
||||
type Reservation,
|
||||
} from "@conusai-sdk/inventory"
|
||||
import { createClient } from "@conusai-sdk/inventory/client"
|
||||
import type { AvailabilityView, ItemView, ReservationView } from "../contract/index.js"
|
||||
import { INVENTORY_SERVICE, type InventoryReads, type InventoryService, inventoryReadsOf } from "../core/ports.js"
|
||||
|
||||
/**
|
||||
* An int64 the service answered, as the exact number a view carries over JSON. The generated client
|
||||
* types every int64 as `bigint` (a service may send any i64); a quantity beyond 2^53 cannot be shown
|
||||
* exactly, so it is refused rather than rounded.
|
||||
*/
|
||||
function exact(value: bigint, field: string): number {
|
||||
if (value > BigInt(Number.MAX_SAFE_INTEGER) || value < BigInt(Number.MIN_SAFE_INTEGER))
|
||||
throw new RangeError(`${field} is outside the range a number holds exactly`)
|
||||
return Number(value)
|
||||
}
|
||||
|
||||
const itemView = (item: Item): ItemView => ({
|
||||
itemId: item.id,
|
||||
sku: item.sku,
|
||||
name: item.name,
|
||||
unit: item.unit,
|
||||
description: item.description ?? null,
|
||||
active: item.active,
|
||||
})
|
||||
|
||||
const availabilityView = (availability: Availability): AvailabilityView => ({
|
||||
itemId: availability.item_id,
|
||||
unit: availability.unit,
|
||||
onHand: exact(availability.on_hand, "on_hand"),
|
||||
reserved: exact(availability.reserved, "reserved"),
|
||||
available: exact(availability.available, "available"),
|
||||
locations: availability.locations.map((level) => ({
|
||||
locationId: level.location_id,
|
||||
onHand: exact(level.on_hand, "on_hand"),
|
||||
reserved: exact(level.reserved, "reserved"),
|
||||
available: exact(level.available, "available"),
|
||||
})),
|
||||
})
|
||||
|
||||
const reservationView = (reservation: Reservation): ReservationView => ({
|
||||
reservationId: reservation.id,
|
||||
itemId: reservation.item_id,
|
||||
locationId: reservation.location_id,
|
||||
quantity: exact(reservation.quantity, "quantity"),
|
||||
outstanding: exact(reservation.outstanding, "outstanding"),
|
||||
released: exact(reservation.released, "released"),
|
||||
issued: exact(reservation.issued, "issued"),
|
||||
status: reservation.status,
|
||||
createdAt: reservation.created_at,
|
||||
})
|
||||
|
||||
/** What a generated operation answered: its data, or the kit's app error for the refusal (`businessAppError`). */
|
||||
async function answer<T>(
|
||||
call: Promise<{ data?: T; error?: unknown; response?: Response }>,
|
||||
): Promise<Exclude<T, undefined>> {
|
||||
let result: { data?: T; error?: unknown; response?: Response }
|
||||
try {
|
||||
result = await call
|
||||
} catch (error) {
|
||||
throw businessAppError(toBusinessError(error, undefined))
|
||||
}
|
||||
if (result.error !== undefined || result.data === undefined) {
|
||||
throw businessAppError(toBusinessError(result.error, result.response))
|
||||
}
|
||||
return result.data as Exclude<T, undefined>
|
||||
}
|
||||
|
||||
/** The port over one client, in one organization of one platform project. */
|
||||
export function sdkInventoryService(binding: { client: Client; orgSlug: string; bindingId: string }): InventoryService {
|
||||
const { client } = binding
|
||||
const org = { org_slug: binding.orgSlug }
|
||||
return {
|
||||
bindingId: binding.bindingId,
|
||||
async listItems(query) {
|
||||
const page = await answer(
|
||||
inventoryListItems({ client, path: org, query: { ...query, limit: BigInt(query.limit) } }),
|
||||
)
|
||||
return { rows: page.data.map(itemView), ...(page.next_after ? { after: page.next_after } : {}) }
|
||||
},
|
||||
async getItem(itemId) {
|
||||
return itemView(await answer(inventoryGetItem({ client, path: { ...org, item_id: itemId } })))
|
||||
},
|
||||
async availability(itemId) {
|
||||
return availabilityView(await answer(inventoryGetItemAvailability({ client, path: { ...org, item_id: itemId } })))
|
||||
},
|
||||
async listReservations(query) {
|
||||
const page = await answer(
|
||||
inventoryListReservations({ client, path: org, query: { ...query, limit: BigInt(query.limit) } }),
|
||||
)
|
||||
return { rows: page.data.map(reservationView), ...(page.next_after ? { after: page.next_after } : {}) }
|
||||
},
|
||||
async getReservation(reservationId) {
|
||||
const reservation = await answer(
|
||||
inventoryGetReservation({ client, path: { ...org, reservation_id: reservationId } }),
|
||||
)
|
||||
return { reservation: reservationView(reservation), reference: reservation.reference }
|
||||
},
|
||||
async createItem(input) {
|
||||
return itemView(await answer(inventoryCreateItem({ client, path: org, body: input })))
|
||||
},
|
||||
async reserve(input) {
|
||||
const outcome = await answer(
|
||||
inventoryCreateReservation({
|
||||
client,
|
||||
path: org,
|
||||
body: {
|
||||
item_id: input.itemId,
|
||||
location_id: input.locationId,
|
||||
quantity: BigInt(input.quantity),
|
||||
reference: input.reference,
|
||||
idempotency_key: input.key,
|
||||
},
|
||||
}),
|
||||
)
|
||||
return reservationView(outcome.reservation)
|
||||
},
|
||||
async release(input) {
|
||||
const outcome = await answer(
|
||||
inventoryReleaseReservation({
|
||||
client,
|
||||
path: { ...org, reservation_id: input.reservationId },
|
||||
body: {
|
||||
idempotency_key: input.key,
|
||||
...(input.quantity === undefined ? {} : { quantity: BigInt(input.quantity) }),
|
||||
},
|
||||
}),
|
||||
)
|
||||
return reservationView(outcome.reservation)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The service a handler of `kind` may use: the bound one (the token's view for the kind applies), or
|
||||
* the current verified request's, narrowed to its reads for a `read` or `write` handler as the token would.
|
||||
* A deployment without the contract is kit-core's `service_configuration_missing` refusal.
|
||||
*/
|
||||
export function inventoryServiceOf(container: Container, kind: "effect"): InventoryService
|
||||
export function inventoryServiceOf(container: Container, kind: ProcedureKind): InventoryReads
|
||||
export function inventoryServiceOf(container: Container, kind: ProcedureKind): InventoryReads {
|
||||
if (container.has(INVENTORY_SERVICE)) return container.resolve(INVENTORY_SERVICE) as InventoryReads
|
||||
let service: InventoryService
|
||||
try {
|
||||
const binding = requestBusinessBinding(container)
|
||||
service = sdkInventoryService({
|
||||
client: binding.client(createClient),
|
||||
orgSlug: binding.orgSlug,
|
||||
bindingId: binding.bindingId,
|
||||
})
|
||||
} catch (error) {
|
||||
throw businessAppError(error)
|
||||
}
|
||||
return kind === "effect" ? service : inventoryReadsOf(service)
|
||||
}
|
||||
Reference in New Issue
Block a user