@endora-commerce/mod-shipments 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 +51 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts +6 -0
- package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
- package/dist/backend/email-templates/transactional-defaults.js +27 -0
- package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
- package/dist/backend/entities/shipment.entity.d.ts +40 -0
- package/dist/backend/entities/shipment.entity.d.ts.map +1 -0
- package/dist/backend/entities/shipment.entity.js +98 -0
- package/dist/backend/entities/shipment.entity.js.map +1 -0
- package/dist/backend/index.d.ts +71 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +151 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +29 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +35 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/auto-shipment-on-paid.d.ts +28 -0
- package/dist/backend/services/auto-shipment-on-paid.d.ts.map +1 -0
- package/dist/backend/services/auto-shipment-on-paid.js +58 -0
- package/dist/backend/services/auto-shipment-on-paid.js.map +1 -0
- package/dist/backend/services/events.d.ts +40 -0
- package/dist/backend/services/events.d.ts.map +1 -0
- package/dist/backend/services/events.js +2 -0
- package/dist/backend/services/events.js.map +1 -0
- package/dist/backend/services/receive-shipment-handler.d.ts +93 -0
- package/dist/backend/services/receive-shipment-handler.d.ts.map +1 -0
- package/dist/backend/services/receive-shipment-handler.js +203 -0
- package/dist/backend/services/receive-shipment-handler.js.map +1 -0
- package/dist/backend/services/shipment-email-notifier.d.ts +78 -0
- package/dist/backend/services/shipment-email-notifier.d.ts.map +1 -0
- package/dist/backend/services/shipment-email-notifier.js +76 -0
- package/dist/backend/services/shipment-email-notifier.js.map +1 -0
- package/dist/backend/services/shipment-read.service.d.ts +19 -0
- package/dist/backend/services/shipment-read.service.d.ts.map +1 -0
- package/dist/backend/services/shipment-read.service.js +38 -0
- package/dist/backend/services/shipment-read.service.js.map +1 -0
- package/dist/backend/services/shipment-service.d.ts +79 -0
- package/dist/backend/services/shipment-service.d.ts.map +1 -0
- package/dist/backend/services/shipment-service.js +203 -0
- package/dist/backend/services/shipment-service.js.map +1 -0
- package/dist/backend/services/shipment-usage.service.d.ts +23 -0
- package/dist/backend/services/shipment-usage.service.d.ts.map +1 -0
- package/dist/backend/services/shipment-usage.service.js +26 -0
- package/dist/backend/services/shipment-usage.service.js.map +1 -0
- package/dist/backend/services/shipping-email-renderer.d.ts +22 -0
- package/dist/backend/services/shipping-email-renderer.d.ts.map +1 -0
- package/dist/backend/services/shipping-email-renderer.js +14 -0
- package/dist/backend/services/shipping-email-renderer.js.map +1 -0
- package/dist/manifest.d.ts +200 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +63 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260817T194652_shipments_order_fk.d.ts +32 -0
- package/dist/migrations/20260817T194652_shipments_order_fk.d.ts.map +1 -0
- package/dist/migrations/20260817T194652_shipments_order_fk.js +66 -0
- package/dist/migrations/20260817T194652_shipments_order_fk.js.map +1 -0
- package/dist/migrations/20260819T171006_shipments_status_pending_manual.d.ts +19 -0
- package/dist/migrations/20260819T171006_shipments_status_pending_manual.d.ts.map +1 -0
- package/dist/migrations/20260819T171006_shipments_status_pending_manual.js +32 -0
- package/dist/migrations/20260819T171006_shipments_status_pending_manual.js.map +1 -0
- package/dist/migrations/index.d.ts +35 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +38 -0
- package/dist/migrations/index.js.map +1 -0
- package/docs/shipments.md +96 -0
- package/package.json +69 -0
package/dist/manifest.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { defineModuleManifest, defineModuleSettingsManifest } from '@endora-commerce/contracts';
|
|
2
|
+
export const shipmentsSettingsManifest = defineModuleSettingsManifest({
|
|
3
|
+
moduleCode: 'shipments',
|
|
4
|
+
groups: [{ code: 'shipments', name: 'Shipments' }],
|
|
5
|
+
settings: [
|
|
6
|
+
{
|
|
7
|
+
code: 'shipments.enabled',
|
|
8
|
+
name: 'Shipments enabled',
|
|
9
|
+
description: 'Switches the shipment lifecycle on or off: creating a shipment for an order (which is also how a failed one is retried), the carrier receive_shipment ingress and the per-order shipment history. Nothing is dropped — every shipment, its status transitions and its carrier references stay in the database, and an order mid-fulfilment resumes exactly where it was.',
|
|
10
|
+
groupCode: 'shipments',
|
|
11
|
+
valueType: 'boolean',
|
|
12
|
+
defaultValue: true,
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* Shipments module (feature 035) — the Shipment entity and its
|
|
18
|
+
* generate/receive lifecycle. The delivery-side twin of `payments`.
|
|
19
|
+
*
|
|
20
|
+
* Generating is also retrying (FR-024): a second call after a failure appends
|
|
21
|
+
* attempt n+1 and asks the carrier for it. The separate retry route that used
|
|
22
|
+
* to sit beside it was deleted by issue #257 — it opened the row and asked
|
|
23
|
+
* nobody.
|
|
24
|
+
*
|
|
25
|
+
* The shipping-method *catalog* (adapter registry, reconciler, eligibility)
|
|
26
|
+
* lives in the sibling `delivery_methods` module; this module owns the
|
|
27
|
+
* first-class Shipment record and the `receive_shipment` ingress. Since feature
|
|
28
|
+
* 072 (T124) it registers its own services and routes through `backend.ts`;
|
|
29
|
+
* before that `orders/plugin.ts` constructed and mounted them, which is why
|
|
30
|
+
* switching this module off used to do nothing. No install/uninstall hook —
|
|
31
|
+
* schema is owned by migration 052.
|
|
32
|
+
*/
|
|
33
|
+
export const manifest = defineModuleManifest({
|
|
34
|
+
id: 'shipments',
|
|
35
|
+
docs: { dir: 'docs' },
|
|
36
|
+
name: 'Shipments',
|
|
37
|
+
description: 'Shipment record and the order_created / shipment_created / receive_shipment lifecycle.',
|
|
38
|
+
version: '1.0.0',
|
|
39
|
+
// Feature 075 Phase C — `customer_accounts` joins the three that were already
|
|
40
|
+
// here: the shipment-created e-mail resolves its recipient over
|
|
41
|
+
// `customerAccountReadPort` instead of reading the `CustomerAccount` entity.
|
|
42
|
+
dependencies: [
|
|
43
|
+
'customer_accounts',
|
|
44
|
+
'delivery_methods',
|
|
45
|
+
'orders',
|
|
46
|
+
'transactional_emails',
|
|
47
|
+
],
|
|
48
|
+
// Feature 073 (Constitution XVII) — the operator's activation control. It
|
|
49
|
+
// only became real in T124: until this module registered its own routes there
|
|
50
|
+
// was no seam for a gate to sit on.
|
|
51
|
+
activation: { settingCode: 'shipments.enabled', default: true },
|
|
52
|
+
settings: shipmentsSettingsManifest,
|
|
53
|
+
// Feature 047 — admin-editable transactional email owned by this module.
|
|
54
|
+
transactionalEmails: [
|
|
55
|
+
{
|
|
56
|
+
code: 'shipment_created',
|
|
57
|
+
name: 'Shipment created',
|
|
58
|
+
group: 'shipments',
|
|
59
|
+
variables: [{ key: 'order.businessId', label: 'Order number', sampleValue: 'ORD-1042' }],
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
});
|
|
63
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../src/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,4BAA4B,EAAE,MAAM,4BAA4B,CAAC;AAEhG,MAAM,CAAC,MAAM,yBAAyB,GAAG,4BAA4B,CAAC;IACpE,UAAU,EAAE,WAAW;IACvB,MAAM,EAAE,CAAC,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,WAAW,EAAE,CAAC;IAClD,QAAQ,EAAE;QACR;YACE,IAAI,EAAE,mBAAmB;YACzB,IAAI,EAAE,mBAAmB;YACzB,WAAW,EACT,0WAA0W;YAC5W,SAAS,EAAE,WAAW;YACtB,SAAS,EAAE,SAAS;YACpB,YAAY,EAAE,IAAI;SACnB;KACF;CACF,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,oBAAoB,CAAC;IAC3C,EAAE,EAAE,WAAW;IACf,IAAI,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE;IACrB,IAAI,EAAE,WAAW;IACjB,WAAW,EAAE,wFAAwF;IACrG,OAAO,EAAE,OAAO;IAChB,8EAA8E;IAC9E,gEAAgE;IAChE,6EAA6E;IAC7E,YAAY,EAAE;QACZ,mBAAmB;QACnB,kBAAkB;QAClB,QAAQ;QACR,sBAAsB;KACvB;IACD,0EAA0E;IAC1E,8EAA8E;IAC9E,oCAAoC;IACpC,UAAU,EAAE,EAAE,WAAW,EAAE,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE;IAC/D,QAAQ,EAAE,yBAAyB;IACnC,yEAAyE;IACzE,mBAAmB,EAAE;QACnB;YACE,IAAI,EAAE,kBAAkB;YACxB,IAAI,EAAE,kBAAkB;YACxB,KAAK,EAAE,WAAW;YAClB,SAAS,EAAE,CAAC,EAAE,GAAG,EAAE,kBAAkB,EAAE,KAAK,EAAE,cAAc,EAAE,WAAW,EAAE,UAAU,EAAE,CAAC;SACzF;KACF;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* `shipments.order_id` gains the foreign key it never had (D-90).
|
|
4
|
+
*
|
|
5
|
+
* `receive-shipment-handler.ts` moves the shipment row and the order's status
|
|
6
|
+
* inside one `em.transactional`, so a carrier callback records both or neither.
|
|
7
|
+
* D-78 point 2 rules such a seam permanent — kept on the caller's
|
|
8
|
+
* `EntityManager` and *declared* — and what makes the declaration honest is a
|
|
9
|
+
* constraint. `payments` has carried exactly that since the core commerce init
|
|
10
|
+
* (`payments_order_fk`, `on delete restrict`); this column was created with a
|
|
11
|
+
* primary key, a status check and two indexes and no constraint at all
|
|
12
|
+
* (`delivery_methods/migrations/20260611T140354_…`, which is also why the module
|
|
13
|
+
* that owns the entity has never owned its schema).
|
|
14
|
+
*
|
|
15
|
+
* `on delete restrict` for the same reason `payments` uses it: an order with
|
|
16
|
+
* shipments against it is not a row anyone should be able to delete out from
|
|
17
|
+
* under them.
|
|
18
|
+
*
|
|
19
|
+
* The table accepted unconstrained `order_id`s from 2026-06-11 until this
|
|
20
|
+
* migration, so the constraint is preceded by an explicit orphan report. An
|
|
21
|
+
* operator whose data has orphans gets a sentence naming the count and the
|
|
22
|
+
* first ten values, not a bare `23503` from Postgres — the orphans are a data
|
|
23
|
+
* question with a name, not a migration that will not apply.
|
|
24
|
+
*
|
|
25
|
+
* `shipments` already declares `orders` in its manifest `dependencies`, so this
|
|
26
|
+
* adds no edge to the module graph and nothing to the migration order.
|
|
27
|
+
*/
|
|
28
|
+
export declare class Migration20260817T194652ShipmentsOrderFk extends Migration {
|
|
29
|
+
up(): Promise<void>;
|
|
30
|
+
down(): Promise<void>;
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=20260817T194652_shipments_order_fk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260817T194652_shipments_order_fk.d.ts","sourceRoot":"","sources":["../../src/migrations/20260817T194652_shipments_order_fk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,qBAAa,wCAAyC,SAAQ,SAAS;IACtD,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAmCnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* `shipments.order_id` gains the foreign key it never had (D-90).
|
|
4
|
+
*
|
|
5
|
+
* `receive-shipment-handler.ts` moves the shipment row and the order's status
|
|
6
|
+
* inside one `em.transactional`, so a carrier callback records both or neither.
|
|
7
|
+
* D-78 point 2 rules such a seam permanent — kept on the caller's
|
|
8
|
+
* `EntityManager` and *declared* — and what makes the declaration honest is a
|
|
9
|
+
* constraint. `payments` has carried exactly that since the core commerce init
|
|
10
|
+
* (`payments_order_fk`, `on delete restrict`); this column was created with a
|
|
11
|
+
* primary key, a status check and two indexes and no constraint at all
|
|
12
|
+
* (`delivery_methods/migrations/20260611T140354_…`, which is also why the module
|
|
13
|
+
* that owns the entity has never owned its schema).
|
|
14
|
+
*
|
|
15
|
+
* `on delete restrict` for the same reason `payments` uses it: an order with
|
|
16
|
+
* shipments against it is not a row anyone should be able to delete out from
|
|
17
|
+
* under them.
|
|
18
|
+
*
|
|
19
|
+
* The table accepted unconstrained `order_id`s from 2026-06-11 until this
|
|
20
|
+
* migration, so the constraint is preceded by an explicit orphan report. An
|
|
21
|
+
* operator whose data has orphans gets a sentence naming the count and the
|
|
22
|
+
* first ten values, not a bare `23503` from Postgres — the orphans are a data
|
|
23
|
+
* question with a name, not a migration that will not apply.
|
|
24
|
+
*
|
|
25
|
+
* `shipments` already declares `orders` in its manifest `dependencies`, so this
|
|
26
|
+
* adds no edge to the module graph and nothing to the migration order.
|
|
27
|
+
*/
|
|
28
|
+
export class Migration20260817T194652ShipmentsOrderFk extends Migration {
|
|
29
|
+
async up() {
|
|
30
|
+
this.addSql(`
|
|
31
|
+
do $$
|
|
32
|
+
declare
|
|
33
|
+
orphan_count bigint;
|
|
34
|
+
sample text;
|
|
35
|
+
begin
|
|
36
|
+
select count(*) into orphan_count
|
|
37
|
+
from "shipments" s
|
|
38
|
+
left join "orders" o on o."id" = s."order_id"
|
|
39
|
+
where o."id" is null;
|
|
40
|
+
|
|
41
|
+
if orphan_count > 0 then
|
|
42
|
+
select string_agg(x."order_id"::text, ', ') into sample
|
|
43
|
+
from (
|
|
44
|
+
select distinct s."order_id"
|
|
45
|
+
from "shipments" s
|
|
46
|
+
left join "orders" o on o."id" = s."order_id"
|
|
47
|
+
where o."id" is null
|
|
48
|
+
limit 10
|
|
49
|
+
) x;
|
|
50
|
+
raise exception
|
|
51
|
+
'shipments_order_fk cannot be added: % orphaned shipments.order_id value(s) reference no orders row (first ten: %). Delete those shipments or restore the missing orders, then re-run the migration.',
|
|
52
|
+
orphan_count, sample;
|
|
53
|
+
end if;
|
|
54
|
+
end $$;
|
|
55
|
+
`);
|
|
56
|
+
this.addSql(`
|
|
57
|
+
alter table "shipments"
|
|
58
|
+
add constraint "shipments_order_fk" foreign key ("order_id")
|
|
59
|
+
references "orders" ("id") on delete restrict;
|
|
60
|
+
`);
|
|
61
|
+
}
|
|
62
|
+
async down() {
|
|
63
|
+
this.addSql(`alter table "shipments" drop constraint if exists "shipments_order_fk";`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=20260817T194652_shipments_order_fk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260817T194652_shipments_order_fk.js","sourceRoot":"","sources":["../../src/migrations/20260817T194652_shipments_order_fk.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,OAAO,wCAAyC,SAAQ,SAAS;IAC5D,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;KAyBX,CAAC,CAAC;QAEH,IAAI,CAAC,MAAM,CAAC;;;;KAIX,CAAC,CAAC;IACL,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,yEAAyE,CAAC,CAAC;IACzF,CAAC;CACF"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* `shipments.status` gains `pending_manual` (issue #250).
|
|
4
|
+
*
|
|
5
|
+
* A shipment opened while the module contributing its carrier adapter is not
|
|
6
|
+
* present was written as `pending` and was then indistinguishable from a
|
|
7
|
+
* shipment the carrier had actually been asked for. The fourth value is what
|
|
8
|
+
* makes that row say so; the check constraint is what keeps the set of values
|
|
9
|
+
* a single decision instead of a convention.
|
|
10
|
+
*
|
|
11
|
+
* The constraint was created by `delivery_methods` when this table was still
|
|
12
|
+
* part of that module's schema; the table has been `shipments`' own since the
|
|
13
|
+
* `shipments_order_fk` migration, so the widening belongs here.
|
|
14
|
+
*/
|
|
15
|
+
export declare class Migration20260819T171006ShipmentsStatusPendingManual extends Migration {
|
|
16
|
+
up(): Promise<void>;
|
|
17
|
+
down(): Promise<void>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=20260819T171006_shipments_status_pending_manual.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260819T171006_shipments_status_pending_manual.d.ts","sourceRoot":"","sources":["../../src/migrations/20260819T171006_shipments_status_pending_manual.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,qBAAa,oDAAqD,SAAQ,SAAS;IAClE,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAQnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAYrC"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Migration } from '@mikro-orm/migrations';
|
|
2
|
+
/**
|
|
3
|
+
* `shipments.status` gains `pending_manual` (issue #250).
|
|
4
|
+
*
|
|
5
|
+
* A shipment opened while the module contributing its carrier adapter is not
|
|
6
|
+
* present was written as `pending` and was then indistinguishable from a
|
|
7
|
+
* shipment the carrier had actually been asked for. The fourth value is what
|
|
8
|
+
* makes that row say so; the check constraint is what keeps the set of values
|
|
9
|
+
* a single decision instead of a convention.
|
|
10
|
+
*
|
|
11
|
+
* The constraint was created by `delivery_methods` when this table was still
|
|
12
|
+
* part of that module's schema; the table has been `shipments`' own since the
|
|
13
|
+
* `shipments_order_fk` migration, so the widening belongs here.
|
|
14
|
+
*/
|
|
15
|
+
export class Migration20260819T171006ShipmentsStatusPendingManual extends Migration {
|
|
16
|
+
async up() {
|
|
17
|
+
this.addSql(`alter table "shipments" drop constraint if exists "shipments_status_check";`);
|
|
18
|
+
this.addSql(`alter table "shipments" add constraint "shipments_status_check" ` +
|
|
19
|
+
`check ("status" in ('pending', 'pending_manual', 'success', 'failure'));`);
|
|
20
|
+
}
|
|
21
|
+
async down() {
|
|
22
|
+
// A rollback removes the vocabulary, not the rows. `pending_manual` folds
|
|
23
|
+
// back to `pending`, which is where those shipments sat before this
|
|
24
|
+
// migration and is the only pre-existing value that still describes them:
|
|
25
|
+
// they are open, and nothing was rejected.
|
|
26
|
+
this.addSql(`update "shipments" set "status" = 'pending' where "status" = 'pending_manual';`);
|
|
27
|
+
this.addSql(`alter table "shipments" drop constraint if exists "shipments_status_check";`);
|
|
28
|
+
this.addSql(`alter table "shipments" add constraint "shipments_status_check" ` +
|
|
29
|
+
`check ("status" in ('pending', 'success', 'failure'));`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=20260819T171006_shipments_status_pending_manual.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"20260819T171006_shipments_status_pending_manual.js","sourceRoot":"","sources":["../../src/migrations/20260819T171006_shipments_status_pending_manual.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,MAAM,OAAO,oDAAqD,SAAQ,SAAS;IACxE,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC,6EAA6E,CAAC,CAAC;QAC3F,IAAI,CAAC,MAAM,CACT,kEAAkE;YAChE,0EAA0E,CAC7E,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,0EAA0E;QAC1E,oEAAoE;QACpE,0EAA0E;QAC1E,2CAA2C;QAC3C,IAAI,CAAC,MAAM,CAAC,gFAAgF,CAAC,CAAC;QAC9F,IAAI,CAAC,MAAM,CAAC,6EAA6E,CAAC,CAAC;QAC3F,IAAI,CAAC,MAAM,CACT,kEAAkE;YAChE,wDAAwD,CAC3D,CAAC;IACJ,CAAC;CACF"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `./migrations` subpath — every migration class this module owns, as one
|
|
3
|
+
* ordered `migrations` array.
|
|
4
|
+
*
|
|
5
|
+
* The array is what the platform reads when this module is **installed**:
|
|
6
|
+
* `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
|
|
7
|
+
* the package outright when it is absent (D-168).
|
|
8
|
+
*
|
|
9
|
+
* **Two classes, listed in ascending timestamp, and both stamped after
|
|
10
|
+
* `BASELINE_THROUGH`** — so they are ordered by the manifest `dependencies`
|
|
11
|
+
* graph rather than by the frozen historical prefix, and this is one of the two
|
|
12
|
+
* modules in the first batch where that path actually runs. A timestamp orders
|
|
13
|
+
* this module's own migrations and nothing else (feature 081):
|
|
14
|
+
* `…T194652_shipments_order_fk` adds `shipments.order_id -> orders.id`, and what
|
|
15
|
+
* puts this block after `orders`' is `shipments`' manifest `dependencies`, never
|
|
16
|
+
* the stamp. A foreign key needs the **table**, never the owner's entity class
|
|
17
|
+
* (D-169), which is what lets that constraint stand while `./backend` publishes
|
|
18
|
+
* no entity class by name.
|
|
19
|
+
*
|
|
20
|
+
* The **named** exports stay beside the array, and the asymmetry with
|
|
21
|
+
* `./backend` — which publishes an array and no named class (D-168) — is
|
|
22
|
+
* deliberate. `db/migrations-registry.generated.ts` imports each class by name
|
|
23
|
+
* from this specifier, and a migration class name is contract in a way an entity
|
|
24
|
+
* class name is not: `mikro_orm_migrations` persists it, so it is a string every
|
|
25
|
+
* already-migrated database holds.
|
|
26
|
+
*
|
|
27
|
+
* A class that is in neither the array nor the barrel is a migration that does
|
|
28
|
+
* not run: `migration:pending` reports nothing pending and the first symptom is
|
|
29
|
+
* a query against a table nobody created.
|
|
30
|
+
*/
|
|
31
|
+
import { Migration20260817T194652ShipmentsOrderFk } from './20260817T194652_shipments_order_fk.js';
|
|
32
|
+
import { Migration20260819T171006ShipmentsStatusPendingManual } from './20260819T171006_shipments_status_pending_manual.js';
|
|
33
|
+
export declare const migrations: (typeof Migration20260817T194652ShipmentsOrderFk)[];
|
|
34
|
+
export { Migration20260817T194652ShipmentsOrderFk, Migration20260819T171006ShipmentsStatusPendingManual, };
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,wCAAwC,EAAE,MAAM,yCAAyC,CAAC;AACnG,OAAO,EAAE,oDAAoD,EAAE,MAAM,sDAAsD,CAAC;AAE5H,eAAO,MAAM,UAAU,qDAGtB,CAAC;AAEF,OAAO,EACL,wCAAwC,EACxC,oDAAoD,GACrD,CAAC"}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `./migrations` subpath — every migration class this module owns, as one
|
|
3
|
+
* ordered `migrations` array.
|
|
4
|
+
*
|
|
5
|
+
* The array is what the platform reads when this module is **installed**:
|
|
6
|
+
* `src/packages/package-runtime.ts` takes `exported['migrations']` and refuses
|
|
7
|
+
* the package outright when it is absent (D-168).
|
|
8
|
+
*
|
|
9
|
+
* **Two classes, listed in ascending timestamp, and both stamped after
|
|
10
|
+
* `BASELINE_THROUGH`** — so they are ordered by the manifest `dependencies`
|
|
11
|
+
* graph rather than by the frozen historical prefix, and this is one of the two
|
|
12
|
+
* modules in the first batch where that path actually runs. A timestamp orders
|
|
13
|
+
* this module's own migrations and nothing else (feature 081):
|
|
14
|
+
* `…T194652_shipments_order_fk` adds `shipments.order_id -> orders.id`, and what
|
|
15
|
+
* puts this block after `orders`' is `shipments`' manifest `dependencies`, never
|
|
16
|
+
* the stamp. A foreign key needs the **table**, never the owner's entity class
|
|
17
|
+
* (D-169), which is what lets that constraint stand while `./backend` publishes
|
|
18
|
+
* no entity class by name.
|
|
19
|
+
*
|
|
20
|
+
* The **named** exports stay beside the array, and the asymmetry with
|
|
21
|
+
* `./backend` — which publishes an array and no named class (D-168) — is
|
|
22
|
+
* deliberate. `db/migrations-registry.generated.ts` imports each class by name
|
|
23
|
+
* from this specifier, and a migration class name is contract in a way an entity
|
|
24
|
+
* class name is not: `mikro_orm_migrations` persists it, so it is a string every
|
|
25
|
+
* already-migrated database holds.
|
|
26
|
+
*
|
|
27
|
+
* A class that is in neither the array nor the barrel is a migration that does
|
|
28
|
+
* not run: `migration:pending` reports nothing pending and the first symptom is
|
|
29
|
+
* a query against a table nobody created.
|
|
30
|
+
*/
|
|
31
|
+
import { Migration20260817T194652ShipmentsOrderFk } from './20260817T194652_shipments_order_fk.js';
|
|
32
|
+
import { Migration20260819T171006ShipmentsStatusPendingManual } from './20260819T171006_shipments_status_pending_manual.js';
|
|
33
|
+
export const migrations = [
|
|
34
|
+
Migration20260817T194652ShipmentsOrderFk,
|
|
35
|
+
Migration20260819T171006ShipmentsStatusPendingManual,
|
|
36
|
+
];
|
|
37
|
+
export { Migration20260817T194652ShipmentsOrderFk, Migration20260819T171006ShipmentsStatusPendingManual, };
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAEH,OAAO,EAAE,wCAAwC,EAAE,MAAM,yCAAyC,CAAC;AACnG,OAAO,EAAE,oDAAoD,EAAE,MAAM,sDAAsD,CAAC;AAE5H,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,wCAAwC;IACxC,oDAAoD;CACrD,CAAC;AAEF,OAAO,EACL,wCAAwC,EACxC,oDAAoD,GACrD,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: shipments
|
|
3
|
+
description: The retryable Shipment record and its lifecycle — the delivery-side twin of payments
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# `shipments`
|
|
7
|
+
|
|
8
|
+
The `Shipment` record and its lifecycle. The
|
|
9
|
+
delivery-side twin of `payments`: the shipping-method *catalog* and adapter
|
|
10
|
+
registry live in [`delivery_methods`](./delivery_methods.md); this module owns
|
|
11
|
+
the first-class, retryable `Shipment` and the `receive_shipment` ingress.
|
|
12
|
+
|
|
13
|
+
## Entity
|
|
14
|
+
|
|
15
|
+
`Shipment` — one shipment-generation attempt against an Order:
|
|
16
|
+
`orderId`, `deliveryMethodId`, `status` (`pending` → `success` | `failure`, plus
|
|
17
|
+
`pending_manual`), `externalReference`, `providerDetails` (JSONB),
|
|
18
|
+
`failureReason`, `attemptNo`, timestamps. An Order may have many Shipments
|
|
19
|
+
(failed-generation retries; a future split into multiple parcels). The `status`
|
|
20
|
+
here is the shipment-process status, distinct from the Order status the method
|
|
21
|
+
maps to.
|
|
22
|
+
|
|
23
|
+
### `pending_manual` — the carrier was never asked
|
|
24
|
+
|
|
25
|
+
A shipment opens `pending_manual` when the adapter its delivery method names is
|
|
26
|
+
contributed by a module that is **not present** — switched off by the operator,
|
|
27
|
+
or not available in this deployment. The registry filters that adapter out at
|
|
28
|
+
enumeration (the contribution-point policy), so nothing is sent: no label,
|
|
29
|
+
no tracking number, no pickup. The row records what happened rather than looking
|
|
30
|
+
like every other shipment:
|
|
31
|
+
|
|
32
|
+
- `status = 'pending_manual'`, which is the same word — and the same instruction
|
|
33
|
+
to the same operator — as a refund the platform could not settle automatically: *a human has to finish this*;
|
|
34
|
+
- `failureReason` names the module, e.g. `The "my_carrier" module is not
|
|
35
|
+
switched on here, so the carrier was never asked to create this shipment.
|
|
36
|
+
Switch the module back on and generate the shipment again.`;
|
|
37
|
+
- an audit entry `shipment.carrier_not_contacted` on the shipment, written
|
|
38
|
+
co-transactionally with the row;
|
|
39
|
+
- **no** `shipment_created` e-mail. The notifier answers
|
|
40
|
+
`{ sent: false, reason: 'carrier_not_contacted' }` and logs it — telling a
|
|
41
|
+
buyer their order has shipped when nothing was handed to anyone is worse than
|
|
42
|
+
telling them nothing, and it is not a message anyone can take back.
|
|
43
|
+
|
|
44
|
+
It is deliberately **not** `failure` — nothing was rejected, because nothing was
|
|
45
|
+
sent — and deliberately not plain `pending`, which means a carrier that knows
|
|
46
|
+
about the shipment has yet to report back.
|
|
47
|
+
|
|
48
|
+
A delivery method whose adapter key **nobody ever contributed** is untouched: it
|
|
49
|
+
keeps opening `pending`, because there is no module to switch on and an offline
|
|
50
|
+
method has always been finished by hand. The two are told apart by
|
|
51
|
+
`ShippingAdapterRegistry.absentOwnerFor`, not by `get()` — which answers
|
|
52
|
+
`undefined` for both.
|
|
53
|
+
|
|
54
|
+
**Recovery is an operator action, not an automatic sweep.** Switching the module
|
|
55
|
+
back on changes nothing about the shipments already opened; the operator
|
|
56
|
+
generates the shipment again (`POST /api/v1/admin/orders/:id/shipments`), which
|
|
57
|
+
appends a new attempt and asks the carrier. Reacting to the activation setting
|
|
58
|
+
would mean the platform calling a carrier for parcels an operator may already
|
|
59
|
+
have handled by hand, without anyone asking it to. The Delivery tab of the order
|
|
60
|
+
surfaces the state, the reason and the button.
|
|
61
|
+
|
|
62
|
+
There used to be a second endpoint here, `POST .../shipments/retry`, and it
|
|
63
|
+
has been deleted: it appended attempt n+1 and contacted no adapter in any state,
|
|
64
|
+
so an operator who used it got a fresh `pending` row that nothing had been asked
|
|
65
|
+
about. Retrying **is** generating again — the generate endpoint appends the next
|
|
66
|
+
attempt, refuses only once one has succeeded, and asks the carrier for it.
|
|
67
|
+
|
|
68
|
+
## Lifecycle
|
|
69
|
+
|
|
70
|
+
| Event | Trigger | Effect |
|
|
71
|
+
| --- | --- | --- |
|
|
72
|
+
| `order_created` | Order created (storefront / admin / API) | The shipping adapter's `onOrderCreated` fires. Offline adapters are no-ops; **no** Shipment is opened here. |
|
|
73
|
+
| `shipment_created` | Admin "Generate shipment" / API | A `pending` Shipment is opened (`attemptNo = max+1`); the adapter's `onShipmentCreated` runs; `shipment.created.v1` is emitted, carrying the state the row opened in. With the adapter's module absent the row opens `pending_manual` and no adapter is called — see above. |
|
|
74
|
+
| `receive_shipment` | Carrier/adapter ingress | The Shipment is resolved; the Order moves to the method's `statusOnSuccess` / `statusOnFailure`; `shipment.received.v1` / `shipment.failed.v1` is emitted. |
|
|
75
|
+
|
|
76
|
+
`receive_shipment` is **idempotent**: a success after a terminal `success` is a
|
|
77
|
+
no-op; a failure after success is rejected (409, no downgrade); a missing /
|
|
78
|
+
already-resolved reference is rejected without corrupting records. A failed
|
|
79
|
+
generation is retried by generating again, which opens the next Shipment attempt
|
|
80
|
+
and asks the carrier for it, leaving prior attempts intact.
|
|
81
|
+
|
|
82
|
+
## Public surface
|
|
83
|
+
|
|
84
|
+
| Verb + Path | Audience | Purpose |
|
|
85
|
+
| --- | --- | --- |
|
|
86
|
+
| `POST /api/v1/admin/orders/:id/shipments` | admin (`orders:write`) | Generate a shipment (`shipment_created`) — and retry a failed one, by generating the next attempt |
|
|
87
|
+
| `GET /api/v1/admin/orders/:id/shipments` | admin (`orders:read`) | Full shipment history for the order |
|
|
88
|
+
| `POST /api/v1/shipments/receive` | adapter/carrier ingress (admin-guarded for MVP) | `receive_shipment` outcome ingress |
|
|
89
|
+
|
|
90
|
+
## Order-status mapping
|
|
91
|
+
|
|
92
|
+
On a `receive_shipment` outcome the handler writes `orders.status` directly
|
|
93
|
+
(bypassing the `transitionStatus` state graph) to the method's `statusOnSuccess`
|
|
94
|
+
/ `statusOnFailure`, validated through the `OrderStatusRegistry` port owned by
|
|
95
|
+
`delivery_methods`. Emitted events flow on the in-process `EventBus` inside the
|
|
96
|
+
handler's transactional scope, so a rolled-back transaction never dispatches.
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@endora-commerce/mod-shipments",
|
|
3
|
+
"version": "0.100.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"description": "Shipment record and the order_created / shipment_created / receive_shipment lifecycle.",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"endora": {
|
|
9
|
+
"type": "module",
|
|
10
|
+
"id": "shipments"
|
|
11
|
+
},
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "git+https://github.com/endora-commerce/endora-commerce.git",
|
|
15
|
+
"directory": "packages/modules/shipments"
|
|
16
|
+
},
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./dist/manifest.d.ts",
|
|
23
|
+
"default": "./dist/manifest.js"
|
|
24
|
+
},
|
|
25
|
+
"./backend": {
|
|
26
|
+
"types": "./dist/backend/index.d.ts",
|
|
27
|
+
"default": "./dist/backend/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./migrations": {
|
|
30
|
+
"types": "./dist/migrations/index.d.ts",
|
|
31
|
+
"default": "./dist/migrations/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./package.json": "./package.json"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"docs"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.18.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependencies": {
|
|
43
|
+
"@mikro-orm/core": "^6",
|
|
44
|
+
"@mikro-orm/migrations": "^6",
|
|
45
|
+
"@mikro-orm/postgresql": "^6",
|
|
46
|
+
"fastify": "^5",
|
|
47
|
+
"@endora-commerce/contracts": "0.100.0",
|
|
48
|
+
"@endora-commerce/email-components": "0.100.0",
|
|
49
|
+
"@endora-commerce/platform": "0.100.0"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"@mikro-orm/core": "^6.6.13",
|
|
53
|
+
"@mikro-orm/migrations": "^6.6.13",
|
|
54
|
+
"@mikro-orm/postgresql": "^6.6.13",
|
|
55
|
+
"@types/node": "^22.9.0",
|
|
56
|
+
"fastify": "^5.12.5",
|
|
57
|
+
"typescript": "^5.9.3",
|
|
58
|
+
"vitest": "^4.1.11",
|
|
59
|
+
"@endora-commerce/contracts": "0.100.0",
|
|
60
|
+
"@endora-commerce/email-components": "0.100.0",
|
|
61
|
+
"@endora-commerce/platform": "0.100.0"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"build": "tsc -p tsconfig.build.json",
|
|
65
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
66
|
+
"lint": "eslint src",
|
|
67
|
+
"test": "vitest run"
|
|
68
|
+
}
|
|
69
|
+
}
|