import { err, ok } from "@tailor-platform/erp-kit/core"; import { mergeCustomFields, type CustomFields } from "../../../shared/domain"; import { DuplicatePurchaseItemError, ItemNotFoundError } from "../lib/errors.generated"; // ===== Aggregate state ===== declare const invariantsSatisfied: unique symbol; /** * An item's purchasing record. The row's existence is the adoption decision: * without one the item cannot reach a purchase order line at all. */ export type PurchaseItem = { readonly id: string; readonly itemId: string; readonly requiresPhysicalReceipt: boolean; readonly customFields: CustomFields; readonly [invariantsSatisfied]: true; }; export type UncheckedPurchaseItem = Omit; // Verifies nothing. The caller is responsible for the invariants. export function markInvariantsSatisfied(purchaseItem: UncheckedPurchaseItem): PurchaseItem { return purchaseItem as PurchaseItem; } // ===== Adoption ===== export interface AdoptParams { itemId: string; requiresPhysicalReceipt: boolean; customFields: CustomFields; } export interface AdoptRefs { // That itemId resolves to a ledger item is a constraint PurchaseItem owns, // so adopt receives the evidence and judges it here. item: object | null; existingRecord: PurchaseItem | null; } /** * Item status is deliberately not a rule here: procurement may prepare the * purchasing 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 DuplicatePurchaseItemError(params.itemId)); } return ok( markInvariantsSatisfied({ id: crypto.randomUUID(), itemId: params.itemId, requiresPhysicalReceipt: params.requiresPhysicalReceipt, customFields: params.customFields, }), ); } // ===== Attribute changes ===== export interface UpdateParams { requiresPhysicalReceipt?: 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(purchaseItem: PurchaseItem, params: UpdateParams): PurchaseItem { return markInvariantsSatisfied({ ...purchaseItem, requiresPhysicalReceipt: params.requiresPhysicalReceipt === undefined ? purchaseItem.requiresPhysicalReceipt : params.requiresPhysicalReceipt, customFields: mergeCustomFields(purchaseItem.customFields, params.customFields), }); }