@endora-commerce/mod-delivery-methods 0.100.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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/admin/api/delivery-methods-client.d.ts +41 -0
- package/dist/admin/api/delivery-methods-client.d.ts.map +1 -0
- package/dist/admin/api/delivery-methods-client.js +20 -0
- package/dist/admin/api/delivery-methods-client.js.map +1 -0
- package/dist/admin/index.d.ts +34 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +44 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/DeliveryMethodsPage.d.ts +4 -0
- package/dist/admin/pages/DeliveryMethodsPage.d.ts.map +1 -0
- package/dist/admin/pages/DeliveryMethodsPage.js +167 -0
- package/dist/admin/pages/DeliveryMethodsPage.js.map +1 -0
- package/dist/admin/renderers/registry.d.ts +15 -0
- package/dist/admin/renderers/registry.d.ts.map +1 -0
- package/dist/admin/renderers/registry.js +18 -0
- package/dist/admin/renderers/registry.js.map +1 -0
- package/dist/backend/adapters/built-in-adapters.d.ts +46 -0
- package/dist/backend/adapters/built-in-adapters.d.ts.map +1 -0
- package/dist/backend/adapters/built-in-adapters.js +62 -0
- package/dist/backend/adapters/built-in-adapters.js.map +1 -0
- package/dist/backend/commands/delivery-method.commands.d.ts +61 -0
- package/dist/backend/commands/delivery-method.commands.d.ts.map +1 -0
- package/dist/backend/commands/delivery-method.commands.js +107 -0
- package/dist/backend/commands/delivery-method.commands.js.map +1 -0
- package/dist/backend/demo/reset.d.ts +12 -0
- package/dist/backend/demo/reset.d.ts.map +1 -0
- package/dist/backend/demo/reset.js +13 -0
- package/dist/backend/demo/reset.js.map +1 -0
- package/dist/backend/demo/rows.d.ts +24 -0
- package/dist/backend/demo/rows.d.ts.map +1 -0
- package/dist/backend/demo/rows.js +22 -0
- package/dist/backend/demo/rows.js.map +1 -0
- package/dist/backend/demo/seed.d.ts +14 -0
- package/dist/backend/demo/seed.d.ts.map +1 -0
- package/dist/backend/demo/seed.js +28 -0
- package/dist/backend/demo/seed.js.map +1 -0
- package/dist/backend/entities/delivery-method.entity.d.ts +30 -0
- package/dist/backend/entities/delivery-method.entity.d.ts.map +1 -0
- package/dist/backend/entities/delivery-method.entity.js +91 -0
- package/dist/backend/entities/delivery-method.entity.js.map +1 -0
- package/dist/backend/index.d.ts +88 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +144 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +51 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +152 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/delivery-method-read-port.d.ts +25 -0
- package/dist/backend/services/delivery-method-read-port.d.ts.map +1 -0
- package/dist/backend/services/delivery-method-read-port.js +56 -0
- package/dist/backend/services/delivery-method-read-port.js.map +1 -0
- package/dist/backend/services/delivery-method-reconciler.d.ts +97 -0
- package/dist/backend/services/delivery-method-reconciler.d.ts.map +1 -0
- package/dist/backend/services/delivery-method-reconciler.js +157 -0
- package/dist/backend/services/delivery-method-reconciler.js.map +1 -0
- package/dist/backend/services/order-status-registry.port.d.ts +50 -0
- package/dist/backend/services/order-status-registry.port.d.ts.map +1 -0
- package/dist/backend/services/order-status-registry.port.js +45 -0
- package/dist/backend/services/order-status-registry.port.js.map +1 -0
- package/dist/backend/services/registry-singleton.d.ts +20 -0
- package/dist/backend/services/registry-singleton.d.ts.map +1 -0
- package/dist/backend/services/registry-singleton.js +21 -0
- package/dist/backend/services/registry-singleton.js.map +1 -0
- package/dist/backend/services/shipment-usage-guard.d.ts +31 -0
- package/dist/backend/services/shipment-usage-guard.d.ts.map +1 -0
- package/dist/backend/services/shipment-usage-guard.js +33 -0
- package/dist/backend/services/shipment-usage-guard.js.map +1 -0
- package/dist/backend/services/shipping-adapter-registry.d.ts +98 -0
- package/dist/backend/services/shipping-adapter-registry.d.ts.map +1 -0
- package/dist/backend/services/shipping-adapter-registry.js +92 -0
- package/dist/backend/services/shipping-adapter-registry.js.map +1 -0
- package/dist/backend/services/shipping-method-eligibility.d.ts +24 -0
- package/dist/backend/services/shipping-method-eligibility.d.ts.map +1 -0
- package/dist/backend/services/shipping-method-eligibility.js +46 -0
- package/dist/backend/services/shipping-method-eligibility.js.map +1 -0
- package/dist/install/index.d.ts +59 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +59 -0
- package/dist/install/index.js.map +1 -0
- package/dist/manifest.d.ts +193 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +185 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.d.ts +30 -0
- package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.d.ts.map +1 -0
- package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.js +72 -0
- package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.js.map +1 -0
- package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.d.ts +24 -0
- package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.d.ts.map +1 -0
- package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.js +28 -0
- package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.js.map +1 -0
- package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.d.ts +27 -0
- package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.d.ts.map +1 -0
- package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.js +44 -0
- package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.js.map +1 -0
- package/dist/migrations/index.d.ts +29 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +33 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/ports/index.d.ts +129 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/index.js.map +1 -0
- package/docs/delivery_methods.md +197 -0
- package/i18n/en.json +5 -0
- package/i18n/pl.json +5 -0
- package/package.json +100 -0
- package/tailwind.css +14 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { SalesChannel } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { DeliveryMethod } from '../entities/delivery-method.entity.js';
|
|
3
|
+
/**
|
|
4
|
+
* DeliveryMethodReconciler (feature 035, FR-002) — the implementation behind
|
|
5
|
+
* `DeliveryMethodSeedApi`, this module's published install surface
|
|
6
|
+
* (`../../ports/index.ts`, `createDeliveryMethodSeeder` below).
|
|
7
|
+
*
|
|
8
|
+
* A module that must create its `delivery_methods` row from code calls
|
|
9
|
+
* `ensureMethodForAdapter` from its **install hook**, so installing a
|
|
10
|
+
* shipping-method module surfaces a configurable entry at `/delivery-methods`
|
|
11
|
+
* with no core change. Idempotent and prune-safe: an existing row (matched by
|
|
12
|
+
* `code`) keeps its admin-edited configuration; the reconciler only fills a
|
|
13
|
+
* missing `adapter` link.
|
|
14
|
+
*
|
|
15
|
+
* The row and the adapter are contributed at **different moments, on purpose**.
|
|
16
|
+
* This row is durable state and is written once, at install; the adapter is an
|
|
17
|
+
* in-memory entry in a per-process table and is pushed from the contributing
|
|
18
|
+
* module's boot hook, on every composition (see `shipping-adapter-registry.ts`).
|
|
19
|
+
* Neither waits for the other: a row whose `adapter` key nothing has contributed
|
|
20
|
+
* is simply not offered, and the registry is not read until a request reads it.
|
|
21
|
+
*
|
|
22
|
+
* **The `EntityManager` is a parameter of every method and is never held**
|
|
23
|
+
* (feature 134, FR-064; D-169). It used to be a constructor `emFactory`, which is
|
|
24
|
+
* the shape of a service resolved from a container — and the caller this class
|
|
25
|
+
* was written for has no container: `ModuleLifecycleContext` is
|
|
26
|
+
* `{ em, redis, log, module }` and `module:install` composes nothing (D-46). The
|
|
27
|
+
* hook's own `ctx.em` is therefore what every statement here runs on, handed in
|
|
28
|
+
* at the call, so the write lands in the transaction the orchestrator will
|
|
29
|
+
* commit or revert rather than on a fork of it.
|
|
30
|
+
*
|
|
31
|
+
* **It no longer rebinds sales-channel membership (issue #96)** — see the
|
|
32
|
+
* payment twin (`payment_methods/services/payment-method-reconciler.ts`).
|
|
33
|
+
* `bindToDefaultChannel` exists, and is the seed's own *once, for the rows it
|
|
34
|
+
* creates* half rather than a reconcile: the caller guards it on
|
|
35
|
+
* `created === true`. What issue #96 removed was the unconditional call, which
|
|
36
|
+
* brought a method an operator had deliberately unbound from every channel back
|
|
37
|
+
* to Default at the next boot, and nothing said so. "Unbound" is a state an
|
|
38
|
+
* operator is entitled to reach and to keep.
|
|
39
|
+
*/
|
|
40
|
+
export class DeliveryMethodReconciler {
|
|
41
|
+
async ensureMethodForAdapter(em, adapterKey, defaults) {
|
|
42
|
+
// command-coverage-ignore: idempotent reconciliation of adapter-backed delivery
|
|
43
|
+
// methods — a system-invariant repair, not an operator-initiated write.
|
|
44
|
+
const existing = await em.findOne(DeliveryMethod, { code: defaults.code });
|
|
45
|
+
if (existing) {
|
|
46
|
+
// Prune-safe: never clobber admin configuration. Only backfill a missing
|
|
47
|
+
// adapter link (e.g. a legacy row predating this feature).
|
|
48
|
+
if (!existing.adapter) {
|
|
49
|
+
existing.adapter = adapterKey;
|
|
50
|
+
await em.persistAndFlush(existing);
|
|
51
|
+
}
|
|
52
|
+
return { row: recordOf(existing), created: false };
|
|
53
|
+
}
|
|
54
|
+
const row = em.create(DeliveryMethod, {
|
|
55
|
+
code: defaults.code,
|
|
56
|
+
name: defaults.name,
|
|
57
|
+
adapter: adapterKey,
|
|
58
|
+
cost: defaults.cost ?? '0',
|
|
59
|
+
currency: defaults.currency ?? 'PLN',
|
|
60
|
+
status: defaults.status ?? 'active',
|
|
61
|
+
statusOnSuccess: defaults.statusOnSuccess ?? 'shipment_sent',
|
|
62
|
+
statusOnFailure: defaults.statusOnFailure ?? 'processing',
|
|
63
|
+
});
|
|
64
|
+
await em.persistAndFlush(row);
|
|
65
|
+
return { row: recordOf(row), created: true };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* The seed's channel binding — one membership row in the system-default
|
|
69
|
+
* channel, written against **this module's own** bridge table.
|
|
70
|
+
*
|
|
71
|
+
* Not through `SalesChannelMembershipService`, and the reason is structural
|
|
72
|
+
* rather than a preference: that service needs the `EventBus`, the audit port
|
|
73
|
+
* and the channel-bridge registry, and the registry is contributed from a boot
|
|
74
|
+
* hook. An install composes nothing, so at this seam it holds no registration
|
|
75
|
+
* for `'delivery-method'` and `bridges.require` would refuse (FR-017) before
|
|
76
|
+
* the database was touched. `sales_channel_delivery_methods` is this module's
|
|
77
|
+
* since `specs/120-migration-closure-bridge-ownership/` Phase 2 (D-226), so the
|
|
78
|
+
* statement is the owner's own and crosses no boundary — which is what FR-064
|
|
79
|
+
* buys by putting the writer here instead of in the seeding module.
|
|
80
|
+
*
|
|
81
|
+
* The channel itself is read through the kernel's own `SalesChannel` entity
|
|
82
|
+
* rather than in SQL — `api_keys` reads it the same way — because a raw
|
|
83
|
+
* `select … from "sales_channels"` from a module is a `check:module-boundary`
|
|
84
|
+
* finding against a kernel-owned table, and correctly so: the table is not
|
|
85
|
+
* this module's and the entity is the platform's published name for it.
|
|
86
|
+
*
|
|
87
|
+
* `em.execute` for the insert rather than `em.getConnection().execute`, so the
|
|
88
|
+
* statement runs inside the caller's transaction (issue #200). The bridge has
|
|
89
|
+
* no entity class — the kernel's membership service writes it in SQL too.
|
|
90
|
+
*
|
|
91
|
+
* **No system-default channel answers `false` rather than raising, and that is
|
|
92
|
+
* measured rather than defensive.** The default channel is created by
|
|
93
|
+
* `DefaultChannelReconciler` at **boot**, from `composeApp` — not by a migration
|
|
94
|
+
* and not by an install — and `module:install` composes nothing (D-46). So a
|
|
95
|
+
* database that has been migrated and never booted has no default channel, and
|
|
96
|
+
* a seeding module installed there is the ordinary case, not a broken instance:
|
|
97
|
+
* `pnpm --filter backend run db:fresh && module:install dhl_parcel` is exactly
|
|
98
|
+
* it. The seed migration this replaced degraded the same way, silently — its
|
|
99
|
+
* `cross join "sales_channels" where "system_default"` produced no rows and
|
|
100
|
+
* inserted no membership — so answering `false` is what keeps a fresh install
|
|
101
|
+
* and an upgraded one at the same row state in that state too. Raising instead
|
|
102
|
+
* **aborts the install**, and the hook is not inside a database transaction:
|
|
103
|
+
* measured on a throwaway database, the first of two rows stayed and the second
|
|
104
|
+
* never arrived.
|
|
105
|
+
*/
|
|
106
|
+
async bindToDefaultChannel(em, deliveryMethodId) {
|
|
107
|
+
// command-coverage-ignore: install-time seed membership for a row this seam
|
|
108
|
+
// just created — a system-invariant write with no request and no actor.
|
|
109
|
+
const defaultChannel = await em.findOne(SalesChannel, { systemDefault: true });
|
|
110
|
+
if (!defaultChannel)
|
|
111
|
+
return false;
|
|
112
|
+
const inserted = await em.execute('insert into "sales_channel_delivery_methods" ("sales_channel_id", "delivery_method_id") ' +
|
|
113
|
+
'values (?, ?) on conflict ("sales_channel_id", "delivery_method_id") do nothing ' +
|
|
114
|
+
'returning "delivery_method_id"', [defaultChannel.id, deliveryMethodId]);
|
|
115
|
+
return inserted.length > 0;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* The hard-uninstall half. Channel memberships go with the row: the bridge's
|
|
119
|
+
* foreign key onto `delivery_methods` is `on delete cascade`.
|
|
120
|
+
*/
|
|
121
|
+
async removeMethodForAdapter(em, code) {
|
|
122
|
+
// command-coverage-ignore: hard-uninstall removal of an adapter-backed
|
|
123
|
+
// delivery method — no request, no actor and nothing to attribute an audit
|
|
124
|
+
// entry to; the operator path is `commands/delivery-method.commands.ts`.
|
|
125
|
+
const removed = await em.nativeDelete(DeliveryMethod, { code });
|
|
126
|
+
return removed > 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The published record, mapped field by field rather than by handing the entity
|
|
131
|
+
* back (D-168/D-77): a caller reads values off it and cannot persist through it.
|
|
132
|
+
*/
|
|
133
|
+
function recordOf(row) {
|
|
134
|
+
return {
|
|
135
|
+
id: row.id,
|
|
136
|
+
code: row.code,
|
|
137
|
+
name: row.name,
|
|
138
|
+
cost: row.cost,
|
|
139
|
+
currency: row.currency,
|
|
140
|
+
status: row.status,
|
|
141
|
+
adapter: row.adapter,
|
|
142
|
+
statusOnSuccess: row.statusOnSuccess,
|
|
143
|
+
statusOnFailure: row.statusOnFailure,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The runtime half of this module's install surface (feature 134, FR-064).
|
|
148
|
+
*
|
|
149
|
+
* A seeding module's `installHook` writes `createDeliveryMethodSeeder()` and
|
|
150
|
+
* hands `ctx.em` to each call. The factory takes no arguments because the `em`
|
|
151
|
+
* belongs to the call and not to the seeder: one `em`, named at every statement,
|
|
152
|
+
* with no second source of truth for which transaction the write lands in.
|
|
153
|
+
*/
|
|
154
|
+
export function createDeliveryMethodSeeder() {
|
|
155
|
+
return new DeliveryMethodReconciler();
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=delivery-method-reconciler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delivery-method-reconciler.js","sourceRoot":"","sources":["../../../src/backend/services/delivery-method-reconciler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type OrderStatusOption, type OrderStatusRegistry } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* OrderStatusRegistry port (feature 035 — research.md R3).
|
|
4
|
+
*
|
|
5
|
+
* `statusOnSuccess` / `statusOnFailure` on a delivery method reference *Order
|
|
6
|
+
* Statuses*. Those will eventually be an admin-configurable registry owned by
|
|
7
|
+
* the Orders module; until then this port is backed by the current hard-coded
|
|
8
|
+
* order `status` enum. Consumers (admin upsert validation, receive-shipment
|
|
9
|
+
* handler) depend only on this interface, so the future configurable registry
|
|
10
|
+
* drops in with no change to `delivery_methods`.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately a small, isolated duplicate of the payment_methods port rather
|
|
13
|
+
* than a deep import across modules (constitution Principle I). The port does
|
|
14
|
+
* NOT touch the Order entity — applying a status is the caller's job.
|
|
15
|
+
*
|
|
16
|
+
* The interface moved to `@endora-commerce/contracts` in feature 075's Phase P. It was
|
|
17
|
+
* declared twice — once here and once in `payment_methods`, in the same words
|
|
18
|
+
* — because both modules map an outcome onto an order status; one declaration
|
|
19
|
+
* is what stops the two drifting. Re-exported here for the length of Phase P,
|
|
20
|
+
* which cuts no consumer.
|
|
21
|
+
*/
|
|
22
|
+
export type { OrderStatusRegistry };
|
|
23
|
+
export declare class OrderStatusRegistryError extends Error {
|
|
24
|
+
readonly code: string;
|
|
25
|
+
constructor(code: string);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Default enum-backed implementation. Seed set = the order `status` enum.
|
|
29
|
+
*
|
|
30
|
+
* **The absent-owner policy this registry states (issue #129): honour — and the
|
|
31
|
+
* reason is that there is nothing to skip.** The delivery twin of
|
|
32
|
+
* `payment_methods/services/order-status-registry.port.ts`, and the argument is
|
|
33
|
+
* the same one: no module contributes to it, the option set is
|
|
34
|
+
* `orderStatusSchema` fixed at compile time, and `shipments` reads `has` as a
|
|
35
|
+
* guard before moving an order into the status a dispatched shipment names. A
|
|
36
|
+
* skip would silently leave a shipped order in its old status; every code here
|
|
37
|
+
* is one live orders are already in, and a status an order is in has to stay
|
|
38
|
+
* nameable while the module holding this table is off.
|
|
39
|
+
*
|
|
40
|
+
* The policy is structural rather than promised: the class takes no presence
|
|
41
|
+
* input, so no read of it can be made to drop a status without changing the
|
|
42
|
+
* policy first.
|
|
43
|
+
*/
|
|
44
|
+
export declare class EnumOrderStatusRegistry implements OrderStatusRegistry {
|
|
45
|
+
private readonly codes;
|
|
46
|
+
list(): OrderStatusOption[];
|
|
47
|
+
has(code: string): boolean;
|
|
48
|
+
assertValid(code: string): void;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=order-status-registry.port.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"order-status-registry.port.d.ts","sourceRoot":"","sources":["../../../src/backend/services/order-status-registry.port.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAC;AAEpC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,YAAY,EAAE,mBAAmB,EAAE,CAAC;AAEpC,qBAAa,wBAAyB,SAAQ,KAAK;aACrB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAIzC;AAQD;;;;;;;;;;;;;;;;GAgBG;AACH,qBAAa,uBAAwB,YAAW,mBAAmB;IACjE,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgD;IAEtE,IAAI,IAAI,iBAAiB,EAAE;IAI3B,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;IAI1B,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;CAKhC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { orderStatusSchema, } from '@endora-commerce/contracts';
|
|
2
|
+
export class OrderStatusRegistryError extends Error {
|
|
3
|
+
code;
|
|
4
|
+
constructor(code) {
|
|
5
|
+
super(`Unknown order status reference: "${code}".`);
|
|
6
|
+
this.code = code;
|
|
7
|
+
this.name = 'OrderStatusRegistryError';
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
const humanize = (code) => code
|
|
11
|
+
.split('_')
|
|
12
|
+
.map((part) => (part.length > 0 ? part[0].toUpperCase() + part.slice(1) : part))
|
|
13
|
+
.join(' ');
|
|
14
|
+
/**
|
|
15
|
+
* Default enum-backed implementation. Seed set = the order `status` enum.
|
|
16
|
+
*
|
|
17
|
+
* **The absent-owner policy this registry states (issue #129): honour — and the
|
|
18
|
+
* reason is that there is nothing to skip.** The delivery twin of
|
|
19
|
+
* `payment_methods/services/order-status-registry.port.ts`, and the argument is
|
|
20
|
+
* the same one: no module contributes to it, the option set is
|
|
21
|
+
* `orderStatusSchema` fixed at compile time, and `shipments` reads `has` as a
|
|
22
|
+
* guard before moving an order into the status a dispatched shipment names. A
|
|
23
|
+
* skip would silently leave a shipped order in its old status; every code here
|
|
24
|
+
* is one live orders are already in, and a status an order is in has to stay
|
|
25
|
+
* nameable while the module holding this table is off.
|
|
26
|
+
*
|
|
27
|
+
* The policy is structural rather than promised: the class takes no presence
|
|
28
|
+
* input, so no read of it can be made to drop a status without changing the
|
|
29
|
+
* policy first.
|
|
30
|
+
*/
|
|
31
|
+
export class EnumOrderStatusRegistry {
|
|
32
|
+
codes = orderStatusSchema.options;
|
|
33
|
+
list() {
|
|
34
|
+
return this.codes.map((code) => ({ code, label: humanize(code) }));
|
|
35
|
+
}
|
|
36
|
+
has(code) {
|
|
37
|
+
return this.codes.includes(code);
|
|
38
|
+
}
|
|
39
|
+
assertValid(code) {
|
|
40
|
+
if (!this.has(code)) {
|
|
41
|
+
throw new OrderStatusRegistryError(code);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=order-status-registry.port.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"order-status-registry.port.js","sourceRoot":"","sources":["../../../src/backend/services/order-status-registry.port.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,GAGlB,MAAM,4BAA4B,CAAC;AAwBpC,MAAM,OAAO,wBAAyB,SAAQ,KAAK;IACrB;IAA5B,YAA4B,IAAY;QACtC,KAAK,CAAC,oCAAoC,IAAI,IAAI,CAAC,CAAC;QAD1B,SAAI,GAAJ,IAAI,CAAQ;QAEtC,IAAI,CAAC,IAAI,GAAG,0BAA0B,CAAC;IACzC,CAAC;CACF;AAED,MAAM,QAAQ,GAAG,CAAC,IAAY,EAAU,EAAE,CACxC,IAAI;KACD,KAAK,CAAC,GAAG,CAAC;KACV,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;KAChF,IAAI,CAAC,GAAG,CAAC,CAAC;AAEf;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,OAAO,uBAAuB;IACjB,KAAK,GAAsB,iBAAiB,CAAC,OAAO,CAAC;IAEtE,IAAI;QACF,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;IACrE,CAAC;IAED,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACnC,CAAC;IAED,WAAW,CAAC,IAAY;QACtB,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;YACpB,MAAM,IAAI,wBAAwB,CAAC,IAAI,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;CACF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ShippingAdapterRegistry } from './shipping-adapter-registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Process-wide ShippingAdapterRegistry (feature 035).
|
|
4
|
+
*
|
|
5
|
+
* `delivery_methods` pushes the two bundled offline adapters into this instance
|
|
6
|
+
* from its boot hook; a carrier module contributes its own the same way, naming
|
|
7
|
+
* itself as it does. One instance per process, so an adapter contributed by any
|
|
8
|
+
* module is immediately recognised by the live eligibility, admin-upsert and
|
|
9
|
+
* order-placement paths.
|
|
10
|
+
*
|
|
11
|
+
* Those paths read this table **per request**, never during composition — so a
|
|
12
|
+
* contributor's boot hook pushes and returns, with nothing to wait for, nothing
|
|
13
|
+
* to verify and no absence to react to. See the class doc block for the full
|
|
14
|
+
* push/pull shape and for why a throw there costs the next start.
|
|
15
|
+
*
|
|
16
|
+
* The presence probe is wired here rather than in the class — see the payment
|
|
17
|
+
* twin (issue #96).
|
|
18
|
+
*/
|
|
19
|
+
export declare const shippingAdapterRegistry: ShippingAdapterRegistry;
|
|
20
|
+
//# sourceMappingURL=registry-singleton.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-singleton.d.ts","sourceRoot":"","sources":["../../../src/backend/services/registry-singleton.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAEzE;;;;;;;;;;;;;;;;GAgBG;AACH,eAAO,MAAM,uBAAuB,yBAEnC,CAAC"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { effectiveState } from '@endora-commerce/platform/kernel';
|
|
2
|
+
import { ShippingAdapterRegistry } from './shipping-adapter-registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* Process-wide ShippingAdapterRegistry (feature 035).
|
|
5
|
+
*
|
|
6
|
+
* `delivery_methods` pushes the two bundled offline adapters into this instance
|
|
7
|
+
* from its boot hook; a carrier module contributes its own the same way, naming
|
|
8
|
+
* itself as it does. One instance per process, so an adapter contributed by any
|
|
9
|
+
* module is immediately recognised by the live eligibility, admin-upsert and
|
|
10
|
+
* order-placement paths.
|
|
11
|
+
*
|
|
12
|
+
* Those paths read this table **per request**, never during composition — so a
|
|
13
|
+
* contributor's boot hook pushes and returns, with nothing to wait for, nothing
|
|
14
|
+
* to verify and no absence to react to. See the class doc block for the full
|
|
15
|
+
* push/pull shape and for why a throw there costs the next start.
|
|
16
|
+
*
|
|
17
|
+
* The presence probe is wired here rather than in the class — see the payment
|
|
18
|
+
* twin (issue #96).
|
|
19
|
+
*/
|
|
20
|
+
export const shippingAdapterRegistry = new ShippingAdapterRegistry(undefined, (moduleId) => effectiveState.isPresent(moduleId));
|
|
21
|
+
//# sourceMappingURL=registry-singleton.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry-singleton.js","sourceRoot":"","sources":["../../../src/backend/services/registry-singleton.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AAClE,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAEzE;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,IAAI,uBAAuB,CAAC,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE,CACzF,cAAc,CAAC,SAAS,CAAC,QAAQ,CAAC,CACnC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type ShipmentUsagePort } from '@endora-commerce/contracts';
|
|
2
|
+
import type { ShipmentUsageCounter } from '../commands/delivery-method.commands.js';
|
|
3
|
+
/** What the counter needs, so a test supplies both halves without a container. */
|
|
4
|
+
export interface ShipmentUsageCounterDeps {
|
|
5
|
+
/** `effectiveState.isPresent('shipments')`, injected so this stays testable. */
|
|
6
|
+
readonly isShipmentsPresent: () => boolean;
|
|
7
|
+
/** `lazyPort<ShipmentUsagePort>(ctx, 'shipmentUsagePort')`, resolved per call. */
|
|
8
|
+
readonly shipmentUsage: () => ShipmentUsagePort;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* "How many shipments were created against this delivery method?" — asked of
|
|
12
|
+
* `shipments`, which owns the rows (feature 075, the `delivery_methods` shard).
|
|
13
|
+
*
|
|
14
|
+
* **Presence is decided before the port is resolved**, which is the
|
|
15
|
+
* `auth`/`api_keys` shape and the reason this edge is declared as
|
|
16
|
+
* `nonBindingDependencies` rather than as an ordinary dependency: `shipments`
|
|
17
|
+
* declares this module, so declaring it back closes a manifest cycle, and
|
|
18
|
+
* acknowledging it would put `delivery_methods` among the dependents that
|
|
19
|
+
* refuse the flip — making the shipment module unswitchable for as long as a
|
|
20
|
+
* shop offers delivery. There is deliberately no `catch` anywhere near the
|
|
21
|
+
* resolution: a caught gate is a fail-open degrade nobody declared.
|
|
22
|
+
*
|
|
23
|
+
* **The degradation is a refusal, not a shortcut.** `shipments.delivery_method_id`
|
|
24
|
+
* carries no foreign key, so this count is the only thing standing between a
|
|
25
|
+
* delete and permanently orphaned shipment history — and switching the module
|
|
26
|
+
* off is exactly when nothing on the operator's screen would show that the
|
|
27
|
+
* history exists. Off is meant to be reversible; a delete taken blind here is
|
|
28
|
+
* not. So the operator is told which switch to flip and nothing is destroyed.
|
|
29
|
+
*/
|
|
30
|
+
export declare function makeShipmentUsageCounter(deps: ShipmentUsageCounterDeps): ShipmentUsageCounter;
|
|
31
|
+
//# sourceMappingURL=shipment-usage-guard.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shipment-usage-guard.d.ts","sourceRoot":"","sources":["../../../src/backend/services/shipment-usage-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,iBAAiB,EAAE,MAAM,4BAA4B,CAAC;AAEjF,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,yCAAyC,CAAC;AAEpF,kFAAkF;AAClF,MAAM,WAAW,wBAAwB;IACvC,gFAAgF;IAChF,QAAQ,CAAC,kBAAkB,EAAE,MAAM,OAAO,CAAC;IAC3C,kFAAkF;IAClF,QAAQ,CAAC,aAAa,EAAE,MAAM,iBAAiB,CAAC;CACjD;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,wBAAwB,GAAG,oBAAoB,CAa7F"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ERROR_CODES } from '@endora-commerce/contracts';
|
|
2
|
+
import { HttpError } from '@endora-commerce/platform/http';
|
|
3
|
+
/**
|
|
4
|
+
* "How many shipments were created against this delivery method?" — asked of
|
|
5
|
+
* `shipments`, which owns the rows (feature 075, the `delivery_methods` shard).
|
|
6
|
+
*
|
|
7
|
+
* **Presence is decided before the port is resolved**, which is the
|
|
8
|
+
* `auth`/`api_keys` shape and the reason this edge is declared as
|
|
9
|
+
* `nonBindingDependencies` rather than as an ordinary dependency: `shipments`
|
|
10
|
+
* declares this module, so declaring it back closes a manifest cycle, and
|
|
11
|
+
* acknowledging it would put `delivery_methods` among the dependents that
|
|
12
|
+
* refuse the flip — making the shipment module unswitchable for as long as a
|
|
13
|
+
* shop offers delivery. There is deliberately no `catch` anywhere near the
|
|
14
|
+
* resolution: a caught gate is a fail-open degrade nobody declared.
|
|
15
|
+
*
|
|
16
|
+
* **The degradation is a refusal, not a shortcut.** `shipments.delivery_method_id`
|
|
17
|
+
* carries no foreign key, so this count is the only thing standing between a
|
|
18
|
+
* delete and permanently orphaned shipment history — and switching the module
|
|
19
|
+
* off is exactly when nothing on the operator's screen would show that the
|
|
20
|
+
* history exists. Off is meant to be reversible; a delete taken blind here is
|
|
21
|
+
* not. So the operator is told which switch to flip and nothing is destroyed.
|
|
22
|
+
*/
|
|
23
|
+
export function makeShipmentUsageCounter(deps) {
|
|
24
|
+
return async (deliveryMethodId) => {
|
|
25
|
+
if (!deps.isShipmentsPresent()) {
|
|
26
|
+
throw new HttpError(409, ERROR_CODES.VALIDATION_FAILED, 'Cannot delete delivery method: the "shipments" module is switched off, so the ' +
|
|
27
|
+
'platform cannot tell whether any shipment was created against this method. ' +
|
|
28
|
+
'Switch shipments on and try again, or set this method\'s status to "inactive".');
|
|
29
|
+
}
|
|
30
|
+
return deps.shipmentUsage().countForDeliveryMethod(deliveryMethodId);
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=shipment-usage-guard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shipment-usage-guard.js","sourceRoot":"","sources":["../../../src/backend/services/shipment-usage-guard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAA0B,MAAM,4BAA4B,CAAC;AACjF,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAW3D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,wBAAwB,CAAC,IAA8B;IACrE,OAAO,KAAK,EAAE,gBAAwB,EAAmB,EAAE;QACzD,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,EAAE,CAAC;YAC/B,MAAM,IAAI,SAAS,CACjB,GAAG,EACH,WAAW,CAAC,iBAAiB,EAC7B,gFAAgF;gBAC9E,6EAA6E;gBAC7E,gFAAgF,CACnF,CAAC;QACJ,CAAC;QACD,OAAO,IAAI,CAAC,aAAa,EAAE,CAAC,sBAAsB,CAAC,gBAAgB,CAAC,CAAC;IACvE,CAAC,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ShippingAdapter, ShippingAdapterRegistryPort } from '@endora-commerce/contracts';
|
|
2
|
+
/**
|
|
3
|
+
* ShippingAdapterRegistry (feature 035) — in-memory map of adapter key →
|
|
4
|
+
* implementation. A module is recognised as a shipping-method adapter iff it
|
|
5
|
+
* registers a `ShippingAdapter` here (FR-001). The bundled offline adapters
|
|
6
|
+
* (manual_courier / personal_pickup) are the first entries; a carrier module
|
|
7
|
+
* contributes its own from its boot hook.
|
|
8
|
+
*
|
|
9
|
+
* Owner-stamped and presence-filtered, exactly like the payment twin — see
|
|
10
|
+
* `payment_methods/services/payment-adapter-registry.ts` for the reasoning
|
|
11
|
+
* (issue #96, D-39). The mechanism is identical; the exposure is not, because
|
|
12
|
+
* today the only contributor is `delivery_methods` itself. Keeping the two
|
|
13
|
+
* registries the same shape is what stops a carrier module from landing on the
|
|
14
|
+
* defect the payment side just paid to remove.
|
|
15
|
+
*
|
|
16
|
+
* Collision policy mirrors the PaymentAdapterRegistry: last-writer-wins with a
|
|
17
|
+
* warning, so a re-registration during a hot reload or re-enable does not throw.
|
|
18
|
+
*
|
|
19
|
+
* **The push happens once, during composition; every read happens later, per
|
|
20
|
+
* operation.** That half was written down only for the payment twin, and the
|
|
21
|
+
* omission is what a contributor infers a rule from: nothing here is read while
|
|
22
|
+
* modules are being composed, so an adapter that is not in the table yet — or
|
|
23
|
+
* whose owner is switched off — is not a fault a contributor can observe, let
|
|
24
|
+
* alone react to. The reads are `ShippingMethodEligibilityService.filter` on
|
|
25
|
+
* `GET /api/v1/delivery-methods`, the `isRegistered` guard on the admin upsert
|
|
26
|
+
* `PUT /api/v1/admin/delivery-methods/:code`, `orders` re-validating the chosen
|
|
27
|
+
* method and firing `onOrderCreated` at placement, `ShipmentService.create`
|
|
28
|
+
* firing `onShipmentCreated`, and the order-confirmation e-mail resolving
|
|
29
|
+
* `renderers.email`. Each of those runs inside a request, after every boot hook
|
|
30
|
+
* has run.
|
|
31
|
+
*
|
|
32
|
+
* So a contributor's boot hook **pushes and returns**: it does not check that
|
|
33
|
+
* the table already holds anything, does not verify that `delivery_methods` is
|
|
34
|
+
* present, and never treats an absent adapter as fatal. A throw there is not a
|
|
35
|
+
* delivery method dropping out — `runBootHooks` attributes the failure to the
|
|
36
|
+
* module and re-throws `ModuleCompositionError`, and `index.ts` turns that into
|
|
37
|
+
* `process.exit(1)`, so an operator's switch takes down the next start instead
|
|
38
|
+
* of one adapter. Nor may the push probe presence (D-67/D-68): the enumeration
|
|
39
|
+
* here already answers that question, per read, and a probe at the push would
|
|
40
|
+
* make switching a carrier back on require a restart. A hook that also *does
|
|
41
|
+
* work* is split in two before either rule applies — the working half probes,
|
|
42
|
+
* the contributing half never does.
|
|
43
|
+
*
|
|
44
|
+
* There is likewise no order to get right between contributors. Every push
|
|
45
|
+
* lands in one process-wide table (`registry-singleton.ts`) during composition,
|
|
46
|
+
* and no read of it happens until a request does; an adapter a colleague
|
|
47
|
+
* contributes is visible to the first read either way.
|
|
48
|
+
*/
|
|
49
|
+
export interface RegistryLogger {
|
|
50
|
+
warn(message: string): void;
|
|
51
|
+
}
|
|
52
|
+
/** One contributed adapter, with the module that contributed it. */
|
|
53
|
+
export interface ShippingAdapterEntry {
|
|
54
|
+
readonly adapter: ShippingAdapter;
|
|
55
|
+
readonly module: string;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* `implements` the shape feature 075's Phase P published, which is what stops
|
|
59
|
+
* the registry and its contract drifting: `orders` and `shipments` read the
|
|
60
|
+
* published type, and `tsc` refuses the day a method here stops matching.
|
|
61
|
+
*/
|
|
62
|
+
export declare class ShippingAdapterRegistry implements ShippingAdapterRegistryPort {
|
|
63
|
+
private readonly log;
|
|
64
|
+
private readonly isModulePresent;
|
|
65
|
+
private readonly entries;
|
|
66
|
+
constructor(log?: RegistryLogger, isModulePresent?: (moduleId: string) => boolean);
|
|
67
|
+
register(adapter: ShippingAdapter, moduleId: string): void;
|
|
68
|
+
unregister(adapterKey: string): void;
|
|
69
|
+
/** Whether any module has contributed this key — presence-blind. */
|
|
70
|
+
isRegistered(adapterKey: string): boolean;
|
|
71
|
+
/** The contributed entry, presence-blind. Admin and diagnostics read this. */
|
|
72
|
+
entry(adapterKey: string): ShippingAdapterEntry | undefined;
|
|
73
|
+
/** The module that contributed `adapterKey`, or `null` when nobody did. */
|
|
74
|
+
ownerOf(adapterKey: string): string | null;
|
|
75
|
+
/**
|
|
76
|
+
* The module that contributed `adapterKey` and is **not** effectively
|
|
77
|
+
* present, or `null` when the adapter is available or was never contributed.
|
|
78
|
+
*
|
|
79
|
+
* The fourth reader, and the only one that answers the question instead of
|
|
80
|
+
* exposing the table. `get()` returns `undefined` for two situations an
|
|
81
|
+
* operator cannot act on identically — a key nobody ever contributed, and a
|
|
82
|
+
* key whose carrier module is switched off — so `shipments` asks this one to
|
|
83
|
+
* tell them apart before it decides what state to open the row in (issue
|
|
84
|
+
* #250). Its payment twin is `GatewayRefundRegistry.absentOwnerFor` (D-71).
|
|
85
|
+
*/
|
|
86
|
+
absentOwnerFor(adapterKey: string): string | null;
|
|
87
|
+
/** Registered AND its owner effectively present. */
|
|
88
|
+
isAvailable(adapterKey: string): boolean;
|
|
89
|
+
/** The adapter, or `undefined` when unregistered or its owner is absent. */
|
|
90
|
+
get(adapterKey: string): ShippingAdapter | undefined;
|
|
91
|
+
/** The adapter, or a throw — see the payment twin for the two cases. */
|
|
92
|
+
resolve(adapterKey: string): ShippingAdapter;
|
|
93
|
+
/** Adapter keys whose owner is present (stable insertion order). */
|
|
94
|
+
list(): string[];
|
|
95
|
+
/** Every registered adapter key, presence-blind. */
|
|
96
|
+
listAll(): string[];
|
|
97
|
+
}
|
|
98
|
+
//# sourceMappingURL=shipping-adapter-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shipping-adapter-registry.d.ts","sourceRoot":"","sources":["../../../src/backend/services/shipping-adapter-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,2BAA2B,EAAE,MAAM,4BAA4B,CAAC;AAG/F;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AACH,MAAM,WAAW,cAAc;IAC7B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAMD,oEAAoE;AACpE,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,OAAO,EAAE,eAAe,CAAC;IAClC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,qBAAa,uBAAwB,YAAW,2BAA2B;IAIvE,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,eAAe;IAJlC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA2C;gBAGhD,GAAG,GAAE,cAA8B,EACnC,eAAe,GAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAoB;IAG9E,QAAQ,CAAC,OAAO,EAAE,eAAe,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAW1D,UAAU,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAIpC,oEAAoE;IACpE,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAIzC,8EAA8E;IAC9E,KAAK,CAAC,UAAU,EAAE,MAAM,GAAG,oBAAoB,GAAG,SAAS;IAI3D,2EAA2E;IAC3E,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAI1C;;;;;;;;;;OAUG;IACH,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAMjD,oDAAoD;IACpD,WAAW,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAKxC,4EAA4E;IAC5E,GAAG,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS;IAMpD,wEAAwE;IACxE,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,eAAe;IAW5C,oEAAoE;IACpE,IAAI,IAAI,MAAM,EAAE;IAMhB,oDAAoD;IACpD,OAAO,IAAI,MAAM,EAAE;CAGpB"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { ModuleDisabledError } from '@endora-commerce/platform/kernel';
|
|
2
|
+
const consoleLogger = {
|
|
3
|
+
warn: (message) => console.warn(message),
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* `implements` the shape feature 075's Phase P published, which is what stops
|
|
7
|
+
* the registry and its contract drifting: `orders` and `shipments` read the
|
|
8
|
+
* published type, and `tsc` refuses the day a method here stops matching.
|
|
9
|
+
*/
|
|
10
|
+
export class ShippingAdapterRegistry {
|
|
11
|
+
log;
|
|
12
|
+
isModulePresent;
|
|
13
|
+
entries = new Map();
|
|
14
|
+
constructor(log = consoleLogger, isModulePresent = () => true) {
|
|
15
|
+
this.log = log;
|
|
16
|
+
this.isModulePresent = isModulePresent;
|
|
17
|
+
}
|
|
18
|
+
register(adapter, moduleId) {
|
|
19
|
+
const existing = this.entries.get(adapter.adapterKey);
|
|
20
|
+
if (existing && existing.module !== moduleId) {
|
|
21
|
+
this.log.warn(`ShippingAdapterRegistry: adapter "${adapter.adapterKey}" re-registered by ` +
|
|
22
|
+
`module "${moduleId}" (was "${existing.module}"); overwriting previous registration.`);
|
|
23
|
+
}
|
|
24
|
+
this.entries.set(adapter.adapterKey, { adapter, module: moduleId });
|
|
25
|
+
}
|
|
26
|
+
unregister(adapterKey) {
|
|
27
|
+
this.entries.delete(adapterKey);
|
|
28
|
+
}
|
|
29
|
+
/** Whether any module has contributed this key — presence-blind. */
|
|
30
|
+
isRegistered(adapterKey) {
|
|
31
|
+
return this.entries.has(adapterKey);
|
|
32
|
+
}
|
|
33
|
+
/** The contributed entry, presence-blind. Admin and diagnostics read this. */
|
|
34
|
+
entry(adapterKey) {
|
|
35
|
+
return this.entries.get(adapterKey);
|
|
36
|
+
}
|
|
37
|
+
/** The module that contributed `adapterKey`, or `null` when nobody did. */
|
|
38
|
+
ownerOf(adapterKey) {
|
|
39
|
+
return this.entries.get(adapterKey)?.module ?? null;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The module that contributed `adapterKey` and is **not** effectively
|
|
43
|
+
* present, or `null` when the adapter is available or was never contributed.
|
|
44
|
+
*
|
|
45
|
+
* The fourth reader, and the only one that answers the question instead of
|
|
46
|
+
* exposing the table. `get()` returns `undefined` for two situations an
|
|
47
|
+
* operator cannot act on identically — a key nobody ever contributed, and a
|
|
48
|
+
* key whose carrier module is switched off — so `shipments` asks this one to
|
|
49
|
+
* tell them apart before it decides what state to open the row in (issue
|
|
50
|
+
* #250). Its payment twin is `GatewayRefundRegistry.absentOwnerFor` (D-71).
|
|
51
|
+
*/
|
|
52
|
+
absentOwnerFor(adapterKey) {
|
|
53
|
+
const entry = this.entries.get(adapterKey);
|
|
54
|
+
if (!entry || this.isModulePresent(entry.module))
|
|
55
|
+
return null;
|
|
56
|
+
return entry.module;
|
|
57
|
+
}
|
|
58
|
+
/** Registered AND its owner effectively present. */
|
|
59
|
+
isAvailable(adapterKey) {
|
|
60
|
+
const entry = this.entries.get(adapterKey);
|
|
61
|
+
return entry !== undefined && this.isModulePresent(entry.module);
|
|
62
|
+
}
|
|
63
|
+
/** The adapter, or `undefined` when unregistered or its owner is absent. */
|
|
64
|
+
get(adapterKey) {
|
|
65
|
+
const entry = this.entries.get(adapterKey);
|
|
66
|
+
if (!entry || !this.isModulePresent(entry.module))
|
|
67
|
+
return undefined;
|
|
68
|
+
return entry.adapter;
|
|
69
|
+
}
|
|
70
|
+
/** The adapter, or a throw — see the payment twin for the two cases. */
|
|
71
|
+
resolve(adapterKey) {
|
|
72
|
+
const entry = this.entries.get(adapterKey);
|
|
73
|
+
if (!entry) {
|
|
74
|
+
throw new Error(`ShippingAdapterRegistry: no adapter registered for key "${adapterKey}".`);
|
|
75
|
+
}
|
|
76
|
+
if (!this.isModulePresent(entry.module)) {
|
|
77
|
+
throw new ModuleDisabledError(entry.module);
|
|
78
|
+
}
|
|
79
|
+
return entry.adapter;
|
|
80
|
+
}
|
|
81
|
+
/** Adapter keys whose owner is present (stable insertion order). */
|
|
82
|
+
list() {
|
|
83
|
+
return [...this.entries.entries()]
|
|
84
|
+
.filter(([, entry]) => this.isModulePresent(entry.module))
|
|
85
|
+
.map(([key]) => key);
|
|
86
|
+
}
|
|
87
|
+
/** Every registered adapter key, presence-blind. */
|
|
88
|
+
listAll() {
|
|
89
|
+
return [...this.entries.keys()];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=shipping-adapter-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shipping-adapter-registry.js","sourceRoot":"","sources":["../../../src/backend/services/shipping-adapter-registry.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { ShippingSurface } from '@endora-commerce/contracts';
|
|
2
|
+
import type { DeliveryMethod } from '../entities/delivery-method.entity.js';
|
|
3
|
+
import type { ShippingAdapterRegistry } from './shipping-adapter-registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* ShippingMethodEligibilityService (feature 035, FR-011/FR-012/FR-013/FR-014).
|
|
6
|
+
*
|
|
7
|
+
* Given the active methods (already filtered by status and the per-Organization
|
|
8
|
+
* allow-list at the route), keeps only those whose adapter is currently
|
|
9
|
+
* registered (FR-003 — a disabled adapter drops out) and whose surface
|
|
10
|
+
* validator returns true. Sales-channel-assignment filtering is applied by the
|
|
11
|
+
* caller via the membership bridge.
|
|
12
|
+
*/
|
|
13
|
+
export interface ShippingEligibilityContext {
|
|
14
|
+
salesChannelId: string | null;
|
|
15
|
+
organizationId: string | null;
|
|
16
|
+
customerAccountId: string | null;
|
|
17
|
+
surface: ShippingSurface;
|
|
18
|
+
}
|
|
19
|
+
export declare class ShippingMethodEligibilityService {
|
|
20
|
+
private readonly registry;
|
|
21
|
+
constructor(registry: ShippingAdapterRegistry);
|
|
22
|
+
filter(methods: DeliveryMethod[], ctx: ShippingEligibilityContext): Promise<DeliveryMethod[]>;
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=shipping-method-eligibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"shipping-method-eligibility.d.ts","sourceRoot":"","sources":["../../../src/backend/services/shipping-method-eligibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAC;AAC5E,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAE9E;;;;;;;;GAQG;AACH,MAAM,WAAW,0BAA0B;IACzC,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,iBAAiB,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,OAAO,EAAE,eAAe,CAAC;CAC1B;AAED,qBAAa,gCAAgC;IAC/B,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,uBAAuB;IAExD,MAAM,CACV,OAAO,EAAE,cAAc,EAAE,EACzB,GAAG,EAAE,0BAA0B,GAC9B,OAAO,CAAC,cAAc,EAAE,CAAC;CAuC7B"}
|