@spinekit/purchase 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/LICENSE +75 -0
  3. package/README.md +40 -0
  4. package/dist/bridges.d.mts +167 -0
  5. package/dist/bridges.mjs +151 -0
  6. package/dist/entity-model-D-03ovSg.mjs +23 -0
  7. package/dist/index.d.mts +5 -0
  8. package/dist/index.mjs +140 -0
  9. package/dist/lifecycle/purchase-lifecycle.d.mts +17 -0
  10. package/dist/lifecycle/purchase-lifecycle.mjs +120 -0
  11. package/dist/lifecycle/purchase-lifecycle.types.d.mts +32 -0
  12. package/dist/lifecycle/purchase-lifecycle.types.mjs +1 -0
  13. package/dist/payment/purchase-payment.application.d.mts +8 -0
  14. package/dist/payment/purchase-payment.application.mjs +154 -0
  15. package/dist/payment/purchase-payment.tax.d.mts +30 -0
  16. package/dist/payment/purchase-payment.tax.mjs +37 -0
  17. package/dist/payment/purchase-payment.types.d.mts +2 -0
  18. package/dist/payment/purchase-payment.types.mjs +1 -0
  19. package/dist/purchase-payment.types-hVKAeW3G.d.mts +153 -0
  20. package/dist/receipt/purchase-stock-receipt.d.mts +12 -0
  21. package/dist/receipt/purchase-stock-receipt.mjs +225 -0
  22. package/dist/receipt/purchase-stock-receipt.types.d.mts +167 -0
  23. package/dist/receipt/purchase-stock-receipt.types.mjs +1 -0
  24. package/dist/repositories/purchase-order.repository.d.mts +40 -0
  25. package/dist/repositories/purchase-order.repository.mjs +111 -0
  26. package/dist/resources/purchase-order/purchase-order.resource.d.mts +56 -0
  27. package/dist/resources/purchase-order/purchase-order.resource.mjs +193 -0
  28. package/dist/resources/supplier/supplier.model.d.mts +2 -0
  29. package/dist/resources/supplier/supplier.model.mjs +178 -0
  30. package/dist/resources/supplier/supplier.repository.d.mts +20 -0
  31. package/dist/resources/supplier/supplier.repository.mjs +62 -0
  32. package/dist/resources/supplier/supplier.resource.d.mts +34 -0
  33. package/dist/resources/supplier/supplier.resource.mjs +129 -0
  34. package/dist/resources/supplier/supplier.types.d.mts +2 -0
  35. package/dist/resources/supplier/supplier.types.mjs +1 -0
  36. package/dist/supplier.model-BcAfgyHu.d.mts +88 -0
  37. package/dist/types-T9gsXOf_.d.mts +127 -0
  38. package/package.json +129 -0
@@ -0,0 +1,17 @@
1
+ import { PurchaseLifecycleDeps } from "./purchase-lifecycle.types.mjs";
2
+ import { PurchaseOrderDocument } from "@classytic/purchase";
3
+ //#region src/lifecycle/purchase-lifecycle.d.ts
4
+ declare function createPurchaseLifecycle(deps: PurchaseLifecycleDeps): {
5
+ /** Approve a draft purchase — `draft → approved`, atomically via the kernel's CAS. */
6
+ approve(purchaseId: string, actorId: string | undefined): Promise<PurchaseOrderDocument>;
7
+ /** Cancel a draft or approved purchase — one atomic claim, so no concurrent
8
+ * payment / approval / receive can slip past the FSM gate. */
9
+ cancel(purchaseId: string, actorId: string | undefined, reason?: string): Promise<PurchaseOrderDocument>;
10
+ /**
11
+ * Receive a purchase order. See the module header for the transaction boundary — the part
12
+ * that is genuinely subtle and the part a second host would get wrong.
13
+ */
14
+ receive(purchaseId: string, actorId: string | undefined): Promise<Record<string, unknown>>;
15
+ };
16
+ //#endregion
17
+ export { createPurchaseLifecycle };
@@ -0,0 +1,120 @@
1
+ import { PENDING_EVENTS, flushPending } from "@classytic/purchase";
2
+ import { ConflictError, NotFoundError } from "@classytic/arc/utils";
3
+ //#region src/lifecycle/purchase-lifecycle.ts
4
+ /**
5
+ * Purchase-order LIFECYCLE verbs — approve · cancel · receive.
6
+ *
7
+ * Lifted from be-prod (`purchase-order/actions/{approve,cancel,receive}-purchase-order.ts` plus
8
+ * the state machine that lived in `purchase-order.service.ts`). Each verb is thin because the
9
+ * kernel's repository owns the atomic transition; what was worth extracting is the ORDER of
10
+ * operations around it, which is where every silent failure lives.
11
+ *
12
+ * ## Why the pre-CAS read exists at all
13
+ *
14
+ * The kernel's `claim()` CAS already enforces the FSM — a cancel cannot race past an approve.
15
+ * But a CAS that finds no match returns "no match", which is indistinguishable from losing a
16
+ * concurrency race. The read before it exists purely so a caller learns *"only draft purchases
17
+ * can be approved"* instead of *"concurrent modification"*. It is a MESSAGE, never the guard:
18
+ * removing it costs clarity, and trusting it instead of the CAS would cost correctness.
19
+ *
20
+ * ## Receive: what is deliberately OUTSIDE the transaction
21
+ *
22
+ * The kernel's `receive()` does the CAS first, stamps `pendingStockReceipt` so a crash between
23
+ * the CAS and the bridge heals on the next receive, and compensates the status back if the
24
+ * bridge throws. The stock booking itself runs in the WMS's OWN unit of work — nested Mongo
25
+ * transactions do not exist — so a failure *after* the bridge rolls the PO back to `approved`
26
+ * while the booked stock REMAINS. That divergence is CONVERGED, not prevented: the bridge is
27
+ * idempotent on `purchaseId`, so the healing receive re-fires it as a no-op and completes the
28
+ * flip.
29
+ */
30
+ /**
31
+ * Prior states each verb accepts, for the friendly pre-CAS message only.
32
+ *
33
+ * The kernel's FSM is the authority. This map exists so the two cannot silently disagree in
34
+ * the direction that matters: it may only ever be a SUBSET of what the kernel permits — a
35
+ * superset would produce a helpful-looking message for a transition the CAS then refuses.
36
+ */
37
+ const ALLOWED_PRIOR_STATES = {
38
+ approve: ["draft"],
39
+ cancel: ["draft", "approved"]
40
+ };
41
+ const REFUSAL = {
42
+ approve: "Only draft purchases can be approved",
43
+ cancel: "Only draft or approved purchases can be cancelled"
44
+ };
45
+ function createPurchaseLifecycle(deps) {
46
+ /** Load for the friendly message. Lean — nothing here mutates the document. */
47
+ async function readStatus(purchaseId) {
48
+ const purchase = await deps.engine().models.PurchaseOrder.findById(purchaseId).lean();
49
+ if (!purchase) throw new NotFoundError("Purchase");
50
+ return purchase.status ?? "unknown";
51
+ }
52
+ function assertPrior(verb, status) {
53
+ if (!ALLOWED_PRIOR_STATES[verb].includes(status)) throw new ConflictError(`${REFUSAL[verb]} (current status: ${status})`);
54
+ }
55
+ return {
56
+ /** Approve a draft purchase — `draft → approved`, atomically via the kernel's CAS. */
57
+ async approve(purchaseId, actorId) {
58
+ assertPrior("approve", await readStatus(purchaseId));
59
+ return deps.engine().repositories.purchaseOrder.approve(purchaseId, actorId ? { actorId } : {});
60
+ },
61
+ /** Cancel a draft or approved purchase — one atomic claim, so no concurrent
62
+ * payment / approval / receive can slip past the FSM gate. */
63
+ async cancel(purchaseId, actorId, reason) {
64
+ assertPrior("cancel", await readStatus(purchaseId));
65
+ return deps.engine().repositories.purchaseOrder.cancel(purchaseId, reason, actorId ? { actorId } : {});
66
+ },
67
+ /**
68
+ * Receive a purchase order. See the module header for the transaction boundary — the part
69
+ * that is genuinely subtle and the part a second host would get wrong.
70
+ */
71
+ async receive(purchaseId, actorId) {
72
+ let resolvedSupplierName;
73
+ /**
74
+ * Managed-session contract (purchase 0.2+): given `ctx.session` the kernel QUEUES its
75
+ * domain events rather than publishing mid-transaction. A subscriber must never observe
76
+ * events for a write that may still roll back.
77
+ */
78
+ const pendingEvents = [];
79
+ return deps.withTransaction(async (session) => {
80
+ const engine = deps.engine();
81
+ const repo = engine.repositories.purchaseOrder;
82
+ const ctx = {
83
+ ...actorId !== void 0 ? { actorId } : {},
84
+ ...session ? {
85
+ session,
86
+ [PENDING_EVENTS]: pendingEvents
87
+ } : {}
88
+ };
89
+ const query = engine.models.PurchaseOrder.findById(purchaseId);
90
+ const existing = await (session ? query.session(session) : query);
91
+ if (!existing) throw new NotFoundError("Purchase");
92
+ /**
93
+ * Receiving a draft implies approval — it stamps approvedBy/At and an APPROVED
94
+ * history entry. Routing through the kernel verb rather than an inline status write
95
+ * is what makes the approval-chain gate apply: a PO with a pending multi-step chain
96
+ * can no longer be received *around* its chain.
97
+ */
98
+ if (existing.status === "draft") await repo.approve(purchaseId, ctx);
99
+ const updated = await repo.receive(purchaseId, ctx);
100
+ const supplierId = updated.supplier ? String(updated.supplier) : void 0;
101
+ resolvedSupplierName = (await deps.supplier.getById(supplierId))?.name;
102
+ return typeof updated.toObject === "function" ? updated.toObject() : updated;
103
+ }, { onCommit: async (purchase) => {
104
+ await flushPending(deps.eventTransport, pendingEvents, deps.logger);
105
+ /**
106
+ * Host side effects — notifications, cache invalidation. Deliberately after the
107
+ * drain and deliberately a HOOK: which of these a deployment wants is not this
108
+ * package's business, and running them pre-commit would announce a receive that
109
+ * an abort un-makes.
110
+ */
111
+ await deps.onReceived?.(purchase, {
112
+ actorId,
113
+ supplierName: resolvedSupplierName
114
+ });
115
+ } });
116
+ }
117
+ };
118
+ }
119
+ //#endregion
120
+ export { createPurchaseLifecycle };
@@ -0,0 +1,32 @@
1
+ import { c as PurchaseUnitOfWork, s as PurchaseSupplierLookup } from "../purchase-payment.types-hVKAeW3G.mjs";
2
+ import { PurchaseEngine } from "@classytic/purchase/engine";
3
+ import { flushPending } from "@classytic/purchase";
4
+ import { EventTransport } from "@classytic/primitives/events";
5
+ //#region src/lifecycle/purchase-lifecycle.types.d.ts
6
+ /**
7
+ * Host side effects for a COMMITTED receive — notifications, cache invalidation, anything
8
+ * outward-facing.
9
+ *
10
+ * A hook rather than ports for each concern: which side effects a deployment wants is not the
11
+ * package's business, and enumerating them here would mean editing the package every time a
12
+ * vertical adds one. It runs post-commit, so it can safely announce the receive.
13
+ */
14
+ interface PurchaseReceivedHook {
15
+ (purchase: Record<string, unknown>, context: {
16
+ actorId?: string | undefined;
17
+ supplierName?: string | undefined;
18
+ }): Promise<void> | void;
19
+ }
20
+ interface PurchaseLifecycleDeps {
21
+ /** Getter, not a handle — nothing may allocate the engine during composition. */
22
+ engine: () => PurchaseEngine;
23
+ withTransaction: PurchaseUnitOfWork;
24
+ supplier: PurchaseSupplierLookup;
25
+ /** Where the kernel's queued `purchase:order.{approved,received}` are drained, post-commit. */
26
+ eventTransport: EventTransport;
27
+ /** Derived from the kernel's contract so it cannot drift from `flushPending`. */
28
+ logger: Parameters<typeof flushPending>[2];
29
+ onReceived?: PurchaseReceivedHook | undefined;
30
+ }
31
+ //#endregion
32
+ export { PurchaseLifecycleDeps, PurchaseReceivedHook };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { n as PurchasePaymentApplicationDeps, t as PayPurchaseCommand } from "../purchase-payment.types-hVKAeW3G.mjs";
2
+ //#region src/payment/purchase-payment.application.d.ts
3
+ declare function createPurchasePaymentApplication(deps: PurchasePaymentApplicationDeps): {
4
+ /** Pay a purchase order — the whole compound operation, atomically. */
5
+ pay({ purchaseId, payment, actorId }: PayPurchaseCommand): Promise<unknown>;
6
+ };
7
+ //#endregion
8
+ export { createPurchasePaymentApplication };
@@ -0,0 +1,154 @@
1
+ import { PENDING_EVENTS, flushPending } from "@classytic/purchase";
2
+ import { ConflictError, NotFoundError, ValidationError } from "@classytic/arc/utils";
3
+ import { convertMinorToBaseMinor } from "@classytic/purchase/domain";
4
+ //#region src/payment/purchase-payment.application.ts
5
+ /**
6
+ * Purchase-payment APPLICATION SERVICE — the compound "pay a PO" operation.
7
+ *
8
+ * Lifted from be-prod (`modules/inventory/purchase-order/purchase-payment.application.ts`),
9
+ * where it was ~210 lines no other host could pay a supplier with. The gym's equivalent gap
10
+ * — payment confirmation living in a host — is why an order could be created and never
11
+ * confirmed, with nothing erroring.
12
+ *
13
+ * Owns the unit of work (one `withTransaction`):
14
+ * 1. guards — exists, payable state, integer minor units, cap by amount due
15
+ * 2. tax attribution for partial payments, via the PACK's port
16
+ * 3. `PurchasePaymentRecordingPort.record()` — the payment-side write; revenue and ledger
17
+ * knowledge lives ONLY in the injected adapter
18
+ * 4. the kernel's `purchaseOrder.pay()` — atomic `$add` CAS, FSM-pinned
19
+ * 5. queued `purchase:order.paid` domain events, drained post-commit
20
+ *
21
+ * The kernel never sees revenue or ledger; an HTTP action never sees the orchestration. A
22
+ * test injects a fake recording port and exercises the whole unit.
23
+ *
24
+ * ORDER IS LOAD-BEARING. The payment record is written INSIDE the session and BEFORE the CAS,
25
+ * so a lost CAS rolls the record back. Recording after a successful CAS would leave a paid PO
26
+ * with no cash record if the second write failed — a reconciliation gap that nothing reports.
27
+ */
28
+ /**
29
+ * Guard the transaction date at the boundary — `new Date('garbage')` is an Invalid Date that
30
+ * survives every type check and only explodes later, inside accounting, on a document nobody
31
+ * can trace back to this request.
32
+ */
33
+ function resolveTransactionDate(input) {
34
+ if (input === void 0) return /* @__PURE__ */ new Date();
35
+ const parsed = new Date(input);
36
+ if (Number.isNaN(parsed.getTime())) throw new ValidationError(`transactionDate ${JSON.stringify(input)} is not a valid date`);
37
+ return parsed;
38
+ }
39
+ function createPurchasePaymentApplication(deps) {
40
+ return {
41
+ /** Pay a purchase order — the whole compound operation, atomically. */
42
+ async pay({ purchaseId, payment, actorId }) {
43
+ /**
44
+ * Managed-session contract (purchase 0.2+): given `ctx.session`, the kernel QUEUES its
45
+ * domain events instead of publishing mid-transaction. Publishing inside the
46
+ * transaction announces a payment that a later abort un-makes.
47
+ */
48
+ const pendingEvents = [];
49
+ return deps.withTransaction(async (session) => {
50
+ const engine = deps.engine();
51
+ const query = engine.models.PurchaseOrder.findById(purchaseId);
52
+ const purchase = await (session ? query.session(session) : query);
53
+ if (!purchase) throw new NotFoundError("Purchase");
54
+ /**
55
+ * A precise message before the CAS. The kernel's filter would also reject a
56
+ * cancelled PO, but as a no-match — indistinguishable from a concurrency loss.
57
+ */
58
+ if (purchase.status === "cancelled") throw new ConflictError("Cancelled purchases cannot be paid");
59
+ /**
60
+ * `amount` is integer MINOR units (schemaVersion 2). Defaulting to `dueAmount` is
61
+ * unit-consistent because the document persists minor units too — the one place
62
+ * this could silently break is a host that still sends major units, which the
63
+ * integer guard below does NOT catch (12.50 fails, but 1250 major would pass as
64
+ * 12.50 base). That is why the wire schema, not this guard, is the unit authority.
65
+ */
66
+ const amount = payment.amount ?? purchase.dueAmount;
67
+ if (!Number.isInteger(amount) || amount <= 0) throw new ValidationError("Payment amount must be a positive integer in minor units");
68
+ /**
69
+ * Cap guard — read against this snapshot. A concurrent payment still composes
70
+ * correctly in the kernel's `$add` CAS (no lost increment); a small overshoot
71
+ * between the read and the write is a UX issue, not a correctness one.
72
+ */
73
+ if (amount > purchase.dueAmount) throw new ValidationError("Payment amount exceeds due amount");
74
+ const supplierId = purchase.supplier ? String(purchase.supplier) : void 0;
75
+ const supplier = await deps.supplier.getById(supplierId);
76
+ const purchaseTaxTotal = Number.isFinite(purchase.taxTotal) ? purchase.taxTotal : 0;
77
+ const dominantRatePercent = purchase.items.reduce((max, item) => (item.taxRate ?? 0) > max ? item.taxRate ?? 0 : max, 0);
78
+ const attributed = deps.taxAttribution.attribute({
79
+ purchaseTaxTotalMinor: purchaseTaxTotal,
80
+ paymentAmountMinor: amount,
81
+ purchaseGrandTotalMinor: purchase.grandTotal,
82
+ dominantRatePercent
83
+ });
84
+ const paymentTax = attributed.taxMinor;
85
+ const taxDetails = attributed.details;
86
+ /**
87
+ * A pack is host-supplied code, so its output is an INPUT here. An
88
+ * over-attribution overstates a recoverable-tax position that a filing depends on,
89
+ * and a non-integer minor amount corrupts every downstream sum — both must fail
90
+ * loudly rather than propagate a plausible number.
91
+ */
92
+ if (!Number.isInteger(paymentTax) || paymentTax < 0) throw new ValidationError("Tax attribution must return a non-negative integer");
93
+ if (paymentTax > purchaseTaxTotal) throw new ValidationError("Tax attribution exceeds the order tax total");
94
+ /**
95
+ * Base-currency conversion via the PO's FROZEN snapshot — the same kernel VO the
96
+ * receive / GR-IR / vendor-bill legs use, so the cash record agrees with the
97
+ * inventory and A/P values. Re-fetching a live rate here would make them disagree.
98
+ */
99
+ const fx = purchase.fx ?? null;
100
+ const baseAmountMinor = convertMinorToBaseMinor(amount, fx);
101
+ const baseTaxMinor = convertMinorToBaseMinor(paymentTax, fx);
102
+ const { transactionId } = await deps.recordPayment.record({
103
+ amountMinor: amount,
104
+ taxMinor: paymentTax,
105
+ baseAmountMinor,
106
+ baseTaxMinor,
107
+ fx,
108
+ currency: purchase.currency ?? deps.baseCurrency,
109
+ method: payment.method || "cash",
110
+ purchase: {
111
+ id: String(purchase._id),
112
+ invoiceNumber: purchase.invoiceNumber,
113
+ branchId: String(purchase.branch)
114
+ },
115
+ ...supplier ? { supplier: {
116
+ id: String(supplier._id),
117
+ name: supplier.name
118
+ } } : {},
119
+ paymentDetails: payment.details ?? {},
120
+ taxDetails,
121
+ purchaseTaxTotalMinor: purchaseTaxTotal,
122
+ notes: [
123
+ `Purchase payment: ${purchase.invoiceNumber}`,
124
+ supplier?.name ? `Supplier: ${supplier.name}` : null,
125
+ payment.notes
126
+ ].filter(Boolean).join(". "),
127
+ actorId,
128
+ date: resolveTransactionDate(payment.transactionDate),
129
+ session
130
+ });
131
+ /**
132
+ * 2. Atomic CAS via the kernel's `pay()` — `$add` paidAmount, recompute dueAmount
133
+ * and paymentStatus post-increment, push the transactionId. FSM-pinned and
134
+ * concurrency-safe by construction, which is why nothing here recomputes a
135
+ * payment status itself.
136
+ */
137
+ return engine.repositories.purchaseOrder.pay(purchaseId, {
138
+ amount,
139
+ transactionId,
140
+ ...payment.method ? { method: payment.method } : {}
141
+ }, {
142
+ ...actorId ? { actorId } : {},
143
+ ...session ? {
144
+ session,
145
+ [PENDING_EVENTS]: pendingEvents
146
+ } : {}
147
+ });
148
+ }, { onCommit: async () => {
149
+ await flushPending(deps.eventTransport, pendingEvents, deps.logger);
150
+ } });
151
+ } };
152
+ }
153
+ //#endregion
154
+ export { createPurchasePaymentApplication };
@@ -0,0 +1,30 @@
1
+ import { i as PurchasePaymentTaxAttribution } from "../purchase-payment.types-hVKAeW3G.mjs";
2
+ //#region src/payment/purchase-payment.tax.d.ts
3
+ /** What the pack names the tax it is attributing. */
4
+ interface ProRataTaxLabels {
5
+ /** `vat`, `gst`, `sales_tax`, … */
6
+ type: string;
7
+ /** Taxing authority identifier as the host's books expect it. */
8
+ jurisdiction: string;
9
+ /** Whether the order's line rates are tax-inclusive. Defaults to exclusive. */
10
+ isInclusive?: boolean | undefined;
11
+ }
12
+ /**
13
+ * PAYMENT-basis attribution: `taxTotal × amount / grandTotal`, rounded once.
14
+ *
15
+ * Computed as a single expression rather than `total × (amount / grandTotal)` — the two-step
16
+ * form rounds a float ratio and then multiplies, which drifts by a minor unit on ordinary
17
+ * inputs and makes a sum of payments disagree with the order's own tax total.
18
+ *
19
+ * Zero when the order carries no tax or no total; a rate of zero yields an amount with no
20
+ * `details`, because there is nothing to describe.
21
+ */
22
+ declare function proRataTaxAttribution(labels: ProRataTaxLabels): PurchasePaymentTaxAttribution;
23
+ /**
24
+ * INVOICE-basis attribution — a payment attributes no tax because posting the bill already
25
+ * did. Explicit rather than "just leave the port off": a host states its recognition basis,
26
+ * and a reader of the wiring can see which one.
27
+ */
28
+ declare function invoiceBasisTaxAttribution(): PurchasePaymentTaxAttribution;
29
+ //#endregion
30
+ export { ProRataTaxLabels, invoiceBasisTaxAttribution, proRataTaxAttribution };
@@ -0,0 +1,37 @@
1
+ //#region src/payment/purchase-payment.tax.ts
2
+ /**
3
+ * PAYMENT-basis attribution: `taxTotal × amount / grandTotal`, rounded once.
4
+ *
5
+ * Computed as a single expression rather than `total × (amount / grandTotal)` — the two-step
6
+ * form rounds a float ratio and then multiplies, which drifts by a minor unit on ordinary
7
+ * inputs and makes a sum of payments disagree with the order's own tax total.
8
+ *
9
+ * Zero when the order carries no tax or no total; a rate of zero yields an amount with no
10
+ * `details`, because there is nothing to describe.
11
+ */
12
+ function proRataTaxAttribution(labels) {
13
+ return { attribute({ purchaseTaxTotalMinor, paymentAmountMinor, purchaseGrandTotalMinor, dominantRatePercent }) {
14
+ if (purchaseTaxTotalMinor <= 0 || purchaseGrandTotalMinor <= 0) return { taxMinor: 0 };
15
+ const taxMinor = Math.round(purchaseTaxTotalMinor * paymentAmountMinor / purchaseGrandTotalMinor);
16
+ if (dominantRatePercent <= 0) return { taxMinor };
17
+ return {
18
+ taxMinor,
19
+ details: {
20
+ type: labels.type,
21
+ rate: dominantRatePercent / 100,
22
+ isInclusive: labels.isInclusive ?? false,
23
+ jurisdiction: labels.jurisdiction
24
+ }
25
+ };
26
+ } };
27
+ }
28
+ /**
29
+ * INVOICE-basis attribution — a payment attributes no tax because posting the bill already
30
+ * did. Explicit rather than "just leave the port off": a host states its recognition basis,
31
+ * and a reader of the wiring can see which one.
32
+ */
33
+ function invoiceBasisTaxAttribution() {
34
+ return { attribute: () => ({ taxMinor: 0 }) };
35
+ }
36
+ //#endregion
37
+ export { invoiceBasisTaxAttribution, proRataTaxAttribution };
@@ -0,0 +1,2 @@
1
+ import { a as PurchasePaymentTaxDetails, c as PurchaseUnitOfWork, i as PurchasePaymentTaxAttribution, l as RecordPurchasePaymentInput, n as PurchasePaymentApplicationDeps, o as PurchasePaymentTaxInput, r as PurchasePaymentRecordingPort, s as PurchaseSupplierLookup, t as PayPurchaseCommand } from "../purchase-payment.types-hVKAeW3G.mjs";
2
+ export { PayPurchaseCommand, PurchasePaymentApplicationDeps, PurchasePaymentRecordingPort, PurchasePaymentTaxAttribution, PurchasePaymentTaxDetails, PurchasePaymentTaxInput, PurchaseSupplierLookup, PurchaseUnitOfWork, RecordPurchasePaymentInput };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,153 @@
1
+ import { PurchaseEngine } from "@classytic/purchase/engine";
2
+ import { flushPending } from "@classytic/purchase";
3
+ import { ClientSession } from "mongoose";
4
+ import { EventTransport } from "@classytic/primitives/events";
5
+ import { FxSnapshot } from "@classytic/primitives/currency";
6
+ //#region src/payment/purchase-payment.types.d.ts
7
+ /** How a pack describes an attributed tax amount. Shape only — no type or jurisdiction value. */
8
+ interface PurchasePaymentTaxDetails {
9
+ /** `vat`, `gst`, `sales_tax`, … — the PACK's vocabulary, never this package's. */
10
+ type: string;
11
+ /** Fractional rate (0.15), not percent. */
12
+ rate: number;
13
+ isInclusive: boolean;
14
+ /** Whatever identifies the taxing authority to the host's books. */
15
+ jurisdiction: string;
16
+ }
17
+ interface PurchasePaymentTaxInput {
18
+ /** Whole-order tax total, DOCUMENT-currency minor units. */
19
+ purchaseTaxTotalMinor: number;
20
+ /** This payment, DOCUMENT-currency minor units. */
21
+ paymentAmountMinor: number;
22
+ /** Order grand total, DOCUMENT-currency minor units — the pro-rata denominator. */
23
+ purchaseGrandTotalMinor: number;
24
+ /** Highest line rate on the order, as a PERCENT (15, not 0.15) — the kernel's field unit. */
25
+ dominantRatePercent: number;
26
+ }
27
+ /**
28
+ * When tax is recognised and what it is called. See the neutrality note above.
29
+ *
30
+ * `taxMinor` MUST NOT exceed `purchaseTaxTotalMinor`, and MUST be an integer — the
31
+ * application service enforces both, because an over-attribution silently overstates a
32
+ * recoverable-tax position that a filing later depends on.
33
+ */
34
+ interface PurchasePaymentTaxAttribution {
35
+ attribute(input: PurchasePaymentTaxInput): {
36
+ taxMinor: number;
37
+ details?: PurchasePaymentTaxDetails | undefined;
38
+ };
39
+ }
40
+ interface RecordPurchasePaymentInput {
41
+ /** Payment amount in integer MINOR units of the DOCUMENT currency — what updates
42
+ * paidAmount/dueAmount on the PO. */
43
+ amountMinor: number;
44
+ /** Attributed tax portion of this payment, DOCUMENT-currency minor units. */
45
+ taxMinor: number;
46
+ /**
47
+ * Payment amount converted to the deployment's BASE currency via the PO's frozen
48
+ * FxSnapshot (equal to `amountMinor` on base-currency POs).
49
+ *
50
+ * Financial adapters (revenue / ledger) MUST consume the base fields. Recording a
51
+ * document amount as a book value is the mis-recording class this split exists to kill,
52
+ * and its magnitude is the FX rate — invisible on a base-currency deployment, ~120× on a
53
+ * USD document in a BDT book.
54
+ */
55
+ baseAmountMinor: number;
56
+ /** Attributed tax in BASE-currency minor units. */
57
+ baseTaxMinor: number;
58
+ /** Frozen conversion record — null on base-currency POs. */
59
+ fx: FxSnapshot | null;
60
+ currency: string;
61
+ method: string;
62
+ purchase: {
63
+ id: string;
64
+ invoiceNumber: string;
65
+ branchId: string;
66
+ };
67
+ supplier?: {
68
+ id: string;
69
+ name?: string | undefined;
70
+ } | undefined;
71
+ /**
72
+ * Rail-specific references. Deliberately an open bag: `walletNumber` is a mobile-money
73
+ * concept, `bankName` a wire concept, and a pack adds its own without this package
74
+ * enumerating the world's payment rails.
75
+ */
76
+ paymentDetails: Record<string, string | undefined>;
77
+ taxDetails?: PurchasePaymentTaxDetails | undefined;
78
+ /** Whole-order tax total (minor units) — provenance for reconciliation. */
79
+ purchaseTaxTotalMinor: number;
80
+ notes?: string | undefined;
81
+ actorId?: string | undefined;
82
+ date: Date;
83
+ /** Session of the surrounding unit of work — the write MUST enlist in it. */
84
+ session: ClientSession | null;
85
+ }
86
+ /**
87
+ * Payment-side recording seam. The application service calls this INSIDE its transaction;
88
+ * the adapter's write must enlist in `input.session` so a later kernel-CAS failure rolls the
89
+ * payment record back with everything else.
90
+ *
91
+ * Revenue and ledger knowledge lives ONLY behind this port — the kernel never sees either.
92
+ */
93
+ interface PurchasePaymentRecordingPort {
94
+ record(input: RecordPurchasePaymentInput): Promise<{
95
+ transactionId: string;
96
+ }>;
97
+ }
98
+ /**
99
+ * The surrounding unit of work. A host owns whether its deployment has transactions at all
100
+ * (a standalone mongod has no sessions), which is why `session` is nullable and the port is
101
+ * injected rather than opened here.
102
+ */
103
+ interface PurchaseUnitOfWork {
104
+ <T>(work: (session: ClientSession | null) => Promise<T>,
105
+ /**
106
+ * `onCommit` receives the work's RESULT — the lifecycle's receive hook needs the persisted
107
+ * document, and a callback that took nothing would force a host to smuggle it out through a
108
+ * closure variable assigned inside the transaction.
109
+ */
110
+ options: {
111
+ onCommit: (result: T) => Promise<void>;
112
+ }): Promise<T>;
113
+ }
114
+ /** Supplier name/id for the payment record's provenance. Not a party contract — a lookup. */
115
+ interface PurchaseSupplierLookup {
116
+ getById(id: string | undefined): Promise<{
117
+ _id: unknown;
118
+ name?: string | undefined;
119
+ } | null>;
120
+ }
121
+ interface PurchasePaymentApplicationDeps {
122
+ /**
123
+ * Engine GETTER, not an engine. Captured as `slot.get`, never `slot.get()` — reading the
124
+ * slot during composition throws by name instead of allocating before arc owns the
125
+ * lifecycle.
126
+ */
127
+ engine: () => PurchaseEngine;
128
+ recordPayment: PurchasePaymentRecordingPort;
129
+ withTransaction: PurchaseUnitOfWork;
130
+ supplier: PurchaseSupplierLookup;
131
+ taxAttribution: PurchasePaymentTaxAttribution;
132
+ /** ISO 4217 functional currency — the fallback for a PO that stored none. */
133
+ baseCurrency: string;
134
+ /** Where the kernel's queued `purchase:order.paid` is drained to, post-commit. */
135
+ eventTransport: EventTransport;
136
+ /** Derived from the kernel's own contract so it cannot drift from `flushPending`. */
137
+ logger: Parameters<typeof flushPending>[2];
138
+ }
139
+ interface PayPurchaseCommand {
140
+ purchaseId: string;
141
+ payment: {
142
+ /** Integer MINOR units. Defaults to the PO's `dueAmount` (same unit) when absent. */
143
+ amount?: number | undefined;
144
+ method?: string | undefined;
145
+ transactionDate?: string | undefined;
146
+ notes?: string | undefined;
147
+ /** Rail references — forwarded to the recording port verbatim. */
148
+ details?: Record<string, string | undefined> | undefined;
149
+ };
150
+ actorId?: string | undefined;
151
+ }
152
+ //#endregion
153
+ export { PurchasePaymentTaxDetails as a, PurchaseUnitOfWork as c, PurchasePaymentTaxAttribution as i, RecordPurchasePaymentInput as l, PurchasePaymentApplicationDeps as n, PurchasePaymentTaxInput as o, PurchasePaymentRecordingPort as r, PurchaseSupplierLookup as s, PayPurchaseCommand as t };
@@ -0,0 +1,12 @@
1
+ import { PurchaseReceiptContext, PurchaseReceiptLineError, PurchaseReceiptScope, PurchaseStockReceiptDeps, ReceivePurchaseIntoStockInput } from "./purchase-stock-receipt.types.mjs";
2
+ //#region src/receipt/purchase-stock-receipt.d.ts
3
+ declare function createPurchaseStockReceipt<TCtx extends PurchaseReceiptScope = PurchaseReceiptContext>(deps: PurchaseStockReceiptDeps<TCtx>): {
4
+ /**
5
+ * Book every receivable line, returning the lines that could not be resolved.
6
+ *
7
+ * Safe to retry: see the idempotency and convergence notes in the module header.
8
+ */
9
+ receive({ purchase, supplierName }: ReceivePurchaseIntoStockInput): Promise<PurchaseReceiptLineError[]>;
10
+ };
11
+ //#endregion
12
+ export { createPurchaseStockReceipt };