@@ -0,0 +1,135 @@
|
||||
// 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()
|
||||
Reference in New Issue
Block a user