@endora-commerce/mod-delivery-methods 0.100.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/admin/api/delivery-methods-client.d.ts +41 -0
- package/dist/admin/api/delivery-methods-client.d.ts.map +1 -0
- package/dist/admin/api/delivery-methods-client.js +20 -0
- package/dist/admin/api/delivery-methods-client.js.map +1 -0
- package/dist/admin/index.d.ts +34 -0
- package/dist/admin/index.d.ts.map +1 -0
- package/dist/admin/index.js +44 -0
- package/dist/admin/index.js.map +1 -0
- package/dist/admin/pages/DeliveryMethodsPage.d.ts +4 -0
- package/dist/admin/pages/DeliveryMethodsPage.d.ts.map +1 -0
- package/dist/admin/pages/DeliveryMethodsPage.js +167 -0
- package/dist/admin/pages/DeliveryMethodsPage.js.map +1 -0
- package/dist/admin/renderers/registry.d.ts +15 -0
- package/dist/admin/renderers/registry.d.ts.map +1 -0
- package/dist/admin/renderers/registry.js +18 -0
- package/dist/admin/renderers/registry.js.map +1 -0
- package/dist/backend/adapters/built-in-adapters.d.ts +46 -0
- package/dist/backend/adapters/built-in-adapters.d.ts.map +1 -0
- package/dist/backend/adapters/built-in-adapters.js +62 -0
- package/dist/backend/adapters/built-in-adapters.js.map +1 -0
- package/dist/backend/commands/delivery-method.commands.d.ts +61 -0
- package/dist/backend/commands/delivery-method.commands.d.ts.map +1 -0
- package/dist/backend/commands/delivery-method.commands.js +107 -0
- package/dist/backend/commands/delivery-method.commands.js.map +1 -0
- package/dist/backend/demo/reset.d.ts +12 -0
- package/dist/backend/demo/reset.d.ts.map +1 -0
- package/dist/backend/demo/reset.js +13 -0
- package/dist/backend/demo/reset.js.map +1 -0
- package/dist/backend/demo/rows.d.ts +24 -0
- package/dist/backend/demo/rows.d.ts.map +1 -0
- package/dist/backend/demo/rows.js +22 -0
- package/dist/backend/demo/rows.js.map +1 -0
- package/dist/backend/demo/seed.d.ts +14 -0
- package/dist/backend/demo/seed.d.ts.map +1 -0
- package/dist/backend/demo/seed.js +28 -0
- package/dist/backend/demo/seed.js.map +1 -0
- package/dist/backend/entities/delivery-method.entity.d.ts +30 -0
- package/dist/backend/entities/delivery-method.entity.d.ts.map +1 -0
- package/dist/backend/entities/delivery-method.entity.js +91 -0
- package/dist/backend/entities/delivery-method.entity.js.map +1 -0
- package/dist/backend/index.d.ts +88 -0
- package/dist/backend/index.d.ts.map +1 -0
- package/dist/backend/index.js +144 -0
- package/dist/backend/index.js.map +1 -0
- package/dist/backend/routes.d.ts +51 -0
- package/dist/backend/routes.d.ts.map +1 -0
- package/dist/backend/routes.js +152 -0
- package/dist/backend/routes.js.map +1 -0
- package/dist/backend/services/delivery-method-read-port.d.ts +25 -0
- package/dist/backend/services/delivery-method-read-port.d.ts.map +1 -0
- package/dist/backend/services/delivery-method-read-port.js +56 -0
- package/dist/backend/services/delivery-method-read-port.js.map +1 -0
- package/dist/backend/services/delivery-method-reconciler.d.ts +97 -0
- package/dist/backend/services/delivery-method-reconciler.d.ts.map +1 -0
- package/dist/backend/services/delivery-method-reconciler.js +157 -0
- package/dist/backend/services/delivery-method-reconciler.js.map +1 -0
- package/dist/backend/services/order-status-registry.port.d.ts +50 -0
- package/dist/backend/services/order-status-registry.port.d.ts.map +1 -0
- package/dist/backend/services/order-status-registry.port.js +45 -0
- package/dist/backend/services/order-status-registry.port.js.map +1 -0
- package/dist/backend/services/registry-singleton.d.ts +20 -0
- package/dist/backend/services/registry-singleton.d.ts.map +1 -0
- package/dist/backend/services/registry-singleton.js +21 -0
- package/dist/backend/services/registry-singleton.js.map +1 -0
- package/dist/backend/services/shipment-usage-guard.d.ts +31 -0
- package/dist/backend/services/shipment-usage-guard.d.ts.map +1 -0
- package/dist/backend/services/shipment-usage-guard.js +33 -0
- package/dist/backend/services/shipment-usage-guard.js.map +1 -0
- package/dist/backend/services/shipping-adapter-registry.d.ts +98 -0
- package/dist/backend/services/shipping-adapter-registry.d.ts.map +1 -0
- package/dist/backend/services/shipping-adapter-registry.js +92 -0
- package/dist/backend/services/shipping-adapter-registry.js.map +1 -0
- package/dist/backend/services/shipping-method-eligibility.d.ts +24 -0
- package/dist/backend/services/shipping-method-eligibility.d.ts.map +1 -0
- package/dist/backend/services/shipping-method-eligibility.js +46 -0
- package/dist/backend/services/shipping-method-eligibility.js.map +1 -0
- package/dist/install/index.d.ts +59 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +59 -0
- package/dist/install/index.js.map +1 -0
- package/dist/manifest.d.ts +193 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +185 -0
- package/dist/manifest.js.map +1 -0
- package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.d.ts +30 -0
- package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.d.ts.map +1 -0
- package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.js +72 -0
- package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.js.map +1 -0
- package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.d.ts +24 -0
- package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.d.ts.map +1 -0
- package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.js +28 -0
- package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.js.map +1 -0
- package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.d.ts +27 -0
- package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.d.ts.map +1 -0
- package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.js +44 -0
- package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.js.map +1 -0
- package/dist/migrations/index.d.ts +29 -0
- package/dist/migrations/index.d.ts.map +1 -0
- package/dist/migrations/index.js +33 -0
- package/dist/migrations/index.js.map +1 -0
- package/dist/ports/index.d.ts +129 -0
- package/dist/ports/index.d.ts.map +1 -0
- package/dist/ports/index.js +2 -0
- package/dist/ports/index.js.map +1 -0
- package/docs/delivery_methods.md +197 -0
- package/i18n/en.json +5 -0
- package/i18n/pl.json +5 -0
- package/package.json +100 -0
- package/tailwind.css +14 -0
package/dist/migrations/20260611T140354_delivery_methods_shipping_methods_adapter_and_shipments.js
ADDED
|
@@ -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
|
package/dist/migrations/20260611T140416_delivery_methods_fix_in_person_pickup_adapter.d.ts.map
ADDED
|
@@ -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
|
package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.d.ts.map
ADDED
|
@@ -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
|
package/dist/migrations/20260912T094638_delivery_methods_sales_channel_delivery_methods.js.map
ADDED
|
@@ -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 @@
|
|
|
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.
|