@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,159 @@
1
+ import type { DispatchRecord, FulfillmentDispatchPort, FulfillmentDispatchRequest, FulfillmentDispatchResult, FulfillmentStorePort } from "./contracts/index.js";
2
+ import { durableStrings, acceptedProviderProof, dispatchEvidence, dispatchResult, localFailureReason, shadowDispatchEvidence } from "./dispatchProjection.js";
3
+ import { acknowledgeAcceptedDispatch, recoverDispatchAcceptanceWrite } from "./providerDispatchAcceptance.js";
4
+ import { unknownDispatchFailure } from "./providerDispatchError.js";
5
+
6
+ export async function runFulfillmentDispatchWorker(input: {
7
+ store: FulfillmentStorePort;
8
+ provider: FulfillmentDispatchPort | null;
9
+ listCandidates(limit: number): Promise<readonly FulfillmentDispatchRequest[]>;
10
+ /** Return the original host key bytes, without a new package prefix. */
11
+ keys: {
12
+ request(candidate: FulfillmentDispatchRequest): string;
13
+ mappedAttempt(requestKey: string, mappedAttemptIndex: number): string;
14
+ acceptance(fulfillmentRef: string): { providerAttemptKey: string; labelKey: string };
15
+ };
16
+ config: { mode: DispatchRecord["mode"]; batchLimit: number };
17
+ now?: () => Date;
18
+ }) {
19
+ const privacy = durableStrings();
20
+ const failure = (stage: string, error: unknown) => localFailureReason(stage, error, privacy);
21
+ const diagnostics = () => privacy.redactedFieldCount ? { redactedFieldCount: privacy.redactedFieldCount } : {};
22
+ const result = { ok: true, skipped: false, checked: 0, updated: 0, failures: 0,
23
+ mode: input.config.mode, replayed: 0, providerCalls: 0, readBacks: 0, reason: undefined as string | undefined };
24
+ if (input.config.mode !== "shadow") {
25
+ try { await input.store.markStaleSubmissionsUncertain(new Date((input.now?.() ?? new Date()).getTime() - 5 * 60_000).toISOString()); }
26
+ catch (error) { return { ...result, ok: false, failures: 1, reason: failure("dispatch_stale_submission_sweep_failed", error), ...diagnostics() }; }
27
+ }
28
+ const candidates = await input.listCandidates(input.config.batchLimit);
29
+ result.checked = candidates.length;
30
+ candidateLoop: for (const candidate of candidates) {
31
+ const requestKey = input.keys.request(candidate);
32
+ let stage = "dispatch_read_failed";
33
+ try {
34
+ let readBack = await input.store.read(requestKey);
35
+ if (readBack) result.readBacks += 1;
36
+ if (acceptedProviderProof(readBack)) {
37
+ const ack = await acknowledgeAcceptedDispatch({ store: input.store, record: readBack, keys: input.keys.acceptance(candidate.fulfillmentRef) });
38
+ if (ack.replayed) result.replayed += 1;
39
+ result.updated += ack.replayed ? 0 : 1;
40
+ continue;
41
+ }
42
+ stage = "dispatch_prepare_failed";
43
+ const preparation = await input.store.prepare(candidate.fulfillmentRef);
44
+ if (preparation.disposition === "withheld") continue;
45
+ stage = "dispatch_candidate_read_failed";
46
+ const preparedCandidate = await input.store.readCandidate(candidate.fulfillmentRef);
47
+ if (!preparedCandidate) throw new Error("dispatch_prepared_candidate_missing");
48
+ if (input.config.mode !== "shadow" && !input.provider) {
49
+ result.ok = false; result.failures += 1; result.reason ??= "provider_not_configured"; continue;
50
+ }
51
+ let mappedCandidate = preparedCandidate;
52
+ let dispatchRef: DispatchRecord;
53
+ let evidence: DispatchRecord["evidence"];
54
+ let providerEvidence: DispatchRecord["evidence"];
55
+ let mappedAttemptCount = 0;
56
+ for (let remapCount = 0; ; remapCount += 1) {
57
+ const request = { ...mappedCandidate, requestKey };
58
+ stage = "dispatch_command_prepare_failed";
59
+ const prepared = input.config.mode === "shadow"
60
+ ? { kind: "prepared" as const, evidence: shadowDispatchEvidence(request) }
61
+ : await input.provider!.prepare(request);
62
+ if (prepared.kind === "refused") {
63
+ result.ok = false; result.failures += 1; result.reason ??= privacy.token(prepared.refusal.reason) ?? "redacted"; continue candidateLoop;
64
+ }
65
+ providerEvidence = dispatchEvidence(prepared.evidence);
66
+ evidence = { ...providerEvidence, mapping: { source: "dispatch_worker", mode: input.config.mode, shadowMode: input.config.mode === "shadow" } };
67
+ // Native claims count any JSON change as a remap; attempts compare
68
+ // the whole JSON. Enrichment must not rewrite an existing identity.
69
+ if (readBack && sameCommandIdentity(readBack.evidence, evidence)) evidence = dispatchEvidence(readBack.evidence);
70
+ stage = "dispatch_claim_failed";
71
+ const claim = await input.store.claim({ requestKey, fulfillmentRef: mappedCandidate.fulfillmentRef,
72
+ orderRef: mappedCandidate.orderRef, parcelRef: mappedCandidate.parcelRef, mode: input.config.mode, evidence });
73
+ if (claim.kind === "refused") {
74
+ if (remapCount === 0 && claim.refusal.reason === "contact_stale") {
75
+ const refreshed = await input.store.readCandidate(candidate.fulfillmentRef);
76
+ if (!refreshed) throw new Error("dispatch_contact_remap_candidate_missing");
77
+ mappedCandidate = refreshed; continue;
78
+ }
79
+ result.ok = false; result.failures += 1; result.reason ??= privacy.token(claim.refusal.reason) ?? "redacted"; continue candidateLoop;
80
+ }
81
+ dispatchRef = claim.record;
82
+ if (dispatchRef.replayed) result.replayed += 1;
83
+ // Re-claim may refresh a correctable draft atomically; readback is authoritative.
84
+ stage = "dispatch_readback_failed";
85
+ readBack = await input.store.read(requestKey);
86
+ if (readBack) result.readBacks += 1;
87
+ if (!readBack || readBack.fulfillmentRef !== mappedCandidate.fulfillmentRef) throw new Error("dispatch_readback_failed");
88
+ stage = "dispatch_attempt_write_failed";
89
+ await input.store.recordProviderAttempt(input.keys.mappedAttempt(requestKey, mappedAttemptCount),
90
+ mappedCandidate.fulfillmentRef, evidence, input.config.mode, "recorded");
91
+ mappedAttemptCount += 1;
92
+ if (input.config.mode === "shadow") {
93
+ result.updated += dispatchRef.replayed ? 0 : 1; continue candidateLoop;
94
+ }
95
+ if (readBack.status !== "draft") {
96
+ result.ok = false; result.failures += 1; result.reason ??= `dispatch_not_safe_to_submit:${readBack.status}`; continue candidateLoop;
97
+ }
98
+ stage = "dispatch_begin_failed";
99
+ const submission = await input.store.begin({ dispatchRef: dispatchRef.dispatchRef, commandFingerprint: evidence.commandFingerprint });
100
+ if (submission.kind === "refused") {
101
+ if (remapCount === 0 && submission.refusal.reason === "contact_stale") {
102
+ const refreshed = await input.store.readCandidate(candidate.fulfillmentRef);
103
+ if (!refreshed) throw new Error("dispatch_contact_remap_candidate_missing");
104
+ mappedCandidate = refreshed; continue;
105
+ }
106
+ result.ok = false; result.failures += 1; result.reason ??= privacy.token(submission.refusal.reason) ?? "redacted"; continue candidateLoop;
107
+ }
108
+ if (!submission.begun) {
109
+ stage = "dispatch_concurrent_read_failed";
110
+ const concurrent = await input.store.read(requestKey);
111
+ if (concurrent) result.readBacks += 1;
112
+ if (acceptedProviderProof(concurrent)) {
113
+ const ack = await acknowledgeAcceptedDispatch({ store: input.store, record: concurrent, keys: input.keys.acceptance(mappedCandidate.fulfillmentRef) });
114
+ if (ack.replayed) result.replayed += 1;
115
+ result.updated += ack.replayed ? 0 : 1;
116
+ } else { result.replayed += 1; }
117
+ continue candidateLoop;
118
+ }
119
+ if (submission.record.status !== "submitting") throw new Error("dispatch_submission_state_invalid");
120
+ break;
121
+ }
122
+ result.providerCalls += 1;
123
+ let response: FulfillmentDispatchResult;
124
+ try { response = dispatchResult(await input.provider!.dispatch({ ...mappedCandidate, requestKey }, providerEvidence)); }
125
+ catch { response = unknownDispatchFailure(); }
126
+ if (response.kind === "accepted" && JSON.stringify(response.evidence) !== JSON.stringify(providerEvidence)) {
127
+ response = { kind: "unknown", effectMayHaveSucceeded: true, refusal: { reason: "malformed", phase: "acknowledge", retryable: false },
128
+ error: { code: "dispatch_acceptance_evidence_mismatch", status: null, retryable: false, mayHaveSucceeded: true } };
129
+ }
130
+ if (response.kind !== "accepted") {
131
+ stage = "dispatch_finalize_failed";
132
+ await input.store.finalize({ dispatchRef: dispatchRef.dispatchRef, result: response });
133
+ result.ok = false; result.failures += 1; result.reason ??= response.error?.code ?? response.refusal.reason; continue;
134
+ }
135
+ // All ACK paths use the original host keys, ignoring adapter key fields.
136
+ const acceptance = { ...response, evidence, ...input.keys.acceptance(mappedCandidate.fulfillmentRef) };
137
+ try {
138
+ const ack = await input.store.acknowledge({ dispatchRef: dispatchRef.dispatchRef, acceptance });
139
+ if (ack.replayed) result.replayed += 1;
140
+ } catch (error) {
141
+ const recovery = await recoverDispatchAcceptanceWrite({ store: input.store, requestKey, dispatchRef: dispatchRef.dispatchRef, acceptance, error });
142
+ result.readBacks += recovery.readBacks;
143
+ if (recovery.committed) { result.updated += 1; continue; }
144
+ result.ok = false; result.failures += 1; result.reason ??= "dispatch_proof_write_failed_after_provider_acceptance"; break;
145
+ }
146
+ result.updated += 1;
147
+ } catch (error) {
148
+ result.ok = false; result.failures += 1; result.reason ??= failure(stage, error);
149
+ }
150
+ }
151
+ return { ...result, ...diagnostics() };
152
+ }
153
+
154
+ function sameCommandIdentity(a: DispatchRecord["evidence"], b: DispatchRecord["evidence"]): boolean {
155
+ return a.commandFingerprint === b.commandFingerprint && a.providerRef === b.providerRef
156
+ && a.carrierRef === b.carrierRef && a.serviceRef === b.serviceRef
157
+ && a.contactRevision.schemaVersion === b.contactRevision.schemaVersion
158
+ && a.contactRevision.source === b.contactRevision.source && a.contactRevision.revision === b.contactRevision.revision;
159
+ }
@@ -0,0 +1,154 @@
1
+ import type { FulfillmentProductSyncPort, FulfillmentProductSyncProvider } from "./contracts/index.js";
2
+ type PackKind = "unit" | "collective";
3
+ type PackUpsertConflict = Awaited<ReturnType<FulfillmentProductSyncPort["upsertPulledPack"]>>["conflict"];
4
+ type ReconciliationConflict = Parameters<FulfillmentProductSyncPort["recordReconciliationEvidence"]>[0]["conflictKind"];
5
+ type SyncIdentity = Parameters<FulfillmentProductSyncPort["recordReconciliationEvidence"]>[0]["identity"];
6
+ interface ProductSyncResult {
7
+ ok: boolean;
8
+ pushedProducts: number;
9
+ pushedPacks: number;
10
+ skippedProducts: number;
11
+ pulledSkus: number;
12
+ ingestedPacks: number;
13
+ reconciliations: number;
14
+ replayed: number;
15
+ failures: number;
16
+ providerCalls: number;
17
+ syncRunId: string;
18
+ reason?: string;
19
+ }
20
+
21
+ function packKindForQuantity(quantity: number): PackKind {
22
+ return quantity > 1 ? "collective" : "unit";
23
+ }
24
+
25
+ function reconciliationKindFor(conflict: PackUpsertConflict): ReconciliationConflict | null {
26
+ switch (conflict) {
27
+ case "sku_not_found":
28
+ return "unknown_sku";
29
+ case "ean_owned_by_other_sku":
30
+ return "ean_conflict";
31
+ case "local_locked_mismatch":
32
+ return "kind_quantity_conflict";
33
+ default:
34
+ return null;
35
+ }
36
+ }
37
+
38
+ export async function runFulfillmentProductSyncWorker(input: {
39
+ port: FulfillmentProductSyncPort;
40
+ provider: FulfillmentProductSyncProvider;
41
+ mode?: "push" | "pull" | "both";
42
+ now?: () => Date;
43
+ compatibility: { identity(now: Date): SyncIdentity; evidenceKey(sku: string, ean: string, kind: ReconciliationConflict): string };
44
+ }): Promise<ProductSyncResult> {
45
+ const now = input.now?.() ?? new Date();
46
+ const mode = input.mode ?? "both";
47
+ const identity = input.compatibility.identity(now);
48
+ const syncRunId = identity.syncRunId;
49
+ const result: ProductSyncResult = {
50
+ ok: true,
51
+ pushedProducts: 0,
52
+ pushedPacks: 0,
53
+ skippedProducts: 0,
54
+ pulledSkus: 0,
55
+ ingestedPacks: 0,
56
+ reconciliations: 0,
57
+ replayed: 0,
58
+ failures: 0,
59
+ providerCalls: 0,
60
+ syncRunId,
61
+ };
62
+
63
+ if (mode === "push" || mode === "both") await runPush(input, result);
64
+ if (mode === "pull" || mode === "both") await runPull(input, result, identity);
65
+
66
+ return result;
67
+ }
68
+
69
+ async function runPush(
70
+ input: { port: FulfillmentProductSyncPort; provider: FulfillmentProductSyncProvider },
71
+ result: ProductSyncResult,
72
+ ): Promise<void> {
73
+ const localProducts = await input.port.listLocalProductPacks();
74
+ for (const product of localProducts) {
75
+ try {
76
+ const ensured = await input.provider.ensureProduct(product.sku);
77
+ result.providerCalls += 1;
78
+ if (ensured === null) {
79
+ result.skippedProducts += 1;
80
+ continue;
81
+ }
82
+ result.pushedProducts += ensured.replayed ? 0 : 1;
83
+ if (ensured.replayed) result.replayed += 1;
84
+
85
+ const existingEans = new Set((await input.provider.getProductPacks(product.sku)).map((pack) => pack.ean));
86
+ result.providerCalls += 1;
87
+ for (const pack of product.packs) {
88
+ if (existingEans.has(pack.ean)) continue;
89
+ await input.provider.addProductPack(product.sku, { ean: pack.ean, quantity: pack.quantity });
90
+ result.providerCalls += 1;
91
+ await input.port.markPackSynced({ sku: product.sku, ean: pack.ean });
92
+ result.pushedPacks += 1;
93
+ }
94
+ } catch (error) {
95
+ result.ok = false;
96
+ result.failures += 1;
97
+ result.reason = result.reason ?? "product_sync_operation_failed";
98
+ }
99
+ }
100
+ }
101
+
102
+ async function runPull(
103
+ input: Pick<Parameters<typeof runFulfillmentProductSyncWorker>[0], "port" | "provider" | "compatibility">,
104
+ result: ProductSyncResult,
105
+ identity: SyncIdentity,
106
+ ): Promise<void> {
107
+ const syncRunId = identity.syncRunId;
108
+ const skus = await input.provider.listStockedSkus();
109
+ result.providerCalls += 1;
110
+ for (const sku of skus.filter((value) => value.trim())) {
111
+ try {
112
+ const providerPacks = await input.provider.getProductPacks(sku);
113
+ result.providerCalls += 1;
114
+ result.pulledSkus += 1;
115
+ const activeConflicts = new Set<ReconciliationConflict>();
116
+
117
+ for (const pack of providerPacks.filter((entry) => entry.ean.trim())) {
118
+ const upsert = await input.port.upsertPulledPack({
119
+ sku,
120
+ ean: pack.ean,
121
+ quantity: pack.quantity,
122
+ kind: packKindForQuantity(pack.quantity),
123
+ });
124
+ const conflictKind = reconciliationKindFor(upsert.conflict);
125
+ if (conflictKind === null) {
126
+ if (!upsert.replayed) result.ingestedPacks += 1;
127
+ else result.replayed += 1;
128
+ continue;
129
+ }
130
+ activeConflicts.add(conflictKind);
131
+ const evidence = await input.port.recordReconciliationEvidence({
132
+ requestKey: input.compatibility.evidenceKey(sku, pack.ean, conflictKind),
133
+ sku,
134
+ ean: pack.ean,
135
+ conflictKind,
136
+ providerQuantity: pack.quantity,
137
+ identity, refusal: null,
138
+ });
139
+ if (evidence.replayed) result.replayed += 1;
140
+ else result.reconciliations += 1;
141
+ }
142
+
143
+ await input.port.resolveReconciliationEvidence({
144
+ sku,
145
+ activeConflictKinds: [...activeConflicts],
146
+ syncRunId,
147
+ });
148
+ } catch (error) {
149
+ result.ok = false;
150
+ result.failures += 1;
151
+ result.reason = result.reason ?? "product_sync_operation_failed";
152
+ }
153
+ }
154
+ }
@@ -0,0 +1,20 @@
1
+ import type { FulfillmentObservation } from "./contracts/index.js";
2
+
3
+ /** Host decoding supplies known domain reasons, never a raw provider error message. */
4
+ export async function quarantineReconciliationStateConflict(
5
+ error: unknown,
6
+ observation: FulfillmentObservation,
7
+ classify: (error: unknown) => "fulfillment_not_found" | "stock_consumed_requires_label" | null,
8
+ record: (observation: FulfillmentObservation, reason: "state_conflict", detail: "fulfillment_not_found" | "stock_consumed_requires_label") => Promise<{ replayed: boolean }>,
9
+ result: { quarantined: number; replayed: number; stateConflicts: number },
10
+ ): Promise<unknown | null> {
11
+ const reason = classify(error);
12
+ if (!reason) return error;
13
+ try {
14
+ const quarantine = await record(observation, "state_conflict", reason);
15
+ if (quarantine.replayed) result.replayed += 1;
16
+ result.quarantined += quarantine.replayed ? 0 : 1;
17
+ result.stateConflicts += 1;
18
+ return null;
19
+ } catch (quarantineError) { return quarantineError; }
20
+ }
@@ -0,0 +1,8 @@
1
+ import { FULFILLMENT_STATUS_MAP } from "./statusMap.js";
2
+ const OFF_TRACK_FULFILLMENT_STATUSES: ReadonlySet<string> = new Set(
3
+ FULFILLMENT_STATUS_MAP.stages.filter(stage => !stage.onTrack).flatMap(stage => stage.omsFulfillmentStatus),
4
+ );
5
+ /** Off-track rows still write evidence; acceptance and all effects must skip. */
6
+ export function isOffTrackFulfillmentStatus(status: string | null | undefined): boolean {
7
+ return status !== null && status !== undefined && OFF_TRACK_FULFILLMENT_STATUSES.has(status);
8
+ }
@@ -0,0 +1,34 @@
1
+ const ISO_TIMESTAMP_WITH_EXPLICIT_ZONE =
2
+ /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|[+-](\d{2}):?(\d{2}))$/;
3
+
4
+ export function normalizeReconciliationTimestamp(
5
+ value: string | null | undefined,
6
+ ): string | null {
7
+ const occurredAt = value?.trim();
8
+ if (!occurredAt) return null;
9
+
10
+ const match = ISO_TIMESTAMP_WITH_EXPLICIT_ZONE.exec(occurredAt);
11
+ if (!match || !Number.isFinite(Date.parse(occurredAt))) return null;
12
+
13
+ const year = Number(match[1]);
14
+ const month = Number(match[2]);
15
+ const day = Number(match[3]);
16
+ const hour = Number(match[4]);
17
+ const minute = Number(match[5]);
18
+ const second = Number(match[6]);
19
+ const offsetHour = match[7] === undefined ? 0 : Number(match[7]);
20
+ const offsetMinute = match[8] === undefined ? 0 : Number(match[8]);
21
+ const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
22
+ const daysInMonth = month === 2
23
+ ? leapYear ? 29 : 28
24
+ : [4, 6, 9, 11].includes(month) ? 30 : 31;
25
+
26
+ if (
27
+ month < 1 || month > 12
28
+ || day < 1 || day > daysInMonth
29
+ || hour > 23 || minute > 59 || second > 59
30
+ || offsetHour > 23 || offsetMinute > 59
31
+ ) return null;
32
+
33
+ return occurredAt;
34
+ }
@@ -0,0 +1,206 @@
1
+ import type { FulfillmentObservation, FulfillmentObservationPagePort, FulfillmentReconciliationObservation, FulfillmentOperationError } from "./contracts/index.js";
2
+ import { durableStrings, localFailureReason, safeDiagnosticCode, operationError } from "./dispatchProjection.js";
3
+ import { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
4
+ import { observationStatusPolicy, projectObservation, projectPollEvidence, projectTrackingReference } from "./observationProjection.js";
5
+ import { quarantineReconciliationStateConflict } from "./providerReconciliationError.js";
6
+ import { isOffTrackFulfillmentStatus } from "./providerReconciliationTerminalSkip.js";
7
+ import { normalizeReconciliationTimestamp } from "./providerReconciliationTimestamp.js";
8
+ import { enrichTrackingReference, trackingReferenceComplete, uniqueTrackingReferences } from "./providerTrackingReferences.js";
9
+
10
+ type Ingress = Parameters<typeof import("./verifiedIngress.js").verifyAndNormalizeFulfillmentObservation>[0];
11
+ type DispatchRef = NonNullable<Awaited<ReturnType<Ingress["lifecycle"]["findDispatchRef"]>>>;
12
+ type PollEvidence = Pick<FulfillmentObservation, "correlation" | "eventKey" | "providerStatus" | "diagnostics"> | FulfillmentObservation;
13
+ export interface ReconciliationResult {
14
+ ok: boolean; checked: number; updated: number; trackingRefs: number; quarantined: number;
15
+ stateConflicts: number; exceptions: number; stale: number; offTrackSkips: number; replayed: number;
16
+ redactedFieldCount?: number; failures: number; invoiceIssueFailures: number; invoiceIssueRefused: number;
17
+ invoiceIssueRefusals: { fulfillmentRef: string; identifier: string | null; sqlstate: string; benign: boolean }[];
18
+ providerCalls: number; readBacks: number; skipped: boolean; reason?: string; providerError?: FulfillmentOperationError;
19
+ }
20
+
21
+ export async function reconcileFulfillmentObservations(input: {
22
+ /** Adapter/host maps the private page into typed readback, preserving unknowns. */
23
+ readPage(input: { page: number; size: number }): Promise<readonly FulfillmentReconciliationObservation[]>;
24
+ batchSize: number; page?: number;
25
+ localPersistence: {
26
+ /** G17: local persisted chronology, never inferred from dispatch or provider state. */
27
+ readLocalFulfillmentStatus(ref: string): Promise<string | null>;
28
+ latestStatusOccurredAt(ref: string): Promise<string | null>;
29
+ readHandedOverAt(ref: string): Promise<string | null>;
30
+ };
31
+ lifecycle: Pick<Ingress["lifecycle"], "recordStatusEvidence" | "effects" | "keys"> & {
32
+ findDispatchRef(evidence: PollEvidence): Promise<DispatchRef | null>;
33
+ acknowledgeDispatchAcceptance(ref: DispatchRef, evidence: PollEvidence): Promise<void>;
34
+ recordQuarantine(observation: PollEvidence | null, reason: "missing_correlation" | "missing_external_ref" | "unknown_status" | "state_conflict", detail?: "fulfillment_not_found" | "stock_consumed_requires_label"): Promise<{ replayed: boolean }>;
35
+ recordProviderException(ref: DispatchRef, observation: FulfillmentObservation): Promise<{ replayed: boolean }>;
36
+ readDeliveryCarrier(ref: string): Promise<{ carrierRef: string | null; serviceRef: string | null } | null>;
37
+ trackingCodecs: Parameters<typeof enrichTrackingReference>[2];
38
+ issueAccountingInvoice?(ref: string): Promise<void>;
39
+ /** Private RPC decoding retains permanent/benign distinction and safe refusal identity. */
40
+ invoiceFailure(error: unknown): { permanent: boolean; identifier: string | null; sqlstate: string | null; benign: boolean };
41
+ classifyStateConflict(error: unknown): "fulfillment_not_found" | "stock_consumed_requires_label" | null;
42
+ };
43
+ }): Promise<ReconciliationResult> {
44
+ const privacy = durableStrings();
45
+ const failure = (stage: string, error: unknown) => localFailureReason(stage, error, privacy);
46
+ const result: ReconciliationResult = {
47
+ ok: true, checked: 0, updated: 0, trackingRefs: 0, quarantined: 0, stateConflicts: 0, exceptions: 0,
48
+ stale: 0, offTrackSkips: 0, replayed: 0, failures: 0, invoiceIssueFailures: 0, invoiceIssueRefused: 0,
49
+ invoiceIssueRefusals: [], providerCalls: 0, readBacks: 0, skipped: false,
50
+ };
51
+ const page = await input.readPage({ page: input.page ?? 0, size: input.batchSize });
52
+ result.providerCalls += 1;
53
+ result.checked = page.length;
54
+ const quarantine = async (observation: PollEvidence | null, reason: Parameters<typeof input.lifecycle.recordQuarantine>[1]) => {
55
+ const evidence = await input.lifecycle.recordQuarantine(observation, reason);
56
+ if (evidence.replayed) result.replayed += 1;
57
+ result.quarantined += evidence.replayed ? 0 : 1;
58
+ };
59
+ const invoice = async (ref: string) => {
60
+ try { await input.lifecycle.issueAccountingInvoice?.(ref); }
61
+ catch (error) {
62
+ const refusal = input.lifecycle.invoiceFailure(error);
63
+ if (refusal.permanent) {
64
+ result.invoiceIssueRefused += 1;
65
+ const identifier = privacy.token(refusal.identifier, safeDiagnosticCode), sqlstate = privacy.token(refusal.sqlstate, safeDiagnosticCode);
66
+ if (result.invoiceIssueRefusals.length < 10) result.invoiceIssueRefusals.push({ fulfillmentRef: ref,
67
+ identifier, sqlstate: sqlstate ?? "22023", benign: refusal.benign });
68
+ result.reason ??= failure("reconciliation_invoice_issue_refused", { causeMessage: identifier, causeCode: sqlstate, message: error instanceof Error ? error.message : undefined });
69
+ return;
70
+ }
71
+ result.ok = false; result.failures += 1; result.invoiceIssueFailures += 1;
72
+ result.reason ??= failure("reconciliation_invoice_issue_failed", error);
73
+ }
74
+ };
75
+ for (const readback of page) {
76
+ let observation: FulfillmentObservation | null = null;
77
+ try {
78
+ if (readback.kind === "unknown") {
79
+ result.ok = false; result.failures += 1; result.reason ??= "fulfillment_reconciliation_provider_read_failed";
80
+ continue;
81
+ }
82
+ if (readback.kind === "unmapped") {
83
+ // An unknown primary status still correlates/ACKs, but an off-track row
84
+ // must never trade a benign skip for a quarantine alarm.
85
+ const evidence = projectPollEvidence(readback.evidence);
86
+ const ref = await input.lifecycle.findDispatchRef(evidence);
87
+ if (!ref) { await quarantine(evidence, "missing_correlation"); continue; }
88
+ const localState = await input.localPersistence.readLocalFulfillmentStatus(ref.fulfillmentRef);
89
+ if (isOffTrackFulfillmentStatus(localState)) { result.offTrackSkips += 1; continue; }
90
+ if (!(readback.externalRef?.trim() || ref.externalRef?.trim())) { await quarantine(evidence, "missing_external_ref"); continue; }
91
+ await input.lifecycle.acknowledgeDispatchAcceptance(ref, evidence);
92
+ await quarantine(evidence, "unknown_status");
93
+ continue;
94
+ }
95
+ if (readback.kind !== "found") {
96
+ // Neither unsupported nor a provider absence is a local transition.
97
+ continue;
98
+ }
99
+ observation = projectObservation(readback.observation, input.lifecycle.trackingCodecs.approveTrackingUrl, input.lifecycle.trackingCodecs);
100
+ if (observation.diagnostics?.redactedFieldCount) {
101
+ result.redactedFieldCount = (result.redactedFieldCount ?? 0) + observation.diagnostics.redactedFieldCount;
102
+ }
103
+ const ref = await input.lifecycle.findDispatchRef(observation);
104
+ if (!ref) { await quarantine(observation, "missing_correlation"); continue; }
105
+ const localState = await input.localPersistence.readLocalFulfillmentStatus(ref.fulfillmentRef);
106
+ const offTrack = isOffTrackFulfillmentStatus(localState);
107
+ if (offTrack) result.offTrackSkips += 1;
108
+ if (!offTrack) {
109
+ if (!(readback.externalRef.trim() || ref.externalRef?.trim())) { await quarantine(observation, "missing_external_ref"); continue; }
110
+ await input.lifecycle.acknowledgeDispatchAcceptance(ref, observation);
111
+ }
112
+ const occurredAt = normalizeReconciliationTimestamp(readback.updatedAt);
113
+ const localStatus = observation.fact.canonicalStatus;
114
+ const latest = await input.localPersistence.latestStatusOccurredAt(ref.fulfillmentRef);
115
+ if (latest && occurredAt && Date.parse(occurredAt) < Date.parse(latest)) {
116
+ result.stale += 1;
117
+ if (!offTrack && input.lifecycle.issueAccountingInvoice && await input.localPersistence.readHandedOverAt(ref.fulfillmentRef)) await invoice(ref.fulfillmentRef);
118
+ continue;
119
+ }
120
+ // The normalized fact carries the same nullable occurrence used by dedupe.
121
+ observation = { ...observation, fact: { ...observation.fact, occurredAt } };
122
+ const status = await input.lifecycle.recordStatusEvidence(ref, observation);
123
+ if (status.replayed) result.replayed += 1;
124
+ if (offTrack) continue;
125
+ result.updated += status.replayed ? 0 : 1;
126
+ if (localStatus === "exception") {
127
+ const exception = await input.lifecycle.recordProviderException(ref, observation);
128
+ if (exception.replayed) result.replayed += 1;
129
+ result.exceptions += exception.replayed ? 0 : 1;
130
+ if (input.lifecycle.issueAccountingInvoice && await input.localPersistence.readHandedOverAt(ref.fulfillmentRef)) await invoice(ref.fulfillmentRef);
131
+ continue;
132
+ }
133
+ const trackingRefs = [];
134
+ if (observationStatusPolicy.handsOver(localStatus)) {
135
+ let carrier: Awaited<ReturnType<typeof input.lifecycle.readDeliveryCarrier>> | undefined;
136
+ for (const tracking of uniqueTrackingReferences(observation.trackingRefs)) {
137
+ if (!trackingReferenceComplete(tracking) && carrier === undefined) carrier = await input.lifecycle.readDeliveryCarrier(ref.fulfillmentRef);
138
+ const codecs = input.lifecycle.trackingCodecs;
139
+ const candidate = enrichTrackingReference(tracking, carrier ?? null, { ...codecs, publicUrl: () => null });
140
+ const enriched = projectTrackingReference(candidate, privacy, codecs.approveTrackingUrl, codecs);
141
+ if (!enriched) continue;
142
+ // Host-built links use admitted parts and pass through unchanged, including queries.
143
+ enriched.publicUrl ??= codecs.publicUrl(enriched.carrierRef, enriched.trackingNumber);
144
+ trackingRefs.push({ trackingNumber: enriched.trackingNumber, trackingUrl: enriched.publicUrl, carrierKind: enriched.carrierRef, service: enriched.serviceRef });
145
+ }
146
+ }
147
+ await applyFulfillmentObservationEffects({
148
+ port: input.lifecycle.effects, localStatus, ids: { orderId: ref.orderRef, fulfillmentOrderId: ref.fulfillmentRef }, observation,
149
+ trackingRefs, stockIdempotencyKey: input.lifecycle.keys.stock(ref.fulfillmentRef), handoffIdempotencyKey: input.lifecycle.keys.handoff(ref.fulfillmentRef),
150
+ trackingIdempotencyKey: number => input.lifecycle.keys.tracking(observation!.eventKey, number),
151
+ issueAccountingInvoice: input.lifecycle.issueAccountingInvoice ? () => invoice(ref.fulfillmentRef) : undefined,
152
+ onReplayed: () => { result.replayed += 1; },
153
+ onTrackingRecorded: tracking => {
154
+ if (tracking.replayed) result.replayed += 1;
155
+ if (tracking.readBack) result.readBacks += 1;
156
+ result.trackingRefs += tracking.replayed ? 0 : 1;
157
+ },
158
+ }, observationStatusPolicy);
159
+ } catch (caught) {
160
+ const error = observation ? await quarantineReconciliationStateConflict(caught, observation,
161
+ input.lifecycle.classifyStateConflict, input.lifecycle.recordQuarantine, result) : caught;
162
+ if (error === null) continue;
163
+ result.ok = false; result.failures += 1; result.reason ??= failure("reconciliation_local_operation_failed", error);
164
+ }
165
+ }
166
+ return { ...result, ...(privacy.redactedFieldCount ? { redactedFieldCount: (result.redactedFieldCount ?? 0) + privacy.redactedFieldCount } : {}) };
167
+ }
168
+
169
+
170
+ /** Host owns scheduling, HTTP and durable job-ledger writes. This operation only projects them. */
171
+ export async function runFulfillmentReconciliationPage(input: Omit<Parameters<typeof reconcileFulfillmentObservations>[0], "readPage" | "batchSize" | "page"> & {
172
+ provider: FulfillmentObservationPagePort; pageSize: number; checkpoint: unknown;
173
+ }) {
174
+ if (!Number.isSafeInteger(input.pageSize) || input.pageSize <= 0) throw new Error("reconciliation_page_size_invalid");
175
+ const isPage = (value: unknown): value is number => Number.isSafeInteger(value) && (value as number) >= 0;
176
+ const checkpoint = input.checkpoint as { reconciliationPagination?: { version?: unknown; pageSize?: unknown; page?: unknown; nextPage?: unknown } } | null;
177
+ const saved = checkpoint?.reconciliationPagination;
178
+ const page = saved?.version === 1 && saved.pageSize === input.pageSize && isPage(saved.page) && isPage(saved.nextPage)
179
+ && (saved.nextPage === 0 || saved.nextPage === saved.page || saved.page < Number.MAX_SAFE_INTEGER && saved.nextPage === saved.page + 1) ? saved.nextPage : 0;
180
+ const privacy = durableStrings();
181
+ let result: ReconciliationResult;
182
+ let pageRead = false;
183
+ const empty = (): ReconciliationResult => ({ ok: true, checked: 0, updated: 0, trackingRefs: 0, quarantined: 0, stateConflicts: 0, exceptions: 0,
184
+ stale: 0, offTrackSkips: 0, replayed: 0, failures: 0, invoiceIssueFailures: 0, invoiceIssueRefused: 0, invoiceIssueRefusals: [], providerCalls: 1, readBacks: 0, skipped: false });
185
+ try {
186
+ const response = await input.provider.readPage({ page, size: input.pageSize });
187
+ if (response.kind === "page_exhausted") result = { ...empty(), reason: "page_exhausted" };
188
+ else if (response.kind === "failed") {
189
+ const providerError = response.error ? operationError(response.error) : undefined;
190
+ result = { ...empty(), ok: false, failures: 1, ...(providerError ? { providerError } : {}),
191
+ reason: providerError?.code ?? privacy.token(response.refusal.reason) ?? "redacted" };
192
+ }
193
+ else {
194
+ pageRead = true;
195
+ result = await reconcileFulfillmentObservations({ ...input, batchSize: input.pageSize, page, readPage: async () => response.rows });
196
+ }
197
+ } catch (error) {
198
+ result = { ...empty(), ok: false, failures: 1, reason: localFailureReason(pageRead ? "reconciliation_page_failed" : "reconciliation_provider_read_failed", error, privacy) };
199
+ }
200
+ if (privacy.redactedFieldCount) result.redactedFieldCount = (result.redactedFieldCount ?? 0) + privacy.redactedFieldCount;
201
+ // A pre-traversal abort must not skip a page. Item failures return normally.
202
+ const nextPage = pageRead && result.checked === 0 && !result.ok ? page
203
+ : result.checked === input.pageSize && page < Number.MAX_SAFE_INTEGER ? page + 1 : 0;
204
+ return { status: result.ok ? 200 as const : 502 as const, ledgerStatus: result.ok ? "succeeded" as const : "failed" as const, result,
205
+ metadata: { reconciliationPagination: { version: 1, page, pageSize: input.pageSize, nextPage } } };
206
+ }
@@ -0,0 +1,60 @@
1
+ import type { FulfillmentStockSyncPort, LocalStock } from "./contracts/index.js";
2
+ type StockLine = Awaited<ReturnType<FulfillmentStockSyncPort["readLocalInventoryStock"]>>[number];
3
+ export function aggregateAtpCompatibleStock(lines: readonly StockLine[], now: Date): Map<string, LocalStock> {
4
+ const requestedAt = now.toISOString();
5
+ const balances = new Map<string, LocalStock>();
6
+ for (const line of lines.filter((item) => isAtpUsable(item, requestedAt))) {
7
+ const current = balances.get(line.skuRef) ?? emptyLocal(line.skuRef);
8
+ current.onHand += line.quantity.onHand;
9
+ current.reserved += line.quantity.reserved;
10
+ current.unavailable += line.quantity.unavailable;
11
+ current.safetyStock += line.quantity.safetyStock;
12
+ balances.set(line.skuRef, current);
13
+ }
14
+ return balances;
15
+ }
16
+
17
+ /** Cap the host-local movement window at UTC today, preserving donor overlap. */
18
+ export function stockMovementWindow(
19
+ cursor: { lastMovementOccurredAt: string | null; lastStockSyncedAt: string | null } | null,
20
+ now: Date,
21
+ timeZone: string,
22
+ ): { startLocalDate: string; endLocalDate: string } {
23
+ const startAt = new Date(cursor?.lastMovementOccurredAt ?? cursor?.lastStockSyncedAt ?? now.toISOString());
24
+ const safeStartAt = Number.isNaN(startAt.getTime()) ? now : startAt;
25
+ const endLocalDate = minDate(localDate(now, timeZone), utcDate(now));
26
+ return {
27
+ startLocalDate: minDate(localDate(safeStartAt, timeZone), endLocalDate),
28
+ endLocalDate,
29
+ };
30
+ }
31
+
32
+ function utcDate(date: Date): string {
33
+ return date.toISOString().slice(0, 10);
34
+ }
35
+
36
+ function minDate(a: string, b: string): string {
37
+ return a <= b ? a : b;
38
+ }
39
+
40
+ function isAtpUsable(line: StockLine, requestedAt: string): boolean {
41
+ if (!line.fulfillable || line.locationStatus !== "active") return false;
42
+ if (line.lotStatus && line.lotStatus !== "available") return false;
43
+ if (line.expiresAt && Date.parse(line.expiresAt) <= Date.parse(requestedAt)) return false;
44
+ return true;
45
+ }
46
+
47
+ export function emptyLocal(sku: string): LocalStock {
48
+ return { skuRef: sku, onHand: 0, reserved: 0, unavailable: 0, safetyStock: 0 };
49
+ }
50
+
51
+ function localDate(date: Date, timeZone: string): string {
52
+ const parts = new Intl.DateTimeFormat("en", {
53
+ timeZone,
54
+ year: "numeric",
55
+ month: "2-digit",
56
+ day: "2-digit",
57
+ }).formatToParts(date);
58
+ const byType = new Map(parts.map((part) => [part.type, part.value]));
59
+ return `${byType.get("year")}-${byType.get("month")}-${byType.get("day")}`;
60
+ }