@openlup/platform 0.21.0 → 0.23.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 (349) hide show
  1. package/CHANGELOG.md +432 -146
  2. package/README.md +82 -589
  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 +95 -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 +143 -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 +72 -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 +59 -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 +89 -0
  46. package/dist/fulfillment/contracts/store.js +1 -0
  47. package/dist/fulfillment/contracts/sync.d.ts +226 -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 +26 -0
  56. package/dist/fulfillment/dispatchProjection.js +107 -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 +38 -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 +32 -0
  70. package/dist/fulfillment/migrations.js +9 -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 +32 -0
  76. package/dist/fulfillment/observationProjection.js +90 -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 +39 -0
  80. package/dist/fulfillment/omnipack/client.js +172 -0
  81. package/dist/fulfillment/omnipack/index.d.ts +55 -0
  82. package/dist/fulfillment/omnipack/index.js +408 -0
  83. package/dist/fulfillment/omnipack/mappers.d.ts +79 -0
  84. package/dist/fulfillment/omnipack/mappers.js +199 -0
  85. package/dist/fulfillment/omnipack/observation.d.ts +28 -0
  86. package/dist/fulfillment/omnipack/observation.js +72 -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/reconciliation.d.ts +15 -0
  106. package/dist/fulfillment/omnipack/reconciliation.js +55 -0
  107. package/dist/fulfillment/omnipack/trackingReferences.d.ts +16 -0
  108. package/dist/fulfillment/omnipack/trackingReferences.js +52 -0
  109. package/dist/fulfillment/paidFulfillmentRecovery.d.ts +67 -0
  110. package/dist/fulfillment/paidFulfillmentRecovery.js +154 -0
  111. package/dist/fulfillment/paidHandoff.d.ts +27 -0
  112. package/dist/fulfillment/paidHandoff.js +86 -0
  113. package/dist/fulfillment/paidHandoffPorts.d.ts +24 -0
  114. package/dist/fulfillment/paidHandoffPorts.js +3 -0
  115. package/dist/fulfillment/postgresDeliveryAlignment.d.ts +4 -0
  116. package/dist/fulfillment/postgresDeliveryAlignment.js +12 -0
  117. package/dist/fulfillment/postgresDispatchStore.d.ts +16 -0
  118. package/dist/fulfillment/postgresDispatchStore.js +44 -0
  119. package/dist/fulfillment/postgresEvidenceReadPort.d.ts +25 -0
  120. package/dist/fulfillment/postgresEvidenceReadPort.js +45 -0
  121. package/dist/fulfillment/postgresPaidFulfillmentRecovery.d.ts +25 -0
  122. package/dist/fulfillment/postgresPaidFulfillmentRecovery.js +26 -0
  123. package/dist/fulfillment/postgresPaidHandoffStore.d.ts +4 -0
  124. package/dist/fulfillment/postgresPaidHandoffStore.js +10 -0
  125. package/dist/fulfillment/postgresShipmentSpine.d.ts +70 -0
  126. package/dist/fulfillment/postgresShipmentSpine.js +246 -0
  127. package/dist/fulfillment/postgresStockSyncPort.d.ts +8 -0
  128. package/dist/fulfillment/postgresStockSyncPort.js +66 -0
  129. package/dist/fulfillment/productReconciliationEvidenceHandler.d.ts +36 -0
  130. package/dist/fulfillment/productReconciliationEvidenceHandler.js +50 -0
  131. package/dist/fulfillment/providerCapabilities.d.ts +9 -0
  132. package/dist/fulfillment/providerCapabilities.js +17 -0
  133. package/dist/fulfillment/providerCommandLedger.d.ts +143 -0
  134. package/dist/fulfillment/providerCommandLedger.js +14 -0
  135. package/dist/fulfillment/providerDispatchAcceptance.d.ts +27 -0
  136. package/dist/fulfillment/providerDispatchAcceptance.js +29 -0
  137. package/dist/fulfillment/providerDispatchError.d.ts +6 -0
  138. package/dist/fulfillment/providerDispatchError.js +5 -0
  139. package/dist/fulfillment/providerDispatchWorker.d.ts +44 -0
  140. package/dist/fulfillment/providerDispatchWorker.js +199 -0
  141. package/dist/fulfillment/providerProductSyncWorker.d.ts +28 -0
  142. package/dist/fulfillment/providerProductSyncWorker.js +122 -0
  143. package/dist/fulfillment/providerReconciliationError.d.ts +9 -0
  144. package/dist/fulfillment/providerReconciliationError.js +17 -0
  145. package/dist/fulfillment/providerReconciliationTerminalSkip.d.ts +2 -0
  146. package/dist/fulfillment/providerReconciliationTerminalSkip.js +6 -0
  147. package/dist/fulfillment/providerReconciliationTimestamp.d.ts +1 -0
  148. package/dist/fulfillment/providerReconciliationTimestamp.js +27 -0
  149. package/dist/fulfillment/providerReconciliationWorker.d.ts +90 -0
  150. package/dist/fulfillment/providerReconciliationWorker.js +207 -0
  151. package/dist/fulfillment/providerStockAvailability.d.ts +13 -0
  152. package/dist/fulfillment/providerStockAvailability.js +51 -0
  153. package/dist/fulfillment/providerStockSyncWorker.d.ts +41 -0
  154. package/dist/fulfillment/providerStockSyncWorker.js +295 -0
  155. package/dist/fulfillment/providerTrackingReferences.d.ts +16 -0
  156. package/dist/fulfillment/providerTrackingReferences.js +23 -0
  157. package/dist/fulfillment/replacementRefusals.d.ts +5 -0
  158. package/dist/fulfillment/replacementRefusals.js +38 -0
  159. package/dist/fulfillment/replacementShipmentHandler.d.ts +5 -0
  160. package/dist/fulfillment/replacementShipmentHandler.js +46 -0
  161. package/dist/fulfillment/schemaCapabilities.d.ts +13 -0
  162. package/dist/fulfillment/schemaCapabilities.js +7 -0
  163. package/dist/fulfillment/shipmentsOverviewHandler.d.ts +16 -0
  164. package/dist/fulfillment/shipmentsOverviewHandler.js +54 -0
  165. package/dist/fulfillment/statusMap.d.ts +2 -0
  166. package/dist/fulfillment/statusMap.js +1 -0
  167. package/dist/fulfillment/trackingProgress.d.ts +1 -0
  168. package/dist/fulfillment/trackingProgress.js +1 -0
  169. package/dist/fulfillment/verifiedIngress.d.ts +79 -0
  170. package/dist/fulfillment/verifiedIngress.js +94 -0
  171. package/dist/invoicing/capabilities.d.ts +11 -0
  172. package/dist/invoicing/capabilities.js +10 -0
  173. package/dist/invoicing/contracts/capabilities.d.ts +40 -0
  174. package/dist/invoicing/contracts/capabilities.js +1 -0
  175. package/dist/invoicing/contracts/companyLookup.d.ts +49 -0
  176. package/dist/invoicing/contracts/companyLookup.js +1 -0
  177. package/dist/invoicing/contracts/delivery.d.ts +74 -0
  178. package/dist/invoicing/contracts/delivery.js +1 -0
  179. package/dist/invoicing/contracts/facts.d.ts +100 -0
  180. package/dist/invoicing/contracts/facts.js +1 -0
  181. package/dist/invoicing/contracts/host.d.ts +246 -0
  182. package/dist/invoicing/contracts/host.js +1 -0
  183. package/dist/invoicing/contracts/index.d.ts +16 -0
  184. package/dist/invoicing/contracts/index.js +1 -0
  185. package/dist/invoicing/contracts/provider.d.ts +126 -0
  186. package/dist/invoicing/contracts/provider.js +1 -0
  187. package/dist/invoicing/contracts/settlement.d.ts +66 -0
  188. package/dist/invoicing/contracts/settlement.js +1 -0
  189. package/dist/invoicing/contracts/store.d.ts +267 -0
  190. package/dist/invoicing/contracts/store.js +1 -0
  191. package/dist/invoicing/index.d.ts +1 -0
  192. package/dist/invoicing/index.js +1 -0
  193. package/dist/payments/stripe/index.d.ts +25 -0
  194. package/dist/payments/stripe/index.js +25 -0
  195. package/dist/payments/tpay/index.d.ts +30 -0
  196. package/dist/payments/tpay/index.js +30 -0
  197. package/dist/subscriptions/automaticRenewal.d.ts +44 -0
  198. package/dist/subscriptions/automaticRenewal.js +81 -0
  199. package/dist/subscriptions/automaticRenewalExecution.d.ts +21 -0
  200. package/dist/subscriptions/automaticRenewalExecution.js +44 -0
  201. package/dist/subscriptions/contracts/commands.d.ts +115 -0
  202. package/dist/subscriptions/contracts/commands.js +1 -0
  203. package/dist/subscriptions/contracts/dunning.d.ts +133 -0
  204. package/dist/subscriptions/contracts/dunning.js +1 -0
  205. package/dist/subscriptions/contracts/index.d.ts +6 -0
  206. package/dist/subscriptions/contracts/index.js +1 -0
  207. package/dist/subscriptions/contracts/messages.d.ts +43 -0
  208. package/dist/subscriptions/contracts/messages.js +1 -0
  209. package/dist/subscriptions/contracts/renewal.d.ts +533 -0
  210. package/dist/subscriptions/contracts/renewal.js +1 -0
  211. package/dist/subscriptions/contracts/stockWait.d.ts +73 -0
  212. package/dist/subscriptions/contracts/stockWait.js +1 -0
  213. package/dist/subscriptions/index.d.ts +39 -0
  214. package/dist/subscriptions/index.js +17 -0
  215. package/dist/subscriptions/paymentOutcomes.d.ts +8 -0
  216. package/dist/subscriptions/paymentOutcomes.js +27 -0
  217. package/dist/subscriptions/postgresRenewalStore.d.ts +20 -0
  218. package/dist/subscriptions/postgresRenewalStore.js +61 -0
  219. package/dist/subscriptions/postgresRetryStore.d.ts +78 -0
  220. package/dist/subscriptions/postgresRetryStore.js +142 -0
  221. package/dist/subscriptions/renewalCycleFailureMechanism.d.ts +34 -0
  222. package/dist/subscriptions/renewalCycleFailureMechanism.js +79 -0
  223. package/dist/subscriptions/renewalCycleMechanism.d.ts +46 -0
  224. package/dist/subscriptions/renewalCycleMechanism.js +157 -0
  225. package/dist/subscriptions/renewalCycleRefusalMechanism.d.ts +41 -0
  226. package/dist/subscriptions/renewalCycleRefusalMechanism.js +160 -0
  227. package/dist/subscriptions/renewalIdentity.d.ts +3 -0
  228. package/dist/subscriptions/renewalIdentity.js +4 -0
  229. package/package.json +37 -2
  230. package/release-gates.json +97 -3
  231. package/sql/20260808210000_fulfillment_shipment_rail.sql +810 -0
  232. package/sql/20260819150000_fulfillment_shipment_sequence.sql +98 -0
  233. package/sql/20261022080000_fulfillment_command_ledger.sql +786 -0
  234. package/sql/20261022090000_fulfillment_delivery_alignment.sql +1106 -0
  235. package/sql/20261022160000_subscription_retry_lifecycle.sql +328 -0
  236. package/sql/20261022165000_fulfillment_stock_evidence_replay_reopen.sql +30 -0
  237. package/src/composition/composeBundle.ts +4 -0
  238. package/src/composition/fulfillmentContribution.ts +28 -0
  239. package/src/composition/index.ts +1 -0
  240. package/src/fulfillment/adapterConformance.ts +195 -0
  241. package/src/fulfillment/bindings.ts +26 -0
  242. package/src/fulfillment/commerceFulfillmentHandlers.ts +263 -0
  243. package/src/fulfillment/commerceFulfillmentReadModel.ts +257 -0
  244. package/src/fulfillment/contracts/alignment.ts +13 -0
  245. package/src/fulfillment/contracts/bindings.ts +30 -0
  246. package/src/fulfillment/contracts/capabilities.ts +15 -0
  247. package/src/fulfillment/contracts/conformance.ts +63 -0
  248. package/src/fulfillment/contracts/customerAlignment.ts +4 -0
  249. package/src/fulfillment/contracts/dispatch.ts +73 -0
  250. package/src/fulfillment/contracts/index.ts +18 -0
  251. package/src/fulfillment/contracts/label.ts +9 -0
  252. package/src/fulfillment/contracts/nativeUnsupported.ts +6 -0
  253. package/src/fulfillment/contracts/observation.ts +26 -0
  254. package/src/fulfillment/contracts/operator.ts +31 -0
  255. package/src/fulfillment/contracts/read.ts +42 -0
  256. package/src/fulfillment/contracts/readback.ts +24 -0
  257. package/src/fulfillment/contracts/replacement.ts +15 -0
  258. package/src/fulfillment/contracts/store.ts +27 -0
  259. package/src/fulfillment/contracts/sync.ts +72 -0
  260. package/src/fulfillment/currentFulfillmentParcel.ts +2 -0
  261. package/src/fulfillment/delayReadModel.ts +60 -0
  262. package/src/fulfillment/deliveryAlignmentReadModel.ts +15 -0
  263. package/src/fulfillment/dispatchProjection.ts +105 -0
  264. package/src/fulfillment/fulfillmentObservationEffects.ts +86 -0
  265. package/src/fulfillment/handlerSchemas.ts +255 -0
  266. package/src/fulfillment/handlerSupport.ts +25 -0
  267. package/src/fulfillment/index.ts +39 -0
  268. package/src/fulfillment/labelLifecycle.ts +11 -0
  269. package/src/fulfillment/lowStockEvidenceHandler.ts +60 -0
  270. package/src/fulfillment/migrations.ts +9 -0
  271. package/src/fulfillment/nativeUnsupported.ts +23 -0
  272. package/src/fulfillment/normalizedFulfillmentFacts.ts +2 -0
  273. package/src/fulfillment/observationProjection.ts +92 -0
  274. package/src/fulfillment/omnipack/capabilities.ts +18 -0
  275. package/src/fulfillment/omnipack/client.ts +270 -0
  276. package/src/fulfillment/omnipack/index.ts +369 -0
  277. package/src/fulfillment/omnipack/mappers.ts +282 -0
  278. package/src/fulfillment/omnipack/observation.ts +79 -0
  279. package/src/fulfillment/omnipack/omnipackDispatchPayload.ts +93 -0
  280. package/src/fulfillment/omnipack/omnipackOutboundOrderPayload.ts +194 -0
  281. package/src/fulfillment/omnipack/omnipackProductPackPayload.ts +47 -0
  282. package/src/fulfillment/omnipack/omnipackProductPayload.ts +67 -0
  283. package/src/fulfillment/omnipack/omnipackReconciliationPayload.ts +7 -0
  284. package/src/fulfillment/omnipack/omnipackStatusVocabulary.ts +103 -0
  285. package/src/fulfillment/omnipack/omnipackWebhookPayload.ts +73 -0
  286. package/src/fulfillment/omnipack/productSyncMappers.ts +74 -0
  287. package/src/fulfillment/omnipack/providerError.ts +64 -0
  288. package/src/fulfillment/omnipack/reconciliation.ts +56 -0
  289. package/src/fulfillment/omnipack/trackingReferences.ts +77 -0
  290. package/src/fulfillment/paidFulfillmentRecovery.ts +252 -0
  291. package/src/fulfillment/paidHandoff.ts +122 -0
  292. package/src/fulfillment/paidHandoffPorts.ts +21 -0
  293. package/src/fulfillment/postgresDeliveryAlignment.ts +14 -0
  294. package/src/fulfillment/postgresDispatchStore.ts +55 -0
  295. package/src/fulfillment/postgresEvidenceReadPort.ts +59 -0
  296. package/src/fulfillment/postgresPaidFulfillmentRecovery.ts +29 -0
  297. package/src/fulfillment/postgresPaidHandoffStore.ts +11 -0
  298. package/src/fulfillment/postgresShipmentSpine.ts +338 -0
  299. package/src/fulfillment/postgresStockSyncPort.ts +68 -0
  300. package/src/fulfillment/productReconciliationEvidenceHandler.ts +64 -0
  301. package/src/fulfillment/providerCapabilities.ts +18 -0
  302. package/src/fulfillment/providerCommandLedger.ts +53 -0
  303. package/src/fulfillment/providerDispatchAcceptance.ts +37 -0
  304. package/src/fulfillment/providerDispatchError.ts +7 -0
  305. package/src/fulfillment/providerDispatchWorker.ts +159 -0
  306. package/src/fulfillment/providerProductSyncWorker.ts +154 -0
  307. package/src/fulfillment/providerReconciliationError.ts +20 -0
  308. package/src/fulfillment/providerReconciliationTerminalSkip.ts +8 -0
  309. package/src/fulfillment/providerReconciliationTimestamp.ts +34 -0
  310. package/src/fulfillment/providerReconciliationWorker.ts +206 -0
  311. package/src/fulfillment/providerStockAvailability.ts +60 -0
  312. package/src/fulfillment/providerStockSyncWorker.ts +318 -0
  313. package/src/fulfillment/providerTrackingReferences.ts +32 -0
  314. package/src/fulfillment/replacementRefusals.ts +43 -0
  315. package/src/fulfillment/replacementShipmentHandler.ts +47 -0
  316. package/src/fulfillment/schemaCapabilities.ts +7 -0
  317. package/src/fulfillment/shipmentsOverviewHandler.ts +73 -0
  318. package/src/fulfillment/statusMap.ts +2 -0
  319. package/src/fulfillment/trackingProgress.ts +1 -0
  320. package/src/fulfillment/verifiedIngress.ts +113 -0
  321. package/src/invoicing/capabilities.ts +9 -0
  322. package/src/invoicing/contracts/capabilities.ts +15 -0
  323. package/src/invoicing/contracts/companyLookup.ts +13 -0
  324. package/src/invoicing/contracts/delivery.ts +24 -0
  325. package/src/invoicing/contracts/facts.ts +60 -0
  326. package/src/invoicing/contracts/host.ts +85 -0
  327. package/src/invoicing/contracts/index.ts +16 -0
  328. package/src/invoicing/contracts/provider.ts +78 -0
  329. package/src/invoicing/contracts/settlement.ts +39 -0
  330. package/src/invoicing/contracts/store.ts +98 -0
  331. package/src/invoicing/index.ts +1 -0
  332. package/src/payments/stripe/index.ts +30 -0
  333. package/src/payments/tpay/index.ts +36 -0
  334. package/src/subscriptions/automaticRenewal.ts +87 -0
  335. package/src/subscriptions/automaticRenewalExecution.ts +39 -0
  336. package/src/subscriptions/contracts/commands.ts +46 -0
  337. package/src/subscriptions/contracts/dunning.ts +48 -0
  338. package/src/subscriptions/contracts/index.ts +6 -0
  339. package/src/subscriptions/contracts/messages.ts +18 -0
  340. package/src/subscriptions/contracts/renewal.ts +325 -0
  341. package/src/subscriptions/contracts/stockWait.ts +34 -0
  342. package/src/subscriptions/index.ts +24 -0
  343. package/src/subscriptions/paymentOutcomes.ts +23 -0
  344. package/src/subscriptions/postgresRenewalStore.ts +76 -0
  345. package/src/subscriptions/postgresRetryStore.ts +174 -0
  346. package/src/subscriptions/renewalCycleFailureMechanism.ts +119 -0
  347. package/src/subscriptions/renewalCycleMechanism.ts +225 -0
  348. package/src/subscriptions/renewalCycleRefusalMechanism.ts +215 -0
  349. package/src/subscriptions/renewalIdentity.ts +5 -0
@@ -0,0 +1,318 @@
1
+ import { aggregateAtpCompatibleStock, emptyLocal, stockMovementWindow } from "./providerStockAvailability.js";
2
+ import type { FulfillmentStockSyncPort, FulfillmentStockSyncProvider, LocalStock, StockSnapshot, StockMovement, SyncCursor } from "./contracts/index.js";
3
+ type Threshold = Parameters<FulfillmentStockSyncPort["resolveLowStockEvidence"]>[0]["activeThresholdKinds"][number];
4
+ interface StockSyncResult {
5
+ ok: boolean; checked: number; updated: number; mismatches: number; reservationCoverage: number;
6
+ unclassified: number; lowStock: number; movements: number; replayed: number; failures: number;
7
+ providerCalls: number; readBacks: number; skipped: boolean; syncRunId: string;
8
+ lastMovementOccurredAt: string | null; reason?: string;
9
+ rejected: number; items: NonNullable<SyncCursor["items"]>[number][];
10
+ providerError?: NonNullable<SyncCursor["error"]>["providerError"];
11
+ refusal?: NonNullable<SyncCursor["refusal"]>;
12
+ }
13
+ export async function runFulfillmentStockSyncWorker(input: {
14
+ port: FulfillmentStockSyncPort;
15
+ provider: FulfillmentStockSyncProvider;
16
+ movementBatchSize: number;
17
+ staleSyncThresholdHours?: number;
18
+ now?: () => Date;
19
+ compatibility: {
20
+ syncRunId(now: Date): string; staleEvidenceSku: string; timeZone: string;
21
+ currentKey(run: string, sku: string): string; snapshotKey(run: string, sku: string): string;
22
+ lowStockKey(sku: string, threshold: Threshold): string; staleKey(lastSyncedAt: string): string;
23
+ };
24
+ }): Promise<StockSyncResult> {
25
+ const now = input.now?.() ?? new Date();
26
+ const staleThresholdHours = input.staleSyncThresholdHours && input.staleSyncThresholdHours > 0
27
+ ? input.staleSyncThresholdHours
28
+ : 6;
29
+ const syncRunId = input.compatibility.syncRunId(now);
30
+ const result: StockSyncResult = {
31
+ ok: true,
32
+ rejected: 0,
33
+ items: [],
34
+ checked: 0,
35
+ updated: 0,
36
+ mismatches: 0,
37
+ reservationCoverage: 0,
38
+ unclassified: 0,
39
+ lowStock: 0,
40
+ movements: 0,
41
+ replayed: 0,
42
+ failures: 0,
43
+ providerCalls: 0,
44
+ readBacks: 0,
45
+ skipped: false,
46
+ syncRunId,
47
+ lastMovementOccurredAt: null,
48
+ };
49
+
50
+ const alerts: NonNullable<SyncCursor["alerts"]>[number][] = [];
51
+ let stage = "cursor_read_failed";
52
+ let operationalReason: string | undefined;
53
+ let staleSync: SyncCursor["staleSync"];
54
+ let recoverStaleSync = false;
55
+ let previousCursor: SyncCursor | null = null;
56
+ let rejectedStockResponse = false;
57
+ const currentCoverageGapSkus = new Set<string>();
58
+ try {
59
+ previousCursor = await input.port.readCursor();
60
+
61
+ // Stale-sync alert: if the last successful sync aged past the threshold, open a stale_sync
62
+ // low-stock evidence row (evidence/warn — never blocks sales); a healthy run resolves it
63
+ // below. A never-synced state (no prior cursor) is not "stale".
64
+ const lastSyncedMs = previousCursor?.lastStockSyncedAt ? Date.parse(previousCursor.lastStockSyncedAt) : null;
65
+ const staleGapHours =
66
+ lastSyncedMs !== null && !Number.isNaN(lastSyncedMs) ? (now.getTime() - lastSyncedMs) / 3_600_000 : null;
67
+ const syncIsStale = staleGapHours !== null && staleGapHours >= staleThresholdHours;
68
+ recoverStaleSync = !syncIsStale;
69
+ if (syncIsStale) {
70
+ staleSync = { gapHours: Math.round(staleGapHours! * 10) / 10, thresholdHours: staleThresholdHours, detectedAt: now.toISOString() };
71
+ stage = "stock_evidence_write_failed";
72
+ alerts.push({ skuRef: input.compatibility.staleEvidenceSku, thresholdKind: "stale_sync", severity: "warning",
73
+ providerForSaleQuantity: 0, localAvailableQuantity: 0, ...staleSync });
74
+ await input.port.recordLowStockEvidence({
75
+ requestKey: input.compatibility.staleKey(previousCursor!.lastStockSyncedAt!),
76
+ skuRef: input.compatibility.staleEvidenceSku,
77
+ thresholdKind: "stale_sync",
78
+ severity: "warning",
79
+ providerForSaleQuantity: 0,
80
+ localAvailableQuantity: 0,
81
+ syncRunId,
82
+ });
83
+ }
84
+
85
+ stage = "cursor_write_failed";
86
+ await input.port.recordCursor({
87
+ syncRunId,
88
+ status: "running",
89
+ lastStockSyncedAt: null,
90
+ lastMovementOccurredAt: null,
91
+ continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
92
+ coverageGapSkus: previousCursor?.coverageGapSkus ?? [], refusal: null,
93
+ });
94
+
95
+ stage = "stock_read_failed";
96
+ result.providerCalls += 1;
97
+ const [localStock, providerStock] = await Promise.all([
98
+ input.port.readLocalInventoryStock(now.toISOString()),
99
+ input.provider.getStock(),
100
+ ]);
101
+ const stockItems: { stock: StockSnapshot; itemIndex: number }[] = [];
102
+ for (const [itemIndex, item] of providerStock.entries()) {
103
+ if ("kind" in item && item.kind === "rejected") {
104
+ result.rejected += 1;
105
+ result.failures += 1;
106
+ result.items.push({ kind: "stock", itemIndex: item.itemIndex, skuRef: null, status: "rejected", reason: item.reason });
107
+ } else if ("skuRef" in item && item.skuRef.trim()) {
108
+ stockItems.push({ stock: item, itemIndex });
109
+ }
110
+ }
111
+ const skus = [...new Set(stockItems.map(({ stock }) => stock.skuRef))];
112
+ rejectedStockResponse = providerStock.length > 0 && stockItems.length === 0;
113
+ stage = "stock_inventory_read_failed";
114
+ const [inventoryClasses, activeReservations] = await Promise.all([
115
+ input.port.readSkuInventoryClasses(skus),
116
+ input.port.readActiveReservations(skus, now.toISOString()),
117
+ ]);
118
+
119
+ stage = "stock_movements_read_failed";
120
+ result.providerCalls += 1;
121
+ const movementRows = await input.provider.getStockMovements({ ...stockMovementWindow(previousCursor, now, input.compatibility.timeZone), size: input.movementBatchSize });
122
+ const movements: StockMovement[] = [];
123
+ for (const item of movementRows) {
124
+ if ("kind" in item && item.kind === "rejected") {
125
+ result.rejected += 1;
126
+ result.failures += 1;
127
+ result.items.push({ kind: "movement", itemIndex: item.itemIndex, skuRef: null, status: "rejected", reason: item.reason });
128
+ } else if ("skuRef" in item) {
129
+ movements.push(item);
130
+ }
131
+ }
132
+ if (rejectedStockResponse || (result.rejected > 0 && stockItems.length + movements.length === 0)) {
133
+ result.ok = false;
134
+ result.reason = "invalid_sku_ref";
135
+ }
136
+ result.movements = movements.length;
137
+ result.lastMovementOccurredAt = latestOccurredAt(movements);
138
+
139
+ const localBySku = aggregateAtpCompatibleStock(localStock, now);
140
+ const coverageCandidates: Array<{
141
+ stock: StockSnapshot;
142
+ local: LocalStock;
143
+ activeReserved: number;
144
+ }> = [];
145
+ const packagingSkus = new Set<string>();
146
+ const previousCoverageGapSkus = previousCursor?.status === "succeeded"
147
+ ? readStringSet(previousCursor.coverageGapSkus)
148
+ : new Set<string>();
149
+
150
+ for (const { stock, itemIndex } of stockItems) {
151
+ try {
152
+ const local = localBySku.get(stock.skuRef) ?? emptyLocal(stock.skuRef);
153
+ const inventoryClass = inventoryClasses.get(stock.skuRef) ?? null;
154
+ const activeReserved = activeReservations.get(stock.skuRef) ?? 0;
155
+ const staleAfter = new Date(now.getTime() + staleThresholdHours * 3_600_000).toISOString();
156
+ stage = "stock_current_write_failed";
157
+ const current = await input.port.recordProviderStockCurrent({
158
+ requestKey: input.compatibility.currentKey(syncRunId, stock.skuRef),
159
+ snapshot: stock, inventoryClass, lastSyncedAt: now.toISOString(), staleAfter,
160
+ });
161
+ stage = "snapshot_write_failed";
162
+ const snapshot = await input.port.recordStockSnapshot({
163
+ requestKey: input.compatibility.snapshotKey(syncRunId, stock.skuRef),
164
+ snapshot: stock, local, mismatchKind: "none", inventoryClass,
165
+ });
166
+
167
+ result.items.push({ itemIndex, skuRef: stock.skuRef, status: "recorded", activeReserved,
168
+ sellableNow: inventoryClass === "sellable" ? Math.max(0, stock.forSaleQuantity - activeReserved - local.safetyStock) : null,
169
+ coverageGapQuantity: Math.max(0, activeReserved - stock.forSaleQuantity) });
170
+ result.checked += 1;
171
+ result.updated += (snapshot.replayed ? 0 : 1) + (current.replayed ? 0 : 1);
172
+ if (snapshot.replayed) result.replayed += 1;
173
+ if (current.replayed) result.replayed += 1;
174
+ if (snapshot.readBack) result.readBacks += 1;
175
+ if (current.readBack) result.readBacks += 1;
176
+ if (inventoryClass === null) result.unclassified += 1;
177
+ if (inventoryClass === "packaging") packagingSkus.add(stock.skuRef);
178
+ if (inventoryClass === "sellable") {
179
+ coverageCandidates.push({ stock, local, activeReserved });
180
+ if (activeReserved > stock.forSaleQuantity) currentCoverageGapSkus.add(stock.skuRef);
181
+ }
182
+ } catch (error) {
183
+ result.ok = false;
184
+ result.failures += 1;
185
+ const reason = safeReason(error, stage);
186
+ operationalReason ??= reason;
187
+ result.items.push({ itemIndex, skuRef: stock.skuRef, status: "failed", reason });
188
+ }
189
+ }
190
+
191
+ if (result.ok) {
192
+ for (const { stock, local, activeReserved } of coverageCandidates) {
193
+ const localAvailable = availableQuantity(local);
194
+ const confirmedCoverageGap = currentCoverageGapSkus.has(stock.skuRef) && previousCoverageGapSkus.has(stock.skuRef);
195
+ const lowStockSignals = lowStockSignalsFor(stock, local, activeReserved, confirmedCoverageGap);
196
+ const activeThresholdKinds: Threshold[] = lowStockSignals.map((signal) => signal.thresholdKind);
197
+ if (currentCoverageGapSkus.has(stock.skuRef) && !activeThresholdKinds.includes("reservation_coverage")) {
198
+ activeThresholdKinds.push("reservation_coverage");
199
+ }
200
+ for (const signal of lowStockSignals) {
201
+ alerts.push({ skuRef: stock.skuRef, thresholdKind: signal.thresholdKind, severity: signal.severity,
202
+ providerForSaleQuantity: stock.forSaleQuantity, localAvailableQuantity: localAvailable, detectedAt: now.toISOString(),
203
+ activeReserved, coverageGapQuantity: Math.max(0, activeReserved - stock.forSaleQuantity) });
204
+ stage = "stock_evidence_write_failed";
205
+ const evidence = await input.port.recordLowStockEvidence({
206
+ requestKey: input.compatibility.lowStockKey(stock.skuRef, signal.thresholdKind),
207
+ skuRef: stock.skuRef, thresholdKind: signal.thresholdKind, severity: signal.severity,
208
+ providerForSaleQuantity: stock.forSaleQuantity, localAvailableQuantity: localAvailable, syncRunId,
209
+ });
210
+ if (evidence.replayed) result.replayed += 1;
211
+ result.lowStock += evidence.replayed ? 0 : 1;
212
+ if (signal.thresholdKind === "reservation_coverage") result.reservationCoverage += 1;
213
+ }
214
+ stage = "stock_evidence_resolve_failed";
215
+ await input.port.resolveLowStockEvidence({
216
+ skuRef: stock.skuRef,
217
+ activeThresholdKinds,
218
+ syncRunId,
219
+ });
220
+ }
221
+ for (const sku of packagingSkus) {
222
+ stage = "stock_evidence_resolve_failed";
223
+ await input.port.resolveLowStockEvidence({
224
+ skuRef: sku,
225
+ activeThresholdKinds: [],
226
+ syncRunId,
227
+ });
228
+ }
229
+ }
230
+ } catch (error) {
231
+ result.ok = false;
232
+ result.failures += 1;
233
+ result.providerError = providerDiagnostic(error);
234
+ if (stage === "stock_read_failed" || stage === "stock_movements_read_failed") result.refusal = admissionRefusal(error);
235
+ operationalReason ??= result.providerError ? `provider_${result.providerError.operation}_failed:${result.providerError.code}` : result.refusal?.reason ?? safeReason(error, stage);
236
+ }
237
+
238
+ result.reason = operationalReason ?? result.reason;
239
+ if (!result.ok) result.refusal ??= { reason: result.failures === result.rejected ? "malformed" : "io_failure", phase: "sync", retryable: result.providerError?.retryable ?? result.failures > result.rejected };
240
+ await input.port.recordCursor({
241
+ status: result.ok ? "succeeded" : "failed", syncRunId,
242
+ lastStockSyncedAt: result.ok ? now.toISOString() : null,
243
+ lastMovementOccurredAt: result.lastMovementOccurredAt,
244
+ continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
245
+ coverageGapSkus: result.ok ? [...currentCoverageGapSkus].sort() : rejectedStockResponse ? previousCursor?.coverageGapSkus ?? [] : [],
246
+ refusal: result.refusal ?? null,
247
+ error: result.ok ? null : { reason: result.reason!, ...(result.providerError ? { providerError: result.providerError } : {}) },
248
+ counts: { checked: result.checked, updated: result.updated, movements: result.movements, rejected: result.rejected,
249
+ failures: result.failures, providerCalls: result.providerCalls, readBacks: result.readBacks },
250
+ items: result.items, alerts, staleSync,
251
+ });
252
+ // Recovery follows the durable succeeded cursor; a failed cleanup leaves the completed run intact.
253
+ if (result.ok && recoverStaleSync) {
254
+ try {
255
+ await input.port.resolveLowStockEvidence({
256
+ skuRef: input.compatibility.staleEvidenceSku,
257
+ activeThresholdKinds: [],
258
+ syncRunId,
259
+ });
260
+ } catch { /* Best-effort cleanup must not undo the completed run. */ }
261
+ }
262
+ return result;
263
+ }
264
+
265
+ function lowStockSignalsFor(
266
+ stock: StockSnapshot,
267
+ local: LocalStock,
268
+ activeReserved: number,
269
+ confirmedCoverageGap: boolean,
270
+ ): Array<{ thresholdKind: "safety_stock" | "reservation_coverage"; severity: "warning" | "critical" }> {
271
+ const signals: Array<{ thresholdKind: "safety_stock" | "reservation_coverage"; severity: "warning" | "critical" }> = [];
272
+ const remainingAfterReservations = Math.max(0, stock.forSaleQuantity - activeReserved);
273
+ if (local.safetyStock > 0 && remainingAfterReservations <= local.safetyStock) {
274
+ signals.push({ thresholdKind: "safety_stock", severity: remainingAfterReservations <= 0 ? "critical" : "warning" });
275
+ }
276
+ if (confirmedCoverageGap) signals.push({ thresholdKind: "reservation_coverage", severity: "critical" });
277
+ return signals;
278
+ }
279
+
280
+ function readStringSet(value: unknown): Set<string> {
281
+ return new Set(Array.isArray(value) ? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0) : []);
282
+ }
283
+
284
+ function availableQuantity(balance: LocalStock): number {
285
+ return Math.max(0, balance.onHand - balance.reserved - balance.unavailable - balance.safetyStock);
286
+ }
287
+
288
+ function latestOccurredAt(movements: StockMovement[]): string | null {
289
+ return movements
290
+ .map((movement) => movement.occurredAt)
291
+ .filter((value): value is string => Boolean(value))
292
+ .sort((a, b) => Date.parse(b) - Date.parse(a))[0] ?? null;
293
+ }
294
+
295
+ /** Keep the operation's closed identity and SQLSTATE only, never arbitrary exception prose. */
296
+ function safeReason(error: unknown, operation: string): string {
297
+ const code = error && typeof error === "object" && "code" in error ? error.code : null;
298
+ if (typeof code === "string" && /^[0-9A-Z]{5}$/.test(code)) return `${operation}:${code}`;
299
+ const known = error instanceof Error ? error.message.match(new RegExp(`^${operation}:([0-9A-Z]{5})$`)) : null;
300
+ return known ? `${operation}:${known[1]}` : operation;
301
+ }
302
+ function providerDiagnostic(error: unknown): StockSyncResult["providerError"] {
303
+ if (!error || typeof error !== "object" || !("providerError" in error)) return undefined;
304
+ const detail = error.providerError;
305
+ if (!detail || typeof detail !== "object" || !("operation" in detail) || !("code" in detail) || !("status" in detail) || !("retryable" in detail)) return undefined;
306
+ if (detail.operation !== "get_stock" && detail.operation !== "get_stock_movements") return undefined;
307
+ if (typeof detail.code !== "string" || !/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(detail.code)) return undefined;
308
+ return { operation: detail.operation, code: detail.code,
309
+ status: typeof detail.status === "number" && Number.isInteger(detail.status) && detail.status >= 100 && detail.status <= 599 ? detail.status : null,
310
+ retryable: detail.retryable === true };
311
+ }
312
+
313
+ function admissionRefusal(error: unknown): StockSyncResult["refusal"] {
314
+ if (!error || typeof error !== "object" || !("reason" in error) || !("phase" in error) || !("retryable" in error)) return undefined;
315
+ if (error.phase !== "sync" || typeof error.retryable !== "boolean") return undefined;
316
+ if (error.reason !== "disabled" && error.reason !== "not_ready" && error.reason !== "malformed") return undefined;
317
+ return { reason: error.reason, phase: error.phase, retryable: error.retryable };
318
+ }
@@ -0,0 +1,32 @@
1
+ import type { FulfillmentTrackingReference, FulfillmentObservationCodecs } from "./contracts/index.js";
2
+
3
+ export function trackingReferenceComplete(ref: FulfillmentTrackingReference): boolean {
4
+ return Boolean(ref.carrierRef && ref.publicUrl && ref.serviceRef);
5
+ }
6
+ /** Provider evidence wins as a unit; private carrier/URL codecs stay injected. */
7
+ export function enrichTrackingReference(
8
+ ref: FulfillmentTrackingReference,
9
+ deliveryCarrier: { carrierRef: string | null; serviceRef: string | null } | null,
10
+ codecs: FulfillmentObservationCodecs & {
11
+ approveTrackingUrl?(url: string): boolean;
12
+ normalizeCarrier(input: { carrierRef: string | null; serviceRef: string | null }): string | null;
13
+ publicUrl(carrierRef: string | null, trackingNumber: string): string | null;
14
+ },
15
+ ): FulfillmentTrackingReference {
16
+ const providerCarrier = ref.carrierRef ?? codecs.normalizeCarrier(ref);
17
+ const carrierRef = providerCarrier ?? codecs.normalizeCarrier({ carrierRef: deliveryCarrier?.carrierRef ?? null, serviceRef: deliveryCarrier?.serviceRef ?? null });
18
+ return { trackingNumber: ref.trackingNumber, publicUrl: ref.publicUrl ?? codecs.publicUrl(carrierRef, ref.trackingNumber),
19
+ carrierRef, serviceRef: ref.serviceRef ?? (providerCarrier ? null : deliveryCarrier?.serviceRef ?? null) };
20
+ }
21
+ /** First occurrence wins, including its selection, after trimming the number. */
22
+ export function uniqueTrackingReferences(refs: readonly FulfillmentTrackingReference[]): FulfillmentTrackingReference[] {
23
+ const seen = new Set<string>();
24
+ const result: FulfillmentTrackingReference[] = [];
25
+ for (const ref of refs) {
26
+ const trackingNumber = ref.trackingNumber.trim();
27
+ if (!trackingNumber || seen.has(trackingNumber)) continue;
28
+ seen.add(trackingNumber);
29
+ result.push({ trackingNumber, publicUrl: ref.publicUrl ?? null, carrierRef: ref.carrierRef ?? null, serviceRef: ref.serviceRef ?? null });
30
+ }
31
+ return result;
32
+ }
@@ -0,0 +1,43 @@
1
+ export const FULFILLMENT_REPLACEMENT_COMMAND_REFUSALS = [
2
+ "invalid_input",
3
+ "idempotency_conflict",
4
+ "order_not_found",
5
+ "order_not_replaceable",
6
+ "missing_client",
7
+ "missing_shipping_address",
8
+ "shipping_address_not_found",
9
+ "payment_not_succeeded",
10
+ "subscription_cycle_not_paid",
11
+ "no_parcel_to_replace",
12
+ "undispatched_replacement_exists",
13
+ "missing_order_items",
14
+ "missing_catalog_sku",
15
+ "missing_inventory_reservation",
16
+ "blocked_by_payment_not_succeeded_hold",
17
+ "blocked_by_inventory_review_hold",
18
+ "blocked_by_risk_review_hold",
19
+ "blocked_by_address_review_hold",
20
+ "blocked_by_manual_support_hold",
21
+ ] as const;
22
+
23
+ export const FULFILLMENT_REPLACEMENT_RESERVATION_REFUSALS = [
24
+ "inventory_external_provider_insufficient_available_stock",
25
+ "inventory_external_provider_stock_stale",
26
+ "inventory_external_provider_stock_missing",
27
+ "inventory_external_provider_sku_not_found",
28
+ "inventory_external_provider_location_missing",
29
+ "inventory_reservation_insufficient_available_stock",
30
+ "inventory_reservation_allocation_incomplete",
31
+ "inventory_reservation_payment_status_not_reservable",
32
+ "inventory_reservation_invalid_quantity",
33
+ ] as const;
34
+
35
+ export const UNSPECIFIED_FULFILLMENT_REPLACEMENT_REFUSAL = "unspecified_refusal";
36
+
37
+ export const FULFILLMENT_REPLACEMENT_REFUSALS = [
38
+ ...FULFILLMENT_REPLACEMENT_COMMAND_REFUSALS,
39
+ ...FULFILLMENT_REPLACEMENT_RESERVATION_REFUSALS,
40
+ UNSPECIFIED_FULFILLMENT_REPLACEMENT_REFUSAL,
41
+ ] as const;
42
+
43
+ export type FulfillmentReplacementRefusal = (typeof FULFILLMENT_REPLACEMENT_REFUSALS)[number];
@@ -0,0 +1,47 @@
1
+ import { adminCommerceOrderRequestReplacementShipmentRequestSchema, adminCommerceOrderRequestReplacementShipmentResponseSchema } from './handlerSchemas.js';
2
+ import { authorize, CommerceFulfillmentConflictError, isNativeUnsupported, type BaseDeps, type FulfillmentHandlerRequest, type FulfillmentHandlerResponder } from './handlerSupport.js';
3
+ import type { FulfillmentReplacementPort } from './contracts/replacement.js';
4
+ export function createAdminCommerceOrderRequestReplacementShipmentHandler<Response>({
5
+ authorizeAdmin,
6
+ respond,
7
+ omsPort,
8
+ }: BaseDeps<Response> & {
9
+ omsPort: FulfillmentReplacementPort;
10
+ }) {
11
+ const { sendMethodNotAllowed } = respond;
12
+ return async function handler(req: FulfillmentHandlerRequest, res: Response): Promise<void> {
13
+ if (req.method !== "POST") return sendMethodNotAllowed(res, ["POST"]);
14
+ const auth = await authorize(req, res, authorizeAdmin, respond);
15
+ if (!auth) return;
16
+
17
+ const request = parseOmsRequest(res, respond, adminCommerceOrderRequestReplacementShipmentRequestSchema, req.body ?? {}, "Invalid admin commerce replacement shipment request");
18
+ if (request === undefined) return;
19
+
20
+ await sendOmsMutationResponse(
21
+ res,
22
+ respond,
23
+ () => omsPort.requestReplacementShipment({ ...request, actorUserId: auth.userId }),
24
+ adminCommerceOrderRequestReplacementShipmentResponseSchema,
25
+ "Admin commerce replacement shipment returned invalid response",
26
+ "Admin commerce replacement shipment failed",
27
+ );
28
+ };
29
+ }
30
+
31
+ function parseOmsRequest<Response, T>(res: Response, respond: FulfillmentHandlerResponder<Response>, schema: { safeParse(input: unknown): { success: true; data: T } | { success: false; error: { flatten(): unknown } } }, input: unknown, message: string): T | undefined {
32
+ const request = schema.safeParse(input);
33
+ if (!request.success) { respond.sendBffError(res, 'BAD_REQUEST', message, { details: request.error.flatten() }); return undefined; }
34
+ return request.data;
35
+ }
36
+ async function sendOmsMutationResponse<Response>(res: Response, respond: FulfillmentHandlerResponder<Response>, action: () => Promise<unknown>, schema: typeof adminCommerceOrderRequestReplacementShipmentResponseSchema, invalidMessage: string, failureMessage: string) {
37
+ try {
38
+ const result = await action();
39
+ if (isNativeUnsupported(result)) { respond.sendBffError(res, 'UPSTREAM_UNAVAILABLE', 'Native fulfillment operation unsupported', { details: result }); return; }
40
+ const response = schema.safeParse(result);
41
+ if (!response.success) { respond.sendBffError(res, 'INVALID_RESPONSE', invalidMessage); return; }
42
+ respond.sendBffSuccess(res, response.data);
43
+ } catch (error) {
44
+ if (error instanceof CommerceFulfillmentConflictError) { respond.sendBffError(res, 'CONFLICT', error.message, { details: error.details }); return; }
45
+ respond.sendBffError(res, 'UPSTREAM_UNAVAILABLE', failureMessage);
46
+ }
47
+ }
@@ -0,0 +1,7 @@
1
+ /** Sparse projections require the full canonical graph and a trusted native binding. */
2
+ export const FULFILLMENT_NATIVE_CAPABILITIES = {
3
+ shipment: 'supported', commandFence: 'supported', deliveryAlignment: 'supported',
4
+ stockEvidence: 'supported', observationPersistence: 'supported',
5
+ localInventoryBalances: 'host_required', dispatchContactPreparation: 'host_required',
6
+ productPacks: 'unsupported', providerAccess: 'unsupported', automaticReplacementConfirmation: 'unsupported',
7
+ } as const;
@@ -0,0 +1,73 @@
1
+ import { isNativeUnsupported, type FulfillmentHandlerRequest, type FulfillmentHandlerResponder } from './handlerSupport.js';
2
+ import { z } from 'zod';
3
+ import type { FulfillmentShipmentsOverviewPort, FulfillmentShipmentsOverviewResponse } from './contracts/read.js';
4
+
5
+ const adminShipmentsOverviewRequestSchema = z.object({
6
+ shippedFilter: z.enum(['today', '7d', '30d', 'all']).default('7d'),
7
+ });
8
+
9
+ export interface FulfillmentShipmentsOverviewHandlerDeps<Response> {
10
+ respond: FulfillmentHandlerResponder<Response>;
11
+ overviewPort: FulfillmentShipmentsOverviewPort;
12
+ responseSchema: { safeParse(input: unknown): { success: true; data: FulfillmentShipmentsOverviewResponse } | { success: false } };
13
+ authorizeAdmin: (req: FulfillmentHandlerRequest) => Promise<boolean>;
14
+ }
15
+
16
+ export function createFulfillmentShipmentsOverviewHandler<Response>({
17
+ respond,
18
+ overviewPort,
19
+ responseSchema: adminShipmentsOverviewResponseSchema,
20
+ authorizeAdmin,
21
+ }: FulfillmentShipmentsOverviewHandlerDeps<Response>) {
22
+ const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = respond;
23
+ return async function handler(req: FulfillmentHandlerRequest, res: Response): Promise<void> {
24
+ if (req.method !== "GET") {
25
+ sendMethodNotAllowed(res, ["GET"]);
26
+ return;
27
+ }
28
+
29
+ let authorized = false;
30
+ try {
31
+ authorized = await authorizeAdmin(req);
32
+ } catch {
33
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed");
34
+ return;
35
+ }
36
+
37
+ if (!authorized) {
38
+ sendBffError(res, "UNAUTHORIZED", "Admin session required");
39
+ return;
40
+ }
41
+
42
+ const request = adminShipmentsOverviewRequestSchema.safeParse({
43
+ shippedFilter: scalarQuery(req.query.shippedFilter),
44
+ });
45
+ if (!request.success) {
46
+ sendBffError(res, "BAD_REQUEST", "Invalid shipments overview request", {
47
+ details: request.error.flatten(),
48
+ });
49
+ return;
50
+ }
51
+
52
+ try {
53
+ const result = await overviewPort.getShipmentsOverview(request.data);
54
+ if (isNativeUnsupported(result)) {
55
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Native fulfillment operation unsupported", { details: result });
56
+ return;
57
+ }
58
+ const response = adminShipmentsOverviewResponseSchema.safeParse(result);
59
+ if (!response.success) {
60
+ sendBffError(res, "INVALID_RESPONSE", "Shipments overview returned invalid response");
61
+ return;
62
+ }
63
+
64
+ sendBffSuccess(res, response.data);
65
+ } catch {
66
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Shipments overview read failed");
67
+ }
68
+ };
69
+ }
70
+
71
+ function scalarQuery(value: string | string[] | undefined): string | undefined {
72
+ return Array.isArray(value) ? value[0] : value;
73
+ }
@@ -0,0 +1,2 @@
1
+ export { FULFILLMENT_STATUS_MAP, CANONICAL_LOCAL_FULFILLMENT_STATUSES, CUSTOMER_STEP_SEQUENCE, canonicalStatusToken, stepForToken, customerStepFromSignals, phaseIndexForStep, stepLabelKey, timelineLabelForStep, trackingPhaseKeyForStep, stepUsesTruckIcon } from "@openlup/core/fulfillment";
2
+ export type { FulfillmentStatusStage, FulfillmentStatusMap } from "@openlup/core/fulfillment";
@@ -0,0 +1 @@
1
+ export { fulfillmentProgressPresentation } from "@openlup/core/fulfillment";
@@ -0,0 +1,113 @@
1
+ import type { FulfillmentObservation, FulfillmentObservationCodecs, FulfillmentObservationPort, FulfillmentRefusal } from "./contracts/index.js";
2
+ import { safeDiagnosticCode } from "./dispatchProjection.js";
3
+ import { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
4
+ import { observationStatusPolicy, projectObservation } from "./observationProjection.js";
5
+ import { uniqueTrackingReferences } from "./providerTrackingReferences.js";
6
+
7
+ type EffectInput = Parameters<typeof applyFulfillmentObservationEffects<FulfillmentObservation["fact"]["canonicalStatus"], "in_transit" | "delivered">>[0];
8
+ type DispatchRef = { orderRef: string; fulfillmentRef: string; dispatchRef: string; externalRef: string | null };
9
+ export interface FulfillmentIngressCounts { invalidTimestamps: number; rejectedCorrelationRefs: number; rejectedTrackingRefs: number; rejectedTrackingUrls: number }
10
+ export type FulfillmentIngressResult = { counts: FulfillmentIngressCounts } & (
11
+ | { kind: "refused"; receiptId: null; refusal: FulfillmentRefusal; diagnostics?: { invalidTimestamp?: true; rejectedCorrelationRefs?: number } }
12
+ | { kind: "processed" | "ignored" | "superseded"; receiptId: string; observation: FulfillmentObservation; replayed: boolean; trackingRefs: number; trackingReadBacks: number; supersededCause?: string });
13
+ /** Safe response classes only; the host owns HTTP and localized message text. */
14
+ export function fulfillmentIngressHttpResult(result: FulfillmentIngressResult): { status: 200 | 400 | 403 | 503; messageClass: "accepted" | "bad_request" | "forbidden" | "upstream_unavailable" } {
15
+ if (result.kind !== "refused") return { status: 200, messageClass: "accepted" };
16
+ if (result.refusal.reason === "untrusted") return { status: 403, messageClass: "forbidden" };
17
+ if (result.refusal.reason === "disabled" || result.refusal.reason === "not_ready" || result.refusal.retryable)
18
+ return { status: 503, messageClass: "upstream_unavailable" };
19
+ return { status: 400, messageClass: "bad_request" };
20
+ }
21
+ /** Adapter verification owns auth-before-parse; the host owns durable replay. */
22
+ export async function verifyAndNormalizeFulfillmentObservation(input: {
23
+ enabled: boolean;
24
+ approveTrackingUrl?(url: string): boolean;
25
+ observationCodecs?: FulfillmentObservationCodecs;
26
+ provider: FulfillmentObservationPort;
27
+ verification: Parameters<FulfillmentObservationPort["verifyAndNormalize"]>[0];
28
+ localPersistence: {
29
+ /** G17: atomic insert by eventKey; retries keep the first stored firstReceivedAt. */
30
+ recordObservation(observation: FulfillmentObservation): Promise<{ replayed: boolean; receiptId?: string }>;
31
+ /** Idempotent; only called after completion or a classified superseding conflict. */
32
+ markProcessed(eventKey: string, outcome: { status: "processed" | "ignored"; refusal?: FulfillmentRefusal; supersededCause?: string }): Promise<void>;
33
+ };
34
+ lifecycle: {
35
+ findDispatchRef(observation: FulfillmentObservation): Promise<DispatchRef | null>;
36
+ acknowledgeDispatchAcceptance(ref: DispatchRef, observation: FulfillmentObservation): Promise<void>;
37
+ recordStatusEvidence(ref: DispatchRef, observation: FulfillmentObservation): Promise<{ replayed: boolean }>;
38
+ effects: EffectInput["port"];
39
+ keys: { stock(ref: string): string; handoff(ref: string): string; tracking(eventKey: string, number: string): string };
40
+ issueAccountingInvoice?(ref: string): Promise<void>;
41
+ /** Return a machine identity, never exception prose. */
42
+ supersededCause?(error: unknown): string | null;
43
+ classifyFailure(error: unknown): "stock_contention" | "state_conflict" | "other";
44
+ annotateStockContention?(eventKey: string, fulfillmentRef: string): Promise<void>;
45
+ };
46
+ }): Promise<FulfillmentIngressResult> {
47
+ const counts: FulfillmentIngressCounts = { invalidTimestamps: 0, rejectedCorrelationRefs: 0, rejectedTrackingRefs: 0, rejectedTrackingUrls: 0 };
48
+ if (!input.enabled) return { kind: "refused", receiptId: null, counts, refusal: { reason: "disabled", phase: "verify", retryable: false } };
49
+ const verified = await input.provider.verifyAndNormalize(input.verification);
50
+ if (verified.kind === "refused") {
51
+ if (verified.diagnostics?.invalidTimestamp) counts.invalidTimestamps++;
52
+ const rejected = verified.diagnostics?.rejectedCorrelationRefs;
53
+ if (typeof rejected === "number" && Number.isSafeInteger(rejected) && rejected > 0) counts.rejectedCorrelationRefs = rejected;
54
+ return { kind: "refused", receiptId: null, refusal: verified.refusal, counts,
55
+ ...(counts.invalidTimestamps || counts.rejectedCorrelationRefs ? { diagnostics: {
56
+ ...(counts.invalidTimestamps ? { invalidTimestamp: true as const } : {}),
57
+ ...(counts.rejectedCorrelationRefs ? { rejectedCorrelationRefs: counts.rejectedCorrelationRefs } : {}),
58
+ } } : {}) };
59
+ }
60
+ const observation = projectObservation(verified.observation, input.approveTrackingUrl, input.observationCodecs);
61
+ counts.rejectedTrackingRefs = observation.diagnostics?.admission?.rejectedTrackingRefs ?? 0;
62
+ counts.rejectedTrackingUrls = observation.diagnostics?.admission?.rejectedTrackingUrls ?? 0;
63
+ const inbound = await input.localPersistence.recordObservation(observation);
64
+ const receiptId = inbound.receiptId ?? observation.eventKey;
65
+ const ref = await input.lifecycle.findDispatchRef(observation);
66
+ if (!ref) {
67
+ await input.localPersistence.markProcessed(observation.eventKey, { status: "ignored", refusal: { reason: "local_missing", phase: "apply", retryable: false } });
68
+ return { kind: "ignored", receiptId, counts, observation, replayed: inbound.replayed, trackingRefs: 0, trackingReadBacks: 0 };
69
+ }
70
+ const externalRef = observation.correlation.externalRef?.trim() || ref.externalRef?.trim();
71
+ if (!externalRef) throw new TypeError("fulfillment_dispatch_acceptance_external_ref_missing");
72
+ await input.lifecycle.acknowledgeDispatchAcceptance(ref, observation);
73
+ const statusEvidence = await input.lifecycle.recordStatusEvidence(ref, observation);
74
+ let trackingRefs = 0;
75
+ let trackingReadBacks = 0;
76
+ const effectPort: EffectInput["port"] = {
77
+ ...input.lifecycle.effects,
78
+ async markProviderStockConsumed(request) {
79
+ try { return await input.lifecycle.effects.markProviderStockConsumed(request); }
80
+ catch (error) {
81
+ if (input.lifecycle.classifyFailure(error) === "stock_contention") {
82
+ try {
83
+ await input.lifecycle.annotateStockContention?.(observation.eventKey, ref.fulfillmentRef);
84
+ } catch { /* Annotation is best-effort; preserve the original stock failure. */ }
85
+ }
86
+ throw error;
87
+ }
88
+ },
89
+ };
90
+ const localStatus = observation.fact.canonicalStatus;
91
+ const effects: EffectInput = {
92
+ port: effectPort, localStatus, ids: { orderId: ref.orderRef, fulfillmentOrderId: ref.fulfillmentRef }, observation,
93
+ trackingRefs: uniqueTrackingReferences(observation.trackingRefs).map(tracking => ({ trackingNumber: tracking.trackingNumber, trackingUrl: tracking.publicUrl, carrierKind: tracking.carrierRef, service: tracking.serviceRef })),
94
+ stockIdempotencyKey: input.lifecycle.keys.stock(ref.fulfillmentRef), handoffIdempotencyKey: input.lifecycle.keys.handoff(ref.fulfillmentRef),
95
+ trackingIdempotencyKey: number => input.lifecycle.keys.tracking(observation.eventKey, number),
96
+ issueAccountingInvoice: input.lifecycle.issueAccountingInvoice ? () => input.lifecycle.issueAccountingInvoice!(ref.fulfillmentRef) : undefined,
97
+ onTrackingRecorded: result => { trackingRefs += result.replayed ? 0 : 1; trackingReadBacks += result.readBack ? 1 : 0; },
98
+ };
99
+ if (!observationStatusPolicy.handsOver(localStatus)) await applyFulfillmentObservationEffects({ ...effects, trackingRefs: [] }, observationStatusPolicy);
100
+ else {
101
+ try { await applyFulfillmentObservationEffects(effects, observationStatusPolicy); }
102
+ catch (error) {
103
+ if (input.lifecycle.classifyFailure(error) !== "state_conflict") throw error;
104
+ const cause = input.lifecycle.supersededCause ? input.lifecycle.supersededCause(error)
105
+ : error && typeof error === "object" && "reason" in error ? error.reason : null;
106
+ const supersededCause = safeDiagnosticCode(cause) ?? "fulfillment_state_conflict";
107
+ await input.localPersistence.markProcessed(observation.eventKey, { status: "processed", supersededCause });
108
+ return { kind: "superseded", receiptId, counts, supersededCause, observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
109
+ }
110
+ }
111
+ await input.localPersistence.markProcessed(observation.eventKey, { status: "processed" });
112
+ return { kind: "processed", receiptId, counts, observation, replayed: inbound.replayed || statusEvidence.replayed, trackingRefs, trackingReadBacks };
113
+ }
@@ -0,0 +1,9 @@
1
+ import type { InvoiceCapabilities, InvoiceCapabilitySelection } from "./contracts/capabilities.js";
2
+ /** @consumer Host readiness / installed cold consumer. Default off and explicit
3
+ * provider selection precede capability use; construction never selects a provider.
4
+ */
5
+ export function readInvoiceCapabilities(input: { readonly enabled?: boolean; readonly selection: { readonly providerRef: string } | null; readonly capability: InvoiceCapabilities | null }): InvoiceCapabilitySelection {
6
+ if (input.enabled !== true) return { kind: "refused", code: "disabled", retryable: false, effectMayHaveSucceeded: false };
7
+ if (!input.selection || !input.capability || input.selection.providerRef !== input.capability.providerRef) return { kind: "refused", code: "selection_required", retryable: false, effectMayHaveSucceeded: false };
8
+ return { kind: "ready", capability: input.capability };
9
+ }