@openlup/platform 0.21.0 → 0.22.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 (263) hide show
  1. package/CHANGELOG.md +210 -47
  2. package/README.md +27 -4
  3. package/dist/composition/composeBundle.d.ts +7 -0
  4. package/dist/composition/composeBundle.js +2 -0
  5. package/dist/composition/fulfillmentContribution.d.ts +21 -0
  6. package/dist/composition/fulfillmentContribution.js +19 -0
  7. package/dist/composition/index.d.ts +1 -0
  8. package/dist/composition/index.js +1 -0
  9. package/dist/fulfillment/adapterConformance.d.ts +3 -0
  10. package/dist/fulfillment/adapterConformance.js +223 -0
  11. package/dist/fulfillment/bindings.d.ts +3 -0
  12. package/dist/fulfillment/bindings.js +29 -0
  13. package/dist/fulfillment/commerceFulfillmentHandlers.d.ts +29 -0
  14. package/dist/fulfillment/commerceFulfillmentHandlers.js +184 -0
  15. package/dist/fulfillment/commerceFulfillmentReadModel.d.ts +86 -0
  16. package/dist/fulfillment/commerceFulfillmentReadModel.js +131 -0
  17. package/dist/fulfillment/contracts/alignment.d.ts +25 -0
  18. package/dist/fulfillment/contracts/alignment.js +1 -0
  19. package/dist/fulfillment/contracts/bindings.d.ts +51 -0
  20. package/dist/fulfillment/contracts/bindings.js +1 -0
  21. package/dist/fulfillment/contracts/capabilities.d.ts +29 -0
  22. package/dist/fulfillment/contracts/capabilities.js +1 -0
  23. package/dist/fulfillment/contracts/conformance.d.ts +93 -0
  24. package/dist/fulfillment/contracts/conformance.js +1 -0
  25. package/dist/fulfillment/contracts/customerAlignment.d.ts +6 -0
  26. package/dist/fulfillment/contracts/customerAlignment.js +1 -0
  27. package/dist/fulfillment/contracts/dispatch.d.ts +105 -0
  28. package/dist/fulfillment/contracts/dispatch.js +1 -0
  29. package/dist/fulfillment/contracts/index.d.ts +18 -0
  30. package/dist/fulfillment/contracts/index.js +1 -0
  31. package/dist/fulfillment/contracts/label.d.ts +35 -0
  32. package/dist/fulfillment/contracts/label.js +1 -0
  33. package/dist/fulfillment/contracts/nativeUnsupported.d.ts +6 -0
  34. package/dist/fulfillment/contracts/nativeUnsupported.js +1 -0
  35. package/dist/fulfillment/contracts/observation.d.ts +52 -0
  36. package/dist/fulfillment/contracts/observation.js +1 -0
  37. package/dist/fulfillment/contracts/operator.d.ts +64 -0
  38. package/dist/fulfillment/contracts/operator.js +1 -0
  39. package/dist/fulfillment/contracts/read.d.ts +110 -0
  40. package/dist/fulfillment/contracts/read.js +1 -0
  41. package/dist/fulfillment/contracts/readback.d.ts +38 -0
  42. package/dist/fulfillment/contracts/readback.js +1 -0
  43. package/dist/fulfillment/contracts/replacement.d.ts +34 -0
  44. package/dist/fulfillment/contracts/replacement.js +1 -0
  45. package/dist/fulfillment/contracts/store.d.ts +86 -0
  46. package/dist/fulfillment/contracts/store.js +1 -0
  47. package/dist/fulfillment/contracts/sync.d.ts +170 -0
  48. package/dist/fulfillment/contracts/sync.js +1 -0
  49. package/dist/fulfillment/currentFulfillmentParcel.d.ts +2 -0
  50. package/dist/fulfillment/currentFulfillmentParcel.js +1 -0
  51. package/dist/fulfillment/delayReadModel.d.ts +11 -0
  52. package/dist/fulfillment/delayReadModel.js +45 -0
  53. package/dist/fulfillment/deliveryAlignmentReadModel.d.ts +4 -0
  54. package/dist/fulfillment/deliveryAlignmentReadModel.js +12 -0
  55. package/dist/fulfillment/dispatchProjection.d.ts +7 -0
  56. package/dist/fulfillment/dispatchProjection.js +18 -0
  57. package/dist/fulfillment/fulfillmentObservationEffects.d.ts +66 -0
  58. package/dist/fulfillment/fulfillmentObservationEffects.js +36 -0
  59. package/dist/fulfillment/handlerSchemas.d.ts +647 -0
  60. package/dist/fulfillment/handlerSchemas.js +213 -0
  61. package/dist/fulfillment/handlerSupport.d.ts +33 -0
  62. package/dist/fulfillment/handlerSupport.js +25 -0
  63. package/dist/fulfillment/index.d.ts +37 -0
  64. package/dist/fulfillment/index.js +37 -0
  65. package/dist/fulfillment/labelLifecycle.d.ts +24 -0
  66. package/dist/fulfillment/labelLifecycle.js +9 -0
  67. package/dist/fulfillment/lowStockEvidenceHandler.d.ts +36 -0
  68. package/dist/fulfillment/lowStockEvidenceHandler.js +46 -0
  69. package/dist/fulfillment/migrations.d.ts +27 -0
  70. package/dist/fulfillment/migrations.js +8 -0
  71. package/dist/fulfillment/nativeUnsupported.d.ts +10 -0
  72. package/dist/fulfillment/nativeUnsupported.js +18 -0
  73. package/dist/fulfillment/normalizedFulfillmentFacts.d.ts +2 -0
  74. package/dist/fulfillment/normalizedFulfillmentFacts.js +1 -0
  75. package/dist/fulfillment/observationProjection.d.ts +11 -0
  76. package/dist/fulfillment/observationProjection.js +37 -0
  77. package/dist/fulfillment/omnipack/capabilities.d.ts +3 -0
  78. package/dist/fulfillment/omnipack/capabilities.js +16 -0
  79. package/dist/fulfillment/omnipack/client.d.ts +37 -0
  80. package/dist/fulfillment/omnipack/client.js +162 -0
  81. package/dist/fulfillment/omnipack/index.d.ts +52 -0
  82. package/dist/fulfillment/omnipack/index.js +349 -0
  83. package/dist/fulfillment/omnipack/mappers.d.ts +73 -0
  84. package/dist/fulfillment/omnipack/mappers.js +177 -0
  85. package/dist/fulfillment/omnipack/observation.d.ts +21 -0
  86. package/dist/fulfillment/omnipack/observation.js +57 -0
  87. package/dist/fulfillment/omnipack/omnipackDispatchPayload.d.ts +36 -0
  88. package/dist/fulfillment/omnipack/omnipackDispatchPayload.js +63 -0
  89. package/dist/fulfillment/omnipack/omnipackOutboundOrderPayload.d.ts +83 -0
  90. package/dist/fulfillment/omnipack/omnipackOutboundOrderPayload.js +113 -0
  91. package/dist/fulfillment/omnipack/omnipackProductPackPayload.d.ts +11 -0
  92. package/dist/fulfillment/omnipack/omnipackProductPackPayload.js +38 -0
  93. package/dist/fulfillment/omnipack/omnipackProductPayload.d.ts +18 -0
  94. package/dist/fulfillment/omnipack/omnipackProductPayload.js +51 -0
  95. package/dist/fulfillment/omnipack/omnipackReconciliationPayload.d.ts +1 -0
  96. package/dist/fulfillment/omnipack/omnipackReconciliationPayload.js +3 -0
  97. package/dist/fulfillment/omnipack/omnipackStatusVocabulary.d.ts +4 -0
  98. package/dist/fulfillment/omnipack/omnipackStatusVocabulary.js +87 -0
  99. package/dist/fulfillment/omnipack/omnipackWebhookPayload.d.ts +9 -0
  100. package/dist/fulfillment/omnipack/omnipackWebhookPayload.js +76 -0
  101. package/dist/fulfillment/omnipack/productSyncMappers.d.ts +18 -0
  102. package/dist/fulfillment/omnipack/productSyncMappers.js +43 -0
  103. package/dist/fulfillment/omnipack/providerError.d.ts +25 -0
  104. package/dist/fulfillment/omnipack/providerError.js +47 -0
  105. package/dist/fulfillment/omnipack/trackingReferences.d.ts +16 -0
  106. package/dist/fulfillment/omnipack/trackingReferences.js +52 -0
  107. package/dist/fulfillment/paidFulfillmentRecovery.d.ts +67 -0
  108. package/dist/fulfillment/paidFulfillmentRecovery.js +154 -0
  109. package/dist/fulfillment/paidHandoff.d.ts +27 -0
  110. package/dist/fulfillment/paidHandoff.js +86 -0
  111. package/dist/fulfillment/paidHandoffPorts.d.ts +24 -0
  112. package/dist/fulfillment/paidHandoffPorts.js +3 -0
  113. package/dist/fulfillment/postgresDeliveryAlignment.d.ts +4 -0
  114. package/dist/fulfillment/postgresDeliveryAlignment.js +12 -0
  115. package/dist/fulfillment/postgresDispatchStore.d.ts +16 -0
  116. package/dist/fulfillment/postgresDispatchStore.js +44 -0
  117. package/dist/fulfillment/postgresEvidenceReadPort.d.ts +25 -0
  118. package/dist/fulfillment/postgresEvidenceReadPort.js +45 -0
  119. package/dist/fulfillment/postgresPaidFulfillmentRecovery.d.ts +25 -0
  120. package/dist/fulfillment/postgresPaidFulfillmentRecovery.js +26 -0
  121. package/dist/fulfillment/postgresPaidHandoffStore.d.ts +4 -0
  122. package/dist/fulfillment/postgresPaidHandoffStore.js +10 -0
  123. package/dist/fulfillment/postgresShipmentSpine.d.ts +63 -0
  124. package/dist/fulfillment/postgresShipmentSpine.js +232 -0
  125. package/dist/fulfillment/postgresStockSyncPort.d.ts +8 -0
  126. package/dist/fulfillment/postgresStockSyncPort.js +63 -0
  127. package/dist/fulfillment/productReconciliationEvidenceHandler.d.ts +36 -0
  128. package/dist/fulfillment/productReconciliationEvidenceHandler.js +50 -0
  129. package/dist/fulfillment/providerCapabilities.d.ts +9 -0
  130. package/dist/fulfillment/providerCapabilities.js +17 -0
  131. package/dist/fulfillment/providerCommandLedger.d.ts +143 -0
  132. package/dist/fulfillment/providerCommandLedger.js +14 -0
  133. package/dist/fulfillment/providerDispatchAcceptance.d.ts +26 -0
  134. package/dist/fulfillment/providerDispatchAcceptance.js +23 -0
  135. package/dist/fulfillment/providerDispatchError.d.ts +6 -0
  136. package/dist/fulfillment/providerDispatchError.js +5 -0
  137. package/dist/fulfillment/providerDispatchWorker.d.ts +31 -0
  138. package/dist/fulfillment/providerDispatchWorker.js +172 -0
  139. package/dist/fulfillment/providerProductSyncWorker.d.ts +28 -0
  140. package/dist/fulfillment/providerProductSyncWorker.js +122 -0
  141. package/dist/fulfillment/providerReconciliationError.d.ts +9 -0
  142. package/dist/fulfillment/providerReconciliationError.js +17 -0
  143. package/dist/fulfillment/providerReconciliationTerminalSkip.d.ts +2 -0
  144. package/dist/fulfillment/providerReconciliationTerminalSkip.js +6 -0
  145. package/dist/fulfillment/providerReconciliationTimestamp.d.ts +1 -0
  146. package/dist/fulfillment/providerReconciliationTimestamp.js +27 -0
  147. package/dist/fulfillment/providerReconciliationWorker.d.ts +82 -0
  148. package/dist/fulfillment/providerReconciliationWorker.js +151 -0
  149. package/dist/fulfillment/providerStockAvailability.d.ts +13 -0
  150. package/dist/fulfillment/providerStockAvailability.js +51 -0
  151. package/dist/fulfillment/providerStockSyncWorker.d.ts +37 -0
  152. package/dist/fulfillment/providerStockSyncWorker.js +186 -0
  153. package/dist/fulfillment/providerTrackingReferences.d.ts +15 -0
  154. package/dist/fulfillment/providerTrackingReferences.js +23 -0
  155. package/dist/fulfillment/replacementRefusals.d.ts +5 -0
  156. package/dist/fulfillment/replacementRefusals.js +38 -0
  157. package/dist/fulfillment/replacementShipmentHandler.d.ts +5 -0
  158. package/dist/fulfillment/replacementShipmentHandler.js +46 -0
  159. package/dist/fulfillment/schemaCapabilities.d.ts +13 -0
  160. package/dist/fulfillment/schemaCapabilities.js +7 -0
  161. package/dist/fulfillment/shipmentsOverviewHandler.d.ts +16 -0
  162. package/dist/fulfillment/shipmentsOverviewHandler.js +54 -0
  163. package/dist/fulfillment/statusMap.d.ts +2 -0
  164. package/dist/fulfillment/statusMap.js +1 -0
  165. package/dist/fulfillment/trackingProgress.d.ts +1 -0
  166. package/dist/fulfillment/trackingProgress.js +1 -0
  167. package/dist/fulfillment/verifiedIngress.d.ts +52 -0
  168. package/dist/fulfillment/verifiedIngress.js +66 -0
  169. package/dist/payments/stripe/index.d.ts +25 -0
  170. package/dist/payments/stripe/index.js +25 -0
  171. package/dist/payments/tpay/index.d.ts +30 -0
  172. package/dist/payments/tpay/index.js +30 -0
  173. package/package.json +17 -2
  174. package/release-gates.json +44 -3
  175. package/sql/20260808210000_fulfillment_shipment_rail.sql +810 -0
  176. package/sql/20260819150000_fulfillment_shipment_sequence.sql +98 -0
  177. package/sql/20261022080000_fulfillment_command_ledger.sql +786 -0
  178. package/sql/20261022090000_fulfillment_delivery_alignment.sql +1106 -0
  179. package/src/composition/composeBundle.ts +4 -0
  180. package/src/composition/fulfillmentContribution.ts +28 -0
  181. package/src/composition/index.ts +1 -0
  182. package/src/fulfillment/adapterConformance.ts +195 -0
  183. package/src/fulfillment/bindings.ts +26 -0
  184. package/src/fulfillment/commerceFulfillmentHandlers.ts +263 -0
  185. package/src/fulfillment/commerceFulfillmentReadModel.ts +257 -0
  186. package/src/fulfillment/contracts/alignment.ts +13 -0
  187. package/src/fulfillment/contracts/bindings.ts +30 -0
  188. package/src/fulfillment/contracts/capabilities.ts +15 -0
  189. package/src/fulfillment/contracts/conformance.ts +61 -0
  190. package/src/fulfillment/contracts/customerAlignment.ts +4 -0
  191. package/src/fulfillment/contracts/dispatch.ts +58 -0
  192. package/src/fulfillment/contracts/index.ts +18 -0
  193. package/src/fulfillment/contracts/label.ts +9 -0
  194. package/src/fulfillment/contracts/nativeUnsupported.ts +6 -0
  195. package/src/fulfillment/contracts/observation.ts +18 -0
  196. package/src/fulfillment/contracts/operator.ts +31 -0
  197. package/src/fulfillment/contracts/read.ts +42 -0
  198. package/src/fulfillment/contracts/readback.ts +11 -0
  199. package/src/fulfillment/contracts/replacement.ts +15 -0
  200. package/src/fulfillment/contracts/store.ts +26 -0
  201. package/src/fulfillment/contracts/sync.ts +61 -0
  202. package/src/fulfillment/currentFulfillmentParcel.ts +2 -0
  203. package/src/fulfillment/delayReadModel.ts +60 -0
  204. package/src/fulfillment/deliveryAlignmentReadModel.ts +15 -0
  205. package/src/fulfillment/dispatchProjection.ts +20 -0
  206. package/src/fulfillment/fulfillmentObservationEffects.ts +86 -0
  207. package/src/fulfillment/handlerSchemas.ts +255 -0
  208. package/src/fulfillment/handlerSupport.ts +25 -0
  209. package/src/fulfillment/index.ts +38 -0
  210. package/src/fulfillment/labelLifecycle.ts +11 -0
  211. package/src/fulfillment/lowStockEvidenceHandler.ts +60 -0
  212. package/src/fulfillment/migrations.ts +8 -0
  213. package/src/fulfillment/nativeUnsupported.ts +23 -0
  214. package/src/fulfillment/normalizedFulfillmentFacts.ts +2 -0
  215. package/src/fulfillment/observationProjection.ts +41 -0
  216. package/src/fulfillment/omnipack/capabilities.ts +18 -0
  217. package/src/fulfillment/omnipack/client.ts +258 -0
  218. package/src/fulfillment/omnipack/index.ts +312 -0
  219. package/src/fulfillment/omnipack/mappers.ts +261 -0
  220. package/src/fulfillment/omnipack/observation.ts +61 -0
  221. package/src/fulfillment/omnipack/omnipackDispatchPayload.ts +93 -0
  222. package/src/fulfillment/omnipack/omnipackOutboundOrderPayload.ts +194 -0
  223. package/src/fulfillment/omnipack/omnipackProductPackPayload.ts +47 -0
  224. package/src/fulfillment/omnipack/omnipackProductPayload.ts +67 -0
  225. package/src/fulfillment/omnipack/omnipackReconciliationPayload.ts +7 -0
  226. package/src/fulfillment/omnipack/omnipackStatusVocabulary.ts +103 -0
  227. package/src/fulfillment/omnipack/omnipackWebhookPayload.ts +73 -0
  228. package/src/fulfillment/omnipack/productSyncMappers.ts +74 -0
  229. package/src/fulfillment/omnipack/providerError.ts +64 -0
  230. package/src/fulfillment/omnipack/trackingReferences.ts +77 -0
  231. package/src/fulfillment/paidFulfillmentRecovery.ts +252 -0
  232. package/src/fulfillment/paidHandoff.ts +122 -0
  233. package/src/fulfillment/paidHandoffPorts.ts +21 -0
  234. package/src/fulfillment/postgresDeliveryAlignment.ts +14 -0
  235. package/src/fulfillment/postgresDispatchStore.ts +55 -0
  236. package/src/fulfillment/postgresEvidenceReadPort.ts +59 -0
  237. package/src/fulfillment/postgresPaidFulfillmentRecovery.ts +29 -0
  238. package/src/fulfillment/postgresPaidHandoffStore.ts +11 -0
  239. package/src/fulfillment/postgresShipmentSpine.ts +321 -0
  240. package/src/fulfillment/postgresStockSyncPort.ts +65 -0
  241. package/src/fulfillment/productReconciliationEvidenceHandler.ts +64 -0
  242. package/src/fulfillment/providerCapabilities.ts +18 -0
  243. package/src/fulfillment/providerCommandLedger.ts +53 -0
  244. package/src/fulfillment/providerDispatchAcceptance.ts +31 -0
  245. package/src/fulfillment/providerDispatchError.ts +7 -0
  246. package/src/fulfillment/providerDispatchWorker.ts +131 -0
  247. package/src/fulfillment/providerProductSyncWorker.ts +154 -0
  248. package/src/fulfillment/providerReconciliationError.ts +20 -0
  249. package/src/fulfillment/providerReconciliationTerminalSkip.ts +8 -0
  250. package/src/fulfillment/providerReconciliationTimestamp.ts +34 -0
  251. package/src/fulfillment/providerReconciliationWorker.ts +150 -0
  252. package/src/fulfillment/providerStockAvailability.ts +60 -0
  253. package/src/fulfillment/providerStockSyncWorker.ts +222 -0
  254. package/src/fulfillment/providerTrackingReferences.ts +31 -0
  255. package/src/fulfillment/replacementRefusals.ts +43 -0
  256. package/src/fulfillment/replacementShipmentHandler.ts +47 -0
  257. package/src/fulfillment/schemaCapabilities.ts +7 -0
  258. package/src/fulfillment/shipmentsOverviewHandler.ts +73 -0
  259. package/src/fulfillment/statusMap.ts +2 -0
  260. package/src/fulfillment/trackingProgress.ts +1 -0
  261. package/src/fulfillment/verifiedIngress.ts +80 -0
  262. package/src/payments/stripe/index.ts +30 -0
  263. package/src/payments/tpay/index.ts +36 -0
@@ -0,0 +1,67 @@
1
+ // Pure builder for the OmniPack product-registration request (catalog load). Mirrors the
2
+ // outbound-order payload convention (pure module in server/_lib, wired through the client).
3
+ // The exact OmniPack request key names live behind the constants below so finalizing them
4
+ // against the (auth-gated) OmniPack product API docs is a one-line edit.
5
+
6
+ // Product-registration request key names (confirm against the OmniPack product API docs).
7
+ const KEY_SKU = "sku";
8
+ const KEY_EAN = "ean";
9
+ const KEY_GROUP = "group";
10
+ const KEY_NAME = "name";
11
+ const KEY_DIMENSIONS = "dimensions";
12
+
13
+ export interface OmnipackProductPack {
14
+ weightGrams: number;
15
+ lengthMm: number;
16
+ widthMm: number;
17
+ heightMm: number;
18
+ }
19
+
20
+ export interface OmnipackProductInput {
21
+ sku: string;
22
+ ean: string | null;
23
+ productGroup: string;
24
+ title: string;
25
+ pack: OmnipackProductPack | null;
26
+ }
27
+
28
+ export class OmnipackProductPayloadError extends Error {
29
+ constructor(readonly code: string) {
30
+ super(`omnipack_product_payload_invalid: ${code}`);
31
+ this.name = "OmnipackProductPayloadError";
32
+ }
33
+ }
34
+
35
+ function requireNonEmpty(value: string | null | undefined, code: string): string {
36
+ const trimmed = value?.trim();
37
+ if (!trimmed) throw new OmnipackProductPayloadError(code);
38
+ return trimmed;
39
+ }
40
+
41
+ function requirePositive(value: number, code: string): number {
42
+ if (!Number.isFinite(value) || value <= 0) throw new OmnipackProductPayloadError(code);
43
+ return value;
44
+ }
45
+
46
+ // Builds the product-registration payload. EAN, group, title and a full pack are required —
47
+ // OmniPack matches physical goods by EAN and a batch-tracked group needs known dimensions.
48
+ export function buildOmnipackProductPayload(input: OmnipackProductInput): Record<string, unknown> {
49
+ const sku = requireNonEmpty(input.sku, "missing_sku");
50
+ const ean = requireNonEmpty(input.ean, "missing_ean");
51
+ const group = requireNonEmpty(input.productGroup, "missing_product_group");
52
+ const name = requireNonEmpty(input.title, "missing_title");
53
+ if (!input.pack) throw new OmnipackProductPayloadError("missing_pack");
54
+
55
+ return {
56
+ [KEY_SKU]: sku,
57
+ [KEY_EAN]: ean,
58
+ [KEY_GROUP]: group,
59
+ [KEY_NAME]: name,
60
+ [KEY_DIMENSIONS]: {
61
+ weight: requirePositive(input.pack.weightGrams, "invalid_pack_weight"),
62
+ length: requirePositive(input.pack.lengthMm, "invalid_pack_length"),
63
+ width: requirePositive(input.pack.widthMm, "invalid_pack_width"),
64
+ height: requirePositive(input.pack.heightMm, "invalid_pack_height"),
65
+ },
66
+ };
67
+ }
@@ -0,0 +1,7 @@
1
+ export function providerOccurrenceKey(
2
+ fulfillmentOrderId: string,
3
+ providerStatus: string,
4
+ occurredAt: string | null,
5
+ ): string {
6
+ return `${fulfillmentOrderId}:${providerStatus}:${occurredAt ?? "unknown-time"}`;
7
+ }
@@ -0,0 +1,103 @@
1
+ // Provider status translation only; lifecycle effects remain host-owned.
2
+ export type NormalizedProviderStatus =
3
+ | "new"
4
+ | "in_fulfillment"
5
+ | "ready_for_packing"
6
+ | "awaiting_courier"
7
+ | "shipping"
8
+ | "delivered"
9
+ | "shipping_failed"
10
+ | "returned_to_sender"
11
+ | "cancelled"
12
+ | "suspended";
13
+
14
+ // Provider strings arrive in varied spelling ("IN_FULFILLMENT", "in_fulfillment",
15
+ // "Shipping") — compare on a canonical token so casing/separator drift can never
16
+ // silently zero the pull.
17
+ export function canonicalStatusToken(value: string | null | undefined): string | null {
18
+ const token = value?.trim().toLowerCase().replace(/[\s_-]+/g, "_");
19
+ return token || null;
20
+ }
21
+
22
+ // Map the provider primary `status` to our normalized enum. subStatus is NOT
23
+ // consulted: it is recorded verbatim as evidence but never changes the local
24
+ // state (the provider-status mapping lives entirely in the primary status).
25
+ // Returns null for an unknown/UNKNOWN primary status → the worker quarantines it
26
+ // as omnipack_unknown_provider_status (loud, self-diagnosing) rather than skipping.
27
+ export function normalizeStatus(status: string | null): NormalizedProviderStatus | null {
28
+ switch (canonicalStatusToken(status)) {
29
+ case "new":
30
+ return "new";
31
+ case "in_fulfillment":
32
+ return "in_fulfillment";
33
+ case "ready_for_packing":
34
+ return "ready_for_packing";
35
+ case "awaiting_courier":
36
+ return "awaiting_courier";
37
+ case "shipping":
38
+ return "shipping";
39
+ case "delivered":
40
+ return "delivered";
41
+ case "shipping_failed":
42
+ return "shipping_failed";
43
+ case "returned_to_sender":
44
+ return "returned_to_sender";
45
+ case "cancelled":
46
+ return "cancelled";
47
+ case "suspended":
48
+ return "suspended";
49
+ // "unknown" is OmniPack's documented exceptional value ("in general it
50
+ // shouldn't happen") — quarantine it like any unmapped status.
51
+ default:
52
+ return null;
53
+ }
54
+ }
55
+
56
+ // Local fulfillment status vocabulary (the canonical set used across the
57
+ // commerce domain): provider_received | picking | packed | in_transit |
58
+ // delivered | cancelled | exception. Both ingestion paths — the poller and the
59
+ // webhook — map into it here and nowhere else.
60
+ //
61
+ // A total Record rather than a `switch`: a new member of the union is then a
62
+ // compile error instead of a silent fall-through to whatever the last branch
63
+ // returned. The webhook path used to carry exactly that hole (its private
64
+ // mapper defaulted every unnamed token to `delivered`, the most consequential
65
+ // terminal state); routing it through this table removed it.
66
+ const LOCAL_STATUS_BY_PROVIDER_STATUS: Record<NormalizedProviderStatus, string> = {
67
+ // Created / on hold at the provider. READY_FOR_EXPORT means validation passed
68
+ // and waits for export to warehouse execution; EXPORTED means handed to
69
+ // warehouse execution. Neither proves a label, stock coverage, WMS
70
+ // acceptance, cut-off passage, or literal picking. Neither is shipped.
71
+ new: "provider_received",
72
+ // Canonical `picking` is the coarse provider-neutral warehouse-preparation
73
+ // bucket; IN_FULFILLMENT evidence does not prove a literal picker action, and
74
+ // it does NOT consume provider stock — see stockConsumedFor below.
75
+ in_fulfillment: "picking",
76
+ // Picking completed, awaiting packing. This is the domain's finished-picking
77
+ // consumption boundary: the webhook path maps the same physical moment
78
+ // (order.picked) to local `packed` and consumes provider stock, so the poller
79
+ // must not lag a state behind (push/pull parity). `packed` is not in the
80
+ // handover set, so no dispatch email fires here.
81
+ ready_for_packing: "packed",
82
+ // Packed, waiting for the courier to collect. Stock is consumed; the parcel
83
+ // is NOT yet handed over, so no dispatch email fires here.
84
+ awaiting_courier: "packed",
85
+ // Courier has the parcel — this is the ship boundary: handover + tracking +
86
+ // the shipment.dispatched customer email.
87
+ shipping: "in_transit",
88
+ delivered: "delivered",
89
+ // Shipping failed / order suspended pending merchant clarification / returned
90
+ // to sender / provider-side cancellation: all route to an operational
91
+ // fulfillment_exception hold for OMS review. The verified vendor source emits
92
+ // the existing reassurance-only event before delivery; after durable delivery
93
+ // the hold remains but the future-tense event is suppressed. The distinct
94
+ // provider status is preserved as the hold reason.
95
+ shipping_failed: "exception",
96
+ suspended: "exception",
97
+ returned_to_sender: "exception",
98
+ cancelled: "exception",
99
+ };
100
+
101
+ export function localStatusFor(status: NormalizedProviderStatus): string {
102
+ return LOCAL_STATUS_BY_PROVIDER_STATUS[status];
103
+ }
@@ -0,0 +1,73 @@
1
+ import { timingSafeEqual } from "node:crypto";
2
+ import type { OmnipackWebhookEvidence } from "./mappers.js";
3
+ import type { NormalizedProviderStatus } from "./omnipackStatusVocabulary.js";
4
+
5
+ export interface OmnipackWebhookRequest { headers: Record<string, string | string[] | undefined> }
6
+ export type OmnipackWebhookRouteEvent = "shipment.accepted" | "order.processing_started" | "order.picked" | "order.shipped" | "order.delivered";
7
+
8
+ export const EVENT_TO_STATUS: Record<OmnipackWebhookRouteEvent, NormalizedProviderStatus> = {
9
+ "shipment.accepted": "new",
10
+ "order.processing_started": "in_fulfillment",
11
+ "order.picked": "ready_for_packing",
12
+ "order.shipped": "shipping",
13
+ "order.delivered": "delivered",
14
+ };
15
+
16
+ export function verifyOmnipackWebhookToken(req: OmnipackWebhookRequest, expectedToken: string | undefined): boolean {
17
+ if (!expectedToken) return false;
18
+ const authorization = readHeader(req, "authorization");
19
+ if (safeEqual(authorization, `Bearer ${expectedToken}`)) return true;
20
+ if (verifyBasicAuth(authorization, expectedToken)) return true;
21
+ return safeEqual(readHeader(req, "x-omnipack-webhook-token"), expectedToken);
22
+ }
23
+
24
+ export function buildProviderEventId(evidence: OmnipackWebhookEvidence): string {
25
+ return [
26
+ evidence.event,
27
+ evidence.providerOrderId ?? evidence.orderNumber ?? "unknown-order",
28
+ evidence.fulfilmentNumber ?? "unknown-fulfilment",
29
+ evidence.occurredAt ?? "unknown-time",
30
+ ].join(":");
31
+ }
32
+
33
+ function readHeader(req: OmnipackWebhookRequest, name: string): string | undefined {
34
+ const direct = req.headers[name];
35
+ if (typeof direct === "string") return direct;
36
+ if (Array.isArray(direct)) return direct[0];
37
+ const lowerName = name.toLowerCase();
38
+ for (const [key, value] of Object.entries(req.headers)) {
39
+ if (key.toLowerCase() !== lowerName) continue;
40
+ if (typeof value === "string") return value;
41
+ if (Array.isArray(value)) return value[0];
42
+ }
43
+ return undefined;
44
+ }
45
+
46
+ function verifyBasicAuth(authorization: string | undefined, expectedToken: string): boolean {
47
+ const match = authorization?.match(/^Basic\s+(.+)$/i);
48
+ if (!match) return false;
49
+ const credentials = decodeBase64(match[1]);
50
+ if (!credentials) return false;
51
+ const separator = credentials.indexOf(":");
52
+ if (separator <= 0) return false;
53
+ const username = credentials.slice(0, separator);
54
+ if (!username.trim()) return false;
55
+ const password = credentials.slice(separator + 1);
56
+ if (!password) return false;
57
+ return safeEqual(password, expectedToken);
58
+ }
59
+
60
+ function decodeBase64(value: string): string | null {
61
+ try {
62
+ return Buffer.from(value, "base64").toString("utf8");
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function safeEqual(actual: string | undefined, expected: string): boolean {
69
+ if (!actual) return false;
70
+ const actualBytes = Buffer.from(actual);
71
+ const expectedBytes = Buffer.from(expected);
72
+ return actualBytes.length === expectedBytes.length && timingSafeEqual(actualBytes, expectedBytes);
73
+ }
@@ -0,0 +1,74 @@
1
+ // Response mappers for the two-way OmniPack product sync (one SKU ↔ many EANs / "packs").
2
+ // Kept separate from the order/stock/fulfilment mappers so the product-sync surface stays a
3
+ // cohesive, independently-testable module. Self-contained tiny readers (no cross-file coupling)
4
+ // mirror the defensive parsing convention used in mappers.ts.
5
+ import { sanitizeOmnipackPayload } from "./mappers.js";
6
+
7
+ export interface OmnipackProductPackEvidence {
8
+ ean: string;
9
+ quantity: number;
10
+ }
11
+
12
+ // GET /products/{sku} — the product with its packs[]. The pack list is authoritative for what
13
+ // OmniPack currently holds; the push diff and the pull reconciliation both read it.
14
+ export interface OmnipackProductEvidence {
15
+ provider: "omnipack";
16
+ sku: string;
17
+ packs: OmnipackProductPackEvidence[];
18
+ raw: Record<string, unknown>;
19
+ }
20
+
21
+ export interface OmnipackProductPackCreatedEvidence {
22
+ provider: "omnipack";
23
+ sku: string;
24
+ ean: string;
25
+ raw: Record<string, unknown>;
26
+ }
27
+
28
+ // We tolerate a sparse body and fall back to the request-side sku; packs without an EAN are
29
+ // dropped (an EAN-less pack is not addressable for the host↔OmniPack diff).
30
+ export function mapOmnipackProductResponse(body: unknown, requestSku: string): OmnipackProductEvidence {
31
+ const response = asRecord(body);
32
+ const packs = readArray(response.packs)
33
+ .map((entry) => {
34
+ const pack = asRecord(entry);
35
+ return { ean: readString(pack.ean), quantity: readNumber(pack.quantity) };
36
+ })
37
+ .filter((pack) => pack.ean.length > 0);
38
+ return {
39
+ provider: "omnipack",
40
+ sku: readString(response.sku) || requestSku,
41
+ packs,
42
+ raw: sanitizeOmnipackPayload(response),
43
+ };
44
+ }
45
+
46
+ export function mapOmnipackProductPackCreatedResponse(
47
+ body: unknown,
48
+ requestSku: string,
49
+ requestEan: string,
50
+ ): OmnipackProductPackCreatedEvidence {
51
+ const response = asRecord(body);
52
+ return {
53
+ provider: "omnipack",
54
+ sku: readString(response.sku) || requestSku,
55
+ ean: readString(response.ean) || requestEan,
56
+ raw: sanitizeOmnipackPayload(response),
57
+ };
58
+ }
59
+
60
+ function asRecord(input: unknown): Record<string, unknown> {
61
+ return input && typeof input === "object" && !Array.isArray(input) ? (input as Record<string, unknown>) : {};
62
+ }
63
+
64
+ function readArray(input: unknown): unknown[] {
65
+ return Array.isArray(input) ? input : [];
66
+ }
67
+
68
+ function readString(input: unknown): string {
69
+ return typeof input === "string" ? input : "";
70
+ }
71
+
72
+ function readNumber(input: unknown): number {
73
+ return typeof input === "number" && Number.isFinite(input) ? input : 0;
74
+ }
@@ -0,0 +1,64 @@
1
+ export class OmnipackProviderError extends Error {
2
+ readonly provider = "omnipack";
3
+ readonly operation: string;
4
+ readonly status: number | null;
5
+ readonly code: string;
6
+ readonly retryable: boolean;
7
+ readonly mayHaveSucceeded: boolean;
8
+
9
+ constructor(input: {
10
+ operation: string;
11
+ status: number | null;
12
+ code: string;
13
+ retryable: boolean;
14
+ mayHaveSucceeded: boolean;
15
+ }) {
16
+ super(`OmniPack request failed: ${input.operation}`);
17
+ this.name = "OmnipackProviderError";
18
+ this.operation = input.operation;
19
+ this.status = input.status;
20
+ this.code = input.code;
21
+ this.retryable = input.retryable;
22
+ this.mayHaveSucceeded = input.mayHaveSucceeded;
23
+ }
24
+ }
25
+
26
+ export interface SanitizedOmnipackProviderError {
27
+ code: string;
28
+ mayHaveSucceeded: boolean;
29
+ message: string;
30
+ operation: string | null;
31
+ retryable: boolean;
32
+ status: number | null;
33
+ }
34
+
35
+ export function sanitizeOmnipackProviderError(error: unknown): SanitizedOmnipackProviderError {
36
+ if (error instanceof OmnipackProviderError) {
37
+ return {
38
+ code: error.code,
39
+ mayHaveSucceeded: error.mayHaveSucceeded,
40
+ message: error.message,
41
+ operation: error.operation,
42
+ retryable: error.retryable,
43
+ status: error.status,
44
+ };
45
+ }
46
+ return {
47
+ code: "omnipack_provider_error",
48
+ mayHaveSucceeded: true,
49
+ message: "OmniPack request failed",
50
+ operation: null,
51
+ retryable: true,
52
+ status: null,
53
+ };
54
+ }
55
+
56
+ export function mayPostHaveSucceeded(method: string, status: number | null): boolean {
57
+ if (method !== "POST") return false;
58
+ return status === null
59
+ || status === 408
60
+ || status === 409
61
+ || status === 425
62
+ || status === 429
63
+ || status >= 500;
64
+ }
@@ -0,0 +1,77 @@
1
+ // Provider-payload extraction only. This infra layer stays free of domain
2
+ // imports (provider-infra boundary): it surfaces the feed's raw carrier
3
+ // evidence (carrier code, service, feed-supplied URL) and leaves carrier
4
+ // normalization + tracking-URL building to the fulfillment domain
5
+ // (enrichOmnipackTrackingReference + the shipping carrier registry).
6
+ export interface OmnipackTrackingReferenceEvidence {
7
+ provider: "omnipack";
8
+ trackingNumber: string;
9
+ trackingUrl: string | null;
10
+ /** Raw provider carrier/platform code. */
11
+ carrier: string | null;
12
+ carrierKind: string | null;
13
+ service: string | null;
14
+ }
15
+
16
+ export function mapOmnipackTrackingReferences(input: {
17
+ shipments?: unknown[];
18
+ trackingNumbers?: unknown[];
19
+ readString: (value: unknown) => string;
20
+ readNullableString: (value: unknown) => string | null;
21
+ asRecord: (value: unknown) => Record<string, unknown>;
22
+ }): OmnipackTrackingReferenceEvidence[] {
23
+ const fromShipments = (input.shipments ?? [])
24
+ .map(input.asRecord)
25
+ .map((shipment) => {
26
+ const trackingNumber = input.readString(shipment.trackingNo) || input.readString(shipment.trackingNumber);
27
+ if (!trackingNumber) return null;
28
+ const service = input.readNullableString(shipment.shippingMethod) ?? input.readNullableString(shipment.service);
29
+ return {
30
+ provider: "omnipack" as const,
31
+ trackingNumber,
32
+ trackingUrl:
33
+ input.readNullableString(shipment.trackingUrl) ??
34
+ input.readNullableString(shipment.trackingURL) ??
35
+ input.readNullableString(shipment.trackingLink) ??
36
+ input.readNullableString(shipment.publicTrackingUrl),
37
+ carrier: input.readNullableString(shipment.carrier) ?? input.readNullableString(shipment.carrierCode),
38
+ carrierKind: carrierKindFromService(service),
39
+ service,
40
+ };
41
+ })
42
+ .filter((ref): ref is OmnipackTrackingReferenceEvidence => ref !== null);
43
+
44
+ const fallbackNumbers = (input.trackingNumbers ?? [])
45
+ .map(input.readString)
46
+ .filter(Boolean)
47
+ .map((trackingNumber) => ({
48
+ provider: "omnipack" as const,
49
+ trackingNumber,
50
+ trackingUrl: null,
51
+ carrier: null,
52
+ carrierKind: null,
53
+ service: null,
54
+ }));
55
+
56
+ return uniqueTrackingReferences([...fromShipments, ...fallbackNumbers]);
57
+ }
58
+
59
+ function uniqueTrackingReferences(
60
+ refs: OmnipackTrackingReferenceEvidence[],
61
+ ): OmnipackTrackingReferenceEvidence[] {
62
+ const seen = new Set<string>();
63
+ const result: OmnipackTrackingReferenceEvidence[] = [];
64
+ for (const ref of refs) {
65
+ const key = ref.trackingNumber.trim();
66
+ if (!key || seen.has(key)) continue;
67
+ seen.add(key);
68
+ result.push({ ...ref, trackingNumber: key });
69
+ }
70
+ return result;
71
+ }
72
+
73
+ function carrierKindFromService(service: string | null): string | null {
74
+ if (!service) return null;
75
+ const [carrier] = service.split("_");
76
+ return carrier ? carrier.toLowerCase() : null;
77
+ }