@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.
Files changed (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/backend/email-templates/transactional-defaults.d.ts +6 -0
  4. package/dist/backend/email-templates/transactional-defaults.d.ts.map +1 -0
  5. package/dist/backend/email-templates/transactional-defaults.js +27 -0
  6. package/dist/backend/email-templates/transactional-defaults.js.map +1 -0
  7. package/dist/backend/entities/shipment.entity.d.ts +40 -0
  8. package/dist/backend/entities/shipment.entity.d.ts.map +1 -0
  9. package/dist/backend/entities/shipment.entity.js +98 -0
  10. package/dist/backend/entities/shipment.entity.js.map +1 -0
  11. package/dist/backend/index.d.ts +71 -0
  12. package/dist/backend/index.d.ts.map +1 -0
  13. package/dist/backend/index.js +151 -0
  14. package/dist/backend/index.js.map +1 -0
  15. package/dist/backend/routes.d.ts +29 -0
  16. package/dist/backend/routes.d.ts.map +1 -0
  17. package/dist/backend/routes.js +35 -0
  18. package/dist/backend/routes.js.map +1 -0
  19. package/dist/backend/services/auto-shipment-on-paid.d.ts +28 -0
  20. package/dist/backend/services/auto-shipment-on-paid.d.ts.map +1 -0
  21. package/dist/backend/services/auto-shipment-on-paid.js +58 -0
  22. package/dist/backend/services/auto-shipment-on-paid.js.map +1 -0
  23. package/dist/backend/services/events.d.ts +40 -0
  24. package/dist/backend/services/events.d.ts.map +1 -0
  25. package/dist/backend/services/events.js +2 -0
  26. package/dist/backend/services/events.js.map +1 -0
  27. package/dist/backend/services/receive-shipment-handler.d.ts +93 -0
  28. package/dist/backend/services/receive-shipment-handler.d.ts.map +1 -0
  29. package/dist/backend/services/receive-shipment-handler.js +203 -0
  30. package/dist/backend/services/receive-shipment-handler.js.map +1 -0
  31. package/dist/backend/services/shipment-email-notifier.d.ts +78 -0
  32. package/dist/backend/services/shipment-email-notifier.d.ts.map +1 -0
  33. package/dist/backend/services/shipment-email-notifier.js +76 -0
  34. package/dist/backend/services/shipment-email-notifier.js.map +1 -0
  35. package/dist/backend/services/shipment-read.service.d.ts +19 -0
  36. package/dist/backend/services/shipment-read.service.d.ts.map +1 -0
  37. package/dist/backend/services/shipment-read.service.js +38 -0
  38. package/dist/backend/services/shipment-read.service.js.map +1 -0
  39. package/dist/backend/services/shipment-service.d.ts +79 -0
  40. package/dist/backend/services/shipment-service.d.ts.map +1 -0
  41. package/dist/backend/services/shipment-service.js +203 -0
  42. package/dist/backend/services/shipment-service.js.map +1 -0
  43. package/dist/backend/services/shipment-usage.service.d.ts +23 -0
  44. package/dist/backend/services/shipment-usage.service.d.ts.map +1 -0
  45. package/dist/backend/services/shipment-usage.service.js +26 -0
  46. package/dist/backend/services/shipment-usage.service.js.map +1 -0
  47. package/dist/backend/services/shipping-email-renderer.d.ts +22 -0
  48. package/dist/backend/services/shipping-email-renderer.d.ts.map +1 -0
  49. package/dist/backend/services/shipping-email-renderer.js +14 -0
  50. package/dist/backend/services/shipping-email-renderer.js.map +1 -0
  51. package/dist/manifest.d.ts +200 -0
  52. package/dist/manifest.d.ts.map +1 -0
  53. package/dist/manifest.js +63 -0
  54. package/dist/manifest.js.map +1 -0
  55. package/dist/migrations/20260817T194652_shipments_order_fk.d.ts +32 -0
  56. package/dist/migrations/20260817T194652_shipments_order_fk.d.ts.map +1 -0
  57. package/dist/migrations/20260817T194652_shipments_order_fk.js +66 -0
  58. package/dist/migrations/20260817T194652_shipments_order_fk.js.map +1 -0
  59. package/dist/migrations/20260819T171006_shipments_status_pending_manual.d.ts +19 -0
  60. package/dist/migrations/20260819T171006_shipments_status_pending_manual.d.ts.map +1 -0
  61. package/dist/migrations/20260819T171006_shipments_status_pending_manual.js +32 -0
  62. package/dist/migrations/20260819T171006_shipments_status_pending_manual.js.map +1 -0
  63. package/dist/migrations/index.d.ts +35 -0
  64. package/dist/migrations/index.d.ts.map +1 -0
  65. package/dist/migrations/index.js +38 -0
  66. package/dist/migrations/index.js.map +1 -0
  67. package/docs/shipments.md +96 -0
  68. package/package.json +69 -0
@@ -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
+ }