@openlup/platform 0.20.0 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (458) hide show
  1. package/CHANGELOG.md +272 -278
  2. package/README.md +195 -6
  3. package/dist/composition/composeBundle.d.ts +13 -0
  4. package/dist/composition/composeBundle.js +4 -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 +2 -0
  9. package/dist/fulfillment/adapterConformance.d.ts +3 -0
  10. package/dist/fulfillment/adapterConformance.js +223 -0
  11. package/dist/fulfillment/bindings.d.ts +3 -0
  12. package/dist/fulfillment/bindings.js +29 -0
  13. package/dist/fulfillment/commerceFulfillmentHandlers.d.ts +29 -0
  14. package/dist/fulfillment/commerceFulfillmentHandlers.js +184 -0
  15. package/dist/fulfillment/commerceFulfillmentReadModel.d.ts +86 -0
  16. package/dist/fulfillment/commerceFulfillmentReadModel.js +131 -0
  17. package/dist/fulfillment/contracts/alignment.d.ts +25 -0
  18. package/dist/fulfillment/contracts/alignment.js +1 -0
  19. package/dist/fulfillment/contracts/bindings.d.ts +51 -0
  20. package/dist/fulfillment/contracts/bindings.js +1 -0
  21. package/dist/fulfillment/contracts/capabilities.d.ts +29 -0
  22. package/dist/fulfillment/contracts/capabilities.js +1 -0
  23. package/dist/fulfillment/contracts/conformance.d.ts +93 -0
  24. package/dist/fulfillment/contracts/conformance.js +1 -0
  25. package/dist/fulfillment/contracts/customerAlignment.d.ts +6 -0
  26. package/dist/fulfillment/contracts/customerAlignment.js +1 -0
  27. package/dist/fulfillment/contracts/dispatch.d.ts +105 -0
  28. package/dist/fulfillment/contracts/dispatch.js +1 -0
  29. package/dist/fulfillment/contracts/index.d.ts +18 -0
  30. package/dist/fulfillment/contracts/index.js +1 -0
  31. package/dist/fulfillment/contracts/label.d.ts +35 -0
  32. package/dist/fulfillment/contracts/label.js +1 -0
  33. package/dist/fulfillment/contracts/nativeUnsupported.d.ts +6 -0
  34. package/dist/fulfillment/contracts/nativeUnsupported.js +1 -0
  35. package/dist/fulfillment/contracts/observation.d.ts +52 -0
  36. package/dist/fulfillment/contracts/observation.js +1 -0
  37. package/dist/fulfillment/contracts/operator.d.ts +64 -0
  38. package/dist/fulfillment/contracts/operator.js +1 -0
  39. package/dist/fulfillment/contracts/read.d.ts +110 -0
  40. package/dist/fulfillment/contracts/read.js +1 -0
  41. package/dist/fulfillment/contracts/readback.d.ts +38 -0
  42. package/dist/fulfillment/contracts/readback.js +1 -0
  43. package/dist/fulfillment/contracts/replacement.d.ts +34 -0
  44. package/dist/fulfillment/contracts/replacement.js +1 -0
  45. package/dist/fulfillment/contracts/store.d.ts +86 -0
  46. package/dist/fulfillment/contracts/store.js +1 -0
  47. package/dist/fulfillment/contracts/sync.d.ts +170 -0
  48. package/dist/fulfillment/contracts/sync.js +1 -0
  49. package/dist/fulfillment/currentFulfillmentParcel.d.ts +2 -0
  50. package/dist/fulfillment/currentFulfillmentParcel.js +1 -0
  51. package/dist/fulfillment/delayReadModel.d.ts +11 -0
  52. package/dist/fulfillment/delayReadModel.js +45 -0
  53. package/dist/fulfillment/deliveryAlignmentReadModel.d.ts +4 -0
  54. package/dist/fulfillment/deliveryAlignmentReadModel.js +12 -0
  55. package/dist/fulfillment/dispatchProjection.d.ts +7 -0
  56. package/dist/fulfillment/dispatchProjection.js +18 -0
  57. package/dist/fulfillment/fulfillmentObservationEffects.d.ts +66 -0
  58. package/dist/fulfillment/fulfillmentObservationEffects.js +36 -0
  59. package/dist/fulfillment/handlerSchemas.d.ts +647 -0
  60. package/dist/fulfillment/handlerSchemas.js +213 -0
  61. package/dist/fulfillment/handlerSupport.d.ts +33 -0
  62. package/dist/fulfillment/handlerSupport.js +25 -0
  63. package/dist/fulfillment/index.d.ts +37 -0
  64. package/dist/fulfillment/index.js +37 -0
  65. package/dist/fulfillment/labelLifecycle.d.ts +24 -0
  66. package/dist/fulfillment/labelLifecycle.js +9 -0
  67. package/dist/fulfillment/lowStockEvidenceHandler.d.ts +36 -0
  68. package/dist/fulfillment/lowStockEvidenceHandler.js +46 -0
  69. package/dist/fulfillment/migrations.d.ts +27 -0
  70. package/dist/fulfillment/migrations.js +8 -0
  71. package/dist/fulfillment/nativeUnsupported.d.ts +10 -0
  72. package/dist/fulfillment/nativeUnsupported.js +18 -0
  73. package/dist/fulfillment/normalizedFulfillmentFacts.d.ts +2 -0
  74. package/dist/fulfillment/normalizedFulfillmentFacts.js +1 -0
  75. package/dist/fulfillment/observationProjection.d.ts +11 -0
  76. package/dist/fulfillment/observationProjection.js +37 -0
  77. package/dist/fulfillment/omnipack/capabilities.d.ts +3 -0
  78. package/dist/fulfillment/omnipack/capabilities.js +16 -0
  79. package/dist/fulfillment/omnipack/client.d.ts +37 -0
  80. package/dist/fulfillment/omnipack/client.js +162 -0
  81. package/dist/fulfillment/omnipack/index.d.ts +52 -0
  82. package/dist/fulfillment/omnipack/index.js +349 -0
  83. package/dist/fulfillment/omnipack/mappers.d.ts +73 -0
  84. package/dist/fulfillment/omnipack/mappers.js +177 -0
  85. package/dist/fulfillment/omnipack/observation.d.ts +21 -0
  86. package/dist/fulfillment/omnipack/observation.js +57 -0
  87. package/dist/fulfillment/omnipack/omnipackDispatchPayload.d.ts +36 -0
  88. package/dist/fulfillment/omnipack/omnipackDispatchPayload.js +63 -0
  89. package/dist/fulfillment/omnipack/omnipackOutboundOrderPayload.d.ts +83 -0
  90. package/dist/fulfillment/omnipack/omnipackOutboundOrderPayload.js +113 -0
  91. package/dist/fulfillment/omnipack/omnipackProductPackPayload.d.ts +11 -0
  92. package/dist/fulfillment/omnipack/omnipackProductPackPayload.js +38 -0
  93. package/dist/fulfillment/omnipack/omnipackProductPayload.d.ts +18 -0
  94. package/dist/fulfillment/omnipack/omnipackProductPayload.js +51 -0
  95. package/dist/fulfillment/omnipack/omnipackReconciliationPayload.d.ts +1 -0
  96. package/dist/fulfillment/omnipack/omnipackReconciliationPayload.js +3 -0
  97. package/dist/fulfillment/omnipack/omnipackStatusVocabulary.d.ts +4 -0
  98. package/dist/fulfillment/omnipack/omnipackStatusVocabulary.js +87 -0
  99. package/dist/fulfillment/omnipack/omnipackWebhookPayload.d.ts +9 -0
  100. package/dist/fulfillment/omnipack/omnipackWebhookPayload.js +76 -0
  101. package/dist/fulfillment/omnipack/productSyncMappers.d.ts +18 -0
  102. package/dist/fulfillment/omnipack/productSyncMappers.js +43 -0
  103. package/dist/fulfillment/omnipack/providerError.d.ts +25 -0
  104. package/dist/fulfillment/omnipack/providerError.js +47 -0
  105. package/dist/fulfillment/omnipack/trackingReferences.d.ts +16 -0
  106. package/dist/fulfillment/omnipack/trackingReferences.js +52 -0
  107. package/dist/fulfillment/paidFulfillmentRecovery.d.ts +67 -0
  108. package/dist/fulfillment/paidFulfillmentRecovery.js +154 -0
  109. package/dist/fulfillment/paidHandoff.d.ts +27 -0
  110. package/dist/fulfillment/paidHandoff.js +86 -0
  111. package/dist/fulfillment/paidHandoffPorts.d.ts +24 -0
  112. package/dist/fulfillment/paidHandoffPorts.js +3 -0
  113. package/dist/fulfillment/postgresDeliveryAlignment.d.ts +4 -0
  114. package/dist/fulfillment/postgresDeliveryAlignment.js +12 -0
  115. package/dist/fulfillment/postgresDispatchStore.d.ts +16 -0
  116. package/dist/fulfillment/postgresDispatchStore.js +44 -0
  117. package/dist/fulfillment/postgresEvidenceReadPort.d.ts +25 -0
  118. package/dist/fulfillment/postgresEvidenceReadPort.js +45 -0
  119. package/dist/fulfillment/postgresPaidFulfillmentRecovery.d.ts +25 -0
  120. package/dist/fulfillment/postgresPaidFulfillmentRecovery.js +26 -0
  121. package/dist/fulfillment/postgresPaidHandoffStore.d.ts +4 -0
  122. package/dist/fulfillment/postgresPaidHandoffStore.js +10 -0
  123. package/dist/fulfillment/postgresShipmentSpine.d.ts +63 -0
  124. package/dist/fulfillment/postgresShipmentSpine.js +232 -0
  125. package/dist/fulfillment/postgresStockSyncPort.d.ts +8 -0
  126. package/dist/fulfillment/postgresStockSyncPort.js +63 -0
  127. package/dist/fulfillment/productReconciliationEvidenceHandler.d.ts +36 -0
  128. package/dist/fulfillment/productReconciliationEvidenceHandler.js +50 -0
  129. package/dist/fulfillment/providerCapabilities.d.ts +9 -0
  130. package/dist/fulfillment/providerCapabilities.js +17 -0
  131. package/dist/fulfillment/providerCommandLedger.d.ts +143 -0
  132. package/dist/fulfillment/providerCommandLedger.js +14 -0
  133. package/dist/fulfillment/providerDispatchAcceptance.d.ts +26 -0
  134. package/dist/fulfillment/providerDispatchAcceptance.js +23 -0
  135. package/dist/fulfillment/providerDispatchError.d.ts +6 -0
  136. package/dist/fulfillment/providerDispatchError.js +5 -0
  137. package/dist/fulfillment/providerDispatchWorker.d.ts +31 -0
  138. package/dist/fulfillment/providerDispatchWorker.js +172 -0
  139. package/dist/fulfillment/providerProductSyncWorker.d.ts +28 -0
  140. package/dist/fulfillment/providerProductSyncWorker.js +122 -0
  141. package/dist/fulfillment/providerReconciliationError.d.ts +9 -0
  142. package/dist/fulfillment/providerReconciliationError.js +17 -0
  143. package/dist/fulfillment/providerReconciliationTerminalSkip.d.ts +2 -0
  144. package/dist/fulfillment/providerReconciliationTerminalSkip.js +6 -0
  145. package/dist/fulfillment/providerReconciliationTimestamp.d.ts +1 -0
  146. package/dist/fulfillment/providerReconciliationTimestamp.js +27 -0
  147. package/dist/fulfillment/providerReconciliationWorker.d.ts +82 -0
  148. package/dist/fulfillment/providerReconciliationWorker.js +151 -0
  149. package/dist/fulfillment/providerStockAvailability.d.ts +13 -0
  150. package/dist/fulfillment/providerStockAvailability.js +51 -0
  151. package/dist/fulfillment/providerStockSyncWorker.d.ts +37 -0
  152. package/dist/fulfillment/providerStockSyncWorker.js +186 -0
  153. package/dist/fulfillment/providerTrackingReferences.d.ts +15 -0
  154. package/dist/fulfillment/providerTrackingReferences.js +23 -0
  155. package/dist/fulfillment/replacementRefusals.d.ts +5 -0
  156. package/dist/fulfillment/replacementRefusals.js +38 -0
  157. package/dist/fulfillment/replacementShipmentHandler.d.ts +5 -0
  158. package/dist/fulfillment/replacementShipmentHandler.js +46 -0
  159. package/dist/fulfillment/schemaCapabilities.d.ts +13 -0
  160. package/dist/fulfillment/schemaCapabilities.js +7 -0
  161. package/dist/fulfillment/shipmentsOverviewHandler.d.ts +16 -0
  162. package/dist/fulfillment/shipmentsOverviewHandler.js +54 -0
  163. package/dist/fulfillment/statusMap.d.ts +2 -0
  164. package/dist/fulfillment/statusMap.js +1 -0
  165. package/dist/fulfillment/trackingProgress.d.ts +1 -0
  166. package/dist/fulfillment/trackingProgress.js +1 -0
  167. package/dist/fulfillment/verifiedIngress.d.ts +52 -0
  168. package/dist/fulfillment/verifiedIngress.js +66 -0
  169. package/dist/payments/adapterConformance.d.ts +8 -0
  170. package/dist/payments/adapterConformance.js +109 -0
  171. package/dist/payments/bindings.d.ts +3 -0
  172. package/dist/payments/bindings.js +17 -0
  173. package/dist/payments/checkoutActivePaymentActionResolver.d.ts +7 -0
  174. package/dist/payments/checkoutActivePaymentActionResolver.js +77 -0
  175. package/dist/payments/checkoutRecoveryPaymentResolver.d.ts +8 -0
  176. package/dist/payments/checkoutRecoveryPaymentResolver.js +87 -0
  177. package/dist/payments/contracts/adapter.d.ts +113 -0
  178. package/dist/payments/contracts/adapter.js +1 -0
  179. package/dist/payments/contracts/conformance.d.ts +155 -0
  180. package/dist/payments/contracts/conformance.js +1 -0
  181. package/dist/payments/contracts/evidence.d.ts +157 -0
  182. package/dist/payments/contracts/evidence.js +1 -0
  183. package/dist/payments/contracts/index.d.ts +10 -0
  184. package/dist/payments/contracts/index.js +1 -0
  185. package/dist/payments/contracts/preparedAttempt.d.ts +69 -0
  186. package/dist/payments/contracts/preparedAttempt.js +1 -0
  187. package/dist/payments/contracts/reconciliation.d.ts +303 -0
  188. package/dist/payments/contracts/reconciliation.js +1 -0
  189. package/dist/payments/contracts/repair.d.ts +92 -0
  190. package/dist/payments/contracts/repair.js +1 -0
  191. package/dist/payments/contracts/truth.d.ts +92 -0
  192. package/dist/payments/contracts/truth.js +1 -0
  193. package/dist/payments/eventConformanceRunner.d.ts +12 -0
  194. package/dist/payments/eventConformanceRunner.js +125 -0
  195. package/dist/payments/index.d.ts +16 -0
  196. package/dist/payments/index.js +16 -0
  197. package/dist/payments/paymentAttemptSilenceWindow.d.ts +10 -0
  198. package/dist/payments/paymentAttemptSilenceWindow.js +19 -0
  199. package/dist/payments/paymentMethodLifecycle.d.ts +11 -0
  200. package/dist/payments/paymentMethodLifecycle.js +37 -0
  201. package/dist/payments/paymentProviderReconciliationAbsenceReopen.d.ts +7 -0
  202. package/dist/payments/paymentProviderReconciliationAbsenceReopen.js +100 -0
  203. package/dist/payments/paymentProviderReconciliationEvidence.d.ts +36 -0
  204. package/dist/payments/paymentProviderReconciliationEvidence.js +91 -0
  205. package/dist/payments/paymentProviderReconciliationWorker.d.ts +4 -0
  206. package/dist/payments/paymentProviderReconciliationWorker.js +245 -0
  207. package/dist/payments/paymentTruth.d.ts +8 -0
  208. package/dist/payments/paymentTruth.js +125 -0
  209. package/dist/payments/paymentVerifyNowHandler.d.ts +27 -0
  210. package/dist/payments/paymentVerifyNowHandler.js +70 -0
  211. package/dist/payments/paymentVerifyNowService.d.ts +23 -0
  212. package/dist/payments/paymentVerifyNowService.js +93 -0
  213. package/dist/payments/paymentWebhookPostProcessing.d.ts +22 -0
  214. package/dist/payments/paymentWebhookPostProcessing.js +15 -0
  215. package/dist/payments/postgresPaymentTruth.d.ts +10 -0
  216. package/dist/payments/postgresPaymentTruth.js +126 -0
  217. package/dist/payments/preparedAttempt.d.ts +6 -0
  218. package/dist/payments/preparedAttempt.js +70 -0
  219. package/dist/payments/preparedAttemptFinalization.d.ts +5 -0
  220. package/dist/payments/preparedAttemptFinalization.js +12 -0
  221. package/dist/payments/schemaCapabilities.d.ts +10 -0
  222. package/dist/payments/schemaCapabilities.js +10 -0
  223. package/dist/payments/sql.d.ts +20 -0
  224. package/dist/payments/sql.js +6 -0
  225. package/dist/payments/stripe/classifyDecline.d.ts +46 -0
  226. package/dist/payments/stripe/classifyDecline.js +58 -0
  227. package/dist/payments/stripe/declineFailureHints.d.ts +46 -0
  228. package/dist/payments/stripe/declineFailureHints.js +87 -0
  229. package/dist/payments/stripe/donorContracts.d.ts +120 -0
  230. package/dist/payments/stripe/donorContracts.js +1 -0
  231. package/dist/payments/stripe/executionDecline.d.ts +53 -0
  232. package/dist/payments/stripe/executionDecline.js +107 -0
  233. package/dist/payments/stripe/index.d.ts +41 -0
  234. package/dist/payments/stripe/index.js +264 -0
  235. package/dist/payments/stripe/intentMapper.d.ts +26 -0
  236. package/dist/payments/stripe/intentMapper.js +51 -0
  237. package/dist/payments/stripe/methodFacts.d.ts +10 -0
  238. package/dist/payments/stripe/methodFacts.js +12 -0
  239. package/dist/payments/stripe/paymentFailureEvidence.d.ts +26 -0
  240. package/dist/payments/stripe/paymentFailureEvidence.js +33 -0
  241. package/dist/payments/stripe/stripeFailureEvidence.d.ts +41 -0
  242. package/dist/payments/stripe/stripeFailureEvidence.js +82 -0
  243. package/dist/payments/stripe/stripeIntentConfirmationEvidence.d.ts +28 -0
  244. package/dist/payments/stripe/stripeIntentConfirmationEvidence.js +32 -0
  245. package/dist/payments/stripe/stripePaymentReconciliationProvider.d.ts +35 -0
  246. package/dist/payments/stripe/stripePaymentReconciliationProvider.js +222 -0
  247. package/dist/payments/stripe/stripeRefundEvidence.d.ts +8 -0
  248. package/dist/payments/stripe/stripeRefundEvidence.js +17 -0
  249. package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.d.ts +45 -0
  250. package/dist/payments/stripe/stripeSandboxPaymentExecutionAdapter.js +220 -0
  251. package/dist/payments/stripe/stripeWebhookNormalizer.d.ts +14 -0
  252. package/dist/payments/stripe/stripeWebhookNormalizer.js +243 -0
  253. package/dist/payments/stripe/webhookSignature.d.ts +14 -0
  254. package/dist/payments/stripe/webhookSignature.js +43 -0
  255. package/dist/payments/tpay/blikRecurringMapper.d.ts +37 -0
  256. package/dist/payments/tpay/blikRecurringMapper.js +24 -0
  257. package/dist/payments/tpay/capabilities.d.ts +3 -0
  258. package/dist/payments/tpay/capabilities.js +18 -0
  259. package/dist/payments/tpay/classifyDecline.d.ts +46 -0
  260. package/dist/payments/tpay/classifyDecline.js +58 -0
  261. package/dist/payments/tpay/declineFailureHints.d.ts +72 -0
  262. package/dist/payments/tpay/declineFailureHints.js +37 -0
  263. package/dist/payments/tpay/donorContracts.d.ts +140 -0
  264. package/dist/payments/tpay/donorContracts.js +1 -0
  265. package/dist/payments/tpay/index.d.ts +73 -0
  266. package/dist/payments/tpay/index.js +294 -0
  267. package/dist/payments/tpay/paymentFailureEvidence.d.ts +26 -0
  268. package/dist/payments/tpay/paymentFailureEvidence.js +33 -0
  269. package/dist/payments/tpay/preparedProviderAttempt.d.ts +22 -0
  270. package/dist/payments/tpay/preparedProviderAttempt.js +24 -0
  271. package/dist/payments/tpay/providerAttemptFailureDiagnostic.d.ts +3 -0
  272. package/dist/payments/tpay/providerAttemptFailureDiagnostic.js +95 -0
  273. package/dist/payments/tpay/tpayCreateTransactionInput.d.ts +25 -0
  274. package/dist/payments/tpay/tpayCreateTransactionInput.js +128 -0
  275. package/dist/payments/tpay/tpayDeclineEvidence.d.ts +126 -0
  276. package/dist/payments/tpay/tpayDeclineEvidence.js +177 -0
  277. package/dist/payments/tpay/tpayDispatchFailure.d.ts +60 -0
  278. package/dist/payments/tpay/tpayDispatchFailure.js +121 -0
  279. package/dist/payments/tpay/tpayFailureEvidence.d.ts +20 -0
  280. package/dist/payments/tpay/tpayFailureEvidence.js +61 -0
  281. package/dist/payments/tpay/tpayHttpClient.d.ts +52 -0
  282. package/dist/payments/tpay/tpayHttpClient.js +185 -0
  283. package/dist/payments/tpay/tpayHttpFailure.d.ts +57 -0
  284. package/dist/payments/tpay/tpayHttpFailure.js +143 -0
  285. package/dist/payments/tpay/tpayPaymentExecutionAdapter.d.ts +20 -0
  286. package/dist/payments/tpay/tpayPaymentExecutionAdapter.js +249 -0
  287. package/dist/payments/tpay/tpayPaymentReconciliationProvider.d.ts +5 -0
  288. package/dist/payments/tpay/tpayPaymentReconciliationProvider.js +244 -0
  289. package/dist/payments/tpay/tpayTransactionPayload.d.ts +120 -0
  290. package/dist/payments/tpay/tpayTransactionPayload.js +96 -0
  291. package/dist/payments/tpay/wallClockTime.d.ts +31 -0
  292. package/dist/payments/tpay/wallClockTime.js +57 -0
  293. package/dist/payments/tpay/webhookJwsVerifier.d.ts +17 -0
  294. package/dist/payments/tpay/webhookJwsVerifier.js +51 -0
  295. package/dist/payments/tpay/webhookNormalizer.d.ts +54 -0
  296. package/dist/payments/tpay/webhookNormalizer.js +200 -0
  297. package/dist/payments/verifiedEvidenceProjection.d.ts +12 -0
  298. package/dist/payments/verifiedEvidenceProjection.js +166 -0
  299. package/dist/payments/verifiedIngress.d.ts +119 -0
  300. package/dist/payments/verifiedIngress.js +220 -0
  301. package/package.json +37 -2
  302. package/release-gates.json +105 -3
  303. package/sql/20260808210000_fulfillment_shipment_rail.sql +810 -0
  304. package/sql/20260810180000_payment_settlement_rail.sql +574 -0
  305. package/sql/20260815150000_payment_truth_reconciliation.sql +479 -0
  306. package/sql/20260819150000_fulfillment_shipment_sequence.sql +98 -0
  307. package/sql/20260826100000_late_payment_shifts_the_subscription_cycle.sql +215 -0
  308. package/sql/20261022080000_fulfillment_command_ledger.sql +786 -0
  309. package/sql/20261022090000_fulfillment_delivery_alignment.sql +1106 -0
  310. package/src/composition/composeBundle.ts +13 -0
  311. package/src/composition/fulfillmentContribution.ts +28 -0
  312. package/src/composition/index.ts +2 -0
  313. package/src/fulfillment/adapterConformance.ts +195 -0
  314. package/src/fulfillment/bindings.ts +26 -0
  315. package/src/fulfillment/commerceFulfillmentHandlers.ts +263 -0
  316. package/src/fulfillment/commerceFulfillmentReadModel.ts +257 -0
  317. package/src/fulfillment/contracts/alignment.ts +13 -0
  318. package/src/fulfillment/contracts/bindings.ts +30 -0
  319. package/src/fulfillment/contracts/capabilities.ts +15 -0
  320. package/src/fulfillment/contracts/conformance.ts +61 -0
  321. package/src/fulfillment/contracts/customerAlignment.ts +4 -0
  322. package/src/fulfillment/contracts/dispatch.ts +58 -0
  323. package/src/fulfillment/contracts/index.ts +18 -0
  324. package/src/fulfillment/contracts/label.ts +9 -0
  325. package/src/fulfillment/contracts/nativeUnsupported.ts +6 -0
  326. package/src/fulfillment/contracts/observation.ts +18 -0
  327. package/src/fulfillment/contracts/operator.ts +31 -0
  328. package/src/fulfillment/contracts/read.ts +42 -0
  329. package/src/fulfillment/contracts/readback.ts +11 -0
  330. package/src/fulfillment/contracts/replacement.ts +15 -0
  331. package/src/fulfillment/contracts/store.ts +26 -0
  332. package/src/fulfillment/contracts/sync.ts +61 -0
  333. package/src/fulfillment/currentFulfillmentParcel.ts +2 -0
  334. package/src/fulfillment/delayReadModel.ts +60 -0
  335. package/src/fulfillment/deliveryAlignmentReadModel.ts +15 -0
  336. package/src/fulfillment/dispatchProjection.ts +20 -0
  337. package/src/fulfillment/fulfillmentObservationEffects.ts +86 -0
  338. package/src/fulfillment/handlerSchemas.ts +255 -0
  339. package/src/fulfillment/handlerSupport.ts +25 -0
  340. package/src/fulfillment/index.ts +38 -0
  341. package/src/fulfillment/labelLifecycle.ts +11 -0
  342. package/src/fulfillment/lowStockEvidenceHandler.ts +60 -0
  343. package/src/fulfillment/migrations.ts +8 -0
  344. package/src/fulfillment/nativeUnsupported.ts +23 -0
  345. package/src/fulfillment/normalizedFulfillmentFacts.ts +2 -0
  346. package/src/fulfillment/observationProjection.ts +41 -0
  347. package/src/fulfillment/omnipack/capabilities.ts +18 -0
  348. package/src/fulfillment/omnipack/client.ts +258 -0
  349. package/src/fulfillment/omnipack/index.ts +312 -0
  350. package/src/fulfillment/omnipack/mappers.ts +261 -0
  351. package/src/fulfillment/omnipack/observation.ts +61 -0
  352. package/src/fulfillment/omnipack/omnipackDispatchPayload.ts +93 -0
  353. package/src/fulfillment/omnipack/omnipackOutboundOrderPayload.ts +194 -0
  354. package/src/fulfillment/omnipack/omnipackProductPackPayload.ts +47 -0
  355. package/src/fulfillment/omnipack/omnipackProductPayload.ts +67 -0
  356. package/src/fulfillment/omnipack/omnipackReconciliationPayload.ts +7 -0
  357. package/src/fulfillment/omnipack/omnipackStatusVocabulary.ts +103 -0
  358. package/src/fulfillment/omnipack/omnipackWebhookPayload.ts +73 -0
  359. package/src/fulfillment/omnipack/productSyncMappers.ts +74 -0
  360. package/src/fulfillment/omnipack/providerError.ts +64 -0
  361. package/src/fulfillment/omnipack/trackingReferences.ts +77 -0
  362. package/src/fulfillment/paidFulfillmentRecovery.ts +252 -0
  363. package/src/fulfillment/paidHandoff.ts +122 -0
  364. package/src/fulfillment/paidHandoffPorts.ts +21 -0
  365. package/src/fulfillment/postgresDeliveryAlignment.ts +14 -0
  366. package/src/fulfillment/postgresDispatchStore.ts +55 -0
  367. package/src/fulfillment/postgresEvidenceReadPort.ts +59 -0
  368. package/src/fulfillment/postgresPaidFulfillmentRecovery.ts +29 -0
  369. package/src/fulfillment/postgresPaidHandoffStore.ts +11 -0
  370. package/src/fulfillment/postgresShipmentSpine.ts +321 -0
  371. package/src/fulfillment/postgresStockSyncPort.ts +65 -0
  372. package/src/fulfillment/productReconciliationEvidenceHandler.ts +64 -0
  373. package/src/fulfillment/providerCapabilities.ts +18 -0
  374. package/src/fulfillment/providerCommandLedger.ts +53 -0
  375. package/src/fulfillment/providerDispatchAcceptance.ts +31 -0
  376. package/src/fulfillment/providerDispatchError.ts +7 -0
  377. package/src/fulfillment/providerDispatchWorker.ts +131 -0
  378. package/src/fulfillment/providerProductSyncWorker.ts +154 -0
  379. package/src/fulfillment/providerReconciliationError.ts +20 -0
  380. package/src/fulfillment/providerReconciliationTerminalSkip.ts +8 -0
  381. package/src/fulfillment/providerReconciliationTimestamp.ts +34 -0
  382. package/src/fulfillment/providerReconciliationWorker.ts +150 -0
  383. package/src/fulfillment/providerStockAvailability.ts +60 -0
  384. package/src/fulfillment/providerStockSyncWorker.ts +222 -0
  385. package/src/fulfillment/providerTrackingReferences.ts +31 -0
  386. package/src/fulfillment/replacementRefusals.ts +43 -0
  387. package/src/fulfillment/replacementShipmentHandler.ts +47 -0
  388. package/src/fulfillment/schemaCapabilities.ts +7 -0
  389. package/src/fulfillment/shipmentsOverviewHandler.ts +73 -0
  390. package/src/fulfillment/statusMap.ts +2 -0
  391. package/src/fulfillment/trackingProgress.ts +1 -0
  392. package/src/fulfillment/verifiedIngress.ts +80 -0
  393. package/src/payments/adapterConformance.ts +118 -0
  394. package/src/payments/bindings.ts +17 -0
  395. package/src/payments/checkoutActivePaymentActionResolver.ts +97 -0
  396. package/src/payments/checkoutRecoveryPaymentResolver.ts +108 -0
  397. package/src/payments/contracts/adapter.ts +96 -0
  398. package/src/payments/contracts/conformance.ts +171 -0
  399. package/src/payments/contracts/evidence.ts +142 -0
  400. package/src/payments/contracts/index.ts +54 -0
  401. package/src/payments/contracts/preparedAttempt.ts +86 -0
  402. package/src/payments/contracts/reconciliation.ts +296 -0
  403. package/src/payments/contracts/repair.ts +95 -0
  404. package/src/payments/contracts/truth.ts +108 -0
  405. package/src/payments/eventConformanceRunner.ts +149 -0
  406. package/src/payments/index.ts +21 -0
  407. package/src/payments/paymentAttemptSilenceWindow.ts +26 -0
  408. package/src/payments/paymentMethodLifecycle.ts +50 -0
  409. package/src/payments/paymentProviderReconciliationAbsenceReopen.ts +130 -0
  410. package/src/payments/paymentProviderReconciliationEvidence.ts +117 -0
  411. package/src/payments/paymentProviderReconciliationWorker.ts +291 -0
  412. package/src/payments/paymentTruth.ts +131 -0
  413. package/src/payments/paymentVerifyNowHandler.ts +94 -0
  414. package/src/payments/paymentVerifyNowService.ts +125 -0
  415. package/src/payments/paymentWebhookPostProcessing.ts +36 -0
  416. package/src/payments/postgresPaymentTruth.ts +149 -0
  417. package/src/payments/preparedAttempt.ts +90 -0
  418. package/src/payments/preparedAttemptFinalization.ts +13 -0
  419. package/src/payments/schemaCapabilities.ts +10 -0
  420. package/src/payments/sql.ts +6 -0
  421. package/src/payments/stripe/classifyDecline.ts +62 -0
  422. package/src/payments/stripe/declineFailureHints.ts +89 -0
  423. package/src/payments/stripe/donorContracts.ts +87 -0
  424. package/src/payments/stripe/executionDecline.ts +126 -0
  425. package/src/payments/stripe/index.ts +265 -0
  426. package/src/payments/stripe/intentMapper.ts +58 -0
  427. package/src/payments/stripe/methodFacts.ts +21 -0
  428. package/src/payments/stripe/paymentFailureEvidence.ts +58 -0
  429. package/src/payments/stripe/stripeFailureEvidence.ts +115 -0
  430. package/src/payments/stripe/stripeIntentConfirmationEvidence.ts +34 -0
  431. package/src/payments/stripe/stripePaymentReconciliationProvider.ts +263 -0
  432. package/src/payments/stripe/stripeRefundEvidence.ts +19 -0
  433. package/src/payments/stripe/stripeSandboxPaymentExecutionAdapter.ts +283 -0
  434. package/src/payments/stripe/stripeWebhookNormalizer.ts +277 -0
  435. package/src/payments/stripe/webhookSignature.ts +55 -0
  436. package/src/payments/tpay/blikRecurringMapper.ts +42 -0
  437. package/src/payments/tpay/capabilities.ts +21 -0
  438. package/src/payments/tpay/classifyDecline.ts +62 -0
  439. package/src/payments/tpay/declineFailureHints.ts +90 -0
  440. package/src/payments/tpay/donorContracts.ts +104 -0
  441. package/src/payments/tpay/index.ts +293 -0
  442. package/src/payments/tpay/paymentFailureEvidence.ts +58 -0
  443. package/src/payments/tpay/preparedProviderAttempt.ts +38 -0
  444. package/src/payments/tpay/providerAttemptFailureDiagnostic.ts +99 -0
  445. package/src/payments/tpay/tpayCreateTransactionInput.ts +158 -0
  446. package/src/payments/tpay/tpayDeclineEvidence.ts +233 -0
  447. package/src/payments/tpay/tpayDispatchFailure.ts +186 -0
  448. package/src/payments/tpay/tpayFailureEvidence.ts +75 -0
  449. package/src/payments/tpay/tpayHttpClient.ts +290 -0
  450. package/src/payments/tpay/tpayHttpFailure.ts +186 -0
  451. package/src/payments/tpay/tpayPaymentExecutionAdapter.ts +297 -0
  452. package/src/payments/tpay/tpayPaymentReconciliationProvider.ts +285 -0
  453. package/src/payments/tpay/tpayTransactionPayload.ts +207 -0
  454. package/src/payments/tpay/wallClockTime.ts +57 -0
  455. package/src/payments/tpay/webhookJwsVerifier.ts +82 -0
  456. package/src/payments/tpay/webhookNormalizer.ts +279 -0
  457. package/src/payments/verifiedEvidenceProjection.ts +179 -0
  458. package/src/payments/verifiedIngress.ts +317 -0
@@ -0,0 +1,786 @@
1
+ -- Native command ledger: contributed provider-command fences and proofs.
2
+ -- Invoker execution by the deployment owner; no new principal, provider client,
3
+ -- payload backfill or browser permission. G14 attempts persist sanitized evidence.
4
+ CREATE TABLE public.fulfillment_provider_attempts (
5
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
6
+ attempt_key text NOT NULL UNIQUE CHECK (btrim(attempt_key) <> ''),
7
+ shipment_id uuid NOT NULL REFERENCES public.fulfillment_shipments(id),
8
+ provider_kind text NOT NULL,
9
+ mode text NOT NULL CHECK (mode IN ('shadow','stage','live')),
10
+ evidence jsonb NOT NULL,
11
+ status text NOT NULL CHECK (status = 'recorded'),
12
+ created_at timestamptz NOT NULL DEFAULT now()
13
+ );
14
+ CREATE TABLE public.commerce_fulfillment_provider_commands (
15
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
16
+ shipment_id uuid NOT NULL REFERENCES public.fulfillment_shipments(id) ON DELETE CASCADE,
17
+ order_id uuid NOT NULL REFERENCES public.commerce_orders(id) ON DELETE CASCADE,
18
+ provider_kind text NOT NULL
19
+ CHECK (provider_kind ~ '^[a-z][a-z0-9_-]{0,63}$'),
20
+ command_kind text NOT NULL CHECK (command_kind = 'dispatch_create'),
21
+ contract_version text NOT NULL CHECK (contract_version = '1'),
22
+ idempotency_key_fingerprint text NOT NULL CHECK (idempotency_key_fingerprint ~ '^[a-f0-9]{64}$'),
23
+ command_key_fingerprint text NOT NULL CHECK (command_key_fingerprint ~ '^[a-f0-9]{64}$'),
24
+ command_payload_fingerprint text NOT NULL CHECK (command_payload_fingerprint ~ '^[a-f0-9]{64}$'),
25
+ provider_idempotency_key_fingerprint text CHECK (provider_idempotency_key_fingerprint ~ '^[a-f0-9]{64}$'),
26
+ routing_fingerprint text NOT NULL CHECK (routing_fingerprint ~ '^[a-f0-9]{64}$'),
27
+ config_fingerprint text NOT NULL CHECK (config_fingerprint ~ '^[a-f0-9]{64}$'),
28
+ correlation_fingerprint text NOT NULL CHECK (correlation_fingerprint ~ '^[a-f0-9]{64}$'),
29
+ status text NOT NULL DEFAULT 'queued' CHECK (status IN (
30
+ 'queued', 'claimed', 'submitting', 'accepted', 'confirmed', 'retry_wait',
31
+ 'uncertain', 'manual_review', 'terminal_rejected', 'cancelled'
32
+ )),
33
+ attempt_count integer NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
34
+ retry_budget integer NOT NULL DEFAULT 0 CHECK (retry_budget BETWEEN 0 AND 20),
35
+ available_at timestamptz NOT NULL DEFAULT now(),
36
+ claim_token uuid,
37
+ claim_owner_fingerprint text CHECK (claim_owner_fingerprint ~ '^[a-f0-9]{64}$'),
38
+ claim_expires_at timestamptz,
39
+ claim_generation bigint NOT NULL DEFAULT 0 CHECK (claim_generation >= 0),
40
+ version bigint NOT NULL DEFAULT 1 CHECK (version > 0),
41
+ last_provider_attempt_id uuid REFERENCES public.fulfillment_provider_attempts(id) ON DELETE SET NULL,
42
+ provider_reference_fingerprint text CHECK (provider_reference_fingerprint ~ '^[a-f0-9]{64}$'),
43
+ last_reason_code text NOT NULL DEFAULT 'command_enqueued' CHECK (last_reason_code IN (
44
+ 'command_enqueued', 'claimed', 'submission_started', 'provider_accepted',
45
+ 'safe_rejection', 'terminal_rejection', 'write_uncertain', 'lease_expired',
46
+ 'provider_effect_present', 'authoritative_absence', 'effect_unknown',
47
+ 'identity_mismatch', 'invalid_contract', 'unsafe_retry', 'operator_cancelled', 'superseded_by_direct_dispatch'
48
+ )),
49
+ accepted_at timestamptz,
50
+ confirmed_at timestamptz,
51
+ finished_at timestamptz,
52
+ created_at timestamptz NOT NULL DEFAULT now(),
53
+ updated_at timestamptz NOT NULL DEFAULT now(),
54
+ UNIQUE (idempotency_key_fingerprint),
55
+ UNIQUE (shipment_id, provider_kind, command_kind),
56
+ UNIQUE (provider_kind, command_kind, command_key_fingerprint)
57
+ );
58
+
59
+ CREATE INDEX idx_fulfillment_provider_commands_claim
60
+ ON public.commerce_fulfillment_provider_commands (provider_kind, command_kind, available_at, created_at)
61
+ WHERE status IN ('queued', 'retry_wait');
62
+ CREATE INDEX idx_fulfillment_provider_commands_expiry
63
+ ON public.commerce_fulfillment_provider_commands (claim_expires_at)
64
+ WHERE status IN ('claimed', 'submitting');
65
+
66
+ CREATE TABLE public.commerce_fulfillment_provider_command_transitions (
67
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
68
+ command_id uuid NOT NULL REFERENCES public.commerce_fulfillment_provider_commands(id) ON DELETE CASCADE,
69
+ from_status text,
70
+ to_status text NOT NULL CHECK (to_status IN (
71
+ 'queued', 'claimed', 'submitting', 'accepted', 'confirmed', 'retry_wait',
72
+ 'uncertain', 'manual_review', 'terminal_rejected', 'cancelled'
73
+ )),
74
+ reason_code text NOT NULL CHECK (reason_code IN (
75
+ 'command_enqueued', 'claimed', 'submission_started', 'provider_accepted',
76
+ 'safe_rejection', 'terminal_rejection', 'write_uncertain', 'lease_expired',
77
+ 'provider_effect_present', 'authoritative_absence', 'effect_unknown',
78
+ 'identity_mismatch', 'invalid_contract', 'unsafe_retry', 'operator_cancelled', 'superseded_by_direct_dispatch'
79
+ )),
80
+ command_version bigint NOT NULL CHECK (command_version > 0),
81
+ attempt_count integer NOT NULL CHECK (attempt_count >= 0),
82
+ claim_token uuid,
83
+ claim_generation bigint,
84
+ provider_attempt_id uuid REFERENCES public.fulfillment_provider_attempts(id) ON DELETE SET NULL,
85
+ proof_kind text CHECK (proof_kind IN ('provider_reference', 'correlation_key', 'provider_idempotency_key', 'authoritative_absence', 'definitive_provider_rejection')),
86
+ proof_fingerprint text CHECK (proof_fingerprint ~ '^[a-f0-9]{64}$'),
87
+ proof_observed_at timestamptz,
88
+ proof_source text CHECK (proof_source IN ('provider_lookup', 'provider_webhook', 'operator_verified', 'provider_response', 'legacy_backfill')),
89
+ occurred_at timestamptz NOT NULL DEFAULT now(),
90
+ CHECK ((proof_kind IS NULL AND proof_fingerprint IS NULL AND proof_observed_at IS NULL AND proof_source IS NULL)
91
+ OR (proof_kind IS NOT NULL AND proof_fingerprint IS NOT NULL AND proof_observed_at IS NOT NULL AND proof_source IS NOT NULL)),
92
+ UNIQUE (command_id, command_version)
93
+ );
94
+ CREATE INDEX idx_fulfillment_provider_command_transitions_command
95
+ ON public.commerce_fulfillment_provider_command_transitions (command_id, command_version DESC);
96
+
97
+ REVOKE ALL ON TABLE public.commerce_fulfillment_provider_commands FROM PUBLIC, anon, authenticated;
98
+ REVOKE ALL ON TABLE public.commerce_fulfillment_provider_command_transitions FROM PUBLIC, anon, authenticated;
99
+
100
+ CREATE FUNCTION public.commerce_fulfillment_provider_command_transition_allowed(p_from text, p_to text)
101
+ RETURNS boolean LANGUAGE sql IMMUTABLE SET search_path = pg_catalog AS $$
102
+ SELECT (p_from = 'queued' AND p_to IN ('claimed', 'cancelled'))
103
+ OR (p_from = 'claimed' AND p_to IN ('queued', 'retry_wait', 'cancelled', 'submitting'))
104
+ OR (p_from = 'submitting' AND p_to IN ('accepted', 'retry_wait', 'uncertain', 'manual_review', 'terminal_rejected'))
105
+ OR (p_from = 'accepted' AND p_to IN ('confirmed', 'uncertain'))
106
+ OR (p_from = 'retry_wait' AND p_to IN ('claimed', 'cancelled'))
107
+ OR (p_from = 'uncertain' AND p_to IN ('accepted', 'confirmed', 'retry_wait', 'manual_review'))
108
+ OR (p_from = 'manual_review' AND p_to IN ('confirmed', 'retry_wait', 'terminal_rejected', 'cancelled'));
109
+ $$;
110
+
111
+ CREATE FUNCTION public.commerce_fulfillment_provider_command_guard()
112
+ RETURNS trigger LANGUAGE plpgsql SET search_path = pg_catalog AS $$
113
+ BEGIN
114
+ IF NEW.shipment_id IS DISTINCT FROM OLD.shipment_id
115
+ OR NEW.order_id IS DISTINCT FROM OLD.order_id
116
+ OR NEW.provider_kind IS DISTINCT FROM OLD.provider_kind
117
+ OR NEW.command_kind IS DISTINCT FROM OLD.command_kind
118
+ OR NEW.contract_version IS DISTINCT FROM OLD.contract_version
119
+ OR NEW.idempotency_key_fingerprint IS DISTINCT FROM OLD.idempotency_key_fingerprint
120
+ OR NEW.command_key_fingerprint IS DISTINCT FROM OLD.command_key_fingerprint
121
+ OR NEW.command_payload_fingerprint IS DISTINCT FROM OLD.command_payload_fingerprint
122
+ OR NEW.provider_idempotency_key_fingerprint IS DISTINCT FROM OLD.provider_idempotency_key_fingerprint
123
+ OR NEW.routing_fingerprint IS DISTINCT FROM OLD.routing_fingerprint
124
+ OR NEW.config_fingerprint IS DISTINCT FROM OLD.config_fingerprint
125
+ OR NEW.correlation_fingerprint IS DISTINCT FROM OLD.correlation_fingerprint
126
+ OR NEW.retry_budget IS DISTINCT FROM OLD.retry_budget
127
+ THEN RAISE EXCEPTION 'fulfillment_provider_command_identity_immutable' USING ERRCODE = '22023'; END IF;
128
+ IF NEW.attempt_count < OLD.attempt_count OR NEW.claim_generation < OLD.claim_generation THEN
129
+ RAISE EXCEPTION 'fulfillment_provider_command_counter_not_monotonic' USING ERRCODE = '22023';
130
+ END IF;
131
+ IF NEW.version <> OLD.version + 1 THEN RAISE EXCEPTION 'fulfillment_provider_command_version_invalid' USING ERRCODE = '22023'; END IF;
132
+ IF NEW.status = OLD.status THEN RAISE EXCEPTION 'fulfillment_provider_command_transition_required' USING ERRCODE = '22023'; END IF;
133
+ IF NEW.status IS DISTINCT FROM OLD.status
134
+ AND NOT public.commerce_fulfillment_provider_command_transition_allowed(OLD.status, NEW.status)
135
+ THEN RAISE EXCEPTION 'fulfillment_provider_command_transition_invalid' USING ERRCODE = '22023'; END IF;
136
+ RETURN NEW;
137
+ END;
138
+ $$;
139
+ CREATE TRIGGER commerce_fulfillment_provider_command_guard
140
+ BEFORE UPDATE ON public.commerce_fulfillment_provider_commands
141
+ FOR EACH ROW EXECUTE FUNCTION public.commerce_fulfillment_provider_command_guard();
142
+
143
+ CREATE FUNCTION public.commerce_fulfillment_provider_command_result(p_command public.commerce_fulfillment_provider_commands)
144
+ RETURNS jsonb LANGUAGE sql STABLE SET search_path = pg_catalog AS $$
145
+ SELECT jsonb_build_object('commandId', p_command.id, 'status', p_command.status, 'version', p_command.version, 'applied', true);
146
+ $$;
147
+
148
+ CREATE FUNCTION public.commerce_fulfillment_enqueue_provider_command(
149
+ p_shipment_id uuid, p_provider_kind text, p_command_kind text,
150
+ p_contract_version text, p_idempotency_key text,
151
+ p_command_key_fingerprint text, p_command_payload_fingerprint text,
152
+ p_provider_idempotency_key_fingerprint text, p_routing_fingerprint text,
153
+ p_config_fingerprint text, p_correlation_fingerprint text, p_retry_budget integer DEFAULT 0
154
+ ) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
155
+ DECLARE v_command public.commerce_fulfillment_provider_commands%ROWTYPE; v_order_id uuid; v_idempotency_key_fingerprint text;
156
+ BEGIN
157
+ IF p_shipment_id IS NULL OR p_provider_kind !~ '^[a-z][a-z0-9_-]{0,63}$'
158
+ OR p_command_kind <> 'dispatch_create' OR p_contract_version <> '1'
159
+ OR p_idempotency_key !~* '^[a-f0-9]{8}-[a-f0-9]{4}-[1-8][a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$'
160
+ OR p_command_key_fingerprint !~ '^[a-f0-9]{64}$'
161
+ OR p_command_payload_fingerprint !~ '^[a-f0-9]{64}$'
162
+ OR p_routing_fingerprint !~ '^[a-f0-9]{64}$' OR p_config_fingerprint !~ '^[a-f0-9]{64}$'
163
+ OR p_correlation_fingerprint IS NULL OR p_correlation_fingerprint !~ '^[a-f0-9]{64}$'
164
+ OR (p_provider_idempotency_key_fingerprint IS NOT NULL AND p_provider_idempotency_key_fingerprint !~ '^[a-f0-9]{64}$')
165
+ OR p_retry_budget NOT BETWEEN 0 AND 20
166
+ THEN RAISE EXCEPTION 'fulfillment_provider_command_enqueue_invalid_input' USING ERRCODE = '22023'; END IF;
167
+ PERFORM pg_advisory_xact_lock(hashtextextended('native-dispatch|' || p_shipment_id::text,0));
168
+ PERFORM id FROM public.fulfillment_shipments WHERE id = p_shipment_id FOR UPDATE;
169
+ v_idempotency_key_fingerprint := encode(sha256(convert_to('provider-command-idempotency|' || p_idempotency_key, 'UTF8')), 'hex');
170
+ SELECT * INTO v_command FROM public.commerce_fulfillment_provider_commands WHERE idempotency_key_fingerprint = v_idempotency_key_fingerprint FOR UPDATE;
171
+ IF FOUND THEN
172
+ IF v_command.shipment_id <> p_shipment_id OR v_command.provider_kind <> p_provider_kind
173
+ OR v_command.command_kind <> p_command_kind OR v_command.command_key_fingerprint <> p_command_key_fingerprint
174
+ OR v_command.command_payload_fingerprint <> p_command_payload_fingerprint OR v_command.routing_fingerprint <> p_routing_fingerprint
175
+ OR v_command.config_fingerprint <> p_config_fingerprint OR v_command.correlation_fingerprint <> p_correlation_fingerprint
176
+ OR v_command.provider_idempotency_key_fingerprint IS DISTINCT FROM p_provider_idempotency_key_fingerprint
177
+ THEN RAISE EXCEPTION 'fulfillment_provider_command_enqueue_identity_conflict' USING ERRCODE = '23505'; END IF;
178
+ RETURN public.commerce_fulfillment_provider_command_result(v_command);
179
+ END IF;
180
+ SELECT order_id INTO v_order_id FROM public.fulfillment_shipments WHERE id = p_shipment_id;
181
+ IF NOT FOUND THEN RAISE EXCEPTION 'fulfillment_provider_command_fulfillment_not_found' USING ERRCODE = '22023'; END IF;
182
+ IF EXISTS (SELECT 1 FROM public.fulfillment_dispatch_refs WHERE shipment_id=p_shipment_id AND (external_ref IS NOT NULL OR status IN ('submitting','uncertain','created'))) THEN RAISE EXCEPTION 'fulfillment_command_direct_effect_exists' USING ERRCODE='22023'; END IF;
183
+ INSERT INTO public.commerce_fulfillment_provider_commands (
184
+ shipment_id, order_id, provider_kind, command_kind, contract_version,
185
+ idempotency_key_fingerprint, command_key_fingerprint, command_payload_fingerprint,
186
+ provider_idempotency_key_fingerprint, routing_fingerprint, config_fingerprint,
187
+ correlation_fingerprint, retry_budget
188
+ ) VALUES (p_shipment_id, v_order_id, p_provider_kind, p_command_kind, p_contract_version,
189
+ v_idempotency_key_fingerprint, p_command_key_fingerprint, p_command_payload_fingerprint,
190
+ p_provider_idempotency_key_fingerprint, p_routing_fingerprint, p_config_fingerprint,
191
+ p_correlation_fingerprint, p_retry_budget) RETURNING * INTO v_command;
192
+ INSERT INTO public.commerce_fulfillment_provider_command_transitions(command_id, from_status, to_status, reason_code, command_version, attempt_count)
193
+ VALUES (v_command.id, NULL, 'queued', 'command_enqueued', v_command.version, v_command.attempt_count);
194
+ RETURN public.commerce_fulfillment_provider_command_result(v_command);
195
+ END;
196
+ $$;
197
+
198
+ CREATE FUNCTION public.commerce_fulfillment_claim_provider_commands(
199
+ p_provider_kind text, p_command_kind text, p_limit integer, p_claim_owner text, p_lease_seconds integer
200
+ ) RETURNS SETOF jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
201
+ DECLARE v_command public.commerce_fulfillment_provider_commands%ROWTYPE; v_token uuid; v_now timestamptz := now(); v_from text;
202
+ BEGIN
203
+ IF p_provider_kind !~ '^[a-z][a-z0-9_-]{0,63}$' OR p_command_kind <> 'dispatch_create'
204
+ OR p_claim_owner !~ '^[a-f0-9]{64}$' OR p_limit NOT BETWEEN 1 AND 100 OR p_lease_seconds NOT BETWEEN 10 AND 900
205
+ THEN RAISE EXCEPTION 'fulfillment_provider_command_claim_invalid_input' USING ERRCODE = '22023'; END IF;
206
+ FOR v_command IN SELECT * FROM public.commerce_fulfillment_provider_commands
207
+ WHERE provider_kind = p_provider_kind AND command_kind = p_command_kind
208
+ AND status IN ('queued', 'retry_wait') AND available_at <= v_now
209
+ AND attempt_count < retry_budget
210
+ ORDER BY available_at, created_at LIMIT p_limit
211
+ LOOP
212
+ IF NOT pg_try_advisory_xact_lock(hashtextextended('native-dispatch|' || v_command.shipment_id::text,0)) THEN CONTINUE; END IF;
213
+ SELECT * INTO v_command FROM public.commerce_fulfillment_provider_commands
214
+ WHERE id=v_command.id AND status IN ('queued','retry_wait') AND available_at<=v_now AND attempt_count<retry_budget
215
+ FOR UPDATE SKIP LOCKED;
216
+ IF NOT FOUND THEN CONTINUE; END IF;
217
+ IF EXISTS (SELECT 1 FROM public.fulfillment_dispatch_refs WHERE shipment_id=v_command.shipment_id AND (external_ref IS NOT NULL OR status IN ('submitting','uncertain','created'))) THEN CONTINUE; END IF;
218
+ v_token := gen_random_uuid();
219
+ v_from := v_command.status;
220
+ UPDATE public.commerce_fulfillment_provider_commands SET status='claimed',
221
+ claim_token=v_token, claim_owner_fingerprint=p_claim_owner,
222
+ claim_expires_at=v_now + make_interval(secs => p_lease_seconds), claim_generation=claim_generation+1,
223
+ last_reason_code='claimed', version=version+1, updated_at=v_now WHERE id=v_command.id RETURNING * INTO v_command;
224
+ INSERT INTO public.commerce_fulfillment_provider_command_transitions(command_id, from_status, to_status, reason_code, command_version, attempt_count, claim_token, claim_generation)
225
+ VALUES(v_command.id, v_from, 'claimed', 'claimed', v_command.version, v_command.attempt_count, v_command.claim_token, v_command.claim_generation);
226
+ RETURN NEXT jsonb_build_object('commandId',v_command.id,'fulfillmentRef',v_command.shipment_id,'orderRef',v_command.order_id,
227
+ 'providerRef',v_command.provider_kind,'commandKind',v_command.command_kind,'contractVersion',v_command.contract_version,'status',v_command.status,'version',v_command.version,
228
+ 'commandKeyFingerprint',v_command.command_key_fingerprint,'commandPayloadFingerprint',v_command.command_payload_fingerprint,
229
+ 'providerIdempotencyKeyFingerprint',v_command.provider_idempotency_key_fingerprint,'routingFingerprint',v_command.routing_fingerprint,
230
+ 'configurationFingerprint',v_command.config_fingerprint,'correlationKeyFingerprint',v_command.correlation_fingerprint,
231
+ 'retryBudget',v_command.retry_budget,'retryCount',v_command.attempt_count,'claimToken',v_command.claim_token,
232
+ 'claimGeneration',v_command.claim_generation,'leaseExpiresAt',v_command.claim_expires_at);
233
+ END LOOP;
234
+ END;
235
+ $$;
236
+
237
+ CREATE FUNCTION public.commerce_fulfillment_provider_command_fence(
238
+ p_command_id uuid, p_claim_token uuid, p_claim_generation bigint, p_expected_version bigint
239
+ ) RETURNS public.commerce_fulfillment_provider_commands LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
240
+ DECLARE v_command public.commerce_fulfillment_provider_commands%ROWTYPE;
241
+ BEGIN
242
+ SELECT * INTO v_command FROM public.commerce_fulfillment_provider_commands WHERE id=p_command_id FOR UPDATE;
243
+ IF NOT FOUND THEN RAISE EXCEPTION 'fulfillment_provider_command_not_found' USING ERRCODE='22023'; END IF;
244
+ IF v_command.version <> p_expected_version OR v_command.claim_token IS DISTINCT FROM p_claim_token
245
+ OR v_command.claim_generation <> p_claim_generation OR v_command.claim_expires_at <= now()
246
+ THEN RAISE EXCEPTION 'fulfillment_provider_command_fence_conflict' USING ERRCODE='40001'; END IF;
247
+ RETURN v_command;
248
+ END;
249
+ $$;
250
+
251
+ CREATE FUNCTION public.commerce_fulfillment_mark_command_submitting(
252
+ p_command_id uuid,p_claim_token uuid,p_claim_generation bigint,p_expected_version bigint,p_provider_attempt_id uuid DEFAULT NULL
253
+ ) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
254
+ DECLARE v_command public.commerce_fulfillment_provider_commands%ROWTYPE;
255
+ BEGIN
256
+ v_command := public.commerce_fulfillment_provider_command_fence(p_command_id,p_claim_token,p_claim_generation,p_expected_version);
257
+ IF v_command.status <> 'claimed' THEN RAISE EXCEPTION 'fulfillment_provider_command_not_claimed' USING ERRCODE='22023'; END IF;
258
+ IF p_provider_attempt_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM public.fulfillment_provider_attempts a WHERE a.id=p_provider_attempt_id AND a.shipment_id=v_command.shipment_id AND a.provider_kind=v_command.provider_kind) THEN RAISE EXCEPTION 'fulfillment_provider_command_attempt_mismatch' USING ERRCODE='22023'; END IF;
259
+ UPDATE public.commerce_fulfillment_provider_commands SET status='submitting',attempt_count=attempt_count+1,last_provider_attempt_id=p_provider_attempt_id,last_reason_code='submission_started',version=version+1,updated_at=now() WHERE id=p_command_id RETURNING * INTO v_command;
260
+ INSERT INTO public.commerce_fulfillment_provider_command_transitions(command_id,from_status,to_status,reason_code,command_version,attempt_count,claim_token,claim_generation,provider_attempt_id) VALUES(v_command.id,'claimed','submitting','submission_started',v_command.version,v_command.attempt_count,p_claim_token,p_claim_generation,p_provider_attempt_id);
261
+ RETURN public.commerce_fulfillment_provider_command_result(v_command);
262
+ END; $$;
263
+
264
+ CREATE FUNCTION public.commerce_fulfillment_apply_command_write_outcome(
265
+ p_command_id uuid,p_claim_token uuid,p_claim_generation bigint,p_expected_version bigint,p_identity_fingerprint text,p_outcome_kind text,p_reason_code text,p_retry_not_before timestamptz DEFAULT NULL,p_provider_reference_fingerprint text DEFAULT NULL,p_provider_attempt_id uuid DEFAULT NULL,p_proof_kind text DEFAULT NULL,p_proof_fingerprint text DEFAULT NULL,p_proof_observed_at timestamptz DEFAULT NULL,p_proof_source text DEFAULT NULL
266
+ ) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
267
+ DECLARE v_command public.commerce_fulfillment_provider_commands%ROWTYPE; v_to text;
268
+ BEGIN
269
+ v_command := public.commerce_fulfillment_provider_command_fence(p_command_id,p_claim_token,p_claim_generation,p_expected_version);
270
+ IF v_command.status <> 'submitting' THEN RAISE EXCEPTION 'fulfillment_provider_command_not_submitting' USING ERRCODE='22023'; END IF;
271
+ IF p_provider_attempt_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM public.fulfillment_provider_attempts a WHERE a.id=p_provider_attempt_id AND a.shipment_id=v_command.shipment_id AND a.provider_kind=v_command.provider_kind) THEN RAISE EXCEPTION 'fulfillment_provider_command_attempt_mismatch' USING ERRCODE='22023'; END IF;
272
+ IF p_identity_fingerprint !~ '^[a-f0-9]{64}$' OR (p_identity_fingerprint = v_command.command_key_fingerprint OR p_identity_fingerprint = v_command.correlation_fingerprint OR p_identity_fingerprint = v_command.provider_idempotency_key_fingerprint) IS NOT TRUE THEN RAISE EXCEPTION 'fulfillment_provider_command_outcome_identity_invalid' USING ERRCODE='22023'; END IF;
273
+ IF p_outcome_kind='accepted' AND p_reason_code='provider_accepted' AND p_provider_reference_fingerprint ~ '^[a-f0-9]{64}$'
274
+ AND p_proof_kind IS NULL AND p_proof_fingerprint IS NULL AND p_proof_observed_at IS NULL AND p_proof_source IS NULL THEN v_to:='accepted';
275
+ ELSIF p_outcome_kind='uncertain' AND p_reason_code='write_uncertain'
276
+ AND p_proof_kind IS NULL AND p_proof_fingerprint IS NULL AND p_proof_observed_at IS NULL AND p_proof_source IS NULL THEN v_to:='uncertain';
277
+ ELSIF p_outcome_kind='rejected' AND p_reason_code IN ('terminal_rejection','safe_rejection')
278
+ AND p_proof_kind='definitive_provider_rejection' AND p_proof_fingerprint ~ '^[a-f0-9]{64}$'
279
+ AND p_proof_source='provider_response' AND p_proof_observed_at BETWEEN now()-interval '10 minutes' AND now()
280
+ THEN
281
+ IF p_reason_code='safe_rejection' AND v_command.attempt_count < v_command.retry_budget AND p_retry_not_before > now() THEN v_to:='retry_wait';
282
+ ELSIF p_reason_code='safe_rejection' THEN v_to:='manual_review';
283
+ ELSE v_to:='terminal_rejected'; END IF;
284
+ ELSE RAISE EXCEPTION 'fulfillment_provider_command_outcome_invalid' USING ERRCODE='22023'; END IF;
285
+ UPDATE public.commerce_fulfillment_provider_commands SET status=v_to,available_at=COALESCE(p_retry_not_before,available_at),provider_reference_fingerprint=CASE WHEN v_to='accepted' THEN p_provider_reference_fingerprint ELSE provider_reference_fingerprint END,last_provider_attempt_id=COALESCE(p_provider_attempt_id,last_provider_attempt_id),last_reason_code=p_reason_code,accepted_at=CASE WHEN v_to='accepted' THEN now() ELSE accepted_at END,finished_at=CASE WHEN v_to IN ('terminal_rejected','manual_review') THEN now() ELSE finished_at END,claim_token=CASE WHEN v_to='accepted' THEN claim_token ELSE NULL END,claim_owner_fingerprint=CASE WHEN v_to='accepted' THEN claim_owner_fingerprint ELSE NULL END,claim_expires_at=CASE WHEN v_to='accepted' THEN claim_expires_at ELSE NULL END,version=version+1,updated_at=now() WHERE id=p_command_id RETURNING * INTO v_command;
286
+ INSERT INTO public.commerce_fulfillment_provider_command_transitions(command_id,from_status,to_status,reason_code,command_version,attempt_count,claim_token,claim_generation,provider_attempt_id,proof_kind,proof_fingerprint,proof_observed_at,proof_source) VALUES(v_command.id,'submitting',v_to,p_reason_code,v_command.version,v_command.attempt_count,p_claim_token,p_claim_generation,p_provider_attempt_id,p_proof_kind,p_proof_fingerprint,p_proof_observed_at,p_proof_source);
287
+ RETURN public.commerce_fulfillment_provider_command_result(v_command);
288
+ END; $$;
289
+
290
+ CREATE FUNCTION public.commerce_fulfillment_confirm_provider_command(
291
+ p_command_id uuid,p_claim_token uuid,p_claim_generation bigint,p_expected_version bigint,p_identity_fingerprint text,p_provider_attempt_id uuid DEFAULT NULL,p_proof_kind text DEFAULT NULL,p_proof_fingerprint text DEFAULT NULL,p_proof_observed_at timestamptz DEFAULT NULL,p_proof_source text DEFAULT NULL
292
+ ) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
293
+ DECLARE v_command public.commerce_fulfillment_provider_commands%ROWTYPE;
294
+ BEGIN
295
+ v_command := public.commerce_fulfillment_provider_command_fence(p_command_id,p_claim_token,p_claim_generation,p_expected_version);
296
+ IF v_command.status <> 'accepted' OR v_command.provider_reference_fingerprint IS NULL THEN RAISE EXCEPTION 'fulfillment_provider_command_confirmation_invalid' USING ERRCODE='22023'; END IF;
297
+ IF p_provider_attempt_id IS NOT NULL AND NOT EXISTS (SELECT 1 FROM public.fulfillment_provider_attempts a WHERE a.id=p_provider_attempt_id AND a.shipment_id=v_command.shipment_id AND a.provider_kind=v_command.provider_kind) THEN RAISE EXCEPTION 'fulfillment_provider_command_attempt_mismatch' USING ERRCODE='22023'; END IF;
298
+ IF p_identity_fingerprint !~ '^[a-f0-9]{64}$' OR p_proof_kind NOT IN ('provider_reference','correlation_key','provider_idempotency_key') OR p_proof_fingerprint !~ '^[a-f0-9]{64}$' OR p_proof_source NOT IN ('provider_lookup','provider_webhook','operator_verified') OR p_proof_observed_at > now() OR p_proof_observed_at < now()-interval '24 hours'
299
+ OR (p_identity_fingerprint = v_command.correlation_fingerprint OR p_identity_fingerprint = v_command.provider_idempotency_key_fingerprint) IS NOT TRUE
300
+ OR (p_proof_kind='correlation_key' AND p_identity_fingerprint IS DISTINCT FROM v_command.correlation_fingerprint)
301
+ OR (p_proof_kind='provider_idempotency_key' AND p_identity_fingerprint IS DISTINCT FROM v_command.provider_idempotency_key_fingerprint)
302
+ THEN RAISE EXCEPTION 'fulfillment_provider_command_confirmation_proof_invalid' USING ERRCODE='22023'; END IF;
303
+ UPDATE public.commerce_fulfillment_provider_commands SET status='confirmed',last_provider_attempt_id=COALESCE(p_provider_attempt_id,last_provider_attempt_id),confirmed_at=now(),finished_at=now(),last_reason_code='provider_effect_present',claim_token=NULL,claim_owner_fingerprint=NULL,claim_expires_at=NULL,version=version+1,updated_at=now() WHERE id=p_command_id RETURNING * INTO v_command;
304
+ INSERT INTO public.commerce_fulfillment_provider_command_transitions(command_id,from_status,to_status,reason_code,command_version,attempt_count,claim_token,claim_generation,provider_attempt_id,proof_kind,proof_fingerprint,proof_observed_at,proof_source) VALUES(v_command.id,'accepted','confirmed','provider_effect_present',v_command.version,v_command.attempt_count,p_claim_token,p_claim_generation,p_provider_attempt_id,p_proof_kind,p_proof_fingerprint,p_proof_observed_at,p_proof_source);
305
+ RETURN public.commerce_fulfillment_provider_command_result(v_command);
306
+ END; $$;
307
+
308
+ CREATE FUNCTION public.commerce_fulfillment_reconcile_provider_command(
309
+ p_command_id uuid,p_expected_version bigint,p_identity_fingerprint text,p_proof_kind text,p_proof_fingerprint text,p_proof_observed_at timestamptz,p_proof_source text
310
+ ) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
311
+ DECLARE v_command public.commerce_fulfillment_provider_commands%ROWTYPE; v_from text;
312
+ BEGIN
313
+ SELECT * INTO v_command FROM public.commerce_fulfillment_provider_commands WHERE id=p_command_id FOR UPDATE;
314
+ IF NOT FOUND OR v_command.version<>p_expected_version THEN RAISE EXCEPTION 'fulfillment_provider_command_reconcile_conflict' USING ERRCODE='40001'; END IF;
315
+ IF v_command.status NOT IN ('accepted','uncertain','manual_review') OR p_proof_kind NOT IN ('provider_reference','correlation_key','provider_idempotency_key') OR p_identity_fingerprint !~ '^[a-f0-9]{64}$' OR p_proof_fingerprint !~ '^[a-f0-9]{64}$' OR p_proof_source NOT IN ('provider_lookup','provider_webhook','operator_verified') OR p_proof_observed_at > now() OR p_proof_observed_at < now()-interval '24 hours'
316
+ OR (p_identity_fingerprint = v_command.correlation_fingerprint OR p_identity_fingerprint = v_command.provider_idempotency_key_fingerprint) IS NOT TRUE
317
+ OR (p_proof_kind='correlation_key' AND p_identity_fingerprint IS DISTINCT FROM v_command.correlation_fingerprint)
318
+ OR (p_proof_kind='provider_idempotency_key' AND p_identity_fingerprint IS DISTINCT FROM v_command.provider_idempotency_key_fingerprint)
319
+ THEN RAISE EXCEPTION 'fulfillment_provider_command_reconcile_proof_invalid' USING ERRCODE='22023'; END IF;
320
+ v_from := v_command.status;
321
+ UPDATE public.commerce_fulfillment_provider_commands SET status='confirmed',provider_reference_fingerprint=p_proof_fingerprint,confirmed_at=now(),finished_at=now(),last_reason_code='provider_effect_present',claim_token=NULL,claim_owner_fingerprint=NULL,claim_expires_at=NULL,version=version+1,updated_at=now() WHERE id=p_command_id RETURNING * INTO v_command;
322
+ INSERT INTO public.commerce_fulfillment_provider_command_transitions(command_id,from_status,to_status,reason_code,command_version,attempt_count,proof_kind,proof_fingerprint,proof_observed_at,proof_source) VALUES(v_command.id,v_from,'confirmed','provider_effect_present',v_command.version,v_command.attempt_count,p_proof_kind,p_proof_fingerprint,p_proof_observed_at,p_proof_source);
323
+ RETURN public.commerce_fulfillment_provider_command_result(v_command) || jsonb_build_object('proofRecorded',true);
324
+ END; $$;
325
+
326
+ CREATE FUNCTION public.commerce_fulfillment_resolve_uncertain_command(
327
+ p_command_id uuid,p_expected_version bigint,p_identity_fingerprint text,p_decision text,p_reason_code text,p_proof_kind text,p_proof_fingerprint text,p_proof_observed_at timestamptz,p_proof_source text,p_retry_not_before timestamptz DEFAULT NULL
328
+ ) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
329
+ DECLARE v_command public.commerce_fulfillment_provider_commands%ROWTYPE; v_to text; v_from text;
330
+ BEGIN
331
+ SELECT * INTO v_command FROM public.commerce_fulfillment_provider_commands WHERE id=p_command_id FOR UPDATE;
332
+ IF NOT FOUND OR v_command.version<>p_expected_version OR v_command.status NOT IN ('uncertain','manual_review') THEN RAISE EXCEPTION 'fulfillment_provider_command_resolution_conflict' USING ERRCODE='40001'; END IF;
333
+ IF p_identity_fingerprint !~ '^[a-f0-9]{64}$' OR p_proof_fingerprint !~ '^[a-f0-9]{64}$' OR p_proof_source NOT IN ('provider_lookup','provider_webhook','operator_verified') OR p_proof_observed_at > now() OR p_proof_observed_at < now()-interval '10 minutes'
334
+ OR (p_identity_fingerprint = v_command.correlation_fingerprint OR p_identity_fingerprint = v_command.provider_idempotency_key_fingerprint) IS NOT TRUE
335
+ OR (p_proof_kind='correlation_key' AND p_identity_fingerprint IS DISTINCT FROM v_command.correlation_fingerprint)
336
+ OR (p_proof_kind='provider_idempotency_key' AND p_identity_fingerprint IS DISTINCT FROM v_command.provider_idempotency_key_fingerprint)
337
+ OR (p_proof_kind='authoritative_absence' AND p_proof_fingerprint IS DISTINCT FROM p_identity_fingerprint)
338
+ THEN RAISE EXCEPTION 'fulfillment_provider_command_resolution_proof_invalid' USING ERRCODE='22023'; END IF;
339
+ IF p_decision='manual_review' AND p_reason_code IN ('effect_unknown','identity_mismatch','invalid_contract','unsafe_retry') AND p_proof_kind IN ('provider_reference','correlation_key','provider_idempotency_key') THEN v_to:='manual_review';
340
+ ELSIF p_decision='retry_wait' AND p_reason_code='authoritative_absence' AND p_proof_kind='authoritative_absence' AND p_proof_source='provider_lookup' AND p_retry_not_before > now() THEN v_to:='retry_wait';
341
+ ELSE RAISE EXCEPTION 'fulfillment_provider_command_resolution_unsafe' USING ERRCODE='22023'; END IF;
342
+ v_from := v_command.status;
343
+ UPDATE public.commerce_fulfillment_provider_commands SET status=v_to,available_at=COALESCE(p_retry_not_before,available_at),last_reason_code=p_reason_code,claim_token=NULL,claim_owner_fingerprint=NULL,claim_expires_at=NULL,version=version+1,updated_at=now() WHERE id=p_command_id RETURNING * INTO v_command;
344
+ INSERT INTO public.commerce_fulfillment_provider_command_transitions(command_id,from_status,to_status,reason_code,command_version,attempt_count,proof_kind,proof_fingerprint,proof_observed_at,proof_source) VALUES(v_command.id,v_from,v_to,p_reason_code,v_command.version,v_command.attempt_count,p_proof_kind,p_proof_fingerprint,p_proof_observed_at,p_proof_source);
345
+ RETURN public.commerce_fulfillment_provider_command_result(v_command) || jsonb_build_object('proofRecorded',true);
346
+ END; $$;
347
+
348
+ CREATE FUNCTION public.commerce_fulfillment_release_or_cancel_command(
349
+ p_command_id uuid,p_claim_token uuid,p_claim_generation bigint,p_expected_version bigint,p_action text,p_reason_code text
350
+ ) RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
351
+ DECLARE v_command public.commerce_fulfillment_provider_commands%ROWTYPE; v_to text;
352
+ BEGIN
353
+ v_command := public.commerce_fulfillment_provider_command_fence(p_command_id,p_claim_token,p_claim_generation,p_expected_version);
354
+ IF p_action='release' AND v_command.status='claimed' THEN v_to:='queued';
355
+ ELSIF p_action='cancel' AND v_command.status='claimed' AND p_reason_code='operator_cancelled' THEN v_to:='cancelled';
356
+ ELSE RAISE EXCEPTION 'fulfillment_provider_command_release_invalid' USING ERRCODE='22023'; END IF;
357
+ UPDATE public.commerce_fulfillment_provider_commands SET status=v_to,last_reason_code=CASE WHEN v_to='cancelled' THEN 'operator_cancelled' ELSE 'command_enqueued' END,finished_at=CASE WHEN v_to='cancelled' THEN now() ELSE finished_at END,claim_token=NULL,claim_owner_fingerprint=NULL,claim_expires_at=NULL,version=version+1,updated_at=now() WHERE id=p_command_id RETURNING * INTO v_command;
358
+ INSERT INTO public.commerce_fulfillment_provider_command_transitions(command_id,from_status,to_status,reason_code,command_version,attempt_count,claim_token,claim_generation) VALUES(v_command.id,'claimed',v_to,v_command.last_reason_code,v_command.version,v_command.attempt_count,p_claim_token,p_claim_generation);
359
+ RETURN public.commerce_fulfillment_provider_command_result(v_command);
360
+ END; $$;
361
+
362
+ CREATE FUNCTION public.commerce_fulfillment_recover_expired_provider_commands(p_limit integer DEFAULT 100)
363
+ RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
364
+ DECLARE v_command public.commerce_fulfillment_provider_commands%ROWTYPE; v_results jsonb := '[]'::jsonb; v_from text;
365
+ BEGIN
366
+ IF p_limit NOT BETWEEN 1 AND 500 THEN RAISE EXCEPTION 'fulfillment_provider_command_recovery_invalid_limit' USING ERRCODE='22023'; END IF;
367
+ FOR v_command IN SELECT * FROM public.commerce_fulfillment_provider_commands WHERE status IN ('claimed','submitting','accepted') AND claim_expires_at <= now() ORDER BY claim_expires_at FOR UPDATE SKIP LOCKED LIMIT p_limit LOOP
368
+ v_from := v_command.status;
369
+ UPDATE public.commerce_fulfillment_provider_commands SET status=CASE WHEN v_command.status IN ('submitting','accepted') THEN 'uncertain' ELSE 'queued' END,last_reason_code=CASE WHEN v_command.status IN ('submitting','accepted') THEN 'lease_expired' ELSE 'command_enqueued' END,claim_token=NULL,claim_owner_fingerprint=NULL,claim_expires_at=NULL,version=version+1,updated_at=now() WHERE id=v_command.id RETURNING * INTO v_command;
370
+ INSERT INTO public.commerce_fulfillment_provider_command_transitions(command_id,from_status,to_status,reason_code,command_version,attempt_count) VALUES(v_command.id,v_from,v_command.status,v_command.last_reason_code,v_command.version,v_command.attempt_count);
371
+ v_results := v_results || public.commerce_fulfillment_provider_command_result(v_command);
372
+ END LOOP;
373
+ RETURN v_results;
374
+ END; $$;
375
+
376
+
377
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_provider_command_transition_allowed(text,text) FROM PUBLIC, anon, authenticated;
378
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_provider_command_guard() FROM PUBLIC, anon, authenticated;
379
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_provider_command_result(public.commerce_fulfillment_provider_commands) FROM PUBLIC, anon, authenticated;
380
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_enqueue_provider_command(uuid,text,text,text,text,text,text,text,text,text,text,integer) FROM PUBLIC, anon, authenticated;
381
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_claim_provider_commands(text,text,integer,text,integer) FROM PUBLIC, anon, authenticated;
382
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_provider_command_fence(uuid,uuid,bigint,bigint) FROM PUBLIC, anon, authenticated;
383
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_mark_command_submitting(uuid,uuid,bigint,bigint,uuid) FROM PUBLIC, anon, authenticated;
384
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_apply_command_write_outcome(uuid,uuid,bigint,bigint,text,text,text,timestamptz,text,uuid,text,text,timestamptz,text) FROM PUBLIC, anon, authenticated;
385
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_confirm_provider_command(uuid,uuid,bigint,bigint,text,uuid,text,text,timestamptz,text) FROM PUBLIC, anon, authenticated;
386
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_reconcile_provider_command(uuid,bigint,text,text,text,timestamptz,text) FROM PUBLIC, anon, authenticated;
387
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_resolve_uncertain_command(uuid,bigint,text,text,text,text,text,timestamptz,text,timestamptz) FROM PUBLIC, anon, authenticated;
388
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_release_or_cancel_command(uuid,uuid,bigint,bigint,text,text) FROM PUBLIC, anon, authenticated;
389
+ REVOKE ALL ON FUNCTION public.commerce_fulfillment_recover_expired_provider_commands(integer) FROM PUBLIC, anon, authenticated;
390
+ REVOKE ALL ON public.fulfillment_provider_attempts FROM PUBLIC, anon, authenticated;
391
+
392
+ -- Host-owned contact revision/withholding is synchronized by the selected
393
+ -- preparation binding. It contains no recipient or provider payload.
394
+ CREATE TABLE public.fulfillment_dispatch_contexts (
395
+ shipment_id uuid PRIMARY KEY REFERENCES public.fulfillment_shipments(id),
396
+ contact_revision jsonb NOT NULL,
397
+ provider_ref text NOT NULL CHECK (btrim(provider_ref) <> ''),
398
+ withheld boolean NOT NULL DEFAULT false
399
+ );
400
+ CREATE TABLE public.fulfillment_dispatch_refs (
401
+ id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
402
+ request_key text NOT NULL UNIQUE CHECK (btrim(request_key) <> ''),
403
+ shipment_id uuid NOT NULL UNIQUE REFERENCES public.fulfillment_shipments(id),
404
+ order_id uuid NOT NULL REFERENCES public.commerce_orders(id),
405
+ parcel_ref text,
406
+ external_ref text,
407
+ mode text NOT NULL CHECK (mode IN ('shadow','stage','live')),
408
+ status text NOT NULL DEFAULT 'draft' CHECK (status IN ('draft','submitting','created','uncertain','failed','cancel_requested','cancelled')),
409
+ evidence jsonb NOT NULL,
410
+ remap_count integer NOT NULL DEFAULT 0 CHECK (remap_count BETWEEN 0 AND 1),
411
+ acceptance jsonb,
412
+ updated_at timestamptz NOT NULL DEFAULT now()
413
+ );
414
+ CREATE TABLE public.fulfillment_paid_handoffs (
415
+ request_key text PRIMARY KEY,
416
+ order_id uuid NOT NULL REFERENCES public.commerce_orders(id),
417
+ paid_fact_ref text NOT NULL,
418
+ shipment_id uuid NOT NULL REFERENCES public.fulfillment_shipments(id),
419
+ acknowledged_at timestamptz
420
+ );
421
+ CREATE TABLE public.fulfillment_observations (
422
+ event_key text PRIMARY KEY,
423
+ shipment_id uuid REFERENCES public.fulfillment_shipments(id),
424
+ observation jsonb NOT NULL,
425
+ first_received_at timestamptz NOT NULL,
426
+ outcome jsonb,
427
+ status_recorded boolean NOT NULL DEFAULT false
428
+ );
429
+ CREATE TABLE public.fulfillment_stock_movements (
430
+ source_key text NOT NULL,
431
+ request_key text NOT NULL,
432
+ fingerprint text NOT NULL,
433
+ movement jsonb NOT NULL,
434
+ PRIMARY KEY (source_key, request_key)
435
+ );
436
+
437
+ CREATE FUNCTION public.fulfillment_dispatch_read(p_ref public.fulfillment_dispatch_refs, p_replayed boolean DEFAULT false)
438
+ RETURNS jsonb LANGUAGE sql SECURITY INVOKER SET search_path = pg_catalog AS $$
439
+ SELECT jsonb_build_object('dispatchRef',p_ref.id,'requestKey',p_ref.request_key,
440
+ 'fulfillmentRef',p_ref.shipment_id,'orderRef',p_ref.order_id,'parcelRef',p_ref.parcel_ref,
441
+ 'externalRef',p_ref.external_ref,'mode',p_ref.mode,'status',p_ref.status,
442
+ 'evidence',p_ref.evidence,'replayed',p_replayed);
443
+ $$;
444
+ CREATE FUNCTION public.fulfillment_dispatch_claim(p_key text, p_shipment uuid, p_order uuid, p_parcel text, p_mode text, p_evidence jsonb)
445
+ RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
446
+ DECLARE v_ref public.fulfillment_dispatch_refs%ROWTYPE; v_context public.fulfillment_dispatch_contexts%ROWTYPE; v_replayed boolean;
447
+ BEGIN
448
+ PERFORM pg_advisory_xact_lock(hashtextextended('native-dispatch|' || p_shipment::text,0));
449
+ PERFORM id FROM public.fulfillment_shipments WHERE id=p_shipment AND order_id=p_order FOR UPDATE;
450
+ IF NOT FOUND THEN RAISE EXCEPTION 'fulfillment_dispatch_identity_invalid' USING ERRCODE='22023'; END IF;
451
+ SELECT * INTO v_context FROM public.fulfillment_dispatch_contexts WHERE shipment_id=p_shipment FOR UPDATE;
452
+ IF NOT FOUND OR v_context.withheld THEN RETURN jsonb_build_object('kind','refused','refusal',jsonb_build_object('reason','withheld','phase','claim','retryable',false)); END IF;
453
+ IF v_context.provider_ref IS DISTINCT FROM p_evidence->>'providerRef' THEN RETURN jsonb_build_object('kind','refused','refusal',jsonb_build_object('reason','provider_mismatch','phase','claim','retryable',false)); END IF;
454
+ IF v_context.contact_revision IS DISTINCT FROM p_evidence->'contactRevision' THEN
455
+ RETURN jsonb_build_object('kind','refused','refusal',jsonb_build_object('reason','contact_stale','phase','claim','retryable',true));
456
+ END IF;
457
+ IF p_evidence->>'commandFingerprint' IS NULL OR p_evidence->>'commandFingerprint' !~ '^[a-f0-9]{64}$' OR btrim(COALESCE(p_evidence->>'providerRef',''))='' THEN
458
+ RAISE EXCEPTION 'fulfillment_dispatch_evidence_invalid' USING ERRCODE='22023';
459
+ END IF;
460
+ SELECT * INTO v_ref FROM public.fulfillment_dispatch_refs WHERE request_key=p_key OR shipment_id=p_shipment FOR UPDATE;
461
+ v_replayed := FOUND;
462
+ IF FOUND THEN
463
+ IF v_ref.request_key<>p_key OR v_ref.shipment_id<>p_shipment OR v_ref.order_id<>p_order OR v_ref.mode<>p_mode OR v_ref.parcel_ref IS DISTINCT FROM p_parcel THEN
464
+ RAISE EXCEPTION 'fulfillment_dispatch_key_conflict' USING ERRCODE='23505';
465
+ END IF;
466
+ IF v_ref.evidence IS DISTINCT FROM p_evidence AND v_ref.status='draft' THEN
467
+ IF v_ref.remap_count>=1 THEN RAISE EXCEPTION 'fulfillment_dispatch_remap_exhausted' USING ERRCODE='23505'; END IF;
468
+ UPDATE public.fulfillment_dispatch_refs SET evidence=p_evidence,remap_count=remap_count+1,updated_at=now() WHERE id=v_ref.id RETURNING * INTO v_ref;
469
+ END IF;
470
+ ELSE
471
+ INSERT INTO public.fulfillment_dispatch_refs(request_key,shipment_id,order_id,parcel_ref,mode,evidence)
472
+ VALUES(p_key,p_shipment,p_order,p_parcel,p_mode,p_evidence) RETURNING * INTO v_ref;
473
+ END IF;
474
+ RETURN jsonb_build_object('kind','claimed','record',public.fulfillment_dispatch_read(v_ref,v_replayed));
475
+ END;
476
+ $$;
477
+ CREATE FUNCTION public.fulfillment_record_provider_attempt(p_key text,p_shipment uuid,p_evidence jsonb,p_mode text)
478
+ RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
479
+ DECLARE v_attempt public.fulfillment_provider_attempts%ROWTYPE;
480
+ BEGIN
481
+ PERFORM pg_advisory_xact_lock(hashtextextended('native-dispatch|' || p_shipment::text,0));
482
+ PERFORM id FROM public.fulfillment_shipments WHERE id=p_shipment FOR UPDATE;
483
+ IF NOT FOUND THEN RAISE EXCEPTION 'fulfillment_attempt_shipment_missing' USING ERRCODE='22023'; END IF;
484
+ SELECT * INTO v_attempt FROM public.fulfillment_provider_attempts WHERE attempt_key=p_key;
485
+ IF FOUND THEN
486
+ IF v_attempt.shipment_id<>p_shipment OR v_attempt.evidence IS DISTINCT FROM p_evidence OR v_attempt.mode<>p_mode THEN RAISE EXCEPTION 'fulfillment_attempt_key_conflict' USING ERRCODE='23505'; END IF;
487
+ RETURN jsonb_build_object('replayed',true);
488
+ END IF;
489
+ IF NOT EXISTS (SELECT 1 FROM public.fulfillment_dispatch_refs WHERE shipment_id=p_shipment AND evidence=p_evidence AND mode=p_mode) THEN
490
+ RAISE EXCEPTION 'fulfillment_attempt_unclaimed_evidence' USING ERRCODE='22023';
491
+ END IF;
492
+ INSERT INTO public.fulfillment_provider_attempts(attempt_key,shipment_id,provider_kind,mode,evidence,status)
493
+ VALUES(p_key,p_shipment,p_evidence->>'providerRef',p_mode,p_evidence,'recorded');
494
+ RETURN jsonb_build_object('replayed',false);
495
+ END;
496
+ $$;
497
+ CREATE FUNCTION public.fulfillment_supersede_no_effect_commands(
498
+ p_shipment_id uuid, p_provider_ref text
499
+ )
500
+ RETURNS jsonb
501
+ LANGUAGE plpgsql
502
+ SECURITY INVOKER
503
+ SET search_path = pg_catalog
504
+ AS $$
505
+ DECLARE
506
+ v_command public.commerce_fulfillment_provider_commands%ROWTYPE;
507
+ v_from_status text;
508
+ v_blocked_count integer := 0;
509
+ v_superseded_count integer := 0;
510
+ BEGIN
511
+ IF p_shipment_id IS NULL THEN
512
+ RAISE EXCEPTION 'fulfillment_dispatch_command_supersession_invalid_input'
513
+ USING ERRCODE = '22023';
514
+ END IF;
515
+
516
+ -- Lock and preflight the whole logical command set before mutating any row.
517
+ FOR v_command IN
518
+ SELECT *
519
+ FROM public.commerce_fulfillment_provider_commands AS command
520
+ WHERE command.shipment_id = p_shipment_id
521
+ AND command.provider_kind = p_provider_ref
522
+ AND command.command_kind = 'dispatch_create'
523
+ ORDER BY command.id
524
+ FOR UPDATE
525
+ LOOP
526
+ IF (
527
+ v_command.status = 'queued'
528
+ AND v_command.attempt_count = 0
529
+ AND v_command.claim_generation = 0
530
+ AND v_command.last_reason_code = 'command_enqueued'
531
+ AND 1 = (
532
+ SELECT count(*)
533
+ FROM public.commerce_fulfillment_provider_command_transitions AS transition
534
+ WHERE transition.command_id = v_command.id
535
+ AND transition.from_status IS NULL
536
+ AND transition.to_status = 'queued'
537
+ AND transition.reason_code = 'command_enqueued'
538
+ AND transition.attempt_count = 0
539
+ AND coalesce(transition.claim_generation, 0) = 0
540
+ )
541
+ AND 1 = (
542
+ SELECT count(*)
543
+ FROM public.commerce_fulfillment_provider_command_transitions AS transition
544
+ WHERE transition.command_id = v_command.id
545
+ )
546
+ )
547
+ OR (
548
+ v_command.status = 'cancelled'
549
+ AND v_command.last_reason_code = 'superseded_by_direct_dispatch'
550
+ )
551
+ THEN
552
+ CONTINUE;
553
+ END IF;
554
+ v_blocked_count := v_blocked_count + 1;
555
+ END LOOP;
556
+
557
+ IF v_blocked_count > 0 THEN
558
+ RETURN jsonb_build_object('supersededCount', 0, 'blockedCount', v_blocked_count);
559
+ END IF;
560
+
561
+ FOR v_command IN
562
+ SELECT *
563
+ FROM public.commerce_fulfillment_provider_commands AS command
564
+ WHERE command.shipment_id = p_shipment_id
565
+ AND command.provider_kind = p_provider_ref
566
+ AND command.command_kind = 'dispatch_create'
567
+ AND command.status = 'queued'
568
+ AND command.attempt_count = 0
569
+ AND command.claim_generation = 0
570
+ AND command.last_reason_code = 'command_enqueued'
571
+ AND 1 = (
572
+ SELECT count(*)
573
+ FROM public.commerce_fulfillment_provider_command_transitions AS transition
574
+ WHERE transition.command_id = command.id
575
+ AND transition.from_status IS NULL
576
+ AND transition.to_status = 'queued'
577
+ AND transition.reason_code = 'command_enqueued'
578
+ AND transition.attempt_count = 0
579
+ AND coalesce(transition.claim_generation, 0) = 0
580
+ )
581
+ AND 1 = (
582
+ SELECT count(*)
583
+ FROM public.commerce_fulfillment_provider_command_transitions AS transition
584
+ WHERE transition.command_id = command.id
585
+ )
586
+ ORDER BY command.id
587
+ FOR UPDATE
588
+ LOOP
589
+ v_from_status := v_command.status;
590
+ UPDATE public.commerce_fulfillment_provider_commands
591
+ SET status = 'cancelled',
592
+ last_reason_code = 'superseded_by_direct_dispatch',
593
+ finished_at = now(),
594
+ claim_token = NULL,
595
+ claim_owner_fingerprint = NULL,
596
+ claim_expires_at = NULL,
597
+ version = version + 1,
598
+ updated_at = now()
599
+ WHERE id = v_command.id
600
+ RETURNING * INTO v_command;
601
+
602
+ INSERT INTO public.commerce_fulfillment_provider_command_transitions (
603
+ command_id, from_status, to_status, reason_code, command_version,
604
+ attempt_count, claim_generation
605
+ ) VALUES (
606
+ v_command.id, v_from_status, 'cancelled',
607
+ 'superseded_by_direct_dispatch', v_command.version,
608
+ v_command.attempt_count, v_command.claim_generation
609
+ );
610
+ v_superseded_count := v_superseded_count + 1;
611
+ END LOOP;
612
+
613
+ RETURN jsonb_build_object(
614
+ 'supersededCount', v_superseded_count,
615
+ 'blockedCount', 0
616
+ );
617
+ END;
618
+ $$;
619
+
620
+ REVOKE ALL ON FUNCTION public.fulfillment_supersede_no_effect_commands(uuid,text) FROM PUBLIC, anon, authenticated;
621
+
622
+ CREATE FUNCTION public.fulfillment_dispatch_begin(p_id uuid,p_fingerprint text)
623
+ RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
624
+ DECLARE v_ref public.fulfillment_dispatch_refs%ROWTYPE; v_revision jsonb;
625
+ BEGIN
626
+ SELECT * INTO v_ref FROM public.fulfillment_dispatch_refs WHERE id=p_id;
627
+ IF NOT FOUND THEN RAISE EXCEPTION 'fulfillment_dispatch_missing' USING ERRCODE='22023'; END IF;
628
+ PERFORM pg_advisory_xact_lock(hashtextextended('native-dispatch|' || v_ref.shipment_id::text,0));
629
+ PERFORM id FROM public.fulfillment_shipments WHERE id=v_ref.shipment_id FOR UPDATE;
630
+ SELECT * INTO v_ref FROM public.fulfillment_dispatch_refs WHERE id=p_id FOR UPDATE;
631
+ IF NOT FOUND THEN RAISE EXCEPTION 'fulfillment_dispatch_missing' USING ERRCODE='22023'; END IF;
632
+ SELECT contact_revision INTO v_revision FROM public.fulfillment_dispatch_contexts WHERE shipment_id=v_ref.shipment_id FOR UPDATE;
633
+ IF v_ref.status='draft' AND v_revision IS DISTINCT FROM v_ref.evidence->'contactRevision' THEN
634
+ RETURN jsonb_build_object('kind','refused','refusal',jsonb_build_object('reason','contact_stale','phase','begin','retryable',true));
635
+ END IF;
636
+ IF v_ref.evidence->>'commandFingerprint' IS DISTINCT FROM p_fingerprint THEN RAISE EXCEPTION 'fulfillment_dispatch_fingerprint_conflict' USING ERRCODE='23505'; END IF;
637
+ IF v_ref.status<>'draft' THEN RETURN jsonb_build_object('kind','begun','record',public.fulfillment_dispatch_read(v_ref,true),'begun',false,'replayed',true); END IF;
638
+ PERFORM id FROM public.commerce_orders WHERE id=v_ref.order_id FOR UPDATE;
639
+ PERFORM id FROM public.fulfillment_shipments WHERE id=v_ref.shipment_id FOR UPDATE;
640
+ IF EXISTS (SELECT 1 FROM public.fulfillment_dispatch_contexts WHERE shipment_id=v_ref.shipment_id AND (withheld OR provider_ref IS DISTINCT FROM v_ref.evidence->>'providerRef'))
641
+ OR NOT EXISTS (SELECT 1 FROM public.commerce_orders WHERE id=v_ref.order_id AND status IN ('paid','fulfillment_pending'))
642
+ OR NOT EXISTS (SELECT 1 FROM public.fulfillment_shipments shipment JOIN public.fulfillment_ports port USING(port_key) WHERE shipment.id=v_ref.shipment_id AND shipment.status IN ('created','label_pending') AND port.routable)
643
+ OR EXISTS (SELECT 1 FROM public.commerce_order_holds WHERE order_id=v_ref.order_id AND status='active') THEN
644
+ RETURN jsonb_build_object('kind','refused','refusal',jsonb_build_object('reason','not_dispatchable','phase','begin','retryable',false));
645
+ END IF;
646
+ IF (public.fulfillment_supersede_no_effect_commands(v_ref.shipment_id,v_ref.evidence->>'providerRef')->>'blockedCount')::integer>0 THEN
647
+ RETURN jsonb_build_object('kind','refused','refusal',jsonb_build_object('reason','not_dispatchable','phase','begin','retryable',false));
648
+ END IF;
649
+ IF v_ref.mode='shadow' THEN RAISE EXCEPTION 'fulfillment_dispatch_shadow_submission' USING ERRCODE='22023'; END IF;
650
+ UPDATE public.fulfillment_dispatch_refs SET status='submitting',updated_at=now() WHERE id=p_id RETURNING * INTO v_ref;
651
+ RETURN jsonb_build_object('kind','begun','record',public.fulfillment_dispatch_read(v_ref),'begun',true,'replayed',false);
652
+ END;
653
+ $$;
654
+ CREATE FUNCTION public.fulfillment_dispatch_finish(p_id uuid,p_result jsonb,p_ack boolean)
655
+ RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
656
+ DECLARE v_ref public.fulfillment_dispatch_refs%ROWTYPE; v_replayed boolean := false;
657
+ BEGIN
658
+ SELECT * INTO v_ref FROM public.fulfillment_dispatch_refs WHERE id=p_id;
659
+ IF NOT FOUND THEN RAISE EXCEPTION 'fulfillment_dispatch_missing' USING ERRCODE='22023'; END IF;
660
+ PERFORM pg_advisory_xact_lock(hashtextextended('native-dispatch|' || v_ref.shipment_id::text,0));
661
+ PERFORM id FROM public.fulfillment_shipments WHERE id=v_ref.shipment_id FOR UPDATE;
662
+ SELECT * INTO v_ref FROM public.fulfillment_dispatch_refs WHERE id=p_id FOR UPDATE;
663
+ IF NOT FOUND THEN RAISE EXCEPTION 'fulfillment_dispatch_missing' USING ERRCODE='22023'; END IF;
664
+ IF p_result->>'kind'='accepted' THEN
665
+ IF v_ref.evidence IS DISTINCT FROM p_result->'evidence' OR btrim(COALESCE(p_result->>'externalRef',''))='' THEN RAISE EXCEPTION 'fulfillment_dispatch_acceptance_identity' USING ERRCODE='23505'; END IF;
666
+ IF v_ref.external_ref IS NOT NULL AND v_ref.external_ref IS DISTINCT FROM p_result->>'externalRef' THEN RAISE EXCEPTION 'fulfillment_dispatch_acceptance_conflict' USING ERRCODE='23505'; END IF;
667
+ IF v_ref.status='created' THEN
668
+ IF v_ref.acceptance IS DISTINCT FROM p_result THEN RAISE EXCEPTION 'fulfillment_dispatch_ack_key_conflict' USING ERRCODE='23505'; END IF;
669
+ v_replayed := true;
670
+ ELSIF v_ref.status IN ('submitting','uncertain') THEN
671
+ UPDATE public.fulfillment_dispatch_refs SET external_ref=p_result->>'externalRef',status=CASE WHEN p_ack THEN 'created' ELSE 'uncertain' END,
672
+ acceptance=p_result,updated_at=now() WHERE id=p_id RETURNING * INTO v_ref;
673
+ IF p_ack THEN
674
+ PERFORM public.fulfillment_supersede_no_effect_commands(v_ref.shipment_id,v_ref.evidence->>'providerRef');
675
+ PERFORM public.fulfillment_record_provider_attempt(p_result->>'providerAttemptKey',v_ref.shipment_id,v_ref.evidence,v_ref.mode);
676
+ IF btrim(COALESCE(p_result->>'labelKey',''))='' OR p_result->>'labelKey'=p_result->>'providerAttemptKey' THEN RAISE EXCEPTION 'fulfillment_dispatch_ack_keys_invalid' USING ERRCODE='22023'; END IF;
677
+ -- A 3pl accepts an outbound command; it does not create a label owned by
678
+ -- this kernel. Its native commitment is dispatch evidence, which also
679
+ -- fences local cancellation. The host's label ACK key stays the receipt key.
680
+ PERFORM public.fulfillment_record_evidence(p_result->>'labelKey',v_ref.shipment_id,'push','dispatch',p_result->>'externalRef','{}'::jsonb,now());
681
+ END IF;
682
+ ELSE RAISE EXCEPTION 'fulfillment_dispatch_not_submitted' USING ERRCODE='22023'; END IF;
683
+ ELSE
684
+ IF p_ack OR p_result->>'kind' NOT IN ('unknown','not_dispatched') THEN RAISE EXCEPTION 'fulfillment_dispatch_result_invalid' USING ERRCODE='22023'; END IF;
685
+ IF v_ref.status='submitting' THEN
686
+ UPDATE public.fulfillment_dispatch_refs SET status=CASE WHEN p_result->>'kind'='unknown' THEN 'uncertain' ELSE 'failed' END,updated_at=now() WHERE id=p_id RETURNING * INTO v_ref;
687
+ END IF;
688
+ END IF;
689
+ IF p_ack THEN RETURN jsonb_build_object('acknowledged',true,'replayed',v_replayed,'record',public.fulfillment_dispatch_read(v_ref,v_replayed)); END IF;
690
+ RETURN public.fulfillment_dispatch_read(v_ref,v_replayed);
691
+ END;
692
+ $$;
693
+ REVOKE ALL ON public.fulfillment_dispatch_contexts, public.fulfillment_dispatch_refs, public.fulfillment_paid_handoffs, public.fulfillment_observations, public.fulfillment_stock_movements FROM PUBLIC, anon, authenticated;
694
+ REVOKE ALL ON FUNCTION public.fulfillment_dispatch_read(public.fulfillment_dispatch_refs,boolean) FROM PUBLIC, anon, authenticated;
695
+ REVOKE ALL ON FUNCTION public.fulfillment_dispatch_claim(text,uuid,uuid,text,text,jsonb) FROM PUBLIC, anon, authenticated;
696
+ REVOKE ALL ON FUNCTION public.fulfillment_record_provider_attempt(text,uuid,jsonb,text) FROM PUBLIC, anon, authenticated;
697
+ REVOKE ALL ON FUNCTION public.fulfillment_dispatch_begin(uuid,text) FROM PUBLIC, anon, authenticated;
698
+ REVOKE ALL ON FUNCTION public.fulfillment_dispatch_finish(uuid,jsonb,boolean) FROM PUBLIC, anon, authenticated;
699
+
700
+ CREATE FUNCTION public.fulfillment_ensure_paid_handoff(p_key text,p_order uuid,p_fact text,p_port text)
701
+ RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
702
+ DECLARE v_row public.fulfillment_paid_handoffs%ROWTYPE; v_shipment uuid; v_response jsonb;
703
+ BEGIN
704
+ PERFORM id FROM public.commerce_orders WHERE id=p_order AND status IN ('paid','fulfillment_pending','fulfilled') FOR UPDATE;
705
+ IF NOT FOUND OR btrim(COALESCE(p_fact,''))='' THEN RAISE EXCEPTION 'fulfillment_paid_obligation_requires_paid_order' USING ERRCODE='22023'; END IF;
706
+ SELECT * INTO v_row FROM public.fulfillment_paid_handoffs WHERE request_key=p_key;
707
+ IF FOUND THEN
708
+ IF v_row.order_id<>p_order OR v_row.paid_fact_ref<>p_fact THEN RAISE EXCEPTION 'fulfillment_paid_handoff_key_conflict' USING ERRCODE='23505'; END IF;
709
+ RETURN jsonb_build_object('fulfillmentRef',v_row.shipment_id,'replayed',true);
710
+ END IF;
711
+ SELECT id INTO v_shipment FROM public.fulfillment_shipments WHERE order_id=p_order AND sequence_no=0;
712
+ IF NOT FOUND THEN
713
+ v_response := public.fulfillment_create_shipment(p_key,p_order,p_port,'{}'::jsonb,now());
714
+ v_shipment := (v_response#>>'{shipment,id}')::uuid;
715
+ END IF;
716
+ INSERT INTO public.fulfillment_paid_handoffs(request_key,order_id,paid_fact_ref,shipment_id) VALUES(p_key,p_order,p_fact,v_shipment);
717
+ RETURN jsonb_build_object('fulfillmentRef',v_shipment,'replayed',false);
718
+ END;
719
+ $$;
720
+ CREATE FUNCTION public.fulfillment_ack_paid_handoff(p_key text,p_shipment uuid)
721
+ RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path = pg_catalog AS $$
722
+ DECLARE v_row public.fulfillment_paid_handoffs%ROWTYPE; v_replayed boolean;
723
+ BEGIN
724
+ SELECT * INTO v_row FROM public.fulfillment_paid_handoffs WHERE request_key=p_key FOR UPDATE;
725
+ IF NOT FOUND OR v_row.shipment_id<>p_shipment THEN RAISE EXCEPTION 'fulfillment_paid_handoff_missing' USING ERRCODE='22023'; END IF;
726
+ v_replayed := v_row.acknowledged_at IS NOT NULL;
727
+ UPDATE public.fulfillment_paid_handoffs SET acknowledged_at=COALESCE(acknowledged_at,now()) WHERE request_key=p_key;
728
+ RETURN jsonb_build_object('acknowledged',true,'replayed',v_replayed);
729
+ END;
730
+ $$;
731
+ REVOKE ALL ON FUNCTION public.fulfillment_ensure_paid_handoff(text,uuid,text,text) FROM PUBLIC, anon, authenticated;
732
+ REVOKE ALL ON FUNCTION public.fulfillment_ack_paid_handoff(text,uuid) FROM PUBLIC, anon, authenticated;
733
+
734
+ CREATE FUNCTION public.fulfillment_record_stock_movement(p_source text,p_key text,p_fingerprint text,p_movement jsonb)
735
+ RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
736
+ DECLARE v_row public.fulfillment_stock_movements%ROWTYPE; v_inserted boolean;
737
+ BEGIN
738
+ INSERT INTO public.fulfillment_stock_movements(source_key,request_key,fingerprint,movement) VALUES(p_source,p_key,p_fingerprint,p_movement)
739
+ ON CONFLICT DO NOTHING RETURNING * INTO v_row;
740
+ v_inserted := FOUND;
741
+ IF NOT FOUND THEN
742
+ SELECT * INTO v_row FROM public.fulfillment_stock_movements WHERE source_key=p_source AND request_key=p_key;
743
+ IF v_row.fingerprint IS DISTINCT FROM p_fingerprint THEN RAISE EXCEPTION 'fulfillment_movement_key_conflict' USING ERRCODE='23505'; END IF;
744
+ END IF;
745
+ RETURN jsonb_build_object('replayed',NOT v_inserted,'readBack',true);
746
+ END;
747
+ $$;
748
+ CREATE FUNCTION public.fulfillment_record_observation(p_event text,p_shipment uuid,p_observation jsonb,p_received timestamptz)
749
+ RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
750
+ DECLARE v_row public.fulfillment_observations%ROWTYPE; v_inserted boolean;
751
+ BEGIN
752
+ INSERT INTO public.fulfillment_observations(event_key,shipment_id,observation,first_received_at) VALUES(p_event,p_shipment,p_observation,p_received)
753
+ ON CONFLICT DO NOTHING RETURNING * INTO v_row;
754
+ v_inserted := FOUND;
755
+ IF NOT FOUND THEN
756
+ SELECT * INTO v_row FROM public.fulfillment_observations WHERE event_key=p_event;
757
+ IF v_row.shipment_id IS DISTINCT FROM p_shipment OR v_row.observation->'correlation' IS DISTINCT FROM p_observation->'correlation' THEN RAISE EXCEPTION 'fulfillment_observation_key_conflict' USING ERRCODE='23505'; END IF;
758
+ END IF;
759
+ RETURN jsonb_build_object('replayed',NOT v_inserted);
760
+ END;
761
+ $$;
762
+ CREATE FUNCTION public.fulfillment_mark_observation_processed(p_event text,p_outcome jsonb)
763
+ RETURNS void LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
764
+ DECLARE v_row public.fulfillment_observations%ROWTYPE;
765
+ BEGIN
766
+ SELECT * INTO v_row FROM public.fulfillment_observations WHERE event_key=p_event FOR UPDATE;
767
+ IF NOT FOUND THEN RAISE EXCEPTION 'fulfillment_observation_missing' USING ERRCODE='22023'; END IF;
768
+ IF v_row.outcome IS NOT NULL AND v_row.outcome IS DISTINCT FROM p_outcome THEN RAISE EXCEPTION 'fulfillment_observation_outcome_conflict' USING ERRCODE='23505'; END IF;
769
+ UPDATE public.fulfillment_observations SET outcome=p_outcome WHERE event_key=p_event;
770
+ END;
771
+ $$;
772
+ REVOKE ALL ON FUNCTION public.fulfillment_record_stock_movement(text,text,text,jsonb) FROM PUBLIC, anon, authenticated;
773
+ REVOKE ALL ON FUNCTION public.fulfillment_record_observation(text,uuid,jsonb,timestamptz) FROM PUBLIC, anon, authenticated;
774
+ REVOKE ALL ON FUNCTION public.fulfillment_mark_observation_processed(text,jsonb) FROM PUBLIC, anon, authenticated;
775
+
776
+ CREATE FUNCTION public.fulfillment_record_status_evidence(p_event text,p_shipment uuid)
777
+ RETURNS jsonb LANGUAGE plpgsql SECURITY INVOKER SET search_path=pg_catalog AS $$
778
+ DECLARE v_row public.fulfillment_observations%ROWTYPE;
779
+ BEGIN
780
+ SELECT * INTO v_row FROM public.fulfillment_observations WHERE event_key=p_event FOR UPDATE;
781
+ IF NOT FOUND OR v_row.shipment_id IS DISTINCT FROM p_shipment THEN RAISE EXCEPTION 'fulfillment_status_evidence_correlation' USING ERRCODE='22023'; END IF;
782
+ UPDATE public.fulfillment_observations SET status_recorded=true WHERE event_key=p_event;
783
+ RETURN jsonb_build_object('replayed',v_row.status_recorded);
784
+ END;
785
+ $$;
786
+ REVOKE ALL ON FUNCTION public.fulfillment_record_status_evidence(text,uuid) FROM PUBLIC, anon, authenticated;