import { err, ok } from "@tailor-platform/erp-kit/core"; import { mergeCustomFields, type CustomFields } from "../../../shared/domain"; import { DuplicateSalesItemError, ItemNotFoundError } from "../lib/errors.generated"; // ===== Aggregate state ===== declare const invariantsSatisfied: unique symbol; /** * An item's selling record. The row's existence is the adoption decision: * without one the item cannot reach a sales order line at all. */ export type SalesItem = { readonly id: string; readonly itemId: string; readonly requiresPhysicalFulfillment: boolean; readonly customFields: CustomFields; readonly [invariantsSatisfied]: true; }; export type UncheckedSalesItem = Omit; // Verifies nothing. The caller is responsible for the invariants. export function markInvariantsSatisfied(salesItem: UncheckedSalesItem): SalesItem { return salesItem as SalesItem; } // ===== Adoption ===== export interface AdoptParams { itemId: string; requiresPhysicalFulfillment: boolean; customFields: CustomFields; } export interface AdoptRefs { // That itemId resolves to a ledger item is a constraint SalesItem owns, // so adopt receives the evidence and judges it here. item: object | null; existingRecord: SalesItem | null; } /** * Item status is deliberately not a rule here: sales may prepare the selling * record while the item is still DRAFT, and the ACTIVE requirement belongs * where orders are entered. */ export function adopt(params: AdoptParams, refs: AdoptRefs) { if (!refs.item) { return err(new ItemNotFoundError(params.itemId)); } if (refs.existingRecord) { return err(new DuplicateSalesItemError(params.itemId)); } return ok( markInvariantsSatisfied({ id: crypto.randomUUID(), itemId: params.itemId, requiresPhysicalFulfillment: params.requiresPhysicalFulfillment, customFields: params.customFields, }), ); } // ===== Attribute changes ===== export interface UpdateParams { requiresPhysicalFulfillment?: boolean; customFields?: CustomFields; } // Order lines froze their own copy at entry, so changing the record here never // reaches an open order. Nothing can be left undecided, so this cannot fail. export function update(salesItem: SalesItem, params: UpdateParams): SalesItem { return markInvariantsSatisfied({ ...salesItem, requiresPhysicalFulfillment: params.requiresPhysicalFulfillment === undefined ? salesItem.requiresPhysicalFulfillment : params.requiresPhysicalFulfillment, customFields: mergeCustomFields(salesItem.customFields, params.customFields), }); }