@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.
- package/CHANGELOG.md +61 -0
- package/LICENSE +75 -0
- package/README.md +40 -0
- package/dist/bridges.d.mts +167 -0
- package/dist/bridges.mjs +151 -0
- package/dist/entity-model-D-03ovSg.mjs +23 -0
- package/dist/index.d.mts +5 -0
- package/dist/index.mjs +140 -0
- package/dist/lifecycle/purchase-lifecycle.d.mts +17 -0
- package/dist/lifecycle/purchase-lifecycle.mjs +120 -0
- package/dist/lifecycle/purchase-lifecycle.types.d.mts +32 -0
- package/dist/lifecycle/purchase-lifecycle.types.mjs +1 -0
- package/dist/payment/purchase-payment.application.d.mts +8 -0
- package/dist/payment/purchase-payment.application.mjs +154 -0
- package/dist/payment/purchase-payment.tax.d.mts +30 -0
- package/dist/payment/purchase-payment.tax.mjs +37 -0
- package/dist/payment/purchase-payment.types.d.mts +2 -0
- package/dist/payment/purchase-payment.types.mjs +1 -0
- package/dist/purchase-payment.types-hVKAeW3G.d.mts +153 -0
- package/dist/receipt/purchase-stock-receipt.d.mts +12 -0
- package/dist/receipt/purchase-stock-receipt.mjs +225 -0
- package/dist/receipt/purchase-stock-receipt.types.d.mts +167 -0
- package/dist/receipt/purchase-stock-receipt.types.mjs +1 -0
- package/dist/repositories/purchase-order.repository.d.mts +40 -0
- package/dist/repositories/purchase-order.repository.mjs +111 -0
- package/dist/resources/purchase-order/purchase-order.resource.d.mts +56 -0
- package/dist/resources/purchase-order/purchase-order.resource.mjs +193 -0
- package/dist/resources/supplier/supplier.model.d.mts +2 -0
- package/dist/resources/supplier/supplier.model.mjs +178 -0
- package/dist/resources/supplier/supplier.repository.d.mts +20 -0
- package/dist/resources/supplier/supplier.repository.mjs +62 -0
- package/dist/resources/supplier/supplier.resource.d.mts +34 -0
- package/dist/resources/supplier/supplier.resource.mjs +129 -0
- package/dist/resources/supplier/supplier.types.d.mts +2 -0
- package/dist/resources/supplier/supplier.types.mjs +1 -0
- package/dist/supplier.model-BcAfgyHu.d.mts +88 -0
- package/dist/types-T9gsXOf_.d.mts +127 -0
- 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 };
|
package/dist/bridges.mjs
ADDED
|
@@ -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 };
|
package/dist/index.d.mts
ADDED
|
@@ -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 };
|