import { createContext, DomainError } from "@tailor-platform/erp-kit/app"; import { createResolver, t } from "@tailor-platform/sdk"; import { getDB } from "@/generated/kysely-tailordb"; import { salesModulesExport } from "@/modules"; /** * createSalesOrder — raise a DRAFT sales order for a customer. * * Thin wrapper over the native `createSalesOrder`. The customer must be an * ACTIVE CustomerAccount linked to an ACTIVE BusinessPartner. The order's * company is taken from the account. A shipping address is captured on the * header because submit requires one when any line needs physical fulfillment * (which is the default). */ export default createResolver({ name: "createSalesOrder", operation: "mutation", input: { customerAccountId: t.string().description("Customer account ID"), orderDate: t .string({ optional: true }) .description("Order date (YYYY-MM-DD); defaults to today"), shippingAddress: t .string({ optional: true }) .description("Shipping address; required before the order can be submitted"), billingAddress: t.string({ optional: true }).description("Billing address"), lines: t .object( { itemId: t.string().description("Item being ordered"), quantity: t.string().description("Ordered quantity"), unitPrice: t.string().description("Unit price"), unitId: t.string({ optional: true }).description("Unit of measure"), }, { array: true }, ) .description("Order lines"), }, body: async (context) => { const ctx = createContext(context); const db = getDB("main-db"); return db .transaction() .execute(async (trx) => { if (context.input.lines.length === 0) { throw new DomainError("At least one order line is required."); } const customer = await trx .selectFrom("CustomerAccount") .select(["id", "companyId"]) .where("id", "=", context.input.customerAccountId) .executeTakeFirst(); if (!customer) { throw new DomainError(`Customer not found: ${context.input.customerAccountId}`); } const company = await trx .selectFrom("Company") .select(["baseCurrencyId"]) .where("id", "=", customer.companyId) .executeTakeFirst(); // The kit does not handle FX, so the base currency is the only value the // order accepts. const currencyId = company?.baseCurrencyId; if (!currencyId) { throw new DomainError(`The order company has no base currency: ${customer.companyId}`); } // Default each line's unit to the item's base unit when the caller omits // it, so every sales-order line carries a unit. Downstream steps (raising // a customer invoice from the order) require a unit on every line. const lineItemIds = [...new Set(context.input.lines.map((line) => line.itemId))]; const itemUnitRows = lineItemIds.length ? await trx .selectFrom("Item") .select(["id", "unitId"]) .where("id", "in", lineItemIds) .execute() : []; const itemUnitMap = new Map(itemUnitRows.map((row) => [row.id, row.unitId])); const result = await salesModulesExport.commands.createSalesOrder( trx, { header: { companyId: customer.companyId, customerAccountId: customer.id, currencyId, orderDate: context.input.orderDate ? new Date(context.input.orderDate) : undefined, shippingAddress: context.input.shippingAddress?.trim() || undefined, billingAddress: context.input.billingAddress?.trim() || undefined, }, lines: context.input.lines.map((line) => ({ itemId: line.itemId, quantity: line.quantity, unitPrice: line.unitPrice, unitId: line.unitId ?? itemUnitMap.get(line.itemId) ?? undefined, })), }, ctx, ); if (!result.ok) { switch (result.error.code) { case "SALES_COMPANY_NOT_FOUND": throw new DomainError(`Company not found: ${customer.companyId}`); case "SALES_CURRENCY_MISMATCH": throw new DomainError(`Currency is not the company's base currency: ${currencyId}`); case "SALES_CUSTOMER_NOT_FOUND": throw new DomainError(`Customer not found: ${context.input.customerAccountId}`); case "SALES_CUSTOMER_NOT_ACTIVE": throw new DomainError(`Customer is not active: ${context.input.customerAccountId}`); case "SALES_CROSS_COMPANY_REFERENCE": throw new DomainError(`The customer belongs to a different company.`); case "SALES_EMPTY_ORDER_NOT_ALLOWED": throw new DomainError("A sales order must have at least one line."); case "SALES_INVALID_ORDER_LINE": throw new DomainError(`Invalid order line: ${result.error.message}`); case "SALES_ITEM_NOT_FOUND": throw new DomainError(`Item not found: ${result.error.message}`); case "SALES_ITEM_NOT_ACTIVE": throw new DomainError(`Item is not active: ${result.error.message}`); case "SALES_ITEM_NOT_SELLABLE": throw new DomainError(`Item has no selling record: ${result.error.message}`); 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.salesOrderId, // Creation always lands in DRAFT. status: "DRAFT", }; }) .catch((err: unknown) => { if (err instanceof DomainError) throw err; throw new Error("Failed to create the sales order", { cause: err }); }); }, output: t .object({ id: t.string().description("Sales order ID"), status: t.string().description("Document status (DRAFT)"), }) .description("CreateSalesOrder response"), });