@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,207 @@
1
+ import { durableStrings, localFailureReason, safeDiagnosticCode, operationError } from "./dispatchProjection.js";
2
+ import { applyFulfillmentObservationEffects } from "./fulfillmentObservationEffects.js";
3
+ import { observationStatusPolicy, projectObservation, projectPollEvidence, projectTrackingReference } from "./observationProjection.js";
4
+ import { quarantineReconciliationStateConflict } from "./providerReconciliationError.js";
5
+ import { isOffTrackFulfillmentStatus } from "./providerReconciliationTerminalSkip.js";
6
+ import { normalizeReconciliationTimestamp } from "./providerReconciliationTimestamp.js";
7
+ import { enrichTrackingReference, trackingReferenceComplete, uniqueTrackingReferences } from "./providerTrackingReferences.js";
8
+ export async function reconcileFulfillmentObservations(input) {
9
+ const privacy = durableStrings();
10
+ const failure = (stage, error) => localFailureReason(stage, error, privacy);
11
+ const result = {
12
+ ok: true, checked: 0, updated: 0, trackingRefs: 0, quarantined: 0, stateConflicts: 0, exceptions: 0,
13
+ stale: 0, offTrackSkips: 0, replayed: 0, failures: 0, invoiceIssueFailures: 0, invoiceIssueRefused: 0,
14
+ invoiceIssueRefusals: [], providerCalls: 0, readBacks: 0, skipped: false,
15
+ };
16
+ const page = await input.readPage({ page: input.page ?? 0, size: input.batchSize });
17
+ result.providerCalls += 1;
18
+ result.checked = page.length;
19
+ const quarantine = async (observation, reason) => {
20
+ const evidence = await input.lifecycle.recordQuarantine(observation, reason);
21
+ if (evidence.replayed)
22
+ result.replayed += 1;
23
+ result.quarantined += evidence.replayed ? 0 : 1;
24
+ };
25
+ const invoice = async (ref) => {
26
+ try {
27
+ await input.lifecycle.issueAccountingInvoice?.(ref);
28
+ }
29
+ catch (error) {
30
+ const refusal = input.lifecycle.invoiceFailure(error);
31
+ if (refusal.permanent) {
32
+ result.invoiceIssueRefused += 1;
33
+ const identifier = privacy.token(refusal.identifier, safeDiagnosticCode), sqlstate = privacy.token(refusal.sqlstate, safeDiagnosticCode);
34
+ if (result.invoiceIssueRefusals.length < 10)
35
+ result.invoiceIssueRefusals.push({ fulfillmentRef: ref,
36
+ identifier, sqlstate: sqlstate ?? "22023", benign: refusal.benign });
37
+ result.reason ??= failure("reconciliation_invoice_issue_refused", { causeMessage: identifier, causeCode: sqlstate, message: error instanceof Error ? error.message : undefined });
38
+ return;
39
+ }
40
+ result.ok = false;
41
+ result.failures += 1;
42
+ result.invoiceIssueFailures += 1;
43
+ result.reason ??= failure("reconciliation_invoice_issue_failed", error);
44
+ }
45
+ };
46
+ for (const readback of page) {
47
+ let observation = null;
48
+ try {
49
+ if (readback.kind === "unknown") {
50
+ result.ok = false;
51
+ result.failures += 1;
52
+ result.reason ??= "fulfillment_reconciliation_provider_read_failed";
53
+ continue;
54
+ }
55
+ if (readback.kind === "unmapped") {
56
+ // An unknown primary status still correlates/ACKs, but an off-track row
57
+ // must never trade a benign skip for a quarantine alarm.
58
+ const evidence = projectPollEvidence(readback.evidence);
59
+ const ref = await input.lifecycle.findDispatchRef(evidence);
60
+ if (!ref) {
61
+ await quarantine(evidence, "missing_correlation");
62
+ continue;
63
+ }
64
+ const localState = await input.localPersistence.readLocalFulfillmentStatus(ref.fulfillmentRef);
65
+ if (isOffTrackFulfillmentStatus(localState)) {
66
+ result.offTrackSkips += 1;
67
+ continue;
68
+ }
69
+ if (!(readback.externalRef?.trim() || ref.externalRef?.trim())) {
70
+ await quarantine(evidence, "missing_external_ref");
71
+ continue;
72
+ }
73
+ await input.lifecycle.acknowledgeDispatchAcceptance(ref, evidence);
74
+ await quarantine(evidence, "unknown_status");
75
+ continue;
76
+ }
77
+ if (readback.kind !== "found") {
78
+ // Neither unsupported nor a provider absence is a local transition.
79
+ continue;
80
+ }
81
+ observation = projectObservation(readback.observation, input.lifecycle.trackingCodecs.approveTrackingUrl, input.lifecycle.trackingCodecs);
82
+ if (observation.diagnostics?.redactedFieldCount) {
83
+ result.redactedFieldCount = (result.redactedFieldCount ?? 0) + observation.diagnostics.redactedFieldCount;
84
+ }
85
+ const ref = await input.lifecycle.findDispatchRef(observation);
86
+ if (!ref) {
87
+ await quarantine(observation, "missing_correlation");
88
+ continue;
89
+ }
90
+ const localState = await input.localPersistence.readLocalFulfillmentStatus(ref.fulfillmentRef);
91
+ const offTrack = isOffTrackFulfillmentStatus(localState);
92
+ if (offTrack)
93
+ result.offTrackSkips += 1;
94
+ if (!offTrack) {
95
+ if (!(readback.externalRef.trim() || ref.externalRef?.trim())) {
96
+ await quarantine(observation, "missing_external_ref");
97
+ continue;
98
+ }
99
+ await input.lifecycle.acknowledgeDispatchAcceptance(ref, observation);
100
+ }
101
+ const occurredAt = normalizeReconciliationTimestamp(readback.updatedAt);
102
+ const localStatus = observation.fact.canonicalStatus;
103
+ const latest = await input.localPersistence.latestStatusOccurredAt(ref.fulfillmentRef);
104
+ if (latest && occurredAt && Date.parse(occurredAt) < Date.parse(latest)) {
105
+ result.stale += 1;
106
+ if (!offTrack && input.lifecycle.issueAccountingInvoice && await input.localPersistence.readHandedOverAt(ref.fulfillmentRef))
107
+ await invoice(ref.fulfillmentRef);
108
+ continue;
109
+ }
110
+ // The normalized fact carries the same nullable occurrence used by dedupe.
111
+ observation = { ...observation, fact: { ...observation.fact, occurredAt } };
112
+ const status = await input.lifecycle.recordStatusEvidence(ref, observation);
113
+ if (status.replayed)
114
+ result.replayed += 1;
115
+ if (offTrack)
116
+ continue;
117
+ result.updated += status.replayed ? 0 : 1;
118
+ if (localStatus === "exception") {
119
+ const exception = await input.lifecycle.recordProviderException(ref, observation);
120
+ if (exception.replayed)
121
+ result.replayed += 1;
122
+ result.exceptions += exception.replayed ? 0 : 1;
123
+ if (input.lifecycle.issueAccountingInvoice && await input.localPersistence.readHandedOverAt(ref.fulfillmentRef))
124
+ await invoice(ref.fulfillmentRef);
125
+ continue;
126
+ }
127
+ const trackingRefs = [];
128
+ if (observationStatusPolicy.handsOver(localStatus)) {
129
+ let carrier;
130
+ for (const tracking of uniqueTrackingReferences(observation.trackingRefs)) {
131
+ if (!trackingReferenceComplete(tracking) && carrier === undefined)
132
+ carrier = await input.lifecycle.readDeliveryCarrier(ref.fulfillmentRef);
133
+ const codecs = input.lifecycle.trackingCodecs;
134
+ const candidate = enrichTrackingReference(tracking, carrier ?? null, { ...codecs, publicUrl: () => null });
135
+ const enriched = projectTrackingReference(candidate, privacy, codecs.approveTrackingUrl, codecs);
136
+ if (!enriched)
137
+ continue;
138
+ // Host-built links use admitted parts and pass through unchanged, including queries.
139
+ enriched.publicUrl ??= codecs.publicUrl(enriched.carrierRef, enriched.trackingNumber);
140
+ trackingRefs.push({ trackingNumber: enriched.trackingNumber, trackingUrl: enriched.publicUrl, carrierKind: enriched.carrierRef, service: enriched.serviceRef });
141
+ }
142
+ }
143
+ await applyFulfillmentObservationEffects({
144
+ port: input.lifecycle.effects, localStatus, ids: { orderId: ref.orderRef, fulfillmentOrderId: ref.fulfillmentRef }, observation,
145
+ trackingRefs, stockIdempotencyKey: input.lifecycle.keys.stock(ref.fulfillmentRef), handoffIdempotencyKey: input.lifecycle.keys.handoff(ref.fulfillmentRef),
146
+ trackingIdempotencyKey: number => input.lifecycle.keys.tracking(observation.eventKey, number),
147
+ issueAccountingInvoice: input.lifecycle.issueAccountingInvoice ? () => invoice(ref.fulfillmentRef) : undefined,
148
+ onReplayed: () => { result.replayed += 1; },
149
+ onTrackingRecorded: tracking => {
150
+ if (tracking.replayed)
151
+ result.replayed += 1;
152
+ if (tracking.readBack)
153
+ result.readBacks += 1;
154
+ result.trackingRefs += tracking.replayed ? 0 : 1;
155
+ },
156
+ }, observationStatusPolicy);
157
+ }
158
+ catch (caught) {
159
+ const error = observation ? await quarantineReconciliationStateConflict(caught, observation, input.lifecycle.classifyStateConflict, input.lifecycle.recordQuarantine, result) : caught;
160
+ if (error === null)
161
+ continue;
162
+ result.ok = false;
163
+ result.failures += 1;
164
+ result.reason ??= failure("reconciliation_local_operation_failed", error);
165
+ }
166
+ }
167
+ return { ...result, ...(privacy.redactedFieldCount ? { redactedFieldCount: (result.redactedFieldCount ?? 0) + privacy.redactedFieldCount } : {}) };
168
+ }
169
+ /** Host owns scheduling, HTTP and durable job-ledger writes. This operation only projects them. */
170
+ export async function runFulfillmentReconciliationPage(input) {
171
+ if (!Number.isSafeInteger(input.pageSize) || input.pageSize <= 0)
172
+ throw new Error("reconciliation_page_size_invalid");
173
+ const isPage = (value) => Number.isSafeInteger(value) && value >= 0;
174
+ const checkpoint = input.checkpoint;
175
+ const saved = checkpoint?.reconciliationPagination;
176
+ const page = saved?.version === 1 && saved.pageSize === input.pageSize && isPage(saved.page) && isPage(saved.nextPage)
177
+ && (saved.nextPage === 0 || saved.nextPage === saved.page || saved.page < Number.MAX_SAFE_INTEGER && saved.nextPage === saved.page + 1) ? saved.nextPage : 0;
178
+ const privacy = durableStrings();
179
+ let result;
180
+ let pageRead = false;
181
+ const empty = () => ({ ok: true, checked: 0, updated: 0, trackingRefs: 0, quarantined: 0, stateConflicts: 0, exceptions: 0,
182
+ stale: 0, offTrackSkips: 0, replayed: 0, failures: 0, invoiceIssueFailures: 0, invoiceIssueRefused: 0, invoiceIssueRefusals: [], providerCalls: 1, readBacks: 0, skipped: false });
183
+ try {
184
+ const response = await input.provider.readPage({ page, size: input.pageSize });
185
+ if (response.kind === "page_exhausted")
186
+ result = { ...empty(), reason: "page_exhausted" };
187
+ else if (response.kind === "failed") {
188
+ const providerError = response.error ? operationError(response.error) : undefined;
189
+ result = { ...empty(), ok: false, failures: 1, ...(providerError ? { providerError } : {}),
190
+ reason: providerError?.code ?? privacy.token(response.refusal.reason) ?? "redacted" };
191
+ }
192
+ else {
193
+ pageRead = true;
194
+ result = await reconcileFulfillmentObservations({ ...input, batchSize: input.pageSize, page, readPage: async () => response.rows });
195
+ }
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)
201
+ result.redactedFieldCount = (result.redactedFieldCount ?? 0) + privacy.redactedFieldCount;
202
+ // A pre-traversal abort must not skip a page. Item failures return normally.
203
+ const nextPage = pageRead && result.checked === 0 && !result.ok ? page
204
+ : result.checked === input.pageSize && page < Number.MAX_SAFE_INTEGER ? page + 1 : 0;
205
+ return { status: result.ok ? 200 : 502, ledgerStatus: result.ok ? "succeeded" : "failed", result,
206
+ metadata: { reconciliationPagination: { version: 1, page, pageSize: input.pageSize, nextPage } } };
207
+ }
@@ -0,0 +1,13 @@
1
+ import type { FulfillmentStockSyncPort, LocalStock } from "./contracts/index.js";
2
+ type StockLine = Awaited<ReturnType<FulfillmentStockSyncPort["readLocalInventoryStock"]>>[number];
3
+ export declare function aggregateAtpCompatibleStock(lines: readonly StockLine[], now: Date): Map<string, LocalStock>;
4
+ /** Cap the host-local movement window at UTC today, preserving donor overlap. */
5
+ export declare function stockMovementWindow(cursor: {
6
+ lastMovementOccurredAt: string | null;
7
+ lastStockSyncedAt: string | null;
8
+ } | null, now: Date, timeZone: string): {
9
+ startLocalDate: string;
10
+ endLocalDate: string;
11
+ };
12
+ export declare function emptyLocal(sku: string): LocalStock;
13
+ export {};
@@ -0,0 +1,51 @@
1
+ export function aggregateAtpCompatibleStock(lines, now) {
2
+ const requestedAt = now.toISOString();
3
+ const balances = new Map();
4
+ for (const line of lines.filter((item) => isAtpUsable(item, requestedAt))) {
5
+ const current = balances.get(line.skuRef) ?? emptyLocal(line.skuRef);
6
+ current.onHand += line.quantity.onHand;
7
+ current.reserved += line.quantity.reserved;
8
+ current.unavailable += line.quantity.unavailable;
9
+ current.safetyStock += line.quantity.safetyStock;
10
+ balances.set(line.skuRef, current);
11
+ }
12
+ return balances;
13
+ }
14
+ /** Cap the host-local movement window at UTC today, preserving donor overlap. */
15
+ export function stockMovementWindow(cursor, now, timeZone) {
16
+ const startAt = new Date(cursor?.lastMovementOccurredAt ?? cursor?.lastStockSyncedAt ?? now.toISOString());
17
+ const safeStartAt = Number.isNaN(startAt.getTime()) ? now : startAt;
18
+ const endLocalDate = minDate(localDate(now, timeZone), utcDate(now));
19
+ return {
20
+ startLocalDate: minDate(localDate(safeStartAt, timeZone), endLocalDate),
21
+ endLocalDate,
22
+ };
23
+ }
24
+ function utcDate(date) {
25
+ return date.toISOString().slice(0, 10);
26
+ }
27
+ function minDate(a, b) {
28
+ return a <= b ? a : b;
29
+ }
30
+ function isAtpUsable(line, requestedAt) {
31
+ if (!line.fulfillable || line.locationStatus !== "active")
32
+ return false;
33
+ if (line.lotStatus && line.lotStatus !== "available")
34
+ return false;
35
+ if (line.expiresAt && Date.parse(line.expiresAt) <= Date.parse(requestedAt))
36
+ return false;
37
+ return true;
38
+ }
39
+ export function emptyLocal(sku) {
40
+ return { skuRef: sku, onHand: 0, reserved: 0, unavailable: 0, safetyStock: 0 };
41
+ }
42
+ function localDate(date, timeZone) {
43
+ const parts = new Intl.DateTimeFormat("en", {
44
+ timeZone,
45
+ year: "numeric",
46
+ month: "2-digit",
47
+ day: "2-digit",
48
+ }).formatToParts(date);
49
+ const byType = new Map(parts.map((part) => [part.type, part.value]));
50
+ return `${byType.get("year")}-${byType.get("month")}-${byType.get("day")}`;
51
+ }
@@ -0,0 +1,41 @@
1
+ import type { FulfillmentStockSyncPort, FulfillmentStockSyncProvider, SyncCursor } from "./contracts/index.js";
2
+ type Threshold = Parameters<FulfillmentStockSyncPort["resolveLowStockEvidence"]>[0]["activeThresholdKinds"][number];
3
+ interface StockSyncResult {
4
+ ok: boolean;
5
+ checked: number;
6
+ updated: number;
7
+ mismatches: number;
8
+ reservationCoverage: number;
9
+ unclassified: number;
10
+ lowStock: number;
11
+ movements: number;
12
+ replayed: number;
13
+ failures: number;
14
+ providerCalls: number;
15
+ readBacks: number;
16
+ skipped: boolean;
17
+ syncRunId: string;
18
+ lastMovementOccurredAt: string | null;
19
+ reason?: string;
20
+ rejected: number;
21
+ items: NonNullable<SyncCursor["items"]>[number][];
22
+ providerError?: NonNullable<SyncCursor["error"]>["providerError"];
23
+ refusal?: NonNullable<SyncCursor["refusal"]>;
24
+ }
25
+ export declare function runFulfillmentStockSyncWorker(input: {
26
+ port: FulfillmentStockSyncPort;
27
+ provider: FulfillmentStockSyncProvider;
28
+ movementBatchSize: number;
29
+ staleSyncThresholdHours?: number;
30
+ now?: () => Date;
31
+ compatibility: {
32
+ syncRunId(now: Date): string;
33
+ staleEvidenceSku: string;
34
+ timeZone: string;
35
+ currentKey(run: string, sku: string): string;
36
+ snapshotKey(run: string, sku: string): string;
37
+ lowStockKey(sku: string, threshold: Threshold): string;
38
+ staleKey(lastSyncedAt: string): string;
39
+ };
40
+ }): Promise<StockSyncResult>;
41
+ export {};
@@ -0,0 +1,295 @@
1
+ import { aggregateAtpCompatibleStock, emptyLocal, stockMovementWindow } from "./providerStockAvailability.js";
2
+ export async function runFulfillmentStockSyncWorker(input) {
3
+ const now = input.now?.() ?? new Date();
4
+ const staleThresholdHours = input.staleSyncThresholdHours && input.staleSyncThresholdHours > 0
5
+ ? input.staleSyncThresholdHours
6
+ : 6;
7
+ const syncRunId = input.compatibility.syncRunId(now);
8
+ const result = {
9
+ ok: true,
10
+ rejected: 0,
11
+ items: [],
12
+ checked: 0,
13
+ updated: 0,
14
+ mismatches: 0,
15
+ reservationCoverage: 0,
16
+ unclassified: 0,
17
+ lowStock: 0,
18
+ movements: 0,
19
+ replayed: 0,
20
+ failures: 0,
21
+ providerCalls: 0,
22
+ readBacks: 0,
23
+ skipped: false,
24
+ syncRunId,
25
+ lastMovementOccurredAt: null,
26
+ };
27
+ const alerts = [];
28
+ let stage = "cursor_read_failed";
29
+ let operationalReason;
30
+ let staleSync;
31
+ let recoverStaleSync = false;
32
+ let previousCursor = null;
33
+ let rejectedStockResponse = false;
34
+ const currentCoverageGapSkus = new Set();
35
+ try {
36
+ previousCursor = await input.port.readCursor();
37
+ // Stale-sync alert: if the last successful sync aged past the threshold, open a stale_sync
38
+ // low-stock evidence row (evidence/warn — never blocks sales); a healthy run resolves it
39
+ // below. A never-synced state (no prior cursor) is not "stale".
40
+ const lastSyncedMs = previousCursor?.lastStockSyncedAt ? Date.parse(previousCursor.lastStockSyncedAt) : null;
41
+ const staleGapHours = lastSyncedMs !== null && !Number.isNaN(lastSyncedMs) ? (now.getTime() - lastSyncedMs) / 3_600_000 : null;
42
+ const syncIsStale = staleGapHours !== null && staleGapHours >= staleThresholdHours;
43
+ recoverStaleSync = !syncIsStale;
44
+ if (syncIsStale) {
45
+ staleSync = { gapHours: Math.round(staleGapHours * 10) / 10, thresholdHours: staleThresholdHours, detectedAt: now.toISOString() };
46
+ stage = "stock_evidence_write_failed";
47
+ alerts.push({ skuRef: input.compatibility.staleEvidenceSku, thresholdKind: "stale_sync", severity: "warning",
48
+ providerForSaleQuantity: 0, localAvailableQuantity: 0, ...staleSync });
49
+ await input.port.recordLowStockEvidence({
50
+ requestKey: input.compatibility.staleKey(previousCursor.lastStockSyncedAt),
51
+ skuRef: input.compatibility.staleEvidenceSku,
52
+ thresholdKind: "stale_sync",
53
+ severity: "warning",
54
+ providerForSaleQuantity: 0,
55
+ localAvailableQuantity: 0,
56
+ syncRunId,
57
+ });
58
+ }
59
+ stage = "cursor_write_failed";
60
+ await input.port.recordCursor({
61
+ syncRunId,
62
+ status: "running",
63
+ lastStockSyncedAt: null,
64
+ lastMovementOccurredAt: null,
65
+ continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
66
+ coverageGapSkus: previousCursor?.coverageGapSkus ?? [], refusal: null,
67
+ });
68
+ stage = "stock_read_failed";
69
+ result.providerCalls += 1;
70
+ const [localStock, providerStock] = await Promise.all([
71
+ input.port.readLocalInventoryStock(now.toISOString()),
72
+ input.provider.getStock(),
73
+ ]);
74
+ const stockItems = [];
75
+ for (const [itemIndex, item] of providerStock.entries()) {
76
+ if ("kind" in item && item.kind === "rejected") {
77
+ result.rejected += 1;
78
+ result.failures += 1;
79
+ result.items.push({ kind: "stock", itemIndex: item.itemIndex, skuRef: null, status: "rejected", reason: item.reason });
80
+ }
81
+ else if ("skuRef" in item && item.skuRef.trim()) {
82
+ stockItems.push({ stock: item, itemIndex });
83
+ }
84
+ }
85
+ const skus = [...new Set(stockItems.map(({ stock }) => stock.skuRef))];
86
+ rejectedStockResponse = providerStock.length > 0 && stockItems.length === 0;
87
+ stage = "stock_inventory_read_failed";
88
+ const [inventoryClasses, activeReservations] = await Promise.all([
89
+ input.port.readSkuInventoryClasses(skus),
90
+ input.port.readActiveReservations(skus, now.toISOString()),
91
+ ]);
92
+ stage = "stock_movements_read_failed";
93
+ result.providerCalls += 1;
94
+ const movementRows = await input.provider.getStockMovements({ ...stockMovementWindow(previousCursor, now, input.compatibility.timeZone), size: input.movementBatchSize });
95
+ const movements = [];
96
+ for (const item of movementRows) {
97
+ if ("kind" in item && item.kind === "rejected") {
98
+ result.rejected += 1;
99
+ result.failures += 1;
100
+ result.items.push({ kind: "movement", itemIndex: item.itemIndex, skuRef: null, status: "rejected", reason: item.reason });
101
+ }
102
+ else if ("skuRef" in item) {
103
+ movements.push(item);
104
+ }
105
+ }
106
+ if (rejectedStockResponse || (result.rejected > 0 && stockItems.length + movements.length === 0)) {
107
+ result.ok = false;
108
+ result.reason = "invalid_sku_ref";
109
+ }
110
+ result.movements = movements.length;
111
+ result.lastMovementOccurredAt = latestOccurredAt(movements);
112
+ const localBySku = aggregateAtpCompatibleStock(localStock, now);
113
+ const coverageCandidates = [];
114
+ const packagingSkus = new Set();
115
+ const previousCoverageGapSkus = previousCursor?.status === "succeeded"
116
+ ? readStringSet(previousCursor.coverageGapSkus)
117
+ : new Set();
118
+ for (const { stock, itemIndex } of stockItems) {
119
+ try {
120
+ const local = localBySku.get(stock.skuRef) ?? emptyLocal(stock.skuRef);
121
+ const inventoryClass = inventoryClasses.get(stock.skuRef) ?? null;
122
+ const activeReserved = activeReservations.get(stock.skuRef) ?? 0;
123
+ const staleAfter = new Date(now.getTime() + staleThresholdHours * 3_600_000).toISOString();
124
+ stage = "stock_current_write_failed";
125
+ const current = await input.port.recordProviderStockCurrent({
126
+ requestKey: input.compatibility.currentKey(syncRunId, stock.skuRef),
127
+ snapshot: stock, inventoryClass, lastSyncedAt: now.toISOString(), staleAfter,
128
+ });
129
+ stage = "snapshot_write_failed";
130
+ const snapshot = await input.port.recordStockSnapshot({
131
+ requestKey: input.compatibility.snapshotKey(syncRunId, stock.skuRef),
132
+ snapshot: stock, local, mismatchKind: "none", inventoryClass,
133
+ });
134
+ result.items.push({ itemIndex, skuRef: stock.skuRef, status: "recorded", activeReserved,
135
+ sellableNow: inventoryClass === "sellable" ? Math.max(0, stock.forSaleQuantity - activeReserved - local.safetyStock) : null,
136
+ coverageGapQuantity: Math.max(0, activeReserved - stock.forSaleQuantity) });
137
+ result.checked += 1;
138
+ result.updated += (snapshot.replayed ? 0 : 1) + (current.replayed ? 0 : 1);
139
+ if (snapshot.replayed)
140
+ result.replayed += 1;
141
+ if (current.replayed)
142
+ result.replayed += 1;
143
+ if (snapshot.readBack)
144
+ result.readBacks += 1;
145
+ if (current.readBack)
146
+ result.readBacks += 1;
147
+ if (inventoryClass === null)
148
+ result.unclassified += 1;
149
+ if (inventoryClass === "packaging")
150
+ packagingSkus.add(stock.skuRef);
151
+ if (inventoryClass === "sellable") {
152
+ coverageCandidates.push({ stock, local, activeReserved });
153
+ if (activeReserved > stock.forSaleQuantity)
154
+ currentCoverageGapSkus.add(stock.skuRef);
155
+ }
156
+ }
157
+ catch (error) {
158
+ result.ok = false;
159
+ result.failures += 1;
160
+ const reason = safeReason(error, stage);
161
+ operationalReason ??= reason;
162
+ result.items.push({ itemIndex, skuRef: stock.skuRef, status: "failed", reason });
163
+ }
164
+ }
165
+ if (result.ok) {
166
+ for (const { stock, local, activeReserved } of coverageCandidates) {
167
+ const localAvailable = availableQuantity(local);
168
+ const confirmedCoverageGap = currentCoverageGapSkus.has(stock.skuRef) && previousCoverageGapSkus.has(stock.skuRef);
169
+ const lowStockSignals = lowStockSignalsFor(stock, local, activeReserved, confirmedCoverageGap);
170
+ const activeThresholdKinds = lowStockSignals.map((signal) => signal.thresholdKind);
171
+ if (currentCoverageGapSkus.has(stock.skuRef) && !activeThresholdKinds.includes("reservation_coverage")) {
172
+ activeThresholdKinds.push("reservation_coverage");
173
+ }
174
+ for (const signal of lowStockSignals) {
175
+ alerts.push({ skuRef: stock.skuRef, thresholdKind: signal.thresholdKind, severity: signal.severity,
176
+ providerForSaleQuantity: stock.forSaleQuantity, localAvailableQuantity: localAvailable, detectedAt: now.toISOString(),
177
+ activeReserved, coverageGapQuantity: Math.max(0, activeReserved - stock.forSaleQuantity) });
178
+ stage = "stock_evidence_write_failed";
179
+ const evidence = await input.port.recordLowStockEvidence({
180
+ requestKey: input.compatibility.lowStockKey(stock.skuRef, signal.thresholdKind),
181
+ skuRef: stock.skuRef, thresholdKind: signal.thresholdKind, severity: signal.severity,
182
+ providerForSaleQuantity: stock.forSaleQuantity, localAvailableQuantity: localAvailable, syncRunId,
183
+ });
184
+ if (evidence.replayed)
185
+ result.replayed += 1;
186
+ result.lowStock += evidence.replayed ? 0 : 1;
187
+ if (signal.thresholdKind === "reservation_coverage")
188
+ result.reservationCoverage += 1;
189
+ }
190
+ stage = "stock_evidence_resolve_failed";
191
+ await input.port.resolveLowStockEvidence({
192
+ skuRef: stock.skuRef,
193
+ activeThresholdKinds,
194
+ syncRunId,
195
+ });
196
+ }
197
+ for (const sku of packagingSkus) {
198
+ stage = "stock_evidence_resolve_failed";
199
+ await input.port.resolveLowStockEvidence({
200
+ skuRef: sku,
201
+ activeThresholdKinds: [],
202
+ syncRunId,
203
+ });
204
+ }
205
+ }
206
+ }
207
+ catch (error) {
208
+ result.ok = false;
209
+ result.failures += 1;
210
+ result.providerError = providerDiagnostic(error);
211
+ if (stage === "stock_read_failed" || stage === "stock_movements_read_failed")
212
+ result.refusal = admissionRefusal(error);
213
+ operationalReason ??= result.providerError ? `provider_${result.providerError.operation}_failed:${result.providerError.code}` : result.refusal?.reason ?? safeReason(error, stage);
214
+ }
215
+ result.reason = operationalReason ?? result.reason;
216
+ if (!result.ok)
217
+ result.refusal ??= { reason: result.failures === result.rejected ? "malformed" : "io_failure", phase: "sync", retryable: result.providerError?.retryable ?? result.failures > result.rejected };
218
+ await input.port.recordCursor({
219
+ status: result.ok ? "succeeded" : "failed", syncRunId,
220
+ lastStockSyncedAt: result.ok ? now.toISOString() : null,
221
+ lastMovementOccurredAt: result.lastMovementOccurredAt,
222
+ continuation: { nextRef: null, startLocalDate: null, endLocalDate: null },
223
+ coverageGapSkus: result.ok ? [...currentCoverageGapSkus].sort() : rejectedStockResponse ? previousCursor?.coverageGapSkus ?? [] : [],
224
+ refusal: result.refusal ?? null,
225
+ error: result.ok ? null : { reason: result.reason, ...(result.providerError ? { providerError: result.providerError } : {}) },
226
+ counts: { checked: result.checked, updated: result.updated, movements: result.movements, rejected: result.rejected,
227
+ failures: result.failures, providerCalls: result.providerCalls, readBacks: result.readBacks },
228
+ items: result.items, alerts, staleSync,
229
+ });
230
+ // Recovery follows the durable succeeded cursor; a failed cleanup leaves the completed run intact.
231
+ if (result.ok && recoverStaleSync) {
232
+ try {
233
+ await input.port.resolveLowStockEvidence({
234
+ skuRef: input.compatibility.staleEvidenceSku,
235
+ activeThresholdKinds: [],
236
+ syncRunId,
237
+ });
238
+ }
239
+ catch { /* Best-effort cleanup must not undo the completed run. */ }
240
+ }
241
+ return result;
242
+ }
243
+ function lowStockSignalsFor(stock, local, activeReserved, confirmedCoverageGap) {
244
+ const signals = [];
245
+ const remainingAfterReservations = Math.max(0, stock.forSaleQuantity - activeReserved);
246
+ if (local.safetyStock > 0 && remainingAfterReservations <= local.safetyStock) {
247
+ signals.push({ thresholdKind: "safety_stock", severity: remainingAfterReservations <= 0 ? "critical" : "warning" });
248
+ }
249
+ if (confirmedCoverageGap)
250
+ signals.push({ thresholdKind: "reservation_coverage", severity: "critical" });
251
+ return signals;
252
+ }
253
+ function readStringSet(value) {
254
+ return new Set(Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim().length > 0) : []);
255
+ }
256
+ function availableQuantity(balance) {
257
+ return Math.max(0, balance.onHand - balance.reserved - balance.unavailable - balance.safetyStock);
258
+ }
259
+ function latestOccurredAt(movements) {
260
+ return movements
261
+ .map((movement) => movement.occurredAt)
262
+ .filter((value) => Boolean(value))
263
+ .sort((a, b) => Date.parse(b) - Date.parse(a))[0] ?? null;
264
+ }
265
+ /** Keep the operation's closed identity and SQLSTATE only, never arbitrary exception prose. */
266
+ function safeReason(error, operation) {
267
+ const code = error && typeof error === "object" && "code" in error ? error.code : null;
268
+ if (typeof code === "string" && /^[0-9A-Z]{5}$/.test(code))
269
+ return `${operation}:${code}`;
270
+ const known = error instanceof Error ? error.message.match(new RegExp(`^${operation}:([0-9A-Z]{5})$`)) : null;
271
+ return known ? `${operation}:${known[1]}` : operation;
272
+ }
273
+ function providerDiagnostic(error) {
274
+ if (!error || typeof error !== "object" || !("providerError" in error))
275
+ return undefined;
276
+ const detail = error.providerError;
277
+ if (!detail || typeof detail !== "object" || !("operation" in detail) || !("code" in detail) || !("status" in detail) || !("retryable" in detail))
278
+ return undefined;
279
+ if (detail.operation !== "get_stock" && detail.operation !== "get_stock_movements")
280
+ return undefined;
281
+ if (typeof detail.code !== "string" || !/^[A-Za-z][A-Za-z0-9_]{0,63}$/.test(detail.code))
282
+ return undefined;
283
+ return { operation: detail.operation, code: detail.code,
284
+ status: typeof detail.status === "number" && Number.isInteger(detail.status) && detail.status >= 100 && detail.status <= 599 ? detail.status : null,
285
+ retryable: detail.retryable === true };
286
+ }
287
+ function admissionRefusal(error) {
288
+ if (!error || typeof error !== "object" || !("reason" in error) || !("phase" in error) || !("retryable" in error))
289
+ return undefined;
290
+ if (error.phase !== "sync" || typeof error.retryable !== "boolean")
291
+ return undefined;
292
+ if (error.reason !== "disabled" && error.reason !== "not_ready" && error.reason !== "malformed")
293
+ return undefined;
294
+ return { reason: error.reason, phase: error.phase, retryable: error.retryable };
295
+ }
@@ -0,0 +1,16 @@
1
+ import type { FulfillmentTrackingReference, FulfillmentObservationCodecs } from "./contracts/index.js";
2
+ export declare function trackingReferenceComplete(ref: FulfillmentTrackingReference): boolean;
3
+ /** Provider evidence wins as a unit; private carrier/URL codecs stay injected. */
4
+ export declare function enrichTrackingReference(ref: FulfillmentTrackingReference, deliveryCarrier: {
5
+ carrierRef: string | null;
6
+ serviceRef: string | null;
7
+ } | null, codecs: FulfillmentObservationCodecs & {
8
+ approveTrackingUrl?(url: string): boolean;
9
+ normalizeCarrier(input: {
10
+ carrierRef: string | null;
11
+ serviceRef: string | null;
12
+ }): string | null;
13
+ publicUrl(carrierRef: string | null, trackingNumber: string): string | null;
14
+ }): FulfillmentTrackingReference;
15
+ /** First occurrence wins, including its selection, after trimming the number. */
16
+ export declare function uniqueTrackingReferences(refs: readonly FulfillmentTrackingReference[]): FulfillmentTrackingReference[];
@@ -0,0 +1,23 @@
1
+ export function trackingReferenceComplete(ref) {
2
+ return Boolean(ref.carrierRef && ref.publicUrl && ref.serviceRef);
3
+ }
4
+ /** Provider evidence wins as a unit; private carrier/URL codecs stay injected. */
5
+ export function enrichTrackingReference(ref, deliveryCarrier, codecs) {
6
+ const providerCarrier = ref.carrierRef ?? codecs.normalizeCarrier(ref);
7
+ const carrierRef = providerCarrier ?? codecs.normalizeCarrier({ carrierRef: deliveryCarrier?.carrierRef ?? null, serviceRef: deliveryCarrier?.serviceRef ?? null });
8
+ return { trackingNumber: ref.trackingNumber, publicUrl: ref.publicUrl ?? codecs.publicUrl(carrierRef, ref.trackingNumber),
9
+ carrierRef, serviceRef: ref.serviceRef ?? (providerCarrier ? null : deliveryCarrier?.serviceRef ?? null) };
10
+ }
11
+ /** First occurrence wins, including its selection, after trimming the number. */
12
+ export function uniqueTrackingReferences(refs) {
13
+ const seen = new Set();
14
+ const result = [];
15
+ for (const ref of refs) {
16
+ const trackingNumber = ref.trackingNumber.trim();
17
+ if (!trackingNumber || seen.has(trackingNumber))
18
+ continue;
19
+ seen.add(trackingNumber);
20
+ result.push({ trackingNumber, publicUrl: ref.publicUrl ?? null, carrierRef: ref.carrierRef ?? null, serviceRef: ref.serviceRef ?? null });
21
+ }
22
+ return result;
23
+ }
@@ -0,0 +1,5 @@
1
+ export declare const FULFILLMENT_REPLACEMENT_COMMAND_REFUSALS: readonly ["invalid_input", "idempotency_conflict", "order_not_found", "order_not_replaceable", "missing_client", "missing_shipping_address", "shipping_address_not_found", "payment_not_succeeded", "subscription_cycle_not_paid", "no_parcel_to_replace", "undispatched_replacement_exists", "missing_order_items", "missing_catalog_sku", "missing_inventory_reservation", "blocked_by_payment_not_succeeded_hold", "blocked_by_inventory_review_hold", "blocked_by_risk_review_hold", "blocked_by_address_review_hold", "blocked_by_manual_support_hold"];
2
+ export declare const FULFILLMENT_REPLACEMENT_RESERVATION_REFUSALS: readonly ["inventory_external_provider_insufficient_available_stock", "inventory_external_provider_stock_stale", "inventory_external_provider_stock_missing", "inventory_external_provider_sku_not_found", "inventory_external_provider_location_missing", "inventory_reservation_insufficient_available_stock", "inventory_reservation_allocation_incomplete", "inventory_reservation_payment_status_not_reservable", "inventory_reservation_invalid_quantity"];
3
+ export declare const UNSPECIFIED_FULFILLMENT_REPLACEMENT_REFUSAL = "unspecified_refusal";
4
+ export declare const FULFILLMENT_REPLACEMENT_REFUSALS: readonly ["invalid_input", "idempotency_conflict", "order_not_found", "order_not_replaceable", "missing_client", "missing_shipping_address", "shipping_address_not_found", "payment_not_succeeded", "subscription_cycle_not_paid", "no_parcel_to_replace", "undispatched_replacement_exists", "missing_order_items", "missing_catalog_sku", "missing_inventory_reservation", "blocked_by_payment_not_succeeded_hold", "blocked_by_inventory_review_hold", "blocked_by_risk_review_hold", "blocked_by_address_review_hold", "blocked_by_manual_support_hold", "inventory_external_provider_insufficient_available_stock", "inventory_external_provider_stock_stale", "inventory_external_provider_stock_missing", "inventory_external_provider_sku_not_found", "inventory_external_provider_location_missing", "inventory_reservation_insufficient_available_stock", "inventory_reservation_allocation_incomplete", "inventory_reservation_payment_status_not_reservable", "inventory_reservation_invalid_quantity", "unspecified_refusal"];
5
+ export type FulfillmentReplacementRefusal = (typeof FULFILLMENT_REPLACEMENT_REFUSALS)[number];