@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.
Files changed (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +59 -0
  3. package/dist/admin/api/delivery-methods-client.d.ts +41 -0
  4. package/dist/admin/api/delivery-methods-client.d.ts.map +1 -0
  5. package/dist/admin/api/delivery-methods-client.js +20 -0
  6. package/dist/admin/api/delivery-methods-client.js.map +1 -0
  7. package/dist/admin/index.d.ts +34 -0
  8. package/dist/admin/index.d.ts.map +1 -0
  9. package/dist/admin/index.js +44 -0
  10. package/dist/admin/index.js.map +1 -0
  11. package/dist/admin/pages/DeliveryMethodsPage.d.ts +4 -0
  12. package/dist/admin/pages/DeliveryMethodsPage.d.ts.map +1 -0
  13. package/dist/admin/pages/DeliveryMethodsPage.js +167 -0
  14. package/dist/admin/pages/DeliveryMethodsPage.js.map +1 -0
  15. package/dist/admin/renderers/registry.d.ts +15 -0
  16. package/dist/admin/renderers/registry.d.ts.map +1 -0
  17. package/dist/admin/renderers/registry.js +18 -0
  18. package/dist/admin/renderers/registry.js.map +1 -0
  19. package/dist/backend/adapters/built-in-adapters.d.ts +46 -0
  20. package/dist/backend/adapters/built-in-adapters.d.ts.map +1 -0
  21. package/dist/backend/adapters/built-in-adapters.js +62 -0
  22. package/dist/backend/adapters/built-in-adapters.js.map +1 -0
  23. package/dist/backend/commands/delivery-method.commands.d.ts +61 -0
  24. package/dist/backend/commands/delivery-method.commands.d.ts.map +1 -0
  25. package/dist/backend/commands/delivery-method.commands.js +107 -0
  26. package/dist/backend/commands/delivery-method.commands.js.map +1 -0
  27. package/dist/backend/demo/reset.d.ts +12 -0
  28. package/dist/backend/demo/reset.d.ts.map +1 -0
  29. package/dist/backend/demo/reset.js +13 -0
  30. package/dist/backend/demo/reset.js.map +1 -0
  31. package/dist/backend/demo/rows.d.ts +24 -0
  32. package/dist/backend/demo/rows.d.ts.map +1 -0
  33. package/dist/backend/demo/rows.js +22 -0
  34. package/dist/backend/demo/rows.js.map +1 -0
  35. package/dist/backend/demo/seed.d.ts +14 -0
  36. package/dist/backend/demo/seed.d.ts.map +1 -0
  37. package/dist/backend/demo/seed.js +28 -0
  38. package/dist/backend/demo/seed.js.map +1 -0
  39. package/dist/backend/entities/delivery-method.entity.d.ts +30 -0
  40. package/dist/backend/entities/delivery-method.entity.d.ts.map +1 -0
  41. package/dist/backend/entities/delivery-method.entity.js +91 -0
  42. package/dist/backend/entities/delivery-method.entity.js.map +1 -0
  43. package/dist/backend/index.d.ts +88 -0
  44. package/dist/backend/index.d.ts.map +1 -0
  45. package/dist/backend/index.js +144 -0
  46. package/dist/backend/index.js.map +1 -0
  47. package/dist/backend/routes.d.ts +51 -0
  48. package/dist/backend/routes.d.ts.map +1 -0
  49. package/dist/backend/routes.js +152 -0
  50. package/dist/backend/routes.js.map +1 -0
  51. package/dist/backend/services/delivery-method-read-port.d.ts +25 -0
  52. package/dist/backend/services/delivery-method-read-port.d.ts.map +1 -0
  53. package/dist/backend/services/delivery-method-read-port.js +56 -0
  54. package/dist/backend/services/delivery-method-read-port.js.map +1 -0
  55. package/dist/backend/services/delivery-method-reconciler.d.ts +97 -0
  56. package/dist/backend/services/delivery-method-reconciler.d.ts.map +1 -0
  57. package/dist/backend/services/delivery-method-reconciler.js +157 -0
  58. package/dist/backend/services/delivery-method-reconciler.js.map +1 -0
  59. package/dist/backend/services/order-status-registry.port.d.ts +50 -0
  60. package/dist/backend/services/order-status-registry.port.d.ts.map +1 -0
  61. package/dist/backend/services/order-status-registry.port.js +45 -0
  62. package/dist/backend/services/order-status-registry.port.js.map +1 -0
  63. package/dist/backend/services/registry-singleton.d.ts +20 -0
  64. package/dist/backend/services/registry-singleton.d.ts.map +1 -0
  65. package/dist/backend/services/registry-singleton.js +21 -0
  66. package/dist/backend/services/registry-singleton.js.map +1 -0
  67. package/dist/backend/services/shipment-usage-guard.d.ts +31 -0
  68. package/dist/backend/services/shipment-usage-guard.d.ts.map +1 -0
  69. package/dist/backend/services/shipment-usage-guard.js +33 -0
  70. package/dist/backend/services/shipment-usage-guard.js.map +1 -0
  71. package/dist/backend/services/shipping-adapter-registry.d.ts +98 -0
  72. package/dist/backend/services/shipping-adapter-registry.d.ts.map +1 -0
  73. package/dist/backend/services/shipping-adapter-registry.js +92 -0
  74. package/dist/backend/services/shipping-adapter-registry.js.map +1 -0
  75. package/dist/backend/services/shipping-method-eligibility.d.ts +24 -0
  76. package/dist/backend/services/shipping-method-eligibility.d.ts.map +1 -0
  77. package/dist/backend/services/shipping-method-eligibility.js +46 -0
  78. package/dist/backend/services/shipping-method-eligibility.js.map +1 -0
  79. package/dist/install/index.d.ts +59 -0
  80. package/dist/install/index.d.ts.map +1 -0
  81. package/dist/install/index.js +59 -0
  82. package/dist/install/index.js.map +1 -0
  83. package/dist/manifest.d.ts +193 -0
  84. package/dist/manifest.d.ts.map +1 -0
  85. package/dist/manifest.js +185 -0
  86. package/dist/manifest.js.map +1 -0
  87. package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.d.ts +30 -0
  88. package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.d.ts.map +1 -0
  89. package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.js +72 -0
  90. package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.js.map +1 -0
  91. package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.d.ts +24 -0
  92. package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.d.ts.map +1 -0
  93. package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.js +28 -0
  94. package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.js.map +1 -0
  95. package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.d.ts +27 -0
  96. package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.d.ts.map +1 -0
  97. package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.js +44 -0
  98. package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.js.map +1 -0
  99. package/dist/migrations/index.d.ts +29 -0
  100. package/dist/migrations/index.d.ts.map +1 -0
  101. package/dist/migrations/index.js +33 -0
  102. package/dist/migrations/index.js.map +1 -0
  103. package/dist/ports/index.d.ts +129 -0
  104. package/dist/ports/index.d.ts.map +1 -0
  105. package/dist/ports/index.js +2 -0
  106. package/dist/ports/index.js.map +1 -0
  107. package/docs/delivery_methods.md +197 -0
  108. package/i18n/en.json +5 -0
  109. package/i18n/pl.json +5 -0
  110. package/package.json +100 -0
  111. package/tailwind.css +14 -0
@@ -0,0 +1,72 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Shipping-method adapter framework + Shipment record (feature 035 — Metoda
4
+ * Dostawy).
5
+ *
6
+ * Extends the existing `delivery_methods` entry into a configurable,
7
+ * adapter-backed shipping method and introduces the `shipments` record:
8
+ *
9
+ * delivery_methods:
10
+ * + adapter — registry key of the ShippingAdapter realising the
11
+ * logic (backfilled from the existing `code`)
12
+ * + status_on_success — Order-status reference applied on a successful
13
+ * shipment generation (seed `shipped`)
14
+ * + status_on_failure — Order-status reference applied on a failed
15
+ * shipment generation (seed `in_fulfilment`)
16
+ *
17
+ * shipments (new):
18
+ * a first-class generation attempt against an Order — order_id,
19
+ * delivery_method_id, status (pending|success|failure), external_reference,
20
+ * provider_details, failure_reason, attempt_no, timestamps.
21
+ *
22
+ * `price` reuses the existing `delivery_methods.cost` column (no new column).
23
+ * Seed status mappings reuse the current hard-coded order `status` enum until
24
+ * the Orders module ships a configurable registry (see research.md R3/R10).
25
+ */
26
+ export class Migration20260611T140354DeliveryMethodsShippingMethodsAdapterAndShipments extends Migration {
27
+ async up() {
28
+ // delivery_methods — add nullable, backfill, then tighten to NOT NULL.
29
+ // `adapter` carries a NOT NULL DEFAULT '' so that generic insert paths that
30
+ // predate the framework (sales-channel/membership fixtures, legacy seeds)
31
+ // keep working: a row with an empty adapter is simply not selectable — it
32
+ // never resolves to a registered adapter (FR-003) — while adapter-backed
33
+ // rows always set it explicitly.
34
+ this.addSql(`
35
+ alter table "delivery_methods"
36
+ add column "adapter" varchar(64) not null default '',
37
+ add column "status_on_success" varchar(64) not null default 'shipment_sent',
38
+ add column "status_on_failure" varchar(64) not null default 'processing';
39
+ `);
40
+ // Backfill existing rows: adapter mirrors the existing code.
41
+ this.addSql(`update "delivery_methods" set "adapter" = "code" where "adapter" = '';`);
42
+ // shipments — first-class shipment-generation attempt record.
43
+ this.addSql(`
44
+ create table "shipments" (
45
+ "id" uuid not null,
46
+ "order_id" uuid not null,
47
+ "delivery_method_id" uuid not null,
48
+ "status" varchar(32) not null default 'pending',
49
+ "external_reference" varchar(255) null,
50
+ "provider_details" jsonb null,
51
+ "failure_reason" text null,
52
+ "attempt_no" int not null default 1,
53
+ "created_at" timestamptz not null,
54
+ "updated_at" timestamptz not null,
55
+ constraint "shipments_pkey" primary key ("id"),
56
+ constraint "shipments_status_check" check ("status" in ('pending', 'success', 'failure'))
57
+ );
58
+ `);
59
+ this.addSql(`create index "shipments_order_id_index" on "shipments" ("order_id");`);
60
+ this.addSql(`create index "shipments_status_index" on "shipments" ("status");`);
61
+ }
62
+ async down() {
63
+ this.addSql(`drop table if exists "shipments";`);
64
+ this.addSql(`
65
+ alter table "delivery_methods"
66
+ drop column "adapter",
67
+ drop column "status_on_success",
68
+ drop column "status_on_failure";
69
+ `);
70
+ }
71
+ }
72
+ //# sourceMappingURL=20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.js","sourceRoot":"","sources":["../../src/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,OAAO,yEAA0E,SAAQ,SAAS;IAC7F,KAAK,CAAC,EAAE;QACf,uEAAuE;QACvE,4EAA4E;QAC5E,0EAA0E;QAC1E,0EAA0E;QAC1E,yEAAyE;QACzE,iCAAiC;QACjC,IAAI,CAAC,MAAM,CAAC;;;;;KAKX,CAAC,CAAC;QACH,6DAA6D;QAC7D,IAAI,CAAC,MAAM,CAAC,wEAAwE,CAAC,CAAC;QAEtF,8DAA8D;QAC9D,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;;;;;KAeX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CAAC,sEAAsE,CAAC,CAAC;QACpF,IAAI,CAAC,MAAM,CAAC,kEAAkE,CAAC,CAAC;IAClF,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,mCAAmC,CAAC,CAAC;QACjD,IAAI,CAAC,MAAM,CAAC;;;;;KAKX,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,24 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Fix the built-in in-person pickup delivery method's adapter binding.
4
+ *
5
+ * Migration 052 introduced the adapter framework and backfilled every existing
6
+ * `delivery_methods` row with `adapter = code`. For the platform's bundled
7
+ * in-person pickup method (code `in_person_pickup`) this produced the adapter
8
+ * key `in_person_pickup`, which is NOT a registered ShippingAdapter — the
9
+ * bundled pickup adapter registers under `personal_pickup`
10
+ * (`built-in-adapters.ts`). As a result the eligibility filter
11
+ * (`shipping-method-eligibility.ts`) silently dropped the method from every
12
+ * storefront checkout, since an unregistered-adapter row never resolves
13
+ * (FR-003).
14
+ *
15
+ * The dev seed already pairs `in_person_pickup` with the `personal_pickup`
16
+ * adapter for fresh installs; this migration repoints any pre-existing,
17
+ * backfilled rows to the same registered adapter so the pickup method becomes
18
+ * selectable at checkout.
19
+ */
20
+ export declare class Migration20260611T140416DeliveryMethodsFixInPersonPickupAdapter extends Migration {
21
+ up(): Promise<void>;
22
+ down(): Promise<void>;
23
+ }
24
+ //# sourceMappingURL=20260611T140416_delivery_methods_fix_in_person_pickup_adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260611T140416_delivery_methods_fix_in_person_pickup_adapter.d.ts","sourceRoot":"","sources":["../../src/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;GAiBG;AACH,qBAAa,+DAAgE,SAAQ,SAAS;IAC7E,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAMnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAKrC"}
@@ -0,0 +1,28 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * Fix the built-in in-person pickup delivery method's adapter binding.
4
+ *
5
+ * Migration 052 introduced the adapter framework and backfilled every existing
6
+ * `delivery_methods` row with `adapter = code`. For the platform's bundled
7
+ * in-person pickup method (code `in_person_pickup`) this produced the adapter
8
+ * key `in_person_pickup`, which is NOT a registered ShippingAdapter — the
9
+ * bundled pickup adapter registers under `personal_pickup`
10
+ * (`built-in-adapters.ts`). As a result the eligibility filter
11
+ * (`shipping-method-eligibility.ts`) silently dropped the method from every
12
+ * storefront checkout, since an unregistered-adapter row never resolves
13
+ * (FR-003).
14
+ *
15
+ * The dev seed already pairs `in_person_pickup` with the `personal_pickup`
16
+ * adapter for fresh installs; this migration repoints any pre-existing,
17
+ * backfilled rows to the same registered adapter so the pickup method becomes
18
+ * selectable at checkout.
19
+ */
20
+ export class Migration20260611T140416DeliveryMethodsFixInPersonPickupAdapter extends Migration {
21
+ async up() {
22
+ this.addSql(`update "delivery_methods" set "adapter" = 'personal_pickup' where "code" = 'in_person_pickup' and "adapter" = 'in_person_pickup';`);
23
+ }
24
+ async down() {
25
+ this.addSql(`update "delivery_methods" set "adapter" = 'in_person_pickup' where "code" = 'in_person_pickup' and "adapter" = 'personal_pickup';`);
26
+ }
27
+ }
28
+ //# sourceMappingURL=20260611T140416_delivery_methods_fix_in_person_pickup_adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260611T140416_delivery_methods_fix_in_person_pickup_adapter.js","sourceRoot":"","sources":["../../src/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,+DAAgE,SAAQ,SAAS;IACnF,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CACT,mIAAmI,CACpI,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CACT,mIAAmI,CACpI,CAAC;IACJ,CAAC;CACF"}
@@ -0,0 +1,27 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * The `sales_channel_delivery_methods` sales-channel bridge — the delivery methods available in a channel.
4
+ *
5
+ * It was created by the platform's frozen `Migration20260430T170044CoreSalesChannelsPromote`
6
+ * until `specs/120-migration-closure-bridge-ownership/` Phase 2. Under D-226 a
7
+ * bridge between an always-present near side (`sales_channels`, a kernel table)
8
+ * and a switchable far side belongs to the far side, so this module creates it:
9
+ * an instance that does not install `delivery_methods` no longer has a migration corpus
10
+ * naming a table nothing builds.
11
+ *
12
+ * `if not exists`, because every database that has already applied the frozen
13
+ * migration has this table. The storage keys on the class name and holds no
14
+ * checksum, so nothing is re-offered there and this migration is the no-op it
15
+ * reads as; on a fresh database it is the creation.
16
+ *
17
+ * The statements are the frozen ones verbatim — same columns, same primary key,
18
+ * same two foreign keys, same index — so the two paths reach one schema. Its
19
+ * position needs nothing declared: `sales_channels` is the kernel's and
20
+ * `delivery_methods` is created inside the frozen prefix, which every
21
+ * above-watermark migration runs after.
22
+ */
23
+ export declare class Migration20260912T094638DeliveryMethodsSalesChannelDeliveryMethods extends Migration {
24
+ up(): Promise<void>;
25
+ down(): Promise<void>;
26
+ }
27
+ //# sourceMappingURL=20260912T094638_delivery_methods_sales_channel_delivery_methods.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260912T094638_delivery_methods_sales_channel_delivery_methods.d.ts","sourceRoot":"","sources":["../../src/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,kEAAmE,SAAQ,SAAS;IAChF,EAAE,IAAI,OAAO,CAAC,IAAI,CAAC;IAmBnB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;CAGrC"}
@@ -0,0 +1,44 @@
1
+ import { Migration } from '@mikro-orm/migrations';
2
+ /**
3
+ * The `sales_channel_delivery_methods` sales-channel bridge — the delivery methods available in a channel.
4
+ *
5
+ * It was created by the platform's frozen `Migration20260430T170044CoreSalesChannelsPromote`
6
+ * until `specs/120-migration-closure-bridge-ownership/` Phase 2. Under D-226 a
7
+ * bridge between an always-present near side (`sales_channels`, a kernel table)
8
+ * and a switchable far side belongs to the far side, so this module creates it:
9
+ * an instance that does not install `delivery_methods` no longer has a migration corpus
10
+ * naming a table nothing builds.
11
+ *
12
+ * `if not exists`, because every database that has already applied the frozen
13
+ * migration has this table. The storage keys on the class name and holds no
14
+ * checksum, so nothing is re-offered there and this migration is the no-op it
15
+ * reads as; on a fresh database it is the creation.
16
+ *
17
+ * The statements are the frozen ones verbatim — same columns, same primary key,
18
+ * same two foreign keys, same index — so the two paths reach one schema. Its
19
+ * position needs nothing declared: `sales_channels` is the kernel's and
20
+ * `delivery_methods` is created inside the frozen prefix, which every
21
+ * above-watermark migration runs after.
22
+ */
23
+ export class Migration20260912T094638DeliveryMethodsSalesChannelDeliveryMethods extends Migration {
24
+ async up() {
25
+ this.addSql(`
26
+ create table if not exists "sales_channel_delivery_methods" (
27
+ "sales_channel_id" uuid not null,
28
+ "delivery_method_id" uuid not null,
29
+ constraint "sales_channel_delivery_methods_pkey"
30
+ primary key ("sales_channel_id", "delivery_method_id"),
31
+ constraint "sales_channel_delivery_methods_channel_fk"
32
+ foreign key ("sales_channel_id") references "sales_channels" ("id") on delete cascade,
33
+ constraint "sales_channel_delivery_methods_delivery_method_fk"
34
+ foreign key ("delivery_method_id") references "delivery_methods" ("id") on delete cascade
35
+ );
36
+ `);
37
+ this.addSql('create index if not exists "sales_channel_delivery_methods_delivery_method_id_index" ' +
38
+ 'on "sales_channel_delivery_methods" ("delivery_method_id");');
39
+ }
40
+ async down() {
41
+ this.addSql('drop table if exists "sales_channel_delivery_methods" cascade;');
42
+ }
43
+ }
44
+ //# sourceMappingURL=20260912T094638_delivery_methods_sales_channel_delivery_methods.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"20260912T094638_delivery_methods_sales_channel_delivery_methods.js","sourceRoot":"","sources":["../../src/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,OAAO,kEAAmE,SAAQ,SAAS;IACtF,KAAK,CAAC,EAAE;QACf,IAAI,CAAC,MAAM,CAAC;;;;;;;;;;;KAWX,CAAC,CAAC;QACH,IAAI,CAAC,MAAM,CACT,uFAAuF;YACrF,6DAA6D,CAChE,CAAC;IACJ,CAAC;IAEQ,KAAK,CAAC,IAAI;QACjB,IAAI,CAAC,MAAM,CAAC,gEAAgE,CAAC,CAAC;IAChF,CAAC;CACF"}
@@ -0,0 +1,29 @@
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
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's.
12
+ *
13
+ * The **named** exports stay beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports each class by name
16
+ * from this specifier, and a migration class name is contract in a way an entity
17
+ * class name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom is
22
+ * a query against a table nobody created.
23
+ */
24
+ import { Migration20260611T140354DeliveryMethodsShippingMethodsAdapterAndShipments } from './20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.js';
25
+ import { Migration20260611T140416DeliveryMethodsFixInPersonPickupAdapter } from './20260611T140416_delivery_methods_fix_in_person_pickup_adapter.js';
26
+ import { Migration20260912T094638DeliveryMethodsSalesChannelDeliveryMethods } from './20260912T094638_delivery_methods_sales_channel_delivery_methods.js';
27
+ export declare const migrations: (typeof Migration20260611T140354DeliveryMethodsShippingMethodsAdapterAndShipments)[];
28
+ export { Migration20260611T140354DeliveryMethodsShippingMethodsAdapterAndShipments, Migration20260611T140416DeliveryMethodsFixInPersonPickupAdapter, Migration20260912T094638DeliveryMethodsSalesChannelDeliveryMethods, };
29
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,yEAAyE,EAAE,MAAM,8EAA8E,CAAC;AACzK,OAAO,EAAE,+DAA+D,EAAE,MAAM,oEAAoE,CAAC;AACrJ,OAAO,EAAE,kEAAkE,EAAE,MAAM,sEAAsE,CAAC;AAE1J,eAAO,MAAM,UAAU,sFAItB,CAAC;AAEF,OAAO,EACL,yEAAyE,EACzE,+DAA+D,EAC/D,kEAAkE,GACnE,CAAC"}
@@ -0,0 +1,33 @@
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
+ * Listed in ascending timestamp, which is the order of this module's own
10
+ * migrations and of nothing else (feature 081): a manifest `dependencies` array
11
+ * is the only thing ordering this block against another module's.
12
+ *
13
+ * The **named** exports stay beside the array, and the asymmetry with
14
+ * `./backend` — which publishes an array and no named class (D-168) — is
15
+ * deliberate. `db/migrations-registry.generated.ts` imports each class by name
16
+ * from this specifier, and a migration class name is contract in a way an entity
17
+ * class name is not: `mikro_orm_migrations` persists it, so it is a string every
18
+ * already-migrated database holds.
19
+ *
20
+ * A class that is in neither the array nor the barrel is a migration that does
21
+ * not run: `migration:pending` reports nothing pending and the first symptom is
22
+ * a query against a table nobody created.
23
+ */
24
+ import { Migration20260611T140354DeliveryMethodsShippingMethodsAdapterAndShipments } from './20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.js';
25
+ import { Migration20260611T140416DeliveryMethodsFixInPersonPickupAdapter } from './20260611T140416_delivery_methods_fix_in_person_pickup_adapter.js';
26
+ import { Migration20260912T094638DeliveryMethodsSalesChannelDeliveryMethods } from './20260912T094638_delivery_methods_sales_channel_delivery_methods.js';
27
+ export const migrations = [
28
+ Migration20260611T140354DeliveryMethodsShippingMethodsAdapterAndShipments,
29
+ Migration20260611T140416DeliveryMethodsFixInPersonPickupAdapter,
30
+ Migration20260912T094638DeliveryMethodsSalesChannelDeliveryMethods,
31
+ ];
32
+ export { Migration20260611T140354DeliveryMethodsShippingMethodsAdapterAndShipments, Migration20260611T140416DeliveryMethodsFixInPersonPickupAdapter, Migration20260912T094638DeliveryMethodsSalesChannelDeliveryMethods, };
33
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/migrations/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,yEAAyE,EAAE,MAAM,8EAA8E,CAAC;AACzK,OAAO,EAAE,+DAA+D,EAAE,MAAM,oEAAoE,CAAC;AACrJ,OAAO,EAAE,kEAAkE,EAAE,MAAM,sEAAsE,CAAC;AAE1J,MAAM,CAAC,MAAM,UAAU,GAAG;IACxB,yEAAyE;IACzE,+DAA+D;IAC/D,kEAAkE;CACnE,CAAC;AAEF,OAAO,EACL,yEAAyE,EACzE,+DAA+D,EAC/D,kEAAkE,GACnE,CAAC"}
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The install-time seed surface `delivery_methods` publishes, and **nothing
3
+ * that exists at runtime** (feature 134, FR-064; D-168, D-169, D-171).
4
+ *
5
+ * `tsc` compiles this file to `export {};`. That is the property D-171 makes the
6
+ * boundary decision on — *a subpath is contract surface iff the module it
7
+ * resolves to exports no runtime binding* — so a consumer naming this subpath
8
+ * names a declaration and can name nothing else. The runtime half is one
9
+ * factory, `createDeliveryMethodSeeder`, on this package's `./backend`.
10
+ *
11
+ * ## Why this interface exists at all, and why it is here rather than in
12
+ * `packages/contracts`
13
+ *
14
+ * A module that ships its own carrier rows used to seed them with an `insert`
15
+ * into this module's table from its own migration — `inpost` and `dhl_parcel`
16
+ * each did, which is three `migration-foreign-writes` ledger keys and, **once a
17
+ * seeding module ships separately from the table's owner**, a hard schema
18
+ * dependency with no version range and no compile-time signal at all. That is
19
+ * the whole of the reason, and it holds for any pair of modules published from
20
+ * two places rather than for these two: an `insert` into a table another package
21
+ * owns is a dependency on that table's *shape*, and npm has no way to express
22
+ * one. The repair is that the seed is issued from the seeding module's
23
+ * `installHook` through this surface — **R11 / D-251 / FR-064**, the criterion
24
+ * any module may satisfy.
25
+ *
26
+ * It cannot live in `@endora-commerce/contracts`: every method takes a MikroORM
27
+ * `EntityManager`, that package is compiled by `admin` and `storefront`, and
28
+ * FR-034 keeps it free of `@mikro-orm` imports. That is exactly D-171's
29
+ * qualifying test — *does this signature stop the interface living in
30
+ * `packages/contracts`* — and it is why the `em` is a **required** first
31
+ * parameter on every method and never an optional one (D-169): an optional
32
+ * transactional `em` lets a caller hand a transaction to an implementation that
33
+ * ignores it and receive a silently non-atomic write.
34
+ *
35
+ * ## It carries no `Container name:` marker, deliberately
36
+ *
37
+ * This is not a container port and must not read as one. `ModuleLifecycleContext`
38
+ * is `{ em, redis, log, module }`, both orchestrator construction sites pass no
39
+ * container, and D-46 deleted `ctx.onInstall` because `module:install` composes
40
+ * nothing — so there is no cradle for a hook to resolve a name from, `lazyPort`
41
+ * is structurally unavailable here, and a documented container name would be a
42
+ * name nothing registers (`check:port-shape`'s signal 2, from the other side).
43
+ * The consumer's seam is the factory, not a resolution.
44
+ *
45
+ * ## No entity leaves by this door (D-168)
46
+ *
47
+ * {@link DeliveryMethodSeedRecord} is a published record and not the
48
+ * `DeliveryMethod` entity, type-only included. Handing a caller a managed entity
49
+ * hands it the ability to mutate a delivery method outside the seam, and to
50
+ * persist that change on whichever transaction it happens to hold.
51
+ */
52
+ import type { EntityManager } from '@mikro-orm/postgresql';
53
+ /** The row values a seeding module supplies for its own delivery method. */
54
+ export interface DeliveryMethodSeedDefaults {
55
+ /** Unique within `delivery_methods`; the seam's whole idempotence rests on it. */
56
+ code: string;
57
+ /** Translations by language tag, with a `default` key. */
58
+ name: Record<string, string>;
59
+ cost?: string;
60
+ currency?: string;
61
+ /**
62
+ * `'active'` when omitted. **Pass it explicitly when the module wants
63
+ * anything else**: `dhl_parcel` seeds `'inactive'`, and taking the default
64
+ * there would offer a shipping option to buyers that no operator chose
65
+ * (`foreign-write-repair.md` §2.4).
66
+ */
67
+ status?: 'active' | 'inactive';
68
+ statusOnSuccess?: string;
69
+ statusOnFailure?: string;
70
+ }
71
+ /** What the seam answers about a delivery method — a record, never the entity. */
72
+ export interface DeliveryMethodSeedRecord {
73
+ readonly id: string;
74
+ readonly code: string;
75
+ readonly name: Record<string, string>;
76
+ readonly cost: string;
77
+ readonly currency: string;
78
+ readonly status: 'active' | 'inactive';
79
+ readonly adapter: string;
80
+ readonly statusOnSuccess: string;
81
+ readonly statusOnFailure: string;
82
+ }
83
+ /**
84
+ * Created-versus-found, which the caller needs and cannot derive.
85
+ *
86
+ * `created` is what a channel binding is guarded on: the seed binds **once, for
87
+ * the rows it creates**, and re-binding a method an operator deliberately
88
+ * unbound from every channel is issue #96 verbatim (`foreign-write-repair.md`
89
+ * §2.3).
90
+ */
91
+ export interface DeliveryMethodSeedOutcome {
92
+ readonly row: DeliveryMethodSeedRecord;
93
+ readonly created: boolean;
94
+ }
95
+ export interface DeliveryMethodSeedApi {
96
+ /**
97
+ * Create this module's delivery-method row, or return the existing one
98
+ * untouched. Idempotent and prune-safe: a row matched by `code` keeps its
99
+ * admin-edited configuration and only a missing `adapter` link is backfilled.
100
+ */
101
+ ensureMethodForAdapter(em: EntityManager, adapterKey: string, defaults: DeliveryMethodSeedDefaults): Promise<DeliveryMethodSeedOutcome>;
102
+ /**
103
+ * Put the method in the system-default sales channel.
104
+ *
105
+ * **Call it only when `ensureMethodForAdapter` answered `created === true`.**
106
+ * Never as a reconcile: "unbound from every channel" is a state an operator is
107
+ * entitled to reach and to keep, and an unguarded call brings the method back
108
+ * with nothing saying so (issue #96).
109
+ *
110
+ * Answers whether a membership row was written — `false` when the method was
111
+ * already in that channel, and `false` when the platform has **no**
112
+ * system-default channel yet, which a database that has been migrated and never
113
+ * booted does not: the default channel is created at boot, and an install
114
+ * composes nothing. The row is then seeded and unbound, which is what the seed
115
+ * migration this replaced did in the same state.
116
+ */
117
+ bindToDefaultChannel(em: EntityManager, deliveryMethodId: string): Promise<boolean>;
118
+ /**
119
+ * Remove the method and, by cascade, its channel memberships.
120
+ *
121
+ * For a **hard** uninstall only (`if (!ctx.hard) return;`). A soft uninstall
122
+ * and a deactivation both keep the row: the registry filters the adapter's
123
+ * enumeration by its contributing module's effective state, so an off module
124
+ * is answered at the read and the operator's edits survive being switched back
125
+ * on (Principle XVII). Answers whether a row was removed.
126
+ */
127
+ removeMethodForAdapter(em: EntityManager, code: string): Promise<boolean>;
128
+ }
129
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAkDG;AACH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,uBAAuB,CAAC;AAE3D,4EAA4E;AAC5E,MAAM,WAAW,0BAA0B;IACzC,kFAAkF;IAClF,IAAI,EAAE,MAAM,CAAC;IACb,0DAA0D;IAC1D,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;OAKG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,kFAAkF;AAClF,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,QAAQ,GAAG,UAAU,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;CAClC;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,GAAG,EAAE,wBAAwB,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC;;;;OAIG;IACH,sBAAsB,CACpB,EAAE,EAAE,aAAa,EACjB,UAAU,EAAE,MAAM,EAClB,QAAQ,EAAE,0BAA0B,GACnC,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAEtC;;;;;;;;;;;;;;OAcG;IACH,oBAAoB,CAAC,EAAE,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAEpF;;;;;;;;OAQG;IACH,sBAAsB,CAAC,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC3E"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":""}
@@ -0,0 +1,197 @@
1
+ ---
2
+ title: delivery_methods
3
+ description: Configured delivery options
4
+ ---
5
+
6
+ # `delivery_methods`
7
+
8
+ The platform's **shipping-method framework** (_Metoda Dostawy_).
9
+ The module hosts a pluggable adapter registry over the delivery-method catalog,
10
+ the delivery-side twin of `payment_methods`. The first-class `Shipment` record
11
+ and its lifecycle live in the sibling [`shipments`](./shipments.md) module.
12
+ Carrier integration modules register adapters into this framework. Which ones your
13
+ instance has depends on what is installed, so they are named rather than linked
14
+ here: a link to a sibling page is a broken link in every instance that does not
15
+ install that module, which is what `onBrokenLinks: 'throw'` says about it.
16
+
17
+ A delivery method is never hard-coded: the platform discovers methods from
18
+ whichever **adapter modules** are installed and enabled. Enabling a recognised
19
+ shipping-method adapter module auto-creates a configurable `delivery_methods`
20
+ row visible at `/delivery-methods`.
21
+
22
+ ## Public surface
23
+
24
+ Admin routes are gated by `delivery_methods:read` (reads) and
25
+ `delivery_methods:write` (mutations) — the module's own codes since 2026-08-28.
26
+ They were `catalog:read` / `catalog:write` until then, which meant whoever could
27
+ edit a product could also decide how the shop ships, and delete a delivery
28
+ method outright. A role that was relying on the catalogue codes for this screen
29
+ has to be granted the new ones on `/admin-roles`; nothing grants them
30
+ automatically, deliberately.
31
+
32
+ | Verb + Path | Audience | Gate | Purpose |
33
+ | --- | --- | --- | --- |
34
+ | `GET /api/v1/delivery-methods` | anon | — | Eligible methods for the storefront checkout (active ∩ sales-channel ∩ Organization allow-list ∩ adapter registered ∩ `validateUseOnStorefront`) |
35
+ | `GET /api/v1/admin/delivery-methods` | admin | `delivery_methods:read` | Full list with adapter, status mappings, sales channels, renderer key |
36
+ | `PUT /api/v1/admin/delivery-methods/:code` | admin | `delivery_methods:write` | Upsert by code (name, cost/`price`, status, `statusOnSuccess`/`statusOnFailure`, sales channels) |
37
+ | `DELETE /api/v1/admin/delivery-methods/:id` | admin | `delivery_methods:write` | Hard delete (guarded: rejected with 409 when a `Shipment` references the method — set status `inactive` instead) |
38
+
39
+ The admin status selectors read their options from `GET /api/v1/admin/order-statuses`
40
+ (owned by the payment-methods admin routes; shared `OrderStatusRegistry`). That
41
+ route is gated `payment_methods:read` **or** `delivery_methods:read` — an any-of
42
+ over the two editors that read it, so the code that opens this screen also opens
43
+ its status selectors.
44
+
45
+ ## Entry fields
46
+
47
+ A `delivery_methods` row carries: `code` (unique), `adapter` (registry key),
48
+ per-language `name` (default + overrides), `cost` + `currency` (the `price`
49
+ surcharge added to the order total), `status` (`active`/`inactive`),
50
+ `statusOnSuccess` / `statusOnFailure` (Order-status references applied when
51
+ shipment generation succeeds/fails). There is **no** `statusOnPending` and no
52
+ `kind` column — a shipping method is identified by its `adapter` alone.
53
+
54
+ `statusOnSuccess` / `statusOnFailure` reference Order statuses resolved through
55
+ the `OrderStatusRegistry` port (enum-backed by the order `status` enum until the
56
+ Orders module ships a configurable registry). Seed defaults: `shipped` /
57
+ `in_fulfilment`.
58
+
59
+ Sales-channel scoping reuses the generic `SalesChannelMembershipService`
60
+ (`'delivery-method'`); per-Organization availability reuses
61
+ `OrganizationRestrictionService` (`'delivery_method'`, opt-out blocklist).
62
+
63
+ ## How to build a shipping-method module
64
+
65
+ A platform module is recognised as a shipping-method adapter **iff** it
66
+ registers a `ShippingAdapter` in the process-wide `shippingAdapterRegistry`
67
+ from its boot hook. No core change is required.
68
+
69
+ 1. **Implement the `ShippingAdapter` contract** (`@endora-commerce/contracts`):
70
+
71
+ ```ts
72
+ import type { ShippingAdapter } from '@endora-commerce/contracts';
73
+
74
+ export const myCarrierAdapter: ShippingAdapter = {
75
+ adapterKey: 'my_carrier',
76
+ // Extra conditions per surface; return a constant true when none apply.
77
+ validateUseOnStorefront: async () => true,
78
+ validateUseOnAdmin: async () => true,
79
+ validateUseInApi: async () => true,
80
+ // order_created: may start generation; safe to leave as a no-op.
81
+ onOrderCreated: async () => {},
82
+ // shipment_created: begin generation, return the next action.
83
+ onShipmentCreated: async () => ({ kind: 'pending' }),
84
+ // receive_shipment: map the ingress to a success/failure outcome.
85
+ onReceiveShipment: async (ctx) => ({
86
+ result: 'success',
87
+ externalReference: ctx.externalReference ?? null,
88
+ }),
89
+ // Optional renderer keys; absent ⇒ the platform default is used.
90
+ renderers: { storefront: 'my_carrier', email: 'my_carrier.email' },
91
+ };
92
+ ```
93
+
94
+ 2. **Contribute the adapter from the boot hook**, naming the owning module, and
95
+ ship the method row as a migration:
96
+
97
+ ```ts
98
+ import { shippingAdapterRegistry } from '.../delivery_methods/services/registry-singleton.js';
99
+
100
+ ctx.onBoot(() => {
101
+ shippingAdapterRegistry.register(myCarrierAdapter, 'my_carrier_module');
102
+ });
103
+ ```
104
+
105
+ The owner id is what lets the registry skip the adapter while its module is
106
+ absent, so a carrier an operator switches off stops being offered instead of
107
+ being offered and failing — the same defect the payment twin had, fixed on
108
+ both sides. The `delivery_methods` row itself is static reference data and
109
+ belongs in your module's migration; `DeliveryMethodReconciler` remains
110
+ available from an `installHook` for a row that must be created from code. No
111
+ uninstall hook is needed to withdraw the adapter — a module that is not
112
+ present is not enumerated.
113
+
114
+ The skip does not answer for an order **already placed** on your method: a
115
+ shipment can still be generated for it, and that shipment
116
+ opens `pending_manual` naming your module rather than reading like one you
117
+ accepted. You write no code for it — see *When the registry is read* below.
118
+
119
+ **Your hook pushes and returns.** It does not check what is already in the
120
+ table, does not check whether `delivery_methods` is present, and treats no
121
+ absence as an error — because nothing reads the registry while modules are
122
+ being composed. Boot hooks run whatever a module's effective state is; the
123
+ *enumeration* answers presence, not the registration. A throw in a boot hook
124
+ is not one adapter dropping out: `runBootHooks` re-throws it as
125
+ `ModuleCompositionError` and `index.ts` turns that into `process.exit(1)`, so
126
+ the operator's next start dies over a switch they were entitled to use. Nor
127
+ may a contributing hook probe `effectiveState` — the host already
128
+ filters at enumeration, and a probe at the push would make switching your
129
+ carrier back on require a restart. If your hook also *does work* (a
130
+ reconcile, a Redis or Postgres write), split it in two first: the working
131
+ half probes, the contributing half never does.
132
+
133
+ 3. **Optional renderers** — register custom renderers under the keys you
134
+ declared:
135
+ - Storefront: `registerShippingMethodRenderer(key, fn)` in
136
+ `storefront/lib/shipping-renderers/registry.tsx`.
137
+ - E-mail: `registerShippingEmailRenderer(key, fn)` in
138
+ `shipments/services/shipping-email-renderer.ts`.
139
+ When a renderer is missing for a surface, the platform default is used so the
140
+ method always renders.
141
+
142
+ 4. **Enable the module** from the admin module-lifecycle screen → a configurable
143
+ Delivery Method appears at `/delivery-methods`.
144
+
145
+ The two bundled offline reference adapters — `manual_courier` (_Wysyłka własna_)
146
+ and `personal_pickup` (_Odbiór osobisty_) — need no external carrier and are the
147
+ worked example of the full lifecycle.
148
+
149
+ ## When the registry is read
150
+
151
+ The `shippingAdapterRegistry` is a **process-wide singleton**
152
+ (`delivery_methods/services/registry-singleton.ts`): one table of adapters per
153
+ process, however many times the platform is composed. Contributions are pushed
154
+ into it **once, during composition**. Every read of it happens **later, inside a
155
+ request**:
156
+
157
+ | Read | Where | What an absent adapter means there |
158
+ | --- | --- | --- |
159
+ | Storefront eligibility | `GET /api/v1/delivery-methods` → `ShippingMethodEligibilityService.filter` | the method is not offered |
160
+ | Admin upsert guard | `PUT /api/v1/admin/delivery-methods/:code` → `isRegistered` | an explicitly supplied key nobody contributed is rejected (400); a contributed one whose owner is off is accepted, because the read is presence-blind on purpose |
161
+ | Order placement | `orders` re-validates the chosen method, then fires `onOrderCreated` | a method whose owner is off answers 503 `MODULE_DISABLED`; an unregistered one skips the hook |
162
+ | Shipment generation | `ShipmentService.create` → `onShipmentCreated` | the adapter hook is skipped and the `Shipment` opens **`pending_manual`** naming the absent module — never plain `pending`, which would read as a shipment the carrier had accepted |
163
+ | Order-confirmation e-mail | the method's `renderers.email` key | the platform default renderer is used |
164
+
165
+ Two things follow, and they are the reason this section exists rather than being
166
+ left to be inferred. First, there is **no order to get right** between
167
+ contributors: your adapter is visible to the first read whether it landed before
168
+ or after anybody else's, so a boot hook has nothing to wait for and nothing to
169
+ verify. Second, an absent or switched-off contributor is answered **at the
170
+ read**, by the entry's recorded owner — never at the push. That is what makes
171
+ this a contribution point rather than a gated port: the push is ungated
172
+ on purpose, because gating it would turn one operator flip into a boot failure
173
+ naming a module nobody touched.
174
+
175
+ The presence filter splits the surface by who is asking. `get`, `resolve`,
176
+ `list` and `isAvailable` skip an entry whose owning module is not effectively
177
+ present — a buyer is never offered a carrier that cannot take the parcel, and
178
+ `resolve` raises the ordinary `ModuleDisabledError`. `entry`, `ownerOf`,
179
+ `isRegistered` and `listAll` deliberately do not, because `/delivery-methods`
180
+ has to keep showing the method *and* the reason it is unavailable: switching a
181
+ module off is not uninstalling it.
182
+
183
+ `absentOwnerFor(adapterKey)` is the fifth reader and the only one that answers
184
+ the *question* instead of exposing the table: it names the module that
185
+ contributed the key and is not present, and `null` in every other case. It
186
+ exists because `get()` collapses two situations an operator cannot act on
187
+ identically — a key nobody ever contributed, and a key whose carrier module is
188
+ switched off — and only the second one names something they can switch back on.
189
+ `shipments` asks it to decide which state to open a `Shipment` in; the payment
190
+ twin, `GatewayRefundRegistry.absentOwnerFor`, is the same reader for the same
191
+ reason.
192
+
193
+ ## Lifecycle
194
+
195
+ See [`shipments`](./shipments.md) for the `order_created` → `shipment_created`
196
+ → `receive_shipment` lifecycle, the `Shipment` entity, retries, and the
197
+ order-status mapping.
package/i18n/en.json ADDED
@@ -0,0 +1,5 @@
1
+ {
2
+ "nav.deliveryMethods.label": "Delivery methods",
3
+ "actions.openDeliveryMethods.label": "Delivery methods",
4
+ "actions.openDeliveryMethods.description": "Delivery & shipping methods"
5
+ }