@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,338 @@
1
+ import type { FulfillmentObservationCodecs } from "./contracts/index.js";
2
+ import { safeDiagnosticCode } from "./dispatchProjection.js";
3
+ import { nativeCall } from './postgresDispatchStore.js';
4
+ import { projectObservation } from './observationProjection.js';
5
+ import type { PgExecutor as PgQueryExecutor } from './postgresDispatchStore.js';
6
+ interface ShipmentSpineResult { shipmentId: string; orderId: string; status: string; replayed: boolean }
7
+ interface ShipmentRequest { idempotencyKey: string; shipmentId: string; metadata?: Record<string, unknown>; requestedAt?: string; externalRef?: string }
8
+ interface ShipmentSpineMutationPort {
9
+ createShipment(input: { idempotencyKey: string; orderId: string; metadata?: Record<string, unknown>; requestedAt?: string }): Promise<ShipmentSpineResult>;
10
+ recordLabel(input: ShipmentRequest): Promise<ShipmentSpineResult>;
11
+ handOff(input: ShipmentRequest): Promise<ShipmentSpineResult>;
12
+ recordTracking(input: ShipmentRequest & { status: 'in_transit' | 'delivered' }): Promise<ShipmentSpineResult>;
13
+ cancel(input: ShipmentRequest & { reason: string }): Promise<ShipmentSpineResult>;
14
+ raiseException(input: ShipmentRequest & { reason: string }): Promise<ShipmentSpineResult>;
15
+ }
16
+ class CommerceFulfillmentConflictError extends Error {
17
+ constructor(message: string, readonly detail?: Record<string, unknown>) { super(message); }
18
+ }
19
+ class CommerceFulfillmentPersistenceError extends Error {
20
+ constructor(message: string, readonly detail?: Record<string, unknown>) { super(message); }
21
+ }
22
+ const CONTRACT_VERSION = "platform.fulfillment.shipment.v1";
23
+
24
+ const CREATE_SHIPMENT_SQL = `
25
+ WITH existing_operation AS (
26
+ SELECT 1 AS found
27
+ FROM public.fulfillment_shipment_operations
28
+ WHERE idempotency_key = $1::text || ':operation'
29
+ ), configured_port AS (
30
+ SELECT port_key
31
+ FROM public.fulfillment_ports
32
+ WHERE port_key = $3::text
33
+ AND routable IS TRUE
34
+ AND ($6::text IS NULL OR provider_type = $6::text)
35
+ )
36
+ SELECT
37
+ CASE WHEN configured_port.port_key IS NULL AND existing_operation.found IS NULL THEN NULL
38
+ ELSE public.fulfillment_create_shipment(
39
+ $1::text, $2::uuid, $3::text, $4::jsonb, COALESCE($5::timestamptz, now())
40
+ )
41
+ END AS response,
42
+ configured_port.port_key IS NOT NULL OR existing_operation.found IS NOT NULL AS port_ready
43
+ FROM (SELECT (SELECT port_key FROM configured_port LIMIT 1) AS port_key) configured_port
44
+ CROSS JOIN (SELECT (SELECT found FROM existing_operation LIMIT 1) AS found) existing_operation`;
45
+
46
+ const ADVANCE_SHIPMENT_SQL = `
47
+ SELECT public.fulfillment_advance_shipment(
48
+ $1::text, $2::uuid, $3::text, $4::jsonb, COALESCE($5::timestamptz, now())
49
+ ) AS response`;
50
+
51
+ const RECORD_EVIDENCE_SQL = `
52
+ SELECT public.fulfillment_record_evidence(
53
+ $1::text, $2::uuid, $3::text, $4::text, $5::text, $6::jsonb,
54
+ COALESCE($7::timestamptz, now())
55
+ ) AS response`;
56
+
57
+ const CANCEL_SHIPMENT_SQL = `
58
+ SELECT public.fulfillment_cancel_shipment(
59
+ $1::text, $2::uuid, $3::text, $4::jsonb, COALESCE($5::timestamptz, now())
60
+ ) AS response`;
61
+
62
+ const RAISE_EXCEPTION_SQL = `
63
+ SELECT public.fulfillment_raise_shipment_exception(
64
+ $1::text, $2::uuid, $3::text, $4::jsonb, COALESCE($5::timestamptz, now())
65
+ ) AS response`;
66
+
67
+ export interface PostgresFulfillmentShipmentSpineOptions {
68
+ portKey: string;
69
+ approveTrackingUrl?(url: string): boolean;
70
+ observationCodecs?: FulfillmentObservationCodecs;
71
+ evidenceChannel?: "poll" | "push";
72
+ requiredProviderType?: "simulator";
73
+ }
74
+
75
+ /** Direct node-postgres adapter for the provider-neutral public shipment rail. */
76
+ export function createPostgresFulfillmentShipmentSpinePort(
77
+ executor: PgQueryExecutor,
78
+ options: PostgresFulfillmentShipmentSpineOptions,
79
+ ): ShipmentSpineMutationPort & { localPersistence: ReturnType<typeof localPersistence> } {
80
+ const portKey = options.portKey.trim();
81
+ if (!portKey) throw new Error("fulfillment_shipment_spine_port_key_required");
82
+ const evidenceChannel = options.evidenceChannel;
83
+ if (evidenceChannel !== undefined && evidenceChannel !== "poll" && evidenceChannel !== "push") {
84
+ throw new Error("fulfillment_shipment_spine_evidence_channel_invalid");
85
+ }
86
+
87
+ const advance = (
88
+ idempotencyKey: string,
89
+ shipmentId: string,
90
+ status: string,
91
+ metadata: Record<string, unknown> | undefined,
92
+ requestedAt: string | undefined,
93
+ ) => call(executor, ADVANCE_SHIPMENT_SQL, [
94
+ idempotencyKey,
95
+ shipmentId,
96
+ status,
97
+ metadata ?? {},
98
+ requestedAt ?? null,
99
+ ]);
100
+
101
+ const evidence = (
102
+ idempotencyKey: string,
103
+ shipmentId: string,
104
+ role: "label" | "tracking",
105
+ externalRef: string | undefined,
106
+ metadata: Record<string, unknown> | undefined,
107
+ requestedAt: string | undefined,
108
+ ): Promise<ShipmentSpineResult> | null => {
109
+ if (!evidenceChannel || !externalRef) return null;
110
+ return call(executor, RECORD_EVIDENCE_SQL, [
111
+ idempotencyKey,
112
+ shipmentId,
113
+ evidenceChannel,
114
+ role,
115
+ externalRef,
116
+ metadata ?? {},
117
+ requestedAt ?? null,
118
+ ]);
119
+ };
120
+
121
+ return {
122
+ localPersistence: localPersistence(executor, options.approveTrackingUrl, options.observationCodecs),
123
+ createShipment(request) {
124
+ return callCreate(executor, portKey, [
125
+ request.idempotencyKey,
126
+ request.orderId,
127
+ portKey,
128
+ request.metadata ?? {},
129
+ request.requestedAt ?? null,
130
+ options.requiredProviderType ?? null,
131
+ ]);
132
+ },
133
+
134
+ async recordLabel(request) {
135
+ await advance(
136
+ `${request.idempotencyKey}:packed`,
137
+ request.shipmentId,
138
+ "packed",
139
+ request.metadata,
140
+ request.requestedAt,
141
+ );
142
+ await advance(
143
+ `${request.idempotencyKey}:label-pending`,
144
+ request.shipmentId,
145
+ "label_pending",
146
+ request.metadata,
147
+ request.requestedAt,
148
+ );
149
+ const labelled = await advance(
150
+ `${request.idempotencyKey}:label-created`,
151
+ request.shipmentId,
152
+ "label_created",
153
+ request.metadata,
154
+ request.requestedAt,
155
+ );
156
+ return await evidence(
157
+ `${request.idempotencyKey}:label-evidence`,
158
+ request.shipmentId,
159
+ "label",
160
+ request.externalRef,
161
+ request.metadata,
162
+ request.requestedAt,
163
+ ) ?? labelled;
164
+ },
165
+
166
+ handOff(request) {
167
+ return advance(
168
+ request.idempotencyKey,
169
+ request.shipmentId,
170
+ "handed_over",
171
+ request.metadata,
172
+ request.requestedAt,
173
+ );
174
+ },
175
+
176
+ async recordTracking(request) {
177
+ // Evidence precedes the transition because the public rail correctly
178
+ // closes evidence ingestion once a shipment reaches `delivered`.
179
+ await evidence(
180
+ `${request.idempotencyKey}:tracking-evidence`,
181
+ request.shipmentId,
182
+ "tracking",
183
+ request.externalRef,
184
+ request.metadata,
185
+ request.requestedAt,
186
+ );
187
+ return advance(
188
+ request.idempotencyKey,
189
+ request.shipmentId,
190
+ request.status,
191
+ request.metadata,
192
+ request.requestedAt,
193
+ );
194
+ },
195
+
196
+ cancel(request) {
197
+ return call(executor, CANCEL_SHIPMENT_SQL, [
198
+ request.idempotencyKey,
199
+ request.shipmentId,
200
+ request.reason,
201
+ request.metadata ?? {},
202
+ request.requestedAt ?? null,
203
+ ]);
204
+ },
205
+
206
+ raiseException(request) {
207
+ return call(executor, RAISE_EXCEPTION_SQL, [
208
+ request.idempotencyKey,
209
+ request.shipmentId,
210
+ request.reason,
211
+ request.metadata ?? {},
212
+ request.requestedAt ?? null,
213
+ ]);
214
+ },
215
+ };
216
+ }
217
+
218
+ async function callCreate(
219
+ executor: PgQueryExecutor,
220
+ portKey: string,
221
+ values: unknown[],
222
+ ): Promise<ShipmentSpineResult> {
223
+ try {
224
+ const result = await executor.query(CREATE_SHIPMENT_SQL, values);
225
+ const row = asRecord(result.rows[0]);
226
+ if (row?.port_ready !== true) {
227
+ throw new CommerceFulfillmentConflictError("fulfillment_port_not_ready", {
228
+ reason: "fulfillment_port_not_ready",
229
+ portKey,
230
+ });
231
+ }
232
+ return parseResponse(row.response);
233
+ } catch (error) {
234
+ if (error instanceof CommerceFulfillmentConflictError
235
+ || error instanceof CommerceFulfillmentPersistenceError) throw error;
236
+ throw mapPostgresError(error);
237
+ }
238
+ }
239
+
240
+ async function call(
241
+ executor: PgQueryExecutor,
242
+ sql: string,
243
+ values: unknown[],
244
+ ): Promise<ShipmentSpineResult> {
245
+ try {
246
+ const result = await executor.query(sql, values);
247
+ return parseResponse(asRecord(result.rows[0])?.response);
248
+ } catch (error) {
249
+ if (error instanceof CommerceFulfillmentPersistenceError) throw error;
250
+ throw mapPostgresError(error);
251
+ }
252
+ }
253
+
254
+ function parseResponse(input: unknown): ShipmentSpineResult {
255
+ const response = asRecord(input);
256
+ const shipment = asRecord(response?.shipment);
257
+ if (
258
+ response?.contractVersion !== CONTRACT_VERSION
259
+ || typeof shipment?.id !== "string"
260
+ || typeof shipment.orderId !== "string"
261
+ || typeof shipment.status !== "string"
262
+ || typeof response.replayed !== "boolean"
263
+ ) {
264
+ throw new CommerceFulfillmentPersistenceError("Fulfillment shipment spine response invalid");
265
+ }
266
+ return {
267
+ shipmentId: shipment.id,
268
+ orderId: shipment.orderId,
269
+ status: shipment.status,
270
+ replayed: response.replayed,
271
+ };
272
+ }
273
+
274
+ function mapPostgresError(error: unknown): Error {
275
+ const value = asRecord(error);
276
+ const code = typeof value?.code === "string" ? value.code : undefined;
277
+ const message = typeof value?.message === "string" ? value.message : "";
278
+ if (code === "22023" || code === "23505") {
279
+ return new CommerceFulfillmentConflictError("Fulfillment shipment spine conflict", {
280
+ code,
281
+ reason: classifyConflictReason(message),
282
+ });
283
+ }
284
+ return new CommerceFulfillmentPersistenceError("Fulfillment shipment spine persistence failed", { code });
285
+ }
286
+
287
+ function classifyConflictReason(message: string): string {
288
+ if (message.includes("fulfillment_create_shipment_order_not_fulfillable")) return "order_not_fulfillable";
289
+ if (message.includes("fulfillment_advance_shipment_archetype_forbids_handover")) return "handover_without_label";
290
+ if (message.includes("fulfillment_advance_shipment_invalid_transition")) return "invalid_transition";
291
+ if (message.includes("fulfillment_cancel_shipment_invalid_status")) return "cancel_after_label";
292
+ if (message.includes("idempotency_conflict")) return "idempotency_conflict";
293
+ return "fulfillment_conflict";
294
+ }
295
+
296
+ function asRecord(value: unknown): Record<string, unknown> | null {
297
+ return value && typeof value === "object" && !Array.isArray(value)
298
+ ? value as Record<string, unknown>
299
+ : null;
300
+ }
301
+
302
+ /** G17 native local persistence, composed on the same injected executor. */
303
+ function localPersistence(executor: PgQueryExecutor, approveTrackingUrl?: (url: string) => boolean, observationCodecs?: FulfillmentObservationCodecs) {
304
+ return {
305
+ async readLocalFulfillmentState(ref: string) {
306
+ const result = await executor.query(`SELECT shipment.status,shipment.handed_over_at,
307
+ (SELECT max((observation#>>'{fact,occurredAt}')::timestamptz) FROM public.fulfillment_observations WHERE shipment_id=shipment.id AND status_recorded) AS latest_status_occurred_at
308
+ FROM public.fulfillment_shipments shipment WHERE shipment.id=$1::uuid`, [ref]);
309
+ const row = result.rows[0];
310
+ return { status: typeof row?.status === 'string' ? row.status : null, latestStatusOccurredAt: date(row?.latest_status_occurred_at), handedOverAt: date(row?.handed_over_at) };
311
+ },
312
+ async readLocalFulfillmentStatus(ref: string) {
313
+ const result = await executor.query('SELECT status FROM public.fulfillment_shipments WHERE id=$1::uuid', [ref]);
314
+ return typeof result.rows[0]?.status === 'string' ? result.rows[0].status : null;
315
+ },
316
+ async latestStatusOccurredAt(ref: string) {
317
+ const result = await executor.query("SELECT max((observation#>>'{fact,occurredAt}')::timestamptz) AS latest_status_occurred_at FROM public.fulfillment_observations WHERE shipment_id=$1::uuid AND status_recorded", [ref]);
318
+ return date(result.rows[0]?.latest_status_occurred_at);
319
+ },
320
+ async readHandedOverAt(ref: string) {
321
+ const result = await executor.query('SELECT handed_over_at FROM public.fulfillment_shipments WHERE id=$1::uuid', [ref]);
322
+ return date(result.rows[0]?.handed_over_at);
323
+ },
324
+ async recordObservation(observation: import('./contracts/index.js').FulfillmentObservation) {
325
+ const safe = projectObservation(observation, approveTrackingUrl, observationCodecs);
326
+ return nativeCall<{ replayed: boolean }>(executor,'fulfillment_record_observation',[safe.eventKey,safe.correlation.fulfillmentRef,JSON.stringify(safe),safe.firstReceivedAt]);
327
+ },
328
+ async recordStatusEvidence(ref: { fulfillmentRef: string }, observation: import('./contracts/index.js').FulfillmentObservation) {
329
+ return nativeCall<{ replayed: boolean }>(executor,'fulfillment_record_status_evidence',[observation.eventKey,ref.fulfillmentRef]);
330
+ },
331
+ async markProcessed(eventKey: string, outcome: { status: 'processed' | 'ignored'; refusal?: import('./contracts/index.js').FulfillmentRefusal; supersededCause?: string }) {
332
+ await nativeCall(executor,'fulfillment_mark_observation_processed',[eventKey,JSON.stringify({ status: outcome.status,
333
+ ...(outcome.supersededCause ? { supersededCause: safeDiagnosticCode(outcome.supersededCause) ?? 'fulfillment_state_conflict' } : {}),
334
+ ...(outcome.refusal ? { refusal: { reason: outcome.refusal.reason,phase: outcome.refusal.phase,retryable: outcome.refusal.retryable } } : {}) })]);
335
+ },
336
+ };
337
+ }
338
+ function date(value: unknown): string | null { return value instanceof Date ? value.toISOString() : typeof value === 'string' ? value : null; }
@@ -0,0 +1,68 @@
1
+ import { createHash } from 'node:crypto';
2
+ import type { FulfillmentStockSyncPort, SyncCursor } from './contracts/index.js';
3
+ import { nativeCall, type PgExecutor } from './postgresDispatchStore.js';
4
+
5
+ /** The native catalogue has observed stock, not adopter inventory balances. */
6
+ export function createPostgresFulfillmentStockSyncPort(db: PgExecutor, options: {
7
+ sourceKey: string;
8
+ inventoryClasses?: ReadonlyMap<string, 'sellable' | 'packaging' | null>;
9
+ readLocalInventoryStock: FulfillmentStockSyncPort['readLocalInventoryStock'];
10
+ }): FulfillmentStockSyncPort {
11
+ const source = options.sourceKey.trim();
12
+ if (!source) throw new Error('fulfillment_stock_source_key_required');
13
+ return {
14
+ readLocalInventoryStock: at => options.readLocalInventoryStock(at),
15
+ async readCursor() {
16
+ const result = await db.query('SELECT status,last_stock_synced_at,last_movement_occurred_at,cursor FROM public.fulfillment_stock_sync_cursors WHERE source_key=$1', [source]);
17
+ const row = result.rows[0];
18
+ if (!row) return null;
19
+ const cursor = row.cursor as Partial<SyncCursor>;
20
+ return { ...cursor, status: row.status as SyncCursor['status'], lastStockSyncedAt: timestamp(row.last_stock_synced_at), lastMovementOccurredAt: timestamp(row.last_movement_occurred_at),
21
+ continuation: cursor.continuation ?? { nextRef: null, startLocalDate: null, endLocalDate: null }, coverageGapSkus: cursor.coverageGapSkus ?? [], refusal: cursor.refusal ?? null };
22
+ },
23
+ async recordCursor(v) {
24
+ await db.query(`INSERT INTO public.fulfillment_stock_sync_cursors(source_key,status,last_stock_synced_at,last_movement_occurred_at,cursor,error_code,updated_at)
25
+ VALUES($1,$2,$3,$4,$5::jsonb,$6,now()) ON CONFLICT(source_key) DO UPDATE SET status=EXCLUDED.status,
26
+ last_stock_synced_at=EXCLUDED.last_stock_synced_at,last_movement_occurred_at=EXCLUDED.last_movement_occurred_at,cursor=EXCLUDED.cursor,error_code=EXCLUDED.error_code,updated_at=now()`,
27
+ [source,v.status,v.lastStockSyncedAt,v.lastMovementOccurredAt,JSON.stringify({ syncRunId: v.syncRunId, counts: v.counts, items: v.items, alerts: v.alerts, staleSync: v.staleSync, error: v.error, continuation: { nextRef: v.continuation.nextRef, startLocalDate: v.continuation.startLocalDate, endLocalDate: v.continuation.endLocalDate }, coverageGapSkus: [...(v.coverageGapSkus ?? [])], refusal: v.refusal }),v.error?.reason ?? v.refusal?.reason ?? null]);
28
+ },
29
+ async readSkuInventoryClasses(skus) {
30
+ if (!skus.length) return new Map();
31
+ const rows = (await db.query('SELECT sku FROM public.catalog_skus WHERE sku=ANY($1::text[])', [skus])).rows;
32
+ const known = new Set(rows.map(row => row.sku));
33
+ return new Map(skus.map(sku => [sku, options.inventoryClasses?.has(sku) ? options.inventoryClasses.get(sku)! : known.has(sku) ? 'sellable' as const : null]));
34
+ },
35
+ async readActiveReservations(skus, at) {
36
+ const result = await db.query(`SELECT line.sku,sum(line.quantity)::integer AS quantity FROM public.fulfillment_inventory_reservation_lines line
37
+ JOIN public.fulfillment_inventory_reservations reservation ON reservation.id=line.reservation_id
38
+ WHERE reservation.source_key=$1 AND line.sku=ANY($2::text[]) AND reservation.status IN ('held','committed')
39
+ AND (reservation.status='committed' OR reservation.expires_at>$3::timestamptz) GROUP BY line.sku`, [source,skus,at]);
40
+ return new Map(result.rows.map(row => [String(row.sku), Number(row.quantity)]));
41
+ },
42
+ recordStockSnapshot(v) {
43
+ const s=v.snapshot, l=v.local;
44
+ return nativeCall(db,'fulfillment_record_stock_snapshot',[source,v.requestKey,fingerprint(v),s.skuRef,s.totalQuantity,s.forSaleQuantity,s.reservedOrUnavailableQuantity,l.onHand,l.reserved,l.unavailable,l.safetyStock,v.inventoryClass,s.syncRunId]);
45
+ },
46
+ recordProviderStockCurrent(v) {
47
+ const s=v.snapshot;
48
+ return nativeCall(db,'fulfillment_record_stock_current',[source,v.requestKey,fingerprint(v),s.skuRef,s.totalQuantity,s.forSaleQuantity,s.reservedOrUnavailableQuantity,v.inventoryClass,v.lastSyncedAt,v.staleAfter,s.syncRunId]);
49
+ },
50
+ recordMovement(v) {
51
+ const { providerRef, syncRunId, collectedAt, sourceRevision, revisionSource, skuRef, quantityKind, quantity, occurredAt, operationType, documentRef, lotRef, expirationDate } = v.movement;
52
+ const movement = { providerRef, syncRunId, collectedAt, sourceRevision, revisionSource, skuRef, quantityKind, quantity, occurredAt, operationType, documentRef, lotRef, expirationDate };
53
+ return nativeCall(db,'fulfillment_record_stock_movement',[source,v.requestKey,fingerprint({ requestKey: v.requestKey, movement }),JSON.stringify(movement)]);
54
+ },
55
+ recordLowStockEvidence: v => nativeCall(db,'fulfillment_record_stock_evidence',[source,v.requestKey,
56
+ // Quantities, severity and run evidence are observations of the same alert identity.
57
+ fingerprint({ requestKey: v.requestKey, skuRef: v.skuRef, thresholdKind: v.thresholdKind }),
58
+ v.skuRef,v.thresholdKind,v.severity,v.providerForSaleQuantity,v.localAvailableQuantity]),
59
+ resolveLowStockEvidence: v => nativeCall(db,'fulfillment_resolve_stock_evidence',[source,v.skuRef,v.activeThresholdKinds,v.syncRunId]),
60
+ };
61
+ }
62
+ function timestamp(value: unknown): string | null { return value instanceof Date ? value.toISOString() : typeof value === 'string' ? value : null; }
63
+ function fingerprint(value: unknown): string { return createHash('sha256').update(stable(value)).digest('hex'); }
64
+ function stable(value: unknown): string {
65
+ if (Array.isArray(value)) return `[${value.map(stable).join(',')}]`;
66
+ if (value && typeof value === 'object') return `{${Object.entries(value).sort(([a],[b]) => a.localeCompare(b)).map(([key,item]) => `${JSON.stringify(key)}:${stable(item)}`).join(',')}}`;
67
+ return JSON.stringify(value) ?? 'null';
68
+ }
@@ -0,0 +1,64 @@
1
+ /** Read-only evidence boundary; parsing remains with the owning host schema. */
2
+ export function createProductReconciliationEvidenceHandler<Request extends { method?: string; query: Record<string, string | string[] | undefined> }, Response, Query, Result>({ evidencePort, authorizeAdmin, requestSchema, responseSchema, respond }: {
3
+ evidencePort: { getProductReconciliationEvidence(query: Query): Promise<unknown> };
4
+ authorizeAdmin(request: Request): Promise<boolean>;
5
+ requestSchema: { safeParse(input: unknown): { success: true; data: Query } | { success: false; error: { flatten(): unknown } } };
6
+ responseSchema: { safeParse(input: unknown): { success: true; data: Result } | { success: false } };
7
+ respond: {
8
+ sendBffError(res: Response, code: "UPSTREAM_UNAVAILABLE" | "UNAUTHORIZED" | "BAD_REQUEST" | "INVALID_RESPONSE", message: string, options?: { details: unknown }): void;
9
+ sendBffSuccess(res: Response, data: Result): void;
10
+ sendMethodNotAllowed(res: Response, methods: readonly string[]): void;
11
+ };
12
+ }) {
13
+ const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = respond;
14
+ return async function handler(req: Request, res: Response): Promise<void> {
15
+ if (req.method !== "GET") {
16
+ sendMethodNotAllowed(res, ["GET"]);
17
+ return;
18
+ }
19
+
20
+ let authorized = false;
21
+ try {
22
+ authorized = await authorizeAdmin(req);
23
+ } catch {
24
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Admin authorization failed", {
25
+ details: { reason: "product_reconciliation_authorization_unavailable" },
26
+ });
27
+ return;
28
+ }
29
+
30
+ if (!authorized) {
31
+ sendBffError(res, "UNAUTHORIZED", "Admin session required");
32
+ return;
33
+ }
34
+
35
+ const request = requestSchema.safeParse({
36
+ statusFilter: scalarQuery(req.query.statusFilter),
37
+ });
38
+ if (!request.success) {
39
+ sendBffError(res, "BAD_REQUEST", "Invalid product reconciliation request", {
40
+ details: request.error.flatten(),
41
+ });
42
+ return;
43
+ }
44
+
45
+ try {
46
+ const result = await evidencePort.getProductReconciliationEvidence(request.data);
47
+ const response = responseSchema.safeParse(result);
48
+ if (!response.success) {
49
+ sendBffError(res, "INVALID_RESPONSE", "Product reconciliation returned invalid response");
50
+ return;
51
+ }
52
+
53
+ sendBffSuccess(res, response.data);
54
+ } catch {
55
+ sendBffError(res, "UPSTREAM_UNAVAILABLE", "Product reconciliation read failed", {
56
+ details: { reason: "product_reconciliation_read_unavailable" },
57
+ });
58
+ }
59
+ };
60
+ }
61
+
62
+ function scalarQuery(value: string | string[] | undefined): string | undefined {
63
+ return Array.isArray(value) ? value[0] : value;
64
+ }
@@ -0,0 +1,18 @@
1
+ import type { FulfillmentCapabilityDescriptor, FulfillmentRefusal, FulfillmentSelection } from "./contracts/index.js";
2
+
3
+ /** Injected profiles replace the private registry. Support never activates a provider. */
4
+ export function readFulfillmentCapabilities(
5
+ profiles: readonly FulfillmentCapabilityDescriptor[],
6
+ selection: FulfillmentSelection | null | undefined,
7
+ ): { kind: "selected"; descriptor: FulfillmentCapabilityDescriptor } | { kind: "refused"; refusal: FulfillmentRefusal } {
8
+ const refuse = (reason: FulfillmentRefusal["reason"]): { kind: "refused"; refusal: FulfillmentRefusal } =>
9
+ ({ kind: "refused", refusal: { reason, phase: "selection", retryable: false } });
10
+ const provider = selection?.providerRef?.trim().toLowerCase() || null;
11
+ if (!provider) return refuse("selection_required");
12
+ const descriptor = profiles.find(profile => profile.providerRef === provider);
13
+ if (!descriptor) return refuse("provider_mismatch");
14
+ if (descriptor.role === "warehouse" && (!selection?.carrierRef || !selection.serviceRef)) return refuse("carrier_service_required");
15
+ if (!descriptor.selected) return refuse("disabled");
16
+ if (!descriptor.readiness.ready) return { kind: "refused", refusal: descriptor.readiness.refusal };
17
+ return { kind: "selected", descriptor };
18
+ }
@@ -0,0 +1,53 @@
1
+ /** Structural protocol for the contributed durable ledger. Each operation must
2
+ * be one atomic store transaction, including its transition evidence. P4 owns SQL.
3
+ * This factory holds no leases, retry counters, commands or persistence in memory.
4
+ */
5
+ type CommandStatus = "queued" | "claimed" | "submitting" | "accepted" | "confirmed" | "retry_wait" | "uncertain" | "manual_review" | "terminal_rejected" | "cancelled";
6
+ type CommandResult = { commandId: string; status: CommandStatus; version: number; applied: boolean };
7
+ type Fence = { commandId: string; claimToken: string; claimGeneration: number; expectedVersion: number };
8
+ type Proof = { kind: "provider_reference" | "correlation_key" | "provider_idempotency_key"; fingerprint: string; observedAt: string; source: "provider_lookup" | "provider_webhook" | "operator_verified" };
9
+ type ClaimedCommand = {
10
+ commandId: string; fulfillmentRef: string; orderRef: string; providerRef: string;
11
+ commandKind: "dispatch_create"; contractVersion: "1"; status: "claimed"; version: number;
12
+ commandKeyFingerprint: string; commandPayloadFingerprint: string; providerIdempotencyKeyFingerprint: string | null;
13
+ routingFingerprint: string; configurationFingerprint: string; correlationKeyFingerprint: string;
14
+ retryBudget: number; retryCount: number; claimToken: string; claimGeneration: number; leaseExpiresAt: string;
15
+ };
16
+ type LedgerStore = {
17
+ enqueue(input: { fulfillmentRef: string; providerRef: string; commandKind: "dispatch_create"; contractVersion: "1";
18
+ idempotencyKey: string; commandKeyFingerprint: string; commandPayloadFingerprint: string;
19
+ providerIdempotencyKeyFingerprint: string | null; routingFingerprint: string; configurationFingerprint: string;
20
+ correlationKeyFingerprint: string; retryBudget: number }): Promise<CommandResult>;
21
+ claim(input: { providerRef: string; commandKind: "dispatch_create"; limit: number; claimOwnerFingerprint: string; leaseSeconds: number }): Promise<readonly ClaimedCommand[]>;
22
+ markSubmitting(input: Fence & { providerAttemptRef: string | null }): Promise<CommandResult>;
23
+ applyWriteOutcome(input: Fence & { identityFingerprint: string; providerAttemptRef: string | null } & (
24
+ { kind: "accepted"; reason: "provider_accepted"; providerReferenceFingerprint: string }
25
+ | { kind: "uncertain"; reason: "write_uncertain" }
26
+ | { kind: "rejected"; reason: "safe_rejection" | "terminal_rejection"; retryNotBefore: string | null;
27
+ proof: { kind: "definitive_provider_rejection"; fingerprint: string; observedAt: string; source: "provider_response" } }
28
+ )): Promise<CommandResult>;
29
+ confirm(input: Fence & { identityFingerprint: string; providerAttemptRef: string | null; proof: Proof }): Promise<CommandResult>;
30
+ reconcile(input: { commandId: string; expectedVersion: number; identityFingerprint: string; proof: Proof }): Promise<CommandResult & { proofRecorded: boolean }>;
31
+ resolve(input: { commandId: string; expectedVersion: number; identityFingerprint: string } & (
32
+ { decision: "manual_review"; reason: "effect_unknown" | "identity_mismatch" | "invalid_contract" | "unsafe_retry"; proof: Proof }
33
+ | { decision: "retry_wait"; reason: "authoritative_absence"; retryNotBefore: string;
34
+ proof: { kind: "authoritative_absence"; fingerprint: string; observedAt: string; source: "provider_lookup" } }
35
+ )): Promise<CommandResult & { proofRecorded: boolean }>;
36
+ release(input: Fence & ({ action: "release"; reason: "command_enqueued" } | { action: "cancel"; reason: "operator_cancelled" })): Promise<CommandResult>;
37
+ recoverExpired(input: { limit: number }): Promise<readonly CommandResult[]>;
38
+ };
39
+
40
+ /** Bind the durable protocol without substituting a local state machine.
41
+ * Store fences compare token, generation, version and unexpired lease together.
42
+ * Expired claimed work becomes queued; expired submitting/accepted work becomes
43
+ * uncertain. Only authoritative scoped absence may admit uncertain work to retry.
44
+ * Claim respects availability/retry budget; finalization cannot invent absence.
45
+ */
46
+ export function createFulfillmentCommandLedger(store: LedgerStore): LedgerStore {
47
+ return {
48
+ enqueue: input => store.enqueue(input), claim: input => store.claim(input),
49
+ markSubmitting: input => store.markSubmitting(input), applyWriteOutcome: input => store.applyWriteOutcome(input),
50
+ confirm: input => store.confirm(input), reconcile: input => store.reconcile(input),
51
+ resolve: input => store.resolve(input), release: input => store.release(input), recoverExpired: input => store.recoverExpired(input),
52
+ };
53
+ }
@@ -0,0 +1,37 @@
1
+ import type { DispatchRecord, FulfillmentDispatchResult, FulfillmentStorePort } from "./contracts/index.js";
2
+ import { durableStrings, dispatchEvidence, dispatchResult, localFailureReason } from "./dispatchProjection.js";
3
+
4
+ export function acknowledgeAcceptedDispatch(input: {
5
+ store: FulfillmentStorePort;
6
+ record: DispatchRecord & { externalRef: string };
7
+ keys: { providerAttemptKey: string; labelKey: string };
8
+ }) {
9
+ return input.store.acknowledge({ dispatchRef: input.record.dispatchRef, acceptance: {
10
+ ...(input.record.acceptance ? dispatchResult(input.record.acceptance) : {}),
11
+ ...(input.record.status === "uncertain" ? { recovery: { source: "persisted_acceptance" as const, proof: "persisted_external_ref" as const, noProviderPost: true as const } } : {}),
12
+ kind: "accepted", externalRef: input.record.externalRef, evidence: dispatchEvidence(input.record.evidence), ...input.keys,
13
+ } });
14
+ }
15
+
16
+ export async function recoverDispatchAcceptanceWrite(input: {
17
+ store: FulfillmentStorePort; requestKey: string; dispatchRef: string;
18
+ acceptance: Extract<FulfillmentDispatchResult, { kind: "accepted" }>; error: unknown;
19
+ }): Promise<{ committed: boolean; readBacks: number }> {
20
+ let record = await input.store.read(input.requestKey).catch(() => null);
21
+ let readBacks = record ? 1 : 0;
22
+ const committed = () => record?.status === "created" && record.externalRef === input.acceptance.externalRef;
23
+ if (committed()) return { committed: true, readBacks };
24
+ const privacy = durableStrings();
25
+ const cause = localFailureReason("ack", input.error, privacy);
26
+ try {
27
+ // Accepted proof survives a failed atomic ACK; finalization records uncertainty,
28
+ // and the next worker repairs the ACK locally without another provider call.
29
+ record = await input.store.finalize({ dispatchRef: input.dispatchRef, result: { ...input.acceptance, error: {
30
+ code: "dispatch_acceptance_write_failed", cause, ...(privacy.redactedFieldCount ? { redactedFieldCount: privacy.redactedFieldCount } : {}), status: null, retryable: false, mayHaveSucceeded: true,
31
+ } } });
32
+ readBacks += 1;
33
+ } catch {
34
+ // Submitting remains fenced; the stale sweep moves it to uncertain.
35
+ }
36
+ return { committed: committed(), readBacks };
37
+ }
@@ -0,0 +1,7 @@
1
+ import type { FulfillmentDispatchResult } from "./contracts/index.js";
2
+
3
+ /** Adapters translate structured provider failures into the port result before returning.
4
+ * An unclassified throw after invoking dispatch can follow a successful POST. */
5
+ export function unknownDispatchFailure(): Extract<FulfillmentDispatchResult, { kind: "unknown" }> {
6
+ return { kind: "unknown", effectMayHaveSucceeded: true, refusal: { reason: "io_failure", phase: "dispatch", retryable: true } };
7
+ }