@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
package/CHANGELOG.md ADDED
@@ -0,0 +1,61 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0 - 2026-08-21
4
+
5
+ ### Changed
6
+ - **License:** relicensed from MIT to the **Classytic Source-Available License**
7
+ (Community & Commercial). Evaluation and development use remain free; production
8
+ use now requires a commercial license from Classytic LLC. See `LICENSE`.
9
+
10
+ ### Notes
11
+ - First stable **1.0.0** release of the Classytic `arc` suite. Versions published
12
+ before 1.0.0 remain under their original MIT terms; the new license applies from
13
+ 1.0.0 onward.
14
+
15
+ ## 0.2.1 — 2026-07-21
16
+
17
+ - **Engine teardown symmetry**: a module-created engine now registers
18
+ `onClose` → `engine.destroy()` (closes the kernel's event transport);
19
+ a BYO engine is never closed by the module.
20
+ - **Cast reduction (3 → 0 `as unknown as`)**: verbs flow through the
21
+ kernel-exported `PurchaseOrderRepository` type (`CreateOrderInput` /
22
+ `UpdateOrderInput` / `PurchaseContext` narrow the call sites); the
23
+ `readPopulate` hook wiring uses repo-core's public `RepositoryBase.on`;
24
+ the AnyRecord/entity invariance widens ONCE through the documented
25
+ `entity-model.ts` boundary helpers.
26
+
27
+ ## 0.2.0 — 2026-07-17
28
+
29
+ - **BREAKING: `PurchaseModuleDeps.baseCurrency` is now REQUIRED** — threaded to
30
+ `createPurchaseEngine`, which since purchase 0.5 fail-louds without a valid
31
+ ISO 4217 code (a money kernel has no jurisdiction default; same rule as
32
+ arc-invoice/arc-assets). New optional `allowForeignCurrency?: boolean`
33
+ (default `false`) permits POs/bills in a non-base currency.
34
+ - Peer `@classytic/purchase` floor raised `>=0.3.0` → `>=0.5.0` (0.5's required
35
+ baseCurrency, policy bridge, batch catalog resolution, and `unitCostMinor`
36
+ make older kernels incompatible with this module's composition).
37
+ - **BREAKING: kernel-bypass seams REMOVED** — `controller` / `repository` /
38
+ `customSchemas` / `deleteGate` are gone. The module owns HTTP: CREATE and
39
+ PATCH route through the kernel verbs (`createOrder`, `updateDraft` — FSM
40
+ guard, re-enrichment, paid-floor invariant, currency/FX pairing), the
41
+ kernel's own Zod schemas (`CreateOrderSchema`/`UpdateOrderSchema`) are the
42
+ wire contract, and DELETE is a hard 405 (`purchase.delete_forbidden` —
43
+ immutable audit trail; `action: cancel` is the verb). Hosts extend via
44
+ `extraActions`/`extraRoutes` (which ADD, never bypass) and the new
45
+ `readPopulate` (display joins on HTTP read paths only — list via
46
+ before-read hooks, detail via the controller; kernel verbs and internal
47
+ `getById` stay bare so domain code can rely on bare ObjectId refs).
48
+
49
+ ## 0.1.1 — 2026-07-11
50
+
51
+ - **`PurchaseModuleDeps.tenant?`** — optional `boolean` (default `false`). Pass
52
+ `true` to enable per-branch scoping on purchase orders; previously the module was
53
+ hardcoded to single-tenant. One knob flows to the kernel's multi-tenant plugin.
54
+ - devDep `@classytic/primitives` bumped to `^0.11.0`.
55
+
56
+ ## 0.1.0
57
+
58
+ Initial release — supplier purchase orders (draft→approve→receive with CAS +
59
+ compensation + `pendingStockReceipt` crash heal) composed into arc. Catalog/
60
+ stockReceipt/sequence kernel ports injected; accounting posting + approval stay
61
+ host seams.
package/LICENSE ADDED
@@ -0,0 +1,75 @@
1
+ Classytic Source-Available License (Community & Commercial), Version 1.0
2
+
3
+ Copyright (c) 2026 Classytic LLC. All rights reserved.
4
+
5
+ This software is source-available, not open source. Please read this License
6
+ before using it.
7
+
8
+ 1. DEFINITIONS
9
+ "Software" means the contents of this package and any accompanying source
10
+ code, object code, or documentation, in any form.
11
+ "Licensor" means Classytic LLC, a limited liability company organized in the
12
+ United States.
13
+ "You" means the individual or legal entity exercising rights under this
14
+ License.
15
+ "Evaluation Use" means use for evaluation, development, testing,
16
+ experimentation, or internal non-commercial purposes only.
17
+ "Production Use" means any use of the Software that is live, commercial,
18
+ revenue-generating, or customer-facing, or any use other than Evaluation Use.
19
+ "Commercial License" means a separate written agreement executed with
20
+ Licensor that grants Production Use rights.
21
+
22
+ 2. OWNERSHIP
23
+ The Software is licensed, not sold. Licensor retains all right, title, and
24
+ interest in and to the Software, including all intellectual property rights.
25
+ All rights not expressly granted in this License are reserved by Licensor.
26
+
27
+ 3. EVALUATION GRANT
28
+ Subject to Your compliance with this License, Licensor grants You a
29
+ worldwide, royalty-free, non-exclusive, non-transferable, revocable license
30
+ to download, install, run, and modify the Software solely for Evaluation Use.
31
+
32
+ 4. PRODUCTION USE REQUIRES A COMMERCIAL LICENSE
33
+ Production Use of the Software is NOT permitted under this License and
34
+ requires a Commercial License from Licensor. A Commercial License may be
35
+ granted at Licensor's discretion, with or without a fee, and may confer
36
+ perpetual, irrevocable, and transferable Production Use rights as set out in
37
+ that agreement. Contact licensing@classytic.com to obtain one.
38
+
39
+ 5. RESTRICTIONS
40
+ You may not:
41
+ (a) make any Production Use of the Software without a Commercial License;
42
+ (b) redistribute, resell, rent, sublicense, or otherwise make the Software
43
+ (in whole or in part, original or modified) available to third parties as
44
+ a stand-alone product, framework, library, or hosted/managed service that
45
+ competes with the Software or with Licensor;
46
+ (c) remove, obscure, or alter any copyright, license, or attribution notice.
47
+
48
+ 6. FEEDBACK AND CONTRIBUTIONS
49
+ Any feedback, suggestions, or contributions You provide relating to the
50
+ Software may be used by Licensor for any purpose without obligation to You.
51
+
52
+ 7. TERMINATION
53
+ This License and the Evaluation Grant terminate automatically if You breach
54
+ any term. Upon termination You must cease all use of the Software and destroy
55
+ all copies in Your possession. Rights under a Commercial License are governed
56
+ by that agreement's own terms and survive termination of this License to the
57
+ extent stated there.
58
+
59
+ 8. DISCLAIMER OF WARRANTY
60
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
61
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
62
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT.
63
+
64
+ 9. LIMITATION OF LIABILITY
65
+ TO THE MAXIMUM EXTENT PERMITTED BY LAW, IN NO EVENT SHALL LICENSOR BE LIABLE
66
+ FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
67
+ TORT, OR OTHERWISE, ARISING FROM, OUT OF, OR IN CONNECTION WITH THE SOFTWARE
68
+ OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
69
+
70
+ 10. GOVERNING LAW
71
+ This License is governed by the laws of the State of Delaware, United States,
72
+ without regard to its conflict-of-laws principles. (Adjust to Your LLC's state
73
+ of formation.)
74
+
75
+ For commercial licensing, contact: licensing@classytic.com
package/README.md ADDED
@@ -0,0 +1,40 @@
1
+ # @spinekit/purchase
2
+
3
+ > Purchasing / procurement — purchase orders with approval and receiving.
4
+
5
+ Part of **Spine**, Classytic's ERP — the arc-module layer that composes the
6
+ framework-agnostic `@classytic/purchase` kernel into an [`@classytic/arc`](https://www.npmjs.com/package/@classytic/arc)
7
+ application via `createApp({ modules })`.
8
+
9
+ ## Install
10
+
11
+ ```sh
12
+ npm install @spinekit/purchase @classytic/purchase @classytic/arc @classytic/mongokit mongoose zod
13
+ ```
14
+
15
+ Kernels and the framework are **peer dependencies** — the host installs a single
16
+ copy; this module bundles nothing.
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import { createApp } from '@classytic/arc/factory';
22
+ import { createPurchaseModule } from '@spinekit/purchase';
23
+
24
+ const app = await createApp({
25
+ modules: [
26
+ createPurchaseModule({
27
+ connection, // your mongoose connection
28
+ permissions, // { view, manage, ... } role gates
29
+ // BYO engine + host seams (extraActions, extraRoutes, bridges) as needed
30
+ }),
31
+ ],
32
+ });
33
+ ```
34
+
35
+ The module's `bootstrap` return is recorded at `fastify.arc.modules['purchase']`
36
+ for container-free cross-module wiring.
37
+
38
+ ## License
39
+
40
+ MIT © Classytic
@@ -0,0 +1,167 @@
1
+ //#region src/bridges/ports.d.ts
2
+ /**
3
+ * What a host must supply for the purchase bridges — engines and lookups only.
4
+ *
5
+ * Every port here is a HANDLE or a DATA lookup. None is a rule: numbering,
6
+ * enrichment, totals, payment status and the domain event belong to
7
+ * `@classytic/purchase`; item resolution and terms derivation belong to the
8
+ * bridges. A host that finds itself passing a policy through one of these
9
+ * should be changing the bridge, not the port.
10
+ */
11
+ /**
12
+ * What a resolved line hands back to the kernel.
13
+ *
14
+ * Mirrors `@classytic/purchase`'s `ResolvedPurchaseItem` structurally rather
15
+ * than importing it — the bridge package stays usable without the kernel as a
16
+ * hard type dependency, and the kernel's repository re-validates the currency
17
+ * regardless. `costPrice` is integer MINOR units.
18
+ */
19
+ interface ResolvedPurchaseLine {
20
+ productName: string;
21
+ variantSku?: string | null;
22
+ costPrice: number;
23
+ currency?: string | undefined;
24
+ leadTimeDays?: number | undefined;
25
+ }
26
+ /** Catalog reads for product identity + variant validation. */
27
+ interface PurchaseCatalogEngineLike {
28
+ repositories: {
29
+ product: {
30
+ findAll(filter: Record<string, unknown>, options: Record<string, unknown>): Promise<unknown[]>;
31
+ };
32
+ };
33
+ }
34
+ /** A supplier-price quote from the buy-side pricelist. */
35
+ interface SupplierQuote {
36
+ price: number;
37
+ currency?: string | undefined;
38
+ leadTimeDays?: number | undefined;
39
+ ruleMatched?: boolean | undefined;
40
+ }
41
+ /**
42
+ * Buy-side pricelist. OPTIONAL by design: a deployment with no supplier
43
+ * pricelists resolves costs manually, and that is a valid configuration —
44
+ * pricing here is an assist, never a gate.
45
+ */
46
+ interface PurchasePricelistEngineLike {
47
+ repositories: {
48
+ priceList: {
49
+ resolvePartnerPrice(input: {
50
+ partnerRef: string;
51
+ kind: string;
52
+ productId: string;
53
+ variantSku?: string;
54
+ quantity: number;
55
+ basePrice: number;
56
+ }, scope?: {
57
+ organizationId: string;
58
+ } | undefined): Promise<SupplierQuote | null>;
59
+ };
60
+ };
61
+ }
62
+ interface SupplierLike {
63
+ paymentTerms?: string | undefined;
64
+ creditDays?: number | undefined;
65
+ }
66
+ /** The supplier master — identity check + terms defaults. */
67
+ interface SupplierPort {
68
+ getById(supplierId: string): Promise<SupplierLike | null>;
69
+ }
70
+ /**
71
+ * Branch resolution for RECEIVING.
72
+ *
73
+ * Which branch may receive a purchase is deployment policy (be-prod centralises
74
+ * on head office; another vertical may receive per-site), so the decision is a
75
+ * port rather than a constant here.
76
+ */
77
+ interface BranchPort {
78
+ resolveReceivingBranch(inputBranchId: string | undefined): Promise<string>;
79
+ }
80
+ interface PurchaseLogger {
81
+ warn?(obj: Record<string, unknown>, msg: string): void;
82
+ }
83
+ interface PurchaseIntegrationDeps {
84
+ /** BYO catalog engine — awaited per call so a late boot resolves. */
85
+ catalog: () => Promise<PurchaseCatalogEngineLike> | PurchaseCatalogEngineLike;
86
+ /** Buy-side pricelist, or a getter returning null when none is configured. */
87
+ pricelist?: (() => PurchasePricelistEngineLike | null) | undefined;
88
+ suppliers: SupplierPort;
89
+ branches: BranchPort;
90
+ /** Order currency fallback when an input carries none. */
91
+ baseCurrency: string;
92
+ /** Default when neither the caller nor the supplier states terms. */
93
+ defaultPaymentTerms: string;
94
+ /** `invoiceDate + creditDays` for credit terms; deployment calendar. */
95
+ resolveDueDate(input: {
96
+ paymentTerms: string;
97
+ creditDays: number;
98
+ dueDate: Date | null;
99
+ invoiceDate: Date | undefined;
100
+ }): Date | null;
101
+ /** Raised when a supplier id does not resolve. */
102
+ supplierNotFound(supplierId: string): Error;
103
+ logger?: PurchaseLogger | undefined;
104
+ }
105
+ //#endregion
106
+ //#region src/bridges/catalog.bridge.d.ts
107
+ interface PurchaseItemInputLike {
108
+ product: unknown;
109
+ variantSku?: string | null | undefined;
110
+ quantity?: unknown;
111
+ costPrice?: number | undefined;
112
+ }
113
+ interface ResolveItemsContextLike {
114
+ currency?: string | undefined;
115
+ supplierId?: string | undefined;
116
+ branchId?: string | undefined;
117
+ }
118
+ interface CatalogBridgeDeps extends Pick<PurchaseIntegrationDeps, 'catalog' | 'pricelist' | 'baseCurrency' | 'logger'> {
119
+ /** Thrown for an unresolvable product, variant, or a negative quantity/cost. */
120
+ invalidItem(message: string, status?: number): Error;
121
+ }
122
+ declare function createPurchaseCatalogBridge(deps: CatalogBridgeDeps): {
123
+ resolveItems: (items: ReadonlyArray<PurchaseItemInputLike>, context: ResolveItemsContextLike) => Promise<Array<ResolvedPurchaseLine | null>>;
124
+ };
125
+ //#endregion
126
+ //#region src/bridges/policy.bridge.d.ts
127
+ /**
128
+ * Generic in the terms union, so the KERNEL's union flows through unchanged.
129
+ *
130
+ * `@classytic/purchase` deliberately types `paymentTerms` as its canonical
131
+ * union rather than `string` — its own docblock: "a host adapter returning a
132
+ * typo ('net_30') fails at compile time instead of at persistence". Declaring
133
+ * `string` here would erase exactly that guarantee at the seam meant to
134
+ * preserve it.
135
+ */
136
+ interface ResolvedPaymentTerms<TTerms extends string = string> {
137
+ paymentTerms: TTerms;
138
+ creditDays: number;
139
+ dueDate: Date | null;
140
+ }
141
+ type PolicyBridgeDeps<TTerms extends string = string> = Pick<PurchaseIntegrationDeps, 'suppliers' | 'branches' | 'resolveDueDate' | 'supplierNotFound'> & {
142
+ defaultPaymentTerms: TTerms;
143
+ };
144
+ declare function createPurchasePolicyBridge<TTerms extends string = string>(deps: PolicyBridgeDeps<TTerms>): {
145
+ resolveReceivingBranch(inputBranchId: string | undefined): Promise<string>;
146
+ validateSupplier(supplierId: string): Promise<void>;
147
+ resolvePaymentTerms(input: {
148
+ supplierId?: string | undefined;
149
+ paymentTerms?: TTerms | undefined;
150
+ creditDays?: unknown;
151
+ dueDate?: Date | null | undefined;
152
+ invoiceDate?: Date | undefined;
153
+ }, _ctx?: unknown): Promise<ResolvedPaymentTerms<TTerms>>;
154
+ };
155
+ //#endregion
156
+ //#region src/bridges/integrations.d.ts
157
+ interface PurchaseIntegrations<TTerms extends string = string> {
158
+ catalog: ReturnType<typeof createPurchaseCatalogBridge>;
159
+ policy: ReturnType<typeof createPurchasePolicyBridge<TTerms>>;
160
+ }
161
+ declare function createPurchaseIntegrations<TTerms extends string = string>(deps: Omit<PurchaseIntegrationDeps, 'defaultPaymentTerms'> & {
162
+ defaultPaymentTerms: TTerms;
163
+ /** Thrown for an unresolvable product/variant or a negative quantity/cost. */
164
+ invalidItem(message: string, status?: number): Error;
165
+ }): PurchaseIntegrations<TTerms>;
166
+ //#endregion
167
+ export { type BranchPort, type CatalogBridgeDeps, type PolicyBridgeDeps, type PurchaseCatalogEngineLike, type PurchaseIntegrationDeps, type PurchaseIntegrations, type PurchaseLogger, type PurchasePricelistEngineLike, type ResolvedPaymentTerms, type ResolvedPurchaseLine, type SupplierLike, type SupplierPort, type SupplierQuote, createPurchaseCatalogBridge, createPurchaseIntegrations, createPurchasePolicyBridge };
@@ -0,0 +1,151 @@
1
+ //#region src/bridges/catalog.bridge.ts
2
+ /** Coerce a possibly-string numeric to a finite number, else the fallback. */
3
+ function num(value, fallback) {
4
+ const n = typeof value === "number" ? value : Number(value);
5
+ return Number.isFinite(n) ? n : fallback;
6
+ }
7
+ function createPurchaseCatalogBridge(deps) {
8
+ async function resolveItems(items, context) {
9
+ const catalog = await deps.catalog();
10
+ const currency = context.currency || deps.baseCurrency;
11
+ const catalogCtx = {
12
+ actorId: "purchase-service",
13
+ roles: ["admin"],
14
+ locale: "en",
15
+ currency
16
+ };
17
+ const uniqueIds = [...new Set(items.map((item) => String(item.product)))];
18
+ const products = await catalog.repositories.product.findAll({ _id: { $in: uniqueIds } }, {
19
+ ...catalogCtx,
20
+ lean: true
21
+ });
22
+ const productMap = new Map(products.map((p) => [String(p._id), p]));
23
+ const quotes = /* @__PURE__ */ new Map();
24
+ const pricelist = context.supplierId ? deps.pricelist?.() ?? null : null;
25
+ if (pricelist && context.supplierId) for (let i = 0; i < items.length; i++) {
26
+ const item = items[i];
27
+ if (!item || item.costPrice !== void 0) continue;
28
+ try {
29
+ const quote = await pricelist.repositories.priceList.resolvePartnerPrice({
30
+ partnerRef: String(context.supplierId),
31
+ kind: "purchase",
32
+ productId: String(item.product),
33
+ ...item.variantSku ? { variantSku: item.variantSku } : {},
34
+ quantity: num(item.quantity, 0) || 1,
35
+ basePrice: 0
36
+ }, context.branchId ? { organizationId: context.branchId } : void 0);
37
+ if (!quote?.ruleMatched) continue;
38
+ if (quote.currency && quote.currency !== currency) {
39
+ deps.logger?.warn?.({
40
+ productId: String(item.product),
41
+ quoteCurrency: quote.currency,
42
+ orderCurrency: currency
43
+ }, "purchase pricelist quote dropped: currency mismatch with order");
44
+ continue;
45
+ }
46
+ quotes.set(i, quote);
47
+ } catch {}
48
+ }
49
+ return items.map((item, index) => {
50
+ const quantity = num(item.quantity, 0);
51
+ const costPrice = num(item.costPrice ?? quotes.get(index)?.price, 0);
52
+ if (quantity < 0 || costPrice < 0) throw deps.invalidItem("Quantity and cost price must be non-negative");
53
+ const product = productMap.get(String(item.product));
54
+ if (!product) throw deps.invalidItem(`Product not found: ${String(item.product)}`, 404);
55
+ if (item.variantSku) {
56
+ if (!(product.variants ?? []).find((entry) => entry?.sku === item.variantSku)) throw deps.invalidItem(`Variant not found: ${item.variantSku}`, 404);
57
+ }
58
+ const quote = quotes.get(index);
59
+ return {
60
+ productName: product.name ?? String(item.product),
61
+ variantSku: item.variantSku ?? null,
62
+ costPrice: quote?.price ?? num(item.costPrice, 0),
63
+ currency: quote?.currency ?? context.currency,
64
+ ...quote?.leadTimeDays !== void 0 ? { leadTimeDays: quote.leadTimeDays } : {}
65
+ };
66
+ });
67
+ }
68
+ /** Batch resolution IS the port in purchase 0.5.x. */
69
+ return { resolveItems };
70
+ }
71
+ //#endregion
72
+ //#region src/bridges/policy.bridge.ts
73
+ function createPurchasePolicyBridge(deps) {
74
+ return {
75
+ async resolveReceivingBranch(inputBranchId) {
76
+ return deps.branches.resolveReceivingBranch(inputBranchId);
77
+ },
78
+ async validateSupplier(supplierId) {
79
+ if (!await deps.suppliers.getById(supplierId)) throw deps.supplierNotFound(supplierId);
80
+ },
81
+ async resolvePaymentTerms(input, _ctx) {
82
+ const supplier = input.supplierId ? await deps.suppliers.getById(input.supplierId) : null;
83
+ const paymentTerms = input.paymentTerms || supplier?.paymentTerms || deps.defaultPaymentTerms;
84
+ const creditDays = Number.isFinite(input.creditDays) ? Number(input.creditDays) : Number.isFinite(supplier?.creditDays) ? Number(supplier?.creditDays) : 0;
85
+ return {
86
+ paymentTerms,
87
+ creditDays,
88
+ dueDate: deps.resolveDueDate({
89
+ paymentTerms,
90
+ creditDays,
91
+ dueDate: input.dueDate ?? null,
92
+ invoiceDate: input.invoiceDate
93
+ })
94
+ };
95
+ }
96
+ };
97
+ }
98
+ //#endregion
99
+ //#region src/bridges/integrations.ts
100
+ /**
101
+ * `createPurchaseIntegrations` — ONE call that wires every purchase bridge.
102
+ *
103
+ * This is the shape a host should meet the spine with:
104
+ *
105
+ * ```ts
106
+ * return createPurchaseModule({
107
+ * connection,
108
+ * permissions: purchasePermissions,
109
+ * bridges: createPurchaseIntegrations({
110
+ * catalog: () => ensureCatalogEngine(),
111
+ * pricelist: getPricelistEngineOrNull,
112
+ * suppliers, branches,
113
+ * baseCurrency: BASE_CURRENCY,
114
+ * defaultPaymentTerms: PaymentTerms.CASH,
115
+ * resolveDueDate, supplierNotFound,
116
+ * }),
117
+ * });
118
+ * ```
119
+ *
120
+ * ## Why an aggregator rather than exporting the two factories
121
+ *
122
+ * The bridges share deps — both need the supplier master, both are bounded by
123
+ * the same currency. Wiring them separately means a host restates those, and
124
+ * two hosts restate them differently. It also means the host has to KNOW how
125
+ * many bridges the kernel port set has: adding a third would silently leave
126
+ * every existing host without it, with nothing to say so. One factory returning
127
+ * the whole port set makes that a compile error instead.
128
+ *
129
+ * The individual factories stay exported for tests and for a host that
130
+ * genuinely needs to substitute one.
131
+ */
132
+ function createPurchaseIntegrations(deps) {
133
+ return {
134
+ catalog: createPurchaseCatalogBridge({
135
+ catalog: deps.catalog,
136
+ pricelist: deps.pricelist,
137
+ baseCurrency: deps.baseCurrency,
138
+ logger: deps.logger,
139
+ invalidItem: deps.invalidItem
140
+ }),
141
+ policy: createPurchasePolicyBridge({
142
+ suppliers: deps.suppliers,
143
+ branches: deps.branches,
144
+ defaultPaymentTerms: deps.defaultPaymentTerms,
145
+ resolveDueDate: deps.resolveDueDate,
146
+ supplierNotFound: deps.supplierNotFound
147
+ })
148
+ };
149
+ }
150
+ //#endregion
151
+ export { createPurchaseCatalogBridge, createPurchaseIntegrations, createPurchasePolicyBridge };
@@ -0,0 +1,23 @@
1
+ //#region src/entity-model.ts
2
+ function asEntityModel(model) {
3
+ return model;
4
+ }
5
+ /**
6
+ * Arc's resource layer (custom-controller path) speaks `AnyRecord` — an
7
+ * index-signed record — while the kernel repository is typed on the CLOSED
8
+ * `IPurchaseOrder` interface (no index signature). Same lean entities at
9
+ * runtime; only the static index signature is asserted, and only HERE.
10
+ */
11
+ function asRecordRepository(repo) {
12
+ return repo;
13
+ }
14
+ /**
15
+ * Wire-response widening for the same boundary: a hydrated kernel document
16
+ * going out through arc's `IControllerResponse<AnyRecord>` — the closed
17
+ * document interface has no index signature, the payload is unchanged.
18
+ */
19
+ function asRecord(doc) {
20
+ return doc;
21
+ }
22
+ //#endregion
23
+ export { asRecord as n, asRecordRepository as r, asEntityModel as t };
@@ -0,0 +1,5 @@
1
+ import { n as PurchasePermissions, t as PurchaseModuleDeps } from "./types-T9gsXOf_.mjs";
2
+ //#region src/module.d.ts
3
+ declare function createPurchaseModule(deps: PurchaseModuleDeps): import("@classytic/arc/factory").ArcModule<unknown>;
4
+ //#endregion
5
+ export { type PurchaseModuleDeps, type PurchasePermissions, createPurchaseModule };
package/dist/index.mjs ADDED
@@ -0,0 +1,140 @@
1
+ import { createPurchaseOrderResource } from "./resources/purchase-order/purchase-order.resource.mjs";
2
+ import { defineEngineModule } from "@spinekit/kit/engine-module";
3
+ import { definePurchase } from "@classytic/purchase/engine";
4
+ import { createPurchaseCleanupSteps } from "@classytic/purchase";
5
+ //#region src/module.ts
6
+ /**
7
+ * createPurchaseModule — @spinekit/purchase's single composition surface.
8
+ *
9
+ * createApp({ modules: [createPurchaseModule({
10
+ * connection, permissions,
11
+ * bridges: { catalog, stockReceipt, sequence }, // kernel ports
12
+ * extraActions: { ...withApprovalChain({...}), pay: hostPayAction },
13
+ * })] });
14
+ *
15
+ * Bootstrap RETURNS the engine -> `fastify.arc.modules.purchase` (layer-2
16
+ * export) for container-free cross-module wiring (supplier-performance OTIF
17
+ * scoring, accounting bridges). BYO-engine supported for stranglers.
18
+ * Engine tenancy defaults OFF (kernel `branch` field + arc RequestScope are
19
+ * the boundary — mirrors be-prod's proven wrap).
20
+ *
21
+ * Composed over `defineEngineModule` (`@spinekit/kit/engine-module`) — the ONE
22
+ * primitive for mounting a kernel engine as an arc module. This file used to
23
+ * thread `deferredEngine` + `defineSpineModule` by hand; the primitive now owns
24
+ * the four-piece dance (slot publishing, ownership-conditional teardown,
25
+ * bootstrap-returns-engine, resource threading). Notably, `spine-purchase` is
26
+ * one of the three packages `defineSpineModule`'s `onClose` docblock names as
27
+ * having historically had its host-`onClose` passthrough silently DROPPED
28
+ * whenever `closeOwned` was set — the two arms now COMPOSE (host teardown
29
+ * first, then the owned engine's close), and routing `deps.onClose` through
30
+ * `extend` below keeps riding that composed seam.
31
+ */
32
+ function createPurchaseModule(deps) {
33
+ const { module } = defineEngineModule({
34
+ name: "purchase",
35
+ /**
36
+ * BYO engine (a live engine or a host thunk): the host then owns the
37
+ * lifecycle — `boot` never runs and nothing is closed on teardown. The
38
+ * purchase kernel's `close()` closes its event transport, so closing a
39
+ * host-supplied engine here would silence a bus the host still uses.
40
+ */
41
+ ...deps.engine !== void 0 ? { supplied: deps.engine } : {},
42
+ /**
43
+ * Publish into the HOST's slot when supplied, so a host can READ the engine while this module
44
+ * keeps OWNERSHIP (it binds it, it closes it). Undefined ⇒ private slot, unreadable outside.
45
+ */
46
+ ...deps.slot !== void 0 ? { slot: deps.slot } : {},
47
+ /**
48
+ * Allocated at arc's `bootstrap`, never at composition — a graph that fails
49
+ * validation leaks nothing. (An earlier revision justified construction
50
+ * placement by "eager resolution is what lets `owns` be derived"; that
51
+ * constraint died with arc 2.32's `owns: 'provided'`, and the primitive now
52
+ * owns the timing outright.) A LIVE engine passed by a host is adopted
53
+ * immediately, so this stays backwards compatible.
54
+ *
55
+ * DESCRIBE then BIND: currency policy, tenancy and index policy shape the
56
+ * documents; the connection, bridges and transport are live collaborators.
57
+ */
58
+ boot: () => definePurchase({
59
+ baseCurrency: deps.baseCurrency,
60
+ ...deps.allowForeignCurrency !== void 0 ? { allowForeignCurrency: deps.allowForeignCurrency } : {},
61
+ tenant: deps.tenant ?? false,
62
+ ...deps.autoIndex !== void 0 ? { autoIndex: deps.autoIndex } : {},
63
+ ...deps.forceRecreate !== void 0 ? { forceRecreate: deps.forceRecreate } : {}
64
+ }).bind(deps.connection, {
65
+ ...deps.bridges ? { bridges: deps.bridges } : {},
66
+ ...deps.eventTransport ? { eventTransport: deps.eventTransport } : {},
67
+ ...deps.outbox ? { outbox: deps.outbox } : {},
68
+ /**
69
+ * Transaction waiver — forwarded ONLY when the host stated it.
70
+ *
71
+ * Never `deps.allowNonTransactional ?? true`: that would be a general default overriding
72
+ * a specific decision on a MONEY path, and it fails in the expensive direction (a
73
+ * misconfigured production deployment moving money non-atomically, with nothing thrown).
74
+ * Absent ⇒ the kernel's fail-closed requirement stands.
75
+ */
76
+ ...deps.allowNonTransactional !== void 0 ? { allowNonTransactional: deps.allowNonTransactional } : {}
77
+ }),
78
+ resources: (engine) => {
79
+ const late = deps.wiring?.() ?? {};
80
+ const permissions = deps.permissions ?? late.permissions;
81
+ if (permissions === void 0) throw new Error("[spine-purchase] permissions were not supplied. Pass `permissions` directly, or return them from the `wiring` thunk when they depend on the engine. Refusing to mount purchase resources with no permission gates.");
82
+ const pick = (k) => deps[k] ?? late[k];
83
+ return [createPurchaseOrderResource({
84
+ engine,
85
+ permissions,
86
+ ...deps.prefix ? { prefix: deps.prefix } : {},
87
+ ...pick("extraActions") ? { extraActions: pick("extraActions") } : {},
88
+ ...pick("extraRoutes") ? { extraRoutes: pick("extraRoutes") } : {},
89
+ ...pick("readPopulate") ? { readPopulate: pick("readPopulate") } : {}
90
+ })];
91
+ },
92
+ /**
93
+ * Arms the primitive's config does not enumerate, built against the
94
+ * LATE-BOUND engine accessor: capture `get`, never `get()` — the engine
95
+ * exists only after bootstrap, and each arm below is a thunk for exactly
96
+ * that reason.
97
+ */
98
+ extend: (get) => ({
99
+ /**
100
+ * Host teardown passthrough. Composes with the owned-engine close in
101
+ * `defineSpineModule` (host `onClose` FIRST, then `closeOwned`) — the
102
+ * seam where this exact passthrough was once silently dropped.
103
+ */
104
+ ...deps.onClose !== void 0 ? { onClose: deps.onClose } : {},
105
+ /**
106
+ * Pre-live purge targets — what a reset wipes for this domain, and what must go FIRST.
107
+ *
108
+ * `after` declares the real data dependency, not a preferred position. The host used to
109
+ * encode the whole sequence as numbered comments (`// 0.` … `// 6.`) with nothing
110
+ * verifying them; reordering two blocks silently changed a destructive run.
111
+ */
112
+ preLivePurge: () => ({
113
+ targets: [{
114
+ id: "pre-live.purchase-orders",
115
+ resource: "purchase orders",
116
+ repo: get().repositories.purchaseOrder
117
+ }, {
118
+ id: "pre-live.supplier-bills",
119
+ resource: "supplier bills",
120
+ repo: get().repositories.supplierBill
121
+ }],
122
+ after: ["order"]
123
+ }),
124
+ /**
125
+ * Retention steps this domain contributes.
126
+ *
127
+ * `createPurchaseCleanupSteps` has always existed in the kernel; what was missing was a way
128
+ * to OFFER it. So a host did the offering — re-checking the feature gate that already
129
+ * decided this module composes, re-importing the engine held right here, and calling the
130
+ * kernel itself. The collector needs no list of domains, so nothing has to remember this
131
+ * one exists. Via `extend` (not the dedicated config field) because the thunk closes over
132
+ * the engine accessor.
133
+ */
134
+ cleanupSteps: () => ({ drafts: [createPurchaseCleanupSteps({ repositories: get().repositories }).removeDrafts] })
135
+ })
136
+ });
137
+ return module;
138
+ }
139
+ //#endregion
140
+ export { createPurchaseModule };