Files
template-app/modules/inventory/contract/procedures.ts
T
2026-11-14 09:56:45 +00:00

136 lines
4.5 KiB
TypeScript

// The procedure contracts of the inventory module: schemas and names, no handler and no port.
//
// The module is a client of the platform's Inventory service (B-wave contract §3.14): the rows live
// there, in the deployment's organization, and every procedure here is one service call. A read
// reads; a change is an `effect` (it calls an external service and writes nothing of this app's own).
// Stock is reserved for the project the procedure runs in: the service holds it against the
// project's reference, and a project lists and releases its own reservations only.
import { procedure } from "@conusai-kit/core"
import { z } from "zod"
import { INVENTORY_PERMISSIONS } from "./constants.js"
const inProject = { org: z.string(), workspace: z.string(), project: z.string() }
const id = z.string().uuid()
/** What a form generates once per submission, so a retried request reserves once. */
const requestId = z.string().uuid()
const cursor = { after: id.optional(), limit: z.number().int().min(1).max(200).optional() }
export const itemViewSchema = z.object({
itemId: z.string(),
sku: z.string(),
name: z.string(),
unit: z.string(),
description: z.string().nullable(),
/** An archived item takes no reservation. */
active: z.boolean(),
})
export type ItemView = z.output<typeof itemViewSchema>
export const stockLevelViewSchema = z.object({
locationId: z.string(),
onHand: z.number().int(),
reserved: z.number().int(),
available: z.number().int(),
})
export type StockLevelView = z.output<typeof stockLevelViewSchema>
export const availabilityViewSchema = z.object({
itemId: z.string(),
unit: z.string(),
onHand: z.number().int(),
reserved: z.number().int(),
available: z.number().int(),
/** Every location with a balance, in the service's location order. */
locations: z.array(stockLevelViewSchema),
})
export type AvailabilityView = z.output<typeof availabilityViewSchema>
export const reservationViewSchema = z.object({
reservationId: z.string(),
itemId: z.string(),
locationId: z.string(),
quantity: z.number().int(),
outstanding: z.number().int(),
released: z.number().int(),
issued: z.number().int(),
status: z.enum(["open", "closed"]),
createdAt: z.string(),
})
export type ReservationView = z.output<typeof reservationViewSchema>
/** A page of a list: `after` is what the next page is asked with, absent on the last. */
const pageOf = <T extends z.ZodType>(rows: T) => z.object({ rows: z.array(rows), after: z.string().optional() })
export const itemList = procedure("inventory.item.list")
.kind("read")
.scope("project")
.permission(INVENTORY_PERMISSIONS.read)
.input(z.object({ ...inProject, q: z.string().max(200).optional(), ...cursor }))
.output(pageOf(itemViewSchema))
.contract()
export const itemCreate = procedure("inventory.item.create")
.kind("effect")
.scope("project")
.permission(INVENTORY_PERMISSIONS.write)
.input(
z.object({
...inProject,
sku: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/),
name: z.string().trim().min(1).max(200),
unit: z.string().regex(/^[a-z][a-z0-9_]{0,15}$/),
description: z.string().max(1000).optional(),
}),
)
.output(itemViewSchema)
.contract()
export const itemAvailability = procedure("inventory.item.availability")
.kind("read")
.scope("project")
.permission(INVENTORY_PERMISSIONS.read)
.input(z.object({ ...inProject, itemId: id }))
.output(z.object({ item: itemViewSchema, availability: availabilityViewSchema }))
.contract()
export const reservationList = procedure("inventory.reservation.list")
.kind("read")
.scope("project")
.permission(INVENTORY_PERMISSIONS.read)
.input(z.object({ ...inProject, status: z.enum(["open", "closed"]).optional(), ...cursor }))
.output(pageOf(reservationViewSchema))
.contract()
export const stockReserve = procedure("inventory.stock.reserve")
.kind("effect")
.scope("project")
.permission(INVENTORY_PERMISSIONS.reserve)
.input(
z.object({
...inProject,
itemId: id,
locationId: id,
quantity: z.number().int().min(1).max(1_000_000_000),
requestId,
}),
)
.output(reservationViewSchema)
.contract()
export const stockRelease = procedure("inventory.stock.release")
.kind("effect")
.scope("project")
.permission(INVENTORY_PERMISSIONS.reserve)
.input(
z.object({
...inProject,
reservationId: id,
/** Everything still held when absent. */
quantity: z.number().int().min(1).max(1_000_000_000).optional(),
requestId,
}),
)
.output(reservationViewSchema)
.contract()