import { createContext, DomainError } from "@tailor-platform/erp-kit/app"; import { createResolver, t } from "@tailor-platform/sdk"; import { getDB } from "@/generated/kysely-tailordb"; import { invModulesExport } from "@/modules"; /** * createTransferOrder — app-layer GraphQL mutation. Thin wrapper over erp-kit's * native inventory `createTransferOrder` command. The two app-level extension * columns (`docNumber`, `notes`) are declared in `modules.ts`; `docNumber` is a * DB-assigned serial and `notes` is passed straight through the command header. */ export default createResolver({ name: "createTransferOrder", operation: "mutation", input: { sourceSiteId: t.string().description("Site the goods are dispatched from"), destinationSiteId: t.string().description("Site the goods are received at"), plannedShipmentDate: t .date() .description("Planned source-site shipping date used for stock reservation demand"), expectedReceiptDate: t .date() .description("Planned destination-site receiving date used for inbound supply planning"), notes: t.string({ optional: true }).description("Operator notes"), lines: t .object( { itemId: t.string().description("Item ID"), orderedQuantity: t.string().description("Ordered quantity (decimal string, > 0)"), }, { array: true }, ) .description("Transfer order lines"), }, body: async (context) => { const ctx = createContext(context); const db = getDB("main-db"); return db .transaction() .execute(async (trx) => { const result = await invModulesExport.commands.createTransferOrder( trx, { // erp-kit split create-command input into { header, lines }. // The GraphQL input stays flat; we reshape it here. header: { sourceSiteId: context.input.sourceSiteId, destinationSiteId: context.input.destinationSiteId, // Anchor date-only inputs at UTC noon (same convention as // createPurchaseOrder) so client-side TZ rendering doesn't shift // the calendar date by ±1 day. plannedShipmentDate: new Date(`${context.input.plannedShipmentDate}T12:00:00.000Z`), expectedReceiptDate: new Date(`${context.input.expectedReceiptDate}T12:00:00.000Z`), notes: context.input.notes ?? null, }, lines: context.input.lines.map((line) => ({ itemId: line.itemId, orderedQuantity: line.orderedQuantity, })), }, ctx, ); if (!result.ok) { switch (result.error.code) { case "INVENTORY_INVALID_TRANSFER_LOCATIONS": throw new DomainError(`Invalid transfer locations: ${result.error.message}`); case "INVENTORY_EMPTY_TRANSFER_LINES": throw new DomainError("Transfer order must have at least one line"); case "INVENTORY_INVALID_QUANTITY": throw new DomainError(`Invalid quantity: ${result.error.message}`); case "INVENTORY_ITEM_NOT_FOUND": throw new DomainError(`Item not found: ${result.error.message}`); case "INVENTORY_SITE_NOT_FOUND": throw new DomainError(`Site not found: ${result.error.message}`); case "INVENTORY_CROSS_COMPANY_TRANSFER_NOT_SUPPORTED": throw new DomainError( "Transfers between sites of different companies are not supported", ); case "UNAUTHENTICATED": throw new DomainError("Authentication is required"); case "INSUFFICIENT_PERMISSION": throw new DomainError("You do not have permission to perform this action"); default: throw result.error satisfies never; } } return { id: result.value.transferOrder.id }; }) .catch((err: unknown) => { if (err instanceof DomainError) throw err; throw new Error("Failed to create the transfer order", { cause: err }); }); }, output: t .object({ id: t.string().description("Created transfer order ID"), }) .description("CreateTransferOrder response"), });