|
|
|
@@ -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()
|