@openlup/platform 0.25.0 → 0.27.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 (524) hide show
  1. package/CHANGELOG.md +156 -440
  2. package/README.md +230 -11
  3. package/dist/commerce/adapters/managed/commerceOmsControlPlane.d.ts +3 -0
  4. package/dist/commerce/adapters/managed/commerceOmsControlPlane.js +71 -0
  5. package/dist/commerce/adapters/managed/commerceOmsMutations.d.ts +10 -0
  6. package/dist/commerce/adapters/managed/commerceOmsMutations.js +119 -0
  7. package/dist/commerce/adapters/managed/commerceOmsPort.d.ts +28 -0
  8. package/dist/commerce/adapters/managed/commerceOmsPort.js +101 -0
  9. package/dist/commerce/adapters/managed/oms/accounting.d.ts +3 -0
  10. package/dist/commerce/adapters/managed/oms/accounting.js +13 -0
  11. package/dist/commerce/adapters/managed/oms/fulfillmentReads.d.ts +17 -0
  12. package/dist/commerce/adapters/managed/oms/fulfillmentReads.js +81 -0
  13. package/dist/commerce/adapters/managed/oms/inboundEvents.d.ts +9 -0
  14. package/dist/commerce/adapters/managed/oms/inboundEvents.js +40 -0
  15. package/dist/commerce/adapters/managed/oms/outboxEvents.d.ts +2 -0
  16. package/dist/commerce/adapters/managed/oms/outboxEvents.js +23 -0
  17. package/dist/commerce/adapters/managed/oms/paymentAttempts.d.ts +3 -0
  18. package/dist/commerce/adapters/managed/oms/paymentAttempts.js +15 -0
  19. package/dist/commerce/adapters/managed/oms/queueSelection.d.ts +12 -0
  20. package/dist/commerce/adapters/managed/oms/queueSelection.js +109 -0
  21. package/dist/commerce/adapters/managed/oms/readProjections.d.ts +18 -0
  22. package/dist/commerce/adapters/managed/oms/readProjections.js +13 -0
  23. package/dist/commerce/adapters/managed/oms/readQueries.d.ts +16 -0
  24. package/dist/commerce/adapters/managed/oms/readQueries.js +276 -0
  25. package/dist/commerce/adapters/managed/oms/types.d.ts +2 -0
  26. package/dist/commerce/adapters/managed/oms/types.js +7 -0
  27. package/dist/commerce/checkoutCommandSchemas.d.ts +89 -0
  28. package/dist/commerce/checkoutCommandSchemas.js +70 -0
  29. package/dist/commerce/checkoutHelpers.d.ts +21 -0
  30. package/dist/commerce/checkoutHelpers.js +29 -0
  31. package/dist/commerce/checkoutResumeSchemas.d.ts +158 -0
  32. package/dist/commerce/checkoutResumeSchemas.js +145 -0
  33. package/dist/commerce/checkoutSchemas.d.ts +158 -0
  34. package/dist/commerce/checkoutSchemas.js +161 -0
  35. package/dist/commerce/commerceReturnsPort.d.ts +97 -0
  36. package/dist/commerce/commerceReturnsPort.js +39 -0
  37. package/dist/commerce/configuratorIntentPersistenceSchemas.d.ts +20 -0
  38. package/dist/commerce/configuratorIntentPersistenceSchemas.js +21 -0
  39. package/dist/commerce/configuratorIntentSchemas.d.ts +63 -0
  40. package/dist/commerce/configuratorIntentSchemas.js +43 -0
  41. package/dist/commerce/contractPrimitives.d.ts +50 -0
  42. package/dist/commerce/contractPrimitives.js +25 -0
  43. package/dist/commerce/contracts/availability.d.ts +23 -0
  44. package/dist/commerce/contracts/availability.js +1 -0
  45. package/dist/commerce/contracts/fulfillmentRecovery.d.ts +25 -0
  46. package/dist/commerce/contracts/fulfillmentRecovery.js +1 -0
  47. package/dist/commerce/contracts/index.d.ts +13 -0
  48. package/dist/commerce/contracts/index.js +1 -0
  49. package/dist/commerce/contracts/intent.d.ts +86 -0
  50. package/dist/commerce/contracts/intent.js +1 -0
  51. package/dist/commerce/contracts/omsTypes.d.ts +1216 -0
  52. package/dist/commerce/contracts/omsTypes.js +1 -0
  53. package/dist/commerce/contracts/order.d.ts +39 -0
  54. package/dist/commerce/contracts/order.js +1 -0
  55. package/dist/commerce/contracts/orderMoney.d.ts +52 -0
  56. package/dist/commerce/contracts/orderMoney.js +1 -0
  57. package/dist/commerce/contracts/ports.d.ts +113 -0
  58. package/dist/commerce/contracts/ports.js +1 -0
  59. package/dist/commerce/contracts/recovery.d.ts +11 -0
  60. package/dist/commerce/contracts/recovery.js +1 -0
  61. package/dist/commerce/contracts/runtimePorts.d.ts +196 -0
  62. package/dist/commerce/contracts/runtimePorts.js +1 -0
  63. package/dist/commerce/contracts/state.d.ts +26 -0
  64. package/dist/commerce/contracts/state.js +43 -0
  65. package/dist/commerce/customerDefaultsSnapshotSchemas.d.ts +51 -0
  66. package/dist/commerce/customerDefaultsSnapshotSchemas.js +106 -0
  67. package/dist/commerce/errors.d.ts +18 -0
  68. package/dist/commerce/errors.js +36 -0
  69. package/dist/commerce/fulfillmentEligibility.d.ts +6 -0
  70. package/dist/commerce/fulfillmentEligibility.js +31 -0
  71. package/dist/commerce/index.d.ts +72 -0
  72. package/dist/commerce/index.js +67 -0
  73. package/dist/commerce/model/customerFulfillmentRecovery.d.ts +40 -0
  74. package/dist/commerce/model/customerFulfillmentRecovery.js +144 -0
  75. package/dist/commerce/model/fulfillmentCancellationEligibility.d.ts +7 -0
  76. package/dist/commerce/model/fulfillmentCancellationEligibility.js +9 -0
  77. package/dist/commerce/model/omsAccountingVisibility.d.ts +12 -0
  78. package/dist/commerce/model/omsAccountingVisibility.js +118 -0
  79. package/dist/commerce/model/omsBillingInvoiceBuyer.d.ts +2 -0
  80. package/dist/commerce/model/omsBillingInvoiceBuyer.js +15 -0
  81. package/dist/commerce/model/omsCurrency.d.ts +4 -0
  82. package/dist/commerce/model/omsCurrency.js +5 -0
  83. package/dist/commerce/model/omsFulfillmentDebug.d.ts +20 -0
  84. package/dist/commerce/model/omsFulfillmentDebug.js +122 -0
  85. package/dist/commerce/model/omsFulfillmentSummary.d.ts +16 -0
  86. package/dist/commerce/model/omsFulfillmentSummary.js +189 -0
  87. package/dist/commerce/model/omsListTotals.d.ts +13 -0
  88. package/dist/commerce/model/omsListTotals.js +32 -0
  89. package/dist/commerce/model/omsOperationalLadder.d.ts +51 -0
  90. package/dist/commerce/model/omsOperationalLadder.js +158 -0
  91. package/dist/commerce/model/omsOrderLineMoney.d.ts +4 -0
  92. package/dist/commerce/model/omsOrderLineMoney.js +25 -0
  93. package/dist/commerce/model/omsPaymentMethodSummary.d.ts +5 -0
  94. package/dist/commerce/model/omsPaymentMethodSummary.js +11 -0
  95. package/dist/commerce/model/omsReadModel.d.ts +43 -0
  96. package/dist/commerce/model/omsReadModel.js +211 -0
  97. package/dist/commerce/model/omsReadModelCommunication.d.ts +3 -0
  98. package/dist/commerce/model/omsReadModelCommunication.js +29 -0
  99. package/dist/commerce/model/omsReadModelHelpers.d.ts +125 -0
  100. package/dist/commerce/model/omsReadModelHelpers.js +212 -0
  101. package/dist/commerce/model/omsReadModelList.d.ts +33 -0
  102. package/dist/commerce/model/omsReadModelList.js +50 -0
  103. package/dist/commerce/model/omsReadModelListSummary.d.ts +3 -0
  104. package/dist/commerce/model/omsReadModelListSummary.js +13 -0
  105. package/dist/commerce/model/omsReadModelPricing.d.ts +5 -0
  106. package/dist/commerce/model/omsReadModelPricing.js +36 -0
  107. package/dist/commerce/model/omsReadModelProjections.d.ts +1 -0
  108. package/dist/commerce/model/omsReadModelProjections.js +1 -0
  109. package/dist/commerce/model/omsReadModelRowCore.d.ts +79 -0
  110. package/dist/commerce/model/omsReadModelRowCore.js +149 -0
  111. package/dist/commerce/model/omsReadModelRows.d.ts +1 -0
  112. package/dist/commerce/model/omsReadModelRows.js +6 -0
  113. package/dist/commerce/offerAvailability.d.ts +18 -0
  114. package/dist/commerce/offerAvailability.js +53 -0
  115. package/dist/commerce/offerAvailabilitySchemas.d.ts +43 -0
  116. package/dist/commerce/offerAvailabilitySchemas.js +42 -0
  117. package/dist/commerce/offerEvidenceSchemas.d.ts +7 -0
  118. package/dist/commerce/offerEvidenceSchemas.js +13 -0
  119. package/dist/commerce/oms/omsContractBase.d.ts +130 -0
  120. package/dist/commerce/oms/omsContractBase.js +86 -0
  121. package/dist/commerce/oms/omsContractReadAuxSchemas.d.ts +222 -0
  122. package/dist/commerce/oms/omsContractReadAuxSchemas.js +123 -0
  123. package/dist/commerce/oms/omsContractReadSchemas.d.ts +2266 -0
  124. package/dist/commerce/oms/omsContractReadSchemas.js +277 -0
  125. package/dist/commerce/oms/omsContractRequests.d.ts +196 -0
  126. package/dist/commerce/oms/omsContractRequests.js +118 -0
  127. package/dist/commerce/oms/omsContracts.d.ts +5 -0
  128. package/dist/commerce/oms/omsContracts.js +4 -0
  129. package/dist/commerce/oms/omsControlPlane.d.ts +275 -0
  130. package/dist/commerce/oms/omsControlPlane.js +72 -0
  131. package/dist/commerce/oms/omsFulfillmentContractSchemas.d.ts +203 -0
  132. package/dist/commerce/oms/omsFulfillmentContractSchemas.js +94 -0
  133. package/dist/commerce/oms/omsFulfillmentDebugSchemas.d.ts +69 -0
  134. package/dist/commerce/oms/omsFulfillmentDebugSchemas.js +33 -0
  135. package/dist/commerce/oms/omsFulfillmentHealthSchemas.d.ts +67 -0
  136. package/dist/commerce/oms/omsFulfillmentHealthSchemas.js +44 -0
  137. package/dist/commerce/oms/omsPorts.d.ts +84 -0
  138. package/dist/commerce/oms/omsPorts.js +148 -0
  139. package/dist/commerce/oms/omsPrimitives.d.ts +5 -0
  140. package/dist/commerce/oms/omsPrimitives.js +3 -0
  141. package/dist/commerce/oms/omsProviderEvidenceSchemas.d.ts +15 -0
  142. package/dist/commerce/oms/omsProviderEvidenceSchemas.js +12 -0
  143. package/dist/commerce/orderDraftErrors.d.ts +13 -0
  144. package/dist/commerce/orderDraftErrors.js +20 -0
  145. package/dist/commerce/orderEmailEligibility.d.ts +54 -0
  146. package/dist/commerce/orderEmailEligibility.js +123 -0
  147. package/dist/commerce/orderEmailLines.d.ts +12 -0
  148. package/dist/commerce/orderEmailLines.js +8 -0
  149. package/dist/commerce/orderFeedbackContracts.d.ts +41 -0
  150. package/dist/commerce/orderFeedbackContracts.js +33 -0
  151. package/dist/commerce/orderMoney.d.ts +6 -0
  152. package/dist/commerce/orderMoney.js +168 -0
  153. package/dist/commerce/orderReviewContracts.d.ts +475 -0
  154. package/dist/commerce/orderReviewContracts.js +94 -0
  155. package/dist/commerce/orderReviewPorts.d.ts +140 -0
  156. package/dist/commerce/orderReviewPorts.js +1 -0
  157. package/dist/commerce/outboxDispatchContracts.d.ts +30 -0
  158. package/dist/commerce/outboxDispatchContracts.js +1 -0
  159. package/dist/commerce/outboxEventSchemas.d.ts +162 -0
  160. package/dist/commerce/outboxEventSchemas.js +164 -0
  161. package/dist/commerce/outboxOrderDraftEmailPorts.d.ts +207 -0
  162. package/dist/commerce/outboxOrderDraftEmailPorts.js +12 -0
  163. package/dist/commerce/paymentContinuation.d.ts +4 -0
  164. package/dist/commerce/paymentContinuation.js +4 -0
  165. package/dist/commerce/ports.d.ts +8 -0
  166. package/dist/commerce/ports.js +1 -0
  167. package/dist/commerce/recoveryDestination.d.ts +3 -0
  168. package/dist/commerce/recoveryDestination.js +17 -0
  169. package/dist/commerce/runtimeErrors.d.ts +8 -0
  170. package/dist/commerce/runtimeErrors.js +16 -0
  171. package/dist/commerce/salesChannelEnvelope.d.ts +20 -0
  172. package/dist/commerce/salesChannelEnvelope.js +7 -0
  173. package/dist/commerce/schemas.d.ts +174 -0
  174. package/dist/commerce/schemas.js +84 -0
  175. package/dist/commerce/server/checkoutCompensation.d.ts +21 -0
  176. package/dist/commerce/server/checkoutCompensation.js +32 -0
  177. package/dist/commerce/server/checkoutConflictClassifiers.d.ts +17 -0
  178. package/dist/commerce/server/checkoutConflictClassifiers.js +38 -0
  179. package/dist/commerce/server/checkoutExpiredRecoveryPolicy.d.ts +38 -0
  180. package/dist/commerce/server/checkoutExpiredRecoveryPolicy.js +85 -0
  181. package/dist/commerce/server/checkoutInvoiceBuyerSnapshot.d.ts +22 -0
  182. package/dist/commerce/server/checkoutInvoiceBuyerSnapshot.js +30 -0
  183. package/dist/commerce/server/checkoutOfferPolicyPort.d.ts +33 -0
  184. package/dist/commerce/server/checkoutOfferPolicyPort.js +49 -0
  185. package/dist/commerce/server/checkoutPaymentContinuationCredential.d.ts +58 -0
  186. package/dist/commerce/server/checkoutPaymentContinuationCredential.js +154 -0
  187. package/dist/commerce/server/checkoutQuoteTypes.d.ts +19 -0
  188. package/dist/commerce/server/checkoutQuoteTypes.js +1 -0
  189. package/dist/commerce/server/checkoutRecoveryOrderPort.d.ts +42 -0
  190. package/dist/commerce/server/checkoutRecoveryOrderPort.js +1 -0
  191. package/dist/commerce/server/checkoutRecoveryToken.d.ts +40 -0
  192. package/dist/commerce/server/checkoutRecoveryToken.js +26 -0
  193. package/dist/commerce/server/checkoutRefusalContinuation.d.ts +24 -0
  194. package/dist/commerce/server/checkoutRefusalContinuation.js +18 -0
  195. package/dist/commerce/server/checkoutRuntimeTypes.d.ts +81 -0
  196. package/dist/commerce/server/checkoutRuntimeTypes.js +1 -0
  197. package/dist/commerce/server/commerceCheckoutCommandExecution.d.ts +60 -0
  198. package/dist/commerce/server/commerceCheckoutCommandExecution.js +136 -0
  199. package/dist/commerce/server/commerceCheckoutCompensation.d.ts +26 -0
  200. package/dist/commerce/server/commerceCheckoutCompensation.js +46 -0
  201. package/dist/commerce/server/commerceCheckoutContinuationTerms.d.ts +50 -0
  202. package/dist/commerce/server/commerceCheckoutContinuationTerms.js +66 -0
  203. package/dist/commerce/server/commerceCheckoutCustomerDefaults.d.ts +18 -0
  204. package/dist/commerce/server/commerceCheckoutCustomerDefaults.js +17 -0
  205. package/dist/commerce/server/commerceCheckoutFailureHandler.d.ts +51 -0
  206. package/dist/commerce/server/commerceCheckoutFailureHandler.js +90 -0
  207. package/dist/commerce/server/commerceCheckoutHandler.d.ts +213 -0
  208. package/dist/commerce/server/commerceCheckoutHandler.js +196 -0
  209. package/dist/commerce/server/commerceCheckoutHandlerAdmission.d.ts +58 -0
  210. package/dist/commerce/server/commerceCheckoutHandlerAdmission.js +94 -0
  211. package/dist/commerce/server/commerceCheckoutHandlerHelpers.d.ts +67 -0
  212. package/dist/commerce/server/commerceCheckoutHandlerHelpers.js +155 -0
  213. package/dist/commerce/server/commerceCheckoutOrchestrationError.d.ts +9 -0
  214. package/dist/commerce/server/commerceCheckoutOrchestrationError.js +14 -0
  215. package/dist/commerce/server/commerceCheckoutPromotionPriceChange.d.ts +47 -0
  216. package/dist/commerce/server/commerceCheckoutPromotionPriceChange.js +43 -0
  217. package/dist/commerce/server/commerceCheckoutProviderPayment.d.ts +34 -0
  218. package/dist/commerce/server/commerceCheckoutProviderPayment.js +37 -0
  219. package/dist/commerce/server/commerceCheckoutQuoteGuard.d.ts +62 -0
  220. package/dist/commerce/server/commerceCheckoutQuoteGuard.js +106 -0
  221. package/dist/commerce/server/commerceCheckoutResume.d.ts +29 -0
  222. package/dist/commerce/server/commerceCheckoutResume.js +81 -0
  223. package/dist/commerce/server/commerceCheckoutResumeGuard.d.ts +54 -0
  224. package/dist/commerce/server/commerceCheckoutResumeGuard.js +138 -0
  225. package/dist/commerce/server/commerceCheckoutSavedPaymentMethod.d.ts +39 -0
  226. package/dist/commerce/server/commerceCheckoutSavedPaymentMethod.js +44 -0
  227. package/dist/commerce/server/commerceCheckoutStartRuntimeFailure.d.ts +7 -0
  228. package/dist/commerce/server/commerceCheckoutStartRuntimeFailure.js +39 -0
  229. package/dist/commerce/server/commerceCheckoutTiming.d.ts +8 -0
  230. package/dist/commerce/server/commerceCheckoutTiming.js +31 -0
  231. package/dist/commerce/server/commerceDiagnostics.d.ts +6 -0
  232. package/dist/commerce/server/commerceDiagnostics.js +31 -0
  233. package/dist/commerce/server/commerceOmsCancelOrderHandler.d.ts +7 -0
  234. package/dist/commerce/server/commerceOmsCancelOrderHandler.js +22 -0
  235. package/dist/commerce/server/commerceOmsControlPlaneHandlers.d.ts +18 -0
  236. package/dist/commerce/server/commerceOmsControlPlaneHandlers.js +60 -0
  237. package/dist/commerce/server/commerceOmsHandlers.d.ts +47 -0
  238. package/dist/commerce/server/commerceOmsHandlers.js +176 -0
  239. package/dist/commerce/server/commerceOmsMarkRefundedHandler.d.ts +7 -0
  240. package/dist/commerce/server/commerceOmsMarkRefundedHandler.js +19 -0
  241. package/dist/commerce/server/commerceOrderDraft.d.ts +24 -0
  242. package/dist/commerce/server/commerceOrderDraft.js +92 -0
  243. package/dist/commerce/server/commercePaidOrderOrchestration.d.ts +131 -0
  244. package/dist/commerce/server/commercePaidOrderOrchestration.js +140 -0
  245. package/dist/commerce/server/commerceProviderAttemptFailure.d.ts +133 -0
  246. package/dist/commerce/server/commerceProviderAttemptFailure.js +175 -0
  247. package/dist/commerce/server/commerceReturnsHandlers.d.ts +41 -0
  248. package/dist/commerce/server/commerceReturnsHandlers.js +100 -0
  249. package/dist/commerce/server/commerceRuntimePort.d.ts +16 -0
  250. package/dist/commerce/server/commerceRuntimePort.js +57 -0
  251. package/dist/commerce/server/commerceRuntimeService.d.ts +98 -0
  252. package/dist/commerce/server/commerceRuntimeService.js +186 -0
  253. package/dist/commerce/server/commerceRuntimeStartResponse.d.ts +37 -0
  254. package/dist/commerce/server/commerceRuntimeStartResponse.js +56 -0
  255. package/dist/commerce/server/commerceStarterOfferGuard.d.ts +55 -0
  256. package/dist/commerce/server/commerceStarterOfferGuard.js +128 -0
  257. package/dist/commerce/server/configuratorIntentPersistence.d.ts +47 -0
  258. package/dist/commerce/server/configuratorIntentPersistence.js +85 -0
  259. package/dist/commerce/server/configuratorIntentPersistenceHandler.d.ts +14 -0
  260. package/dist/commerce/server/configuratorIntentPersistenceHandler.js +42 -0
  261. package/dist/commerce/server/emailRuntime.d.ts +47 -0
  262. package/dist/commerce/server/emailRuntime.js +1 -0
  263. package/dist/commerce/server/handlerSupport.d.ts +68 -0
  264. package/dist/commerce/server/handlerSupport.js +1 -0
  265. package/dist/commerce/server/index.d.ts +178 -0
  266. package/dist/commerce/server/index.js +127 -0
  267. package/dist/commerce/server/offerAvailability.d.ts +21 -0
  268. package/dist/commerce/server/offerAvailability.js +163 -0
  269. package/dist/commerce/server/orderDraftRpcPayload.d.ts +20 -0
  270. package/dist/commerce/server/orderDraftRpcPayload.js +21 -0
  271. package/dist/commerce/server/orderFeedbackSubmit.d.ts +20 -0
  272. package/dist/commerce/server/orderFeedbackSubmit.js +16 -0
  273. package/dist/commerce/server/orderFeedbackTokenCheck.d.ts +17 -0
  274. package/dist/commerce/server/orderFeedbackTokenCheck.js +14 -0
  275. package/dist/commerce/server/orderReviewAdminHandlers.d.ts +47 -0
  276. package/dist/commerce/server/orderReviewAdminHandlers.js +41 -0
  277. package/dist/commerce/server/orderReviewHandlers.d.ts +114 -0
  278. package/dist/commerce/server/orderReviewHandlers.js +42 -0
  279. package/dist/commerce/server/orderReviewLifecycle.d.ts +10 -0
  280. package/dist/commerce/server/orderReviewLifecycle.js +121 -0
  281. package/dist/commerce/server/orderReviewMedia.d.ts +11 -0
  282. package/dist/commerce/server/orderReviewMedia.js +172 -0
  283. package/dist/commerce/server/orderReviewRawUploadHandler.d.ts +40 -0
  284. package/dist/commerce/server/orderReviewRawUploadHandler.js +316 -0
  285. package/dist/commerce/server/outboxCheckoutExpiredEmailHandler.d.ts +14 -0
  286. package/dist/commerce/server/outboxCheckoutExpiredEmailHandler.js +57 -0
  287. package/dist/commerce/server/outboxCheckoutRecoveryEmailHandler.d.ts +13 -0
  288. package/dist/commerce/server/outboxCheckoutRecoveryEmailHandler.js +150 -0
  289. package/dist/commerce/server/outboxDispatchRegistry.d.ts +46 -0
  290. package/dist/commerce/server/outboxDispatchRegistry.js +184 -0
  291. package/dist/commerce/server/outboxDispatchRegistryComposition.d.ts +6 -0
  292. package/dist/commerce/server/outboxDispatchRegistryComposition.js +23 -0
  293. package/dist/commerce/server/outboxOrderDraftEmailHandler.d.ts +14 -0
  294. package/dist/commerce/server/outboxOrderDraftEmailHandler.js +130 -0
  295. package/dist/commerce/server/outboxOrderPaidEmailHandler.d.ts +21 -0
  296. package/dist/commerce/server/outboxOrderPaidEmailHandler.js +93 -0
  297. package/dist/commerce/server/outboxOrderPaidFulfillmentHandler.d.ts +3 -0
  298. package/dist/commerce/server/outboxOrderPaidFulfillmentHandler.js +1 -0
  299. package/dist/commerce/server/outboxOrderPaidFulfillmentPorts.d.ts +3 -0
  300. package/dist/commerce/server/outboxOrderPaidFulfillmentPorts.js +1 -0
  301. package/dist/commerce/server/outboxOrderRefundedEmailHandler.d.ts +13 -0
  302. package/dist/commerce/server/outboxOrderRefundedEmailHandler.js +42 -0
  303. package/dist/commerce/server/outboxPaymentFailedEmailHandler.d.ts +14 -0
  304. package/dist/commerce/server/outboxPaymentFailedEmailHandler.js +65 -0
  305. package/dist/commerce/server/outboxReturnEmailHandlers.d.ts +26 -0
  306. package/dist/commerce/server/outboxReturnEmailHandlers.js +53 -0
  307. package/dist/commerce/server/outboxShipmentDeliveredEmailHandler.d.ts +17 -0
  308. package/dist/commerce/server/outboxShipmentDeliveredEmailHandler.js +52 -0
  309. package/dist/commerce/server/outboxShipmentDispatchedEmailHandler.d.ts +13 -0
  310. package/dist/commerce/server/outboxShipmentDispatchedEmailHandler.js +87 -0
  311. package/dist/commerce/server/outboxShipmentExceptionEmailHandler.d.ts +13 -0
  312. package/dist/commerce/server/outboxShipmentExceptionEmailHandler.js +46 -0
  313. package/dist/commerce/server/referenceCheckoutHandler.d.ts +48 -0
  314. package/dist/commerce/server/referenceCheckoutHandler.js +68 -0
  315. package/dist/commerce/server/reservationSweepPort.d.ts +31 -0
  316. package/dist/commerce/server/reservationSweepPort.js +45 -0
  317. package/dist/commerce/server/starterTermsPort.d.ts +22 -0
  318. package/dist/commerce/server/starterTermsPort.js +1 -0
  319. package/dist/commerce/server/supabaseCommerceReturnsPort.d.ts +13 -0
  320. package/dist/commerce/server/supabaseCommerceReturnsPort.js +56 -0
  321. package/dist/communications/consentPurposeRegistry.d.ts +1 -1
  322. package/dist/communications/newsletterIntegrationContracts.d.ts +5 -5
  323. package/dist/composition/commerceContribution.d.ts +21 -0
  324. package/dist/composition/commerceContribution.js +27 -0
  325. package/dist/composition/index.d.ts +1 -0
  326. package/dist/composition/index.js +1 -0
  327. package/dist/fulfillment/handlerSchemas.d.ts +41 -41
  328. package/dist/fulfillment/index.d.ts +1 -1
  329. package/dist/fulfillment/index.js +1 -1
  330. package/dist/fulfillment/paidHandoff.d.ts +12 -1
  331. package/dist/fulfillment/paidHandoff.js +2 -2
  332. package/dist/fulfillment/providerDispatchWorker.d.ts +4 -4
  333. package/dist/identity/customerContracts.d.ts +2 -2
  334. package/dist/inventory/bindings.d.ts +3 -0
  335. package/dist/inventory/bindings.js +10 -4
  336. package/dist/inventory/index.d.ts +1 -0
  337. package/dist/inventory/index.js +1 -0
  338. package/dist/inventory/managedStockAvailability.d.ts +15 -0
  339. package/dist/inventory/managedStockAvailability.js +16 -0
  340. package/dist/inventory/postgresStockAvailability.js +2 -25
  341. package/dist/inventory/schemas.d.ts +1 -1
  342. package/dist/inventory/stockReadResponse.d.ts +4 -0
  343. package/dist/inventory/stockReadResponse.js +27 -0
  344. package/dist/pricing/browser/index.d.ts +6 -0
  345. package/dist/pricing/browser/index.js +6 -0
  346. package/dist/promotions/adminPromotionsContracts.d.ts +2 -2
  347. package/dist/subscriptions/contracts/renewal.d.ts +2 -0
  348. package/dist/subscriptions/index.d.ts +3 -2
  349. package/dist/subscriptions/index.js +3 -2
  350. package/dist/subscriptions/renewalCycleFailureMechanism.js +3 -2
  351. package/package.json +22 -2
  352. package/release-gates.json +63 -8
  353. package/src/commerce/adapters/managed/commerceOmsControlPlane.ts +80 -0
  354. package/src/commerce/adapters/managed/commerceOmsMutations.ts +145 -0
  355. package/src/commerce/adapters/managed/commerceOmsPort.ts +158 -0
  356. package/src/commerce/adapters/managed/oms/accounting.ts +18 -0
  357. package/src/commerce/adapters/managed/oms/fulfillmentReads.ts +82 -0
  358. package/src/commerce/adapters/managed/oms/inboundEvents.ts +45 -0
  359. package/src/commerce/adapters/managed/oms/outboxEvents.ts +30 -0
  360. package/src/commerce/adapters/managed/oms/paymentAttempts.ts +19 -0
  361. package/src/commerce/adapters/managed/oms/queueSelection.ts +132 -0
  362. package/src/commerce/adapters/managed/oms/readProjections.ts +22 -0
  363. package/src/commerce/adapters/managed/oms/readQueries.ts +292 -0
  364. package/src/commerce/adapters/managed/oms/types.ts +9 -0
  365. package/src/commerce/checkoutCommandSchemas.ts +93 -0
  366. package/src/commerce/checkoutHelpers.ts +47 -0
  367. package/src/commerce/checkoutResumeSchemas.ts +164 -0
  368. package/src/commerce/checkoutSchemas.ts +182 -0
  369. package/src/commerce/commerceReturnsPort.ts +67 -0
  370. package/src/commerce/configuratorIntentPersistenceSchemas.ts +26 -0
  371. package/src/commerce/configuratorIntentSchemas.ts +47 -0
  372. package/src/commerce/contractPrimitives.ts +30 -0
  373. package/src/commerce/contracts/availability.ts +21 -0
  374. package/src/commerce/contracts/fulfillmentRecovery.ts +26 -0
  375. package/src/commerce/contracts/index.ts +17 -0
  376. package/src/commerce/contracts/intent.ts +58 -0
  377. package/src/commerce/contracts/omsTypes.ts +1217 -0
  378. package/src/commerce/contracts/order.ts +44 -0
  379. package/src/commerce/contracts/orderMoney.ts +75 -0
  380. package/src/commerce/contracts/ports.ts +124 -0
  381. package/src/commerce/contracts/recovery.ts +24 -0
  382. package/src/commerce/contracts/runtimePorts.ts +201 -0
  383. package/src/commerce/contracts/state.ts +64 -0
  384. package/src/commerce/customerDefaultsSnapshotSchemas.ts +117 -0
  385. package/src/commerce/errors.ts +53 -0
  386. package/src/commerce/fulfillmentEligibility.ts +32 -0
  387. package/src/commerce/index.ts +82 -0
  388. package/src/commerce/model/customerFulfillmentRecovery.ts +185 -0
  389. package/src/commerce/model/fulfillmentCancellationEligibility.ts +18 -0
  390. package/src/commerce/model/omsAccountingVisibility.ts +135 -0
  391. package/src/commerce/model/omsBillingInvoiceBuyer.ts +20 -0
  392. package/src/commerce/model/omsCurrency.ts +8 -0
  393. package/src/commerce/model/omsFulfillmentDebug.ts +157 -0
  394. package/src/commerce/model/omsFulfillmentSummary.ts +225 -0
  395. package/src/commerce/model/omsListTotals.ts +38 -0
  396. package/src/commerce/model/omsOperationalLadder.ts +181 -0
  397. package/src/commerce/model/omsOrderLineMoney.ts +34 -0
  398. package/src/commerce/model/omsPaymentMethodSummary.ts +19 -0
  399. package/src/commerce/model/omsReadModel.ts +302 -0
  400. package/src/commerce/model/omsReadModelCommunication.ts +32 -0
  401. package/src/commerce/model/omsReadModelHelpers.ts +279 -0
  402. package/src/commerce/model/omsReadModelList.ts +90 -0
  403. package/src/commerce/model/omsReadModelListSummary.ts +19 -0
  404. package/src/commerce/model/omsReadModelPricing.ts +57 -0
  405. package/src/commerce/model/omsReadModelProjections.ts +1 -0
  406. package/src/commerce/model/omsReadModelRowCore.ts +193 -0
  407. package/src/commerce/model/omsReadModelRows.ts +7 -0
  408. package/src/commerce/offerAvailability.ts +75 -0
  409. package/src/commerce/offerAvailabilitySchemas.ts +56 -0
  410. package/src/commerce/offerEvidenceSchemas.ts +17 -0
  411. package/src/commerce/oms/omsContractBase.ts +92 -0
  412. package/src/commerce/oms/omsContractReadAuxSchemas.ts +130 -0
  413. package/src/commerce/oms/omsContractReadSchemas.ts +303 -0
  414. package/src/commerce/oms/omsContractRequests.ts +143 -0
  415. package/src/commerce/oms/omsContracts.ts +6 -0
  416. package/src/commerce/oms/omsControlPlane.ts +85 -0
  417. package/src/commerce/oms/omsFulfillmentContractSchemas.ts +113 -0
  418. package/src/commerce/oms/omsFulfillmentDebugSchemas.ts +38 -0
  419. package/src/commerce/oms/omsFulfillmentHealthSchemas.ts +46 -0
  420. package/src/commerce/oms/omsPorts.ts +165 -0
  421. package/src/commerce/oms/omsPrimitives.ts +3 -0
  422. package/src/commerce/oms/omsProviderEvidenceSchemas.ts +13 -0
  423. package/src/commerce/orderDraftErrors.ts +28 -0
  424. package/src/commerce/orderEmailEligibility.ts +131 -0
  425. package/src/commerce/orderEmailLines.ts +22 -0
  426. package/src/commerce/orderFeedbackContracts.ts +47 -0
  427. package/src/commerce/orderMoney.ts +207 -0
  428. package/src/commerce/orderReviewContracts.ts +124 -0
  429. package/src/commerce/orderReviewPorts.ts +94 -0
  430. package/src/commerce/outboxDispatchContracts.ts +30 -0
  431. package/src/commerce/outboxEventSchemas.ts +230 -0
  432. package/src/commerce/outboxOrderDraftEmailPorts.ts +241 -0
  433. package/src/commerce/paymentContinuation.ts +5 -0
  434. package/src/commerce/ports.ts +7 -0
  435. package/src/commerce/recoveryDestination.ts +23 -0
  436. package/src/commerce/runtimeErrors.ts +19 -0
  437. package/src/commerce/salesChannelEnvelope.ts +28 -0
  438. package/src/commerce/schemas.ts +104 -0
  439. package/src/commerce/server/checkoutCompensation.ts +49 -0
  440. package/src/commerce/server/checkoutConflictClassifiers.ts +53 -0
  441. package/src/commerce/server/checkoutExpiredRecoveryPolicy.ts +107 -0
  442. package/src/commerce/server/checkoutInvoiceBuyerSnapshot.ts +36 -0
  443. package/src/commerce/server/checkoutOfferPolicyPort.ts +60 -0
  444. package/src/commerce/server/checkoutPaymentContinuationCredential.ts +222 -0
  445. package/src/commerce/server/checkoutQuoteTypes.ts +9 -0
  446. package/src/commerce/server/checkoutRecoveryOrderPort.ts +40 -0
  447. package/src/commerce/server/checkoutRecoveryToken.ts +63 -0
  448. package/src/commerce/server/checkoutRefusalContinuation.ts +29 -0
  449. package/src/commerce/server/checkoutRuntimeTypes.ts +26 -0
  450. package/src/commerce/server/commerceCheckoutCommandExecution.ts +188 -0
  451. package/src/commerce/server/commerceCheckoutCompensation.ts +66 -0
  452. package/src/commerce/server/commerceCheckoutContinuationTerms.ts +87 -0
  453. package/src/commerce/server/commerceCheckoutCustomerDefaults.ts +24 -0
  454. package/src/commerce/server/commerceCheckoutFailureHandler.ts +144 -0
  455. package/src/commerce/server/commerceCheckoutHandler.ts +269 -0
  456. package/src/commerce/server/commerceCheckoutHandlerAdmission.ts +157 -0
  457. package/src/commerce/server/commerceCheckoutHandlerHelpers.ts +223 -0
  458. package/src/commerce/server/commerceCheckoutOrchestrationError.ts +15 -0
  459. package/src/commerce/server/commerceCheckoutPromotionPriceChange.ts +83 -0
  460. package/src/commerce/server/commerceCheckoutProviderPayment.ts +45 -0
  461. package/src/commerce/server/commerceCheckoutQuoteGuard.ts +172 -0
  462. package/src/commerce/server/commerceCheckoutResume.ts +132 -0
  463. package/src/commerce/server/commerceCheckoutResumeGuard.ts +195 -0
  464. package/src/commerce/server/commerceCheckoutSavedPaymentMethod.ts +85 -0
  465. package/src/commerce/server/commerceCheckoutStartRuntimeFailure.ts +59 -0
  466. package/src/commerce/server/commerceCheckoutTiming.ts +53 -0
  467. package/src/commerce/server/commerceDiagnostics.ts +37 -0
  468. package/src/commerce/server/commerceOmsCancelOrderHandler.ts +43 -0
  469. package/src/commerce/server/commerceOmsControlPlaneHandlers.ts +71 -0
  470. package/src/commerce/server/commerceOmsHandlers.ts +275 -0
  471. package/src/commerce/server/commerceOmsMarkRefundedHandler.ts +39 -0
  472. package/src/commerce/server/commerceOrderDraft.ts +131 -0
  473. package/src/commerce/server/commercePaidOrderOrchestration.ts +275 -0
  474. package/src/commerce/server/commerceProviderAttemptFailure.ts +257 -0
  475. package/src/commerce/server/commerceReturnsHandlers.ts +143 -0
  476. package/src/commerce/server/commerceRuntimePort.ts +75 -0
  477. package/src/commerce/server/commerceRuntimeService.ts +229 -0
  478. package/src/commerce/server/commerceRuntimeStartResponse.ts +76 -0
  479. package/src/commerce/server/commerceStarterOfferGuard.ts +200 -0
  480. package/src/commerce/server/configuratorIntentPersistence.ts +161 -0
  481. package/src/commerce/server/configuratorIntentPersistenceHandler.ts +54 -0
  482. package/src/commerce/server/emailRuntime.ts +30 -0
  483. package/src/commerce/server/handlerSupport.ts +22 -0
  484. package/src/commerce/server/index.ts +181 -0
  485. package/src/commerce/server/offerAvailability.ts +275 -0
  486. package/src/commerce/server/orderDraftRpcPayload.ts +39 -0
  487. package/src/commerce/server/orderFeedbackSubmit.ts +40 -0
  488. package/src/commerce/server/orderFeedbackTokenCheck.ts +28 -0
  489. package/src/commerce/server/orderReviewAdminHandlers.ts +43 -0
  490. package/src/commerce/server/orderReviewHandlers.ts +57 -0
  491. package/src/commerce/server/orderReviewLifecycle.ts +129 -0
  492. package/src/commerce/server/orderReviewMedia.ts +170 -0
  493. package/src/commerce/server/orderReviewRawUploadHandler.ts +300 -0
  494. package/src/commerce/server/outboxCheckoutExpiredEmailHandler.ts +86 -0
  495. package/src/commerce/server/outboxCheckoutRecoveryEmailHandler.ts +188 -0
  496. package/src/commerce/server/outboxDispatchRegistry.ts +269 -0
  497. package/src/commerce/server/outboxDispatchRegistryComposition.ts +31 -0
  498. package/src/commerce/server/outboxOrderDraftEmailHandler.ts +162 -0
  499. package/src/commerce/server/outboxOrderPaidEmailHandler.ts +147 -0
  500. package/src/commerce/server/outboxOrderPaidFulfillmentHandler.ts +4 -0
  501. package/src/commerce/server/outboxOrderPaidFulfillmentPorts.ts +3 -0
  502. package/src/commerce/server/outboxOrderRefundedEmailHandler.ts +58 -0
  503. package/src/commerce/server/outboxPaymentFailedEmailHandler.ts +99 -0
  504. package/src/commerce/server/outboxReturnEmailHandlers.ts +92 -0
  505. package/src/commerce/server/outboxShipmentDeliveredEmailHandler.ts +94 -0
  506. package/src/commerce/server/outboxShipmentDispatchedEmailHandler.ts +112 -0
  507. package/src/commerce/server/outboxShipmentExceptionEmailHandler.ts +73 -0
  508. package/src/commerce/server/referenceCheckoutHandler.ts +87 -0
  509. package/src/commerce/server/reservationSweepPort.ts +67 -0
  510. package/src/commerce/server/starterTermsPort.ts +22 -0
  511. package/src/commerce/server/supabaseCommerceReturnsPort.ts +81 -0
  512. package/src/composition/commerceContribution.ts +35 -0
  513. package/src/composition/index.ts +1 -0
  514. package/src/fulfillment/index.ts +1 -1
  515. package/src/fulfillment/paidHandoff.ts +2 -2
  516. package/src/inventory/bindings.ts +12 -4
  517. package/src/inventory/index.ts +1 -0
  518. package/src/inventory/managedStockAvailability.ts +23 -0
  519. package/src/inventory/postgresStockAvailability.ts +2 -21
  520. package/src/inventory/stockReadResponse.ts +25 -0
  521. package/src/pricing/browser/index.ts +6 -0
  522. package/src/subscriptions/contracts/renewal.ts +2 -1
  523. package/src/subscriptions/index.ts +3 -2
  524. package/src/subscriptions/renewalCycleFailureMechanism.ts +3 -1
@@ -0,0 +1,188 @@
1
+ import type { CommerceEmailRuntime } from "./emailRuntime.js";
2
+ // Checkout-recovery email handler (W3, transactional). Consumes the
3
+ // commerce.checkout_recovery outbox events produced by the W2 enqueue cron and
4
+ // sends a "finish your payment" email with a signed deep-link to complete payment
5
+ // for the SAME unpaid order. Transactional: NO marketing-consent gate. Promotes
6
+ // the W2 producer from dormant to runtime-gated.
7
+ //
8
+ // Three producers feed this one handler: the W2 cron, the operator payment-link
9
+ // command (`operatorIssued: true`), and the buyer's own escape hatch off a payment
10
+ // step that will not finish (`buyerRequested: true`). They differ in one place
11
+ // only — which eligibility question is asked below — and share the template, the
12
+ // dedupe ledger and the deep-link build.
13
+ //
14
+ // handle() flow: parse the minimal payload -> resolve recipient by order ->
15
+ // dedupe on the email_sends ledger -> build the localized deep-link from the raw
16
+ // recovery token -> send -> map the provider outcome to a worker outcome.
17
+
18
+ import { z } from "zod";
19
+ import { COMMERCE_CHECKOUT_RECOVERY_EVENT_TYPE } from "../outboxEventSchemas.js";
20
+ import {
21
+ OUTBOX_CHECKOUT_RECOVERY_TEMPLATE_SLUG,
22
+ type OrderPaymentLifecyclePort,
23
+ type TransactionalEmailPort,
24
+ type OrderRecipientPort,
25
+ } from "../outboxOrderDraftEmailPorts.js";
26
+ import {
27
+ PAYMENT_SESSION_ACTIVE,
28
+ shouldSkipCheckoutRecovery,
29
+ shouldSkipOperatorCheckoutRecovery,
30
+ } from "../orderEmailEligibility.js";
31
+ import type {
32
+ OutboxEventRow,
33
+ OutboxHandler,
34
+ OutboxHandlerOutcome,
35
+ } from "../outboxDispatchContracts.js";
36
+
37
+ const HANDLER_TIMEOUT_MS = 10_000;
38
+
39
+ export const outboxCheckoutRecoveryEmailDescriptor = Object.freeze({
40
+ eventType: COMMERCE_CHECKOUT_RECOVERY_EVENT_TYPE, timeoutMs: HANDLER_TIMEOUT_MS,
41
+ });
42
+
43
+ // Deferrals allowed before a nudge blocked by a live payment session retires.
44
+ // The dispatcher's default backoff is 60 s doubling to a 3600 s cap, so five
45
+ // attempts span ~31 min — past the 15 min activity window that produced the
46
+ // skip, and well inside the 120 min subscription sweep that would cancel the
47
+ // order anyway. It also sits under the default `maxAttempts` of 8: exhausting
48
+ // retries routes a row to the terminal DLQ as `discarded`, which records a
49
+ // dispatch failure for a reminder that was merely untimely. Retiring as
50
+ // `processed` keeps it an honest deliberate skip. A deployment that configures
51
+ // `maxAttempts` below this ceiling has chosen fewer retries globally, and gets
52
+ // that choice here too.
53
+ const PAYMENT_SESSION_RETRY_CEILING = 5;
54
+
55
+ // Frozen minimal parse: orderId (recipient + linkage) and recoveryToken (the
56
+ // deep-link) are load-bearing; mode/reminderHours degrade to safe defaults so a
57
+ // producer tweak never retro-DLQs an in-flight reminder.
58
+ //
59
+ // `operatorIssued` says a human minted this link on the operator surface rather
60
+ // than the cron finding an abandoned order. `buyerRequested` says the BUYER asked
61
+ // for it, from inside the payment step they are stuck on. Both optional, and
62
+ // absent means the cron: every row already in flight when either shipped keeps
63
+ // its exact behaviour.
64
+ const payloadSubsetSchema = z
65
+ .object({
66
+ orderId: z.guid(),
67
+ recoveryToken: z.string().min(1),
68
+ mode: z.string().optional(),
69
+ reminderHours: z.number().int().positive().optional(),
70
+ operatorIssued: z.boolean().optional(),
71
+ buyerRequested: z.boolean().optional(),
72
+ })
73
+ .passthrough();
74
+
75
+ export function createOutboxCheckoutRecoveryEmailHandler<Locale extends string>(deps: {
76
+ runtime: Pick<CommerceEmailRuntime<Locale>, "mapSendOutcome" | "parseOutboxPayload" | "resolveLocale">;
77
+ emailPort: TransactionalEmailPort<Locale>;
78
+ recipientPort: OrderRecipientPort;
79
+ lifecyclePort: OrderPaymentLifecyclePort;
80
+ }): OutboxHandler {
81
+ return {
82
+ ...outboxCheckoutRecoveryEmailDescriptor,
83
+ async handle(row: OutboxEventRow, signal: AbortSignal): Promise<OutboxHandlerOutcome> {
84
+ const parsed = deps.runtime.parseOutboxPayload(payloadSubsetSchema, row.payload);
85
+ if (parsed.ok === false) {
86
+ return parsed.outcome;
87
+ }
88
+
89
+ const lifecycle = await deps.lifecyclePort.read(parsed.data.orderId, signal);
90
+ // Three producers, three questions. The cron asks "is this order still
91
+ // sitting unpaid", and going quiet once it is not is right. An
92
+ // operator-issued link is a decision a human already made about an order
93
+ // that has usually left `pending_payment` — expired is the ordinary case —
94
+ // and the redeem rail recreates it when the customer opens the link.
95
+ // Judging that by the cron's rule would skip it as
96
+ // `order_not_pending_payment` and no-send in silence.
97
+ const skipReason = parsed.data.buyerRequested
98
+ ? skipReasonForBuyerRequestedRecovery(lifecycle)
99
+ : parsed.data.operatorIssued
100
+ ? shouldSkipOperatorCheckoutRecovery(lifecycle)
101
+ : shouldSkipCheckoutRecovery(lifecycle);
102
+ if (skipReason === PAYMENT_SESSION_ACTIVE) {
103
+ // A live payment session is temporary. The producer key is unique per
104
+ // event, so settling it as processed would consume the only reminder
105
+ // without sending. Defer until the session settles, subject to the
106
+ // retry ceiling below.
107
+ //
108
+ // Read `attempts` defensively: `undefined < n` is false, which would
109
+ // retire the nudge on its FIRST deferral — the precise opposite of the
110
+ // intent here, and silent.
111
+ const attempts = Number.isFinite(row.attempts) ? row.attempts : 0;
112
+ if (attempts < PAYMENT_SESSION_RETRY_CEILING) {
113
+ return { kind: "retry", reason: PAYMENT_SESSION_ACTIVE };
114
+ }
115
+ // Past the ceiling, retire deliberately. Letting retries run out routes
116
+ // the row to the terminal DLQ as `discarded`, which reads as a dispatch
117
+ // failure for a reminder that was only untimely.
118
+ return { kind: "processed", detail: { skipped: "payment_session_exhausted" } };
119
+ }
120
+ if (skipReason) {
121
+ return { kind: "processed", detail: { skipped: skipReason } };
122
+ }
123
+
124
+ const recipient = await deps.recipientPort.resolve(parsed.data.orderId, signal);
125
+ if (recipient === null) {
126
+ // No client on the order (or unreadable) — nothing to send, never retry.
127
+ return { kind: "processed", detail: { skipped: "recipient_unresolved" } };
128
+ }
129
+
130
+ const alreadySent = await deps.emailPort.findExistingSend(
131
+ OUTBOX_CHECKOUT_RECOVERY_TEMPLATE_SLUG,
132
+ row.id,
133
+ );
134
+ if (alreadySent) {
135
+ return { kind: "processed", detail: { dedupe: "email_already_sent" } };
136
+ }
137
+
138
+ const locale = deps.runtime.resolveLocale(recipient.country ?? null);
139
+
140
+ const outcome = await deps.emailPort.sendCheckoutRecovery({
141
+ to: recipient.email,
142
+ firstName: recipient.firstName,
143
+ contextName: recipient.contextName ?? null,
144
+ orderId: parsed.data.orderId,
145
+ recoveryToken: parsed.data.recoveryToken,
146
+ mode: parsed.data.mode ?? "one_time",
147
+ reminderHours: parsed.data.reminderHours ?? 1,
148
+ outboxEventId: row.id,
149
+ // Marks the deep-link as the one the buyer mailed themselves off a stuck
150
+ // payment step, so the landing page can tell that arrival apart from a
151
+ // cron nudge. Absent for every other producer, which keeps their URLs
152
+ // byte-identical to what they were.
153
+ linkSource: parsed.data.buyerRequested ? "buyer_hatch" : undefined,
154
+ locale,
155
+ signal,
156
+ });
157
+
158
+ return deps.runtime.mapSendOutcome(outcome);
159
+ },
160
+ };
161
+ }
162
+
163
+ /**
164
+ * Eligibility for a link the BUYER asked for, from inside the payment step.
165
+ *
166
+ * Everything the operator rule suppresses is suppressed here too — the order is
167
+ * gone, or the money is already in — because those make the email actively wrong
168
+ * rather than merely late. Exactly ONE rule is lifted:
169
+ * `payment_session_active`.
170
+ *
171
+ * That rule is a politeness rule: do not remind someone to pay while they are
172
+ * paying. For this producer it is not merely unhelpful, it is inverted. A buyer
173
+ * who taps this control is BY CONSTRUCTION inside a live payment session — that
174
+ * session is the thing that will not finish, and asking for a way out of it is
175
+ * the whole request. Deferring on it would defer every buyer row through the
176
+ * retry ceiling and then retire it as `payment_session_exhausted`, so the hatch
177
+ * would send nothing, ever, and nothing would report a failure.
178
+ *
179
+ * Written here rather than as a fourth predicate in `orderEmailEligibility`: it
180
+ * is not a different question from the operator's, it is the operator's question
181
+ * with its one transient answer already decided by who is asking.
182
+ */
183
+ function skipReasonForBuyerRequestedRecovery(
184
+ lifecycle: Parameters<typeof shouldSkipOperatorCheckoutRecovery>[0],
185
+ ): string | null {
186
+ const reason = shouldSkipOperatorCheckoutRecovery(lifecycle);
187
+ return reason === PAYMENT_SESSION_ACTIVE ? null : reason;
188
+ }
@@ -0,0 +1,269 @@
1
+ import type { CommerceEmailRuntime } from "./emailRuntime.js";
2
+ // Registry = the single source of truth for which event types this wave
3
+ // claims. The claim allowlist is derived from its keys, so the PSP-blocked
4
+ // event types stay structurally unclaimable until their handlers register.
5
+ import type { OutboxHandlerDescriptor } from "@openlup/core/outbox";
6
+ import { composeOutboxHandlerDescriptors, assertOutboxRegistryDescriptors } from "./outboxDispatchRegistryComposition.js";
7
+ import type { OutboxHandler, OutboxHandlerRegistry } from "../outboxDispatchContracts.js";
8
+ import type {
9
+ OrderPaymentLifecyclePort,
10
+ OrderPaidLinesPort,
11
+ OrderRecipientPort,
12
+ TransactionalEmailPort,
13
+ } from "../outboxOrderDraftEmailPorts.js";
14
+ import { createOutboxOrderDraftEmailHandler, outboxOrderDraftEmailDescriptor } from "./outboxOrderDraftEmailHandler.js";
15
+ import { createOutboxCheckoutRecoveryEmailHandler, outboxCheckoutRecoveryEmailDescriptor } from "./outboxCheckoutRecoveryEmailHandler.js";
16
+ import {
17
+ createOutboxOrderPaidEmailHandler, outboxOrderPaidEmailDescriptor,
18
+ type OrderPaidParcelNoteReader,
19
+ } from "./outboxOrderPaidEmailHandler.js";
20
+ import { createOutboxPaymentFailedEmailHandler, outboxPaymentFailedEmailDescriptor } from "./outboxPaymentFailedEmailHandler.js";
21
+ import { createOutboxCheckoutExpiredEmailHandler, outboxCheckoutExpiredEmailDescriptor } from "./outboxCheckoutExpiredEmailHandler.js";
22
+ import { createOutboxOrderRefundedEmailHandler, outboxOrderRefundedEmailDescriptor } from "./outboxOrderRefundedEmailHandler.js";
23
+ import { createOutboxShipmentDispatchedEmailHandler, outboxShipmentDispatchedEmailDescriptor } from "./outboxShipmentDispatchedEmailHandler.js";
24
+ import {
25
+ createOutboxShipmentDeliveredEmailHandler, outboxShipmentDeliveredEmailDescriptor,
26
+ type DeliveredEmailProviderKindReader,
27
+ } from "./outboxShipmentDeliveredEmailHandler.js";
28
+ import { createOutboxShipmentExceptionEmailHandler, outboxShipmentExceptionEmailDescriptor } from "./outboxShipmentExceptionEmailHandler.js";
29
+ import {
30
+ createOutboxReturnApprovedEmailHandler, outboxReturnApprovedEmailDescriptor,
31
+ createOutboxReturnRejectedEmailHandler, outboxReturnRejectedEmailDescriptor,
32
+ } from "./outboxReturnEmailHandlers.js";
33
+ import type { OrderPaidFulfillmentPort } from "./outboxOrderPaidFulfillmentPorts.js";
34
+ import {
35
+ createOutboxOrderPaidFulfillmentHandler, outboxOrderPaidFulfillmentDescriptor,
36
+ type OrderPaidRiskAssessmentPort,
37
+ } from "./outboxOrderPaidFulfillmentHandler.js";
38
+ import {
39
+ platformOwnsBuyerEmail,
40
+ type OrderBuyerCommsPolicy,
41
+ } from "../salesChannelEnvelope.js";
42
+
43
+ /**
44
+ * Reads the sales-source buyer-comms policy for one order. `null` is
45
+ * the answer for an order that names no channel — a storefront order, or one
46
+ * that predates the source axis.
47
+ *
48
+ * Read on every event so changes to `sales_channels.buyer_comms_owner` take
49
+ * effect on the next dispatch. A cached policy could keep sending after
50
+ * ownership has changed.
51
+ */
52
+ export interface OrderBuyerCommsPolicyReader {
53
+ readOrderBuyerCommsPolicy(orderUuid: string): Promise<OrderBuyerCommsPolicy | null>;
54
+ }
55
+
56
+ export type TransactionalEmailRegistryDeps<Locale extends string = string> = {
57
+ runtime: CommerceEmailRuntime<Locale>;
58
+ emailPort: TransactionalEmailPort<Locale>;
59
+ recipientPort: OrderRecipientPort;
60
+ lifecyclePort: OrderPaymentLifecyclePort;
61
+ orderPaidLinesPort: OrderPaidLinesPort;
62
+ // Optional adopter note for the paid receipt; absent keeps today's receipt.
63
+ orderPaidParcelNote?: OrderPaidParcelNoteReader<Locale>;
64
+ // The delivered-email handler suppresses OUR delivered email for providers whose
65
+ // capability profile owns the notice (none today). Absent only in
66
+ // tests/non-transactional composition.
67
+ deliveredProviderKindReader?: DeliveredEmailProviderKindReader;
68
+ // Suppresses OUR buyer-facing email for channel-owned orders. Absent only in
69
+ // tests/non-transactional composition, where it degrades to today's behaviour.
70
+ buyerCommsPolicyReader?: OrderBuyerCommsPolicyReader;
71
+ };
72
+
73
+ function readOrderUuid(payload: Record<string, unknown>): string | null {
74
+ const value = payload.orderUuid;
75
+ return typeof value === "string" && value.length > 0 ? value : null;
76
+ }
77
+
78
+ /**
79
+ * Wraps ONE buyer-facing email handler so a channel that owns buyer
80
+ * communication does not also get ours.
81
+ *
82
+ * WHY THE WRAPPER IS AT THE HANDLER AND NOT AT THE TRIGGER. The checkout email
83
+ * reconciler (`commerce_reconcile_checkout_email_outbox`) back-fills a `commerce.order.paid.email`
84
+ * row for EVERY order that reads `paid` and has none, keyed only on the order
85
+ * id — it does not look at `source_kind`, and outboxDispatchRuntime runs it
86
+ * inline immediately before the dispatch worker in the same invocation. A
87
+ * trigger-level or emitter-level suppression would therefore be undone on the
88
+ * very next reconcile pass, silently, and the buyer would get the email anyway.
89
+ * The dispatcher is the last place that can still decide.
90
+ *
91
+ * Recorded as `processed` rather than `discard` so the event settles once and
92
+ * never retries. `skipped` is one of the DELIVERY_LIFECYCLE_METADATA_KEYS below,
93
+ * so the email-delivery lifecycle trigger reads the suppression as a skip with a
94
+ * reason instead of leaving a delivery hanging — the same shape the
95
+ * carrier-owned delivered notice already uses.
96
+ */
97
+ function suppressForChannel(
98
+ handler: OutboxHandler,
99
+ reader: OrderBuyerCommsPolicyReader | undefined,
100
+ ): OutboxHandler {
101
+ if (!reader) return handler;
102
+ return {
103
+ eventType: handler.eventType,
104
+ timeoutMs: handler.timeoutMs,
105
+ async handle(row, signal, execution) {
106
+ const orderUuid = readOrderUuid(row.payload);
107
+ // A payload with no usable order id is not a policy question. Hand it to
108
+ // the handler, whose own contract parse produces the right discard rather
109
+ // than this wrapper inventing a second failure mode for the same row.
110
+ if (orderUuid !== null) {
111
+ const policy = await reader.readOrderBuyerCommsPolicy(orderUuid);
112
+ if (!platformOwnsBuyerEmail(policy)) {
113
+ return { kind: "processed", detail: { skipped: "channel_owns_buyer_comms" } };
114
+ }
115
+ }
116
+ return handler.handle(row, signal, execution);
117
+ },
118
+ };
119
+ }
120
+
121
+ export function describeCoreOutboxHandlers(selection: {
122
+ transactionalEmail: boolean; fulfillmentEnabled: boolean;
123
+ }): OutboxHandlerDescriptor[] {
124
+ return [
125
+ ...(selection.transactionalEmail ? [
126
+ outboxOrderDraftEmailDescriptor, outboxCheckoutRecoveryEmailDescriptor,
127
+ outboxOrderPaidEmailDescriptor, outboxPaymentFailedEmailDescriptor,
128
+ outboxCheckoutExpiredEmailDescriptor, outboxOrderRefundedEmailDescriptor,
129
+ outboxShipmentDispatchedEmailDescriptor, outboxShipmentDeliveredEmailDescriptor,
130
+ outboxShipmentExceptionEmailDescriptor, outboxReturnApprovedEmailDescriptor,
131
+ outboxReturnRejectedEmailDescriptor,
132
+ ] : []),
133
+ ...(selection.fulfillmentEnabled ? [outboxOrderPaidFulfillmentDescriptor] : []),
134
+ ];
135
+ }
136
+
137
+ export function createOutboxDispatchRegistry<Locale extends string>(deps: {
138
+ /** Bind the existing outbox rail's composition; commerce owns no second rail. */
139
+ uniqueRegistry(handlers: readonly OutboxHandler[]): OutboxHandlerRegistry;
140
+ transactionalEmail?: TransactionalEmailRegistryDeps<Locale>;
141
+ fulfillmentPort?: OrderPaidFulfillmentPort;
142
+ // Optional risk seam, evaluated per-handle via the thunk so the flag-OFF
143
+ // posture can ack without side effects. When absent/disabled, fulfillment runs
144
+ // exactly as before.
145
+ riskPort?: OrderPaidRiskAssessmentPort;
146
+ riskEnabled?: boolean;
147
+ // Read once at job construction. When false, the fulfillment event type is not
148
+ // registered, so commerce.order.paid rows stay unclaimed for later activation.
149
+ fulfillmentEnabled: boolean;
150
+ // Handlers from OTHER domains (subscription, accounting, …), composed by the
151
+ // job so this commerce file never imports cross-domain code. Each must claim a
152
+ // distinct event type. Appended after the commerce handlers.
153
+ extraHandlers?: readonly OutboxHandler[];
154
+ expectedDescriptors?: () => readonly OutboxHandlerDescriptor[];
155
+ }): OutboxHandlerRegistry {
156
+ // Applied to the buyer-facing set below. The four email handlers it is NOT
157
+ // applied to are structurally unreachable for a channel order on the ingest
158
+ // path this repository ships, and each is asserted rather than assumed:
159
+ // - order-draft-created: emitted INSIDE `commerce_create_order_draft`.
160
+ // `commerce_create_channel_order` writes the order header directly and
161
+ // emits `channel.order.ingested`, so no draft event ever exists.
162
+ // - checkout-recovery: minted from an expired-checkout recovery token, which
163
+ // only the checkout-expiry path can create.
164
+ // - checkout-expired and payment-failed: both key off a one-time order
165
+ // reaching status `expired`/`failed`, and the ONLY writer of those statuses
166
+ // for a one-time order is `commerce_payment_apply_result`. The ingest saga
167
+ // calls `applySucceeded` and nothing else — the marketplace collected the
168
+ // money before the order existed here, so there is no decline or session to
169
+ // expire. Nothing sweeps a stale `draft` order to `expired` either.
170
+ // This is an argument about the CURRENT ingest path. A later wave that gives a
171
+ // channel order a decline or expiry rail must revisit this list, not assume it.
172
+ //
173
+ // `commerce.order.canceled` is absent from this enumeration because its email
174
+ // handler (outboxOrderCanceledEmailHandler.ts) is registered by NOTHING — the
175
+ // only claimant of that event type is the accounting reversal handler, which
176
+ // arrives through `extraHandlers` and must keep running for a channel order.
177
+ // Wrapping a handler this registry does not compose would be theatre; if that
178
+ // email is ever wired up, it belongs in the wrapped set.
179
+ const suppress = (handler: OutboxHandler): OutboxHandler =>
180
+ suppressForChannel(handler, deps.transactionalEmail?.buyerCommsPolicyReader);
181
+ const handlers: OutboxHandler[] = [
182
+ ...(deps.transactionalEmail
183
+ ? [
184
+ createOutboxOrderDraftEmailHandler({
185
+ runtime: deps.transactionalEmail.runtime,
186
+ emailPort: deps.transactionalEmail.emailPort,
187
+ recipientPort: deps.transactionalEmail.recipientPort,
188
+ lifecyclePort: deps.transactionalEmail.lifecyclePort,
189
+ }),
190
+ createOutboxCheckoutRecoveryEmailHandler({
191
+ runtime: deps.transactionalEmail.runtime,
192
+ emailPort: deps.transactionalEmail.emailPort,
193
+ recipientPort: deps.transactionalEmail.recipientPort,
194
+ lifecyclePort: deps.transactionalEmail.lifecyclePort,
195
+ }),
196
+ suppress(createOutboxOrderPaidEmailHandler({
197
+ runtime: deps.transactionalEmail.runtime,
198
+ emailPort: deps.transactionalEmail.emailPort,
199
+ recipientPort: deps.transactionalEmail.recipientPort,
200
+ linesPort: deps.transactionalEmail.orderPaidLinesPort,
201
+ describeParcel: deps.transactionalEmail.orderPaidParcelNote,
202
+ })),
203
+ createOutboxPaymentFailedEmailHandler({
204
+ runtime: deps.transactionalEmail.runtime,
205
+ emailPort: deps.transactionalEmail.emailPort,
206
+ recipientPort: deps.transactionalEmail.recipientPort,
207
+ lifecyclePort: deps.transactionalEmail.lifecyclePort,
208
+ }),
209
+ createOutboxCheckoutExpiredEmailHandler({
210
+ runtime: deps.transactionalEmail.runtime,
211
+ emailPort: deps.transactionalEmail.emailPort,
212
+ recipientPort: deps.transactionalEmail.recipientPort,
213
+ lifecyclePort: deps.transactionalEmail.lifecyclePort,
214
+ }),
215
+ suppress(createOutboxOrderRefundedEmailHandler({
216
+ runtime: deps.transactionalEmail.runtime,
217
+ emailPort: deps.transactionalEmail.emailPort,
218
+ recipientPort: deps.transactionalEmail.recipientPort,
219
+ })),
220
+ suppress(createOutboxShipmentDispatchedEmailHandler({
221
+ runtime: deps.transactionalEmail.runtime,
222
+ emailPort: deps.transactionalEmail.emailPort,
223
+ recipientPort: deps.transactionalEmail.recipientPort,
224
+ })),
225
+ suppress(createOutboxShipmentDeliveredEmailHandler({
226
+ runtime: deps.transactionalEmail.runtime,
227
+ emailPort: deps.transactionalEmail.emailPort,
228
+ recipientPort: deps.transactionalEmail.recipientPort,
229
+ providerKindReader: deps.transactionalEmail.deliveredProviderKindReader,
230
+ })),
231
+ suppress(createOutboxShipmentExceptionEmailHandler({
232
+ runtime: deps.transactionalEmail.runtime,
233
+ emailPort: deps.transactionalEmail.emailPort,
234
+ recipientPort: deps.transactionalEmail.recipientPort,
235
+ })),
236
+ suppress(createOutboxReturnApprovedEmailHandler({
237
+ runtime: deps.transactionalEmail.runtime,
238
+ emailPort: deps.transactionalEmail.emailPort,
239
+ recipientPort: deps.transactionalEmail.recipientPort,
240
+ })),
241
+ suppress(createOutboxReturnRejectedEmailHandler({
242
+ runtime: deps.transactionalEmail.runtime,
243
+ emailPort: deps.transactionalEmail.emailPort,
244
+ recipientPort: deps.transactionalEmail.recipientPort,
245
+ })),
246
+ ]
247
+ : []),
248
+ ...(deps.fulfillmentEnabled && deps.fulfillmentPort
249
+ ? [
250
+ createOutboxOrderPaidFulfillmentHandler({
251
+ fulfillmentPort: deps.fulfillmentPort,
252
+ riskPort: deps.riskPort,
253
+ isRiskEnabled: () => deps.riskEnabled === true,
254
+ }),
255
+ ]
256
+ : []),
257
+ ...(deps.extraHandlers ?? []),
258
+ ];
259
+ const registry = deps.uniqueRegistry(handlers);
260
+ const expected = deps.expectedDescriptors?.() ?? composeOutboxHandlerDescriptors([
261
+ ...describeCoreOutboxHandlers({
262
+ transactionalEmail: Boolean(deps.transactionalEmail),
263
+ fulfillmentEnabled: deps.fulfillmentEnabled && Boolean(deps.fulfillmentPort),
264
+ }),
265
+ ...(deps.extraHandlers ?? []).map(({ eventType, timeoutMs }) => ({ eventType, timeoutMs })),
266
+ ]);
267
+ assertOutboxRegistryDescriptors(registry, expected);
268
+ return registry;
269
+ }
@@ -0,0 +1,31 @@
1
+ import type { OutboxHandlerDescriptor } from "@openlup/core/outbox";
2
+ import type { OutboxHandlerRegistry } from "../outboxDispatchContracts.js";
3
+
4
+ /** Preserve native registration order and the timeout sum of same-event handlers. */
5
+ export function composeOutboxHandlerDescriptors(
6
+ handlers: readonly OutboxHandlerDescriptor[],
7
+ ): OutboxHandlerDescriptor[] {
8
+ const descriptors = new Map<string, OutboxHandlerDescriptor>();
9
+ for (const handler of handlers) {
10
+ const existing = descriptors.get(handler.eventType);
11
+ descriptors.set(handler.eventType, {
12
+ eventType: handler.eventType,
13
+ timeoutMs: (existing?.timeoutMs ?? 0) + handler.timeoutMs,
14
+ });
15
+ }
16
+ return [...descriptors.values()];
17
+ }
18
+
19
+ /** Refuse a different claim surface before a worker can call claimBatch. */
20
+ export function assertOutboxRegistryDescriptors(
21
+ registry: OutboxHandlerRegistry,
22
+ expected: readonly OutboxHandlerDescriptor[],
23
+ ): void {
24
+ const actual = [...registry.entries()];
25
+ if (actual.length !== expected.length || actual.some(([key, handler], index) => {
26
+ const descriptor = expected[index];
27
+ return key !== descriptor.eventType || handler.eventType !== key
28
+ || handler.timeoutMs !== descriptor.timeoutMs
29
+ || !Number.isFinite(handler.timeoutMs) || handler.timeoutMs <= 0;
30
+ })) throw new Error("outbox_registry_descriptor_mismatch");
31
+ }
@@ -0,0 +1,162 @@
1
+ import type { CommerceEmailRuntime } from "./emailRuntime.js";
2
+ import { z } from "zod";
3
+ import { COMMERCE_ORDER_DRAFT_CREATED_EVENT_TYPE } from "../outboxEventSchemas.js";
4
+ import type {
5
+ OutboxEventRow,
6
+ OutboxHandler,
7
+ OutboxHandlerOutcome,
8
+ } from "../outboxDispatchContracts.js";
9
+ import {
10
+ OUTBOX_ORDER_CONFIRMATION_TEMPLATE_SLUG,
11
+ type OrderEmailTotals,
12
+ type OrderPaymentLifecyclePort,
13
+ type OrderRecipientPort,
14
+ type TransactionalEmailPort,
15
+ } from "../outboxOrderDraftEmailPorts.js";
16
+ import { buildItems, type RawLine } from "../orderEmailLines.js";
17
+ import { shouldSkipOrderDraftConfirmation } from "../orderEmailEligibility.js";
18
+
19
+ export { OUTBOX_ORDER_CONFIRMATION_TEMPLATE_SLUG };
20
+
21
+ const HANDLER_TIMEOUT_MS = 10_000;
22
+
23
+ export const outboxOrderDraftEmailDescriptor = Object.freeze({
24
+ eventType: COMMERCE_ORDER_DRAFT_CREATED_EVENT_TYPE, timeoutMs: HANDLER_TIMEOUT_MS,
25
+ });
26
+
27
+ // Frozen minimal parse (challenge R-m5): validate ONLY the fields this handler
28
+ // consumes, with schemas owned HERE — never the live quote/order-draft zod
29
+ // schemas. A producer-side schema deploy must not retro-DLQ in-flight events.
30
+ // Everything except orderUuid/orderId is optional-tolerant: malformed lines or
31
+ // totals degrade the email content, they never fail the event.
32
+ const payloadSubsetSchema = z
33
+ .object({
34
+ orderUuid: z.guid(),
35
+ orderId: z.string().min(1),
36
+ orderDraftSnapshot: z.unknown().optional(),
37
+ })
38
+ .passthrough();
39
+
40
+ const moneySubsetSchema = z
41
+ .object({
42
+ amountMinor: z.number().int().nonnegative(),
43
+ currency: z.string().trim().min(1),
44
+ })
45
+ .passthrough();
46
+
47
+ const lineSubsetSchema = z
48
+ .object({
49
+ quantity: z.number().int().positive(),
50
+ productSlug: z.string().trim().min(1).optional(),
51
+ sku: z.string().trim().min(1).optional(),
52
+ lineSubtotalGross: moneySubsetSchema.optional(),
53
+ })
54
+ .passthrough();
55
+
56
+ const snapshotSubsetSchema = z
57
+ .object({
58
+ lines: z.array(z.unknown()).optional(),
59
+ totals: z
60
+ .object({
61
+ subtotalGross: z.unknown().optional(),
62
+ discountTotalGross: z.unknown().optional(),
63
+ totalGross: z.unknown().optional(),
64
+ })
65
+ .passthrough()
66
+ .optional(),
67
+ })
68
+ .passthrough();
69
+
70
+ function extractLines(snapshot: unknown): RawLine[] {
71
+ const parsed = snapshotSubsetSchema.safeParse(snapshot);
72
+ if (!parsed.success || parsed.data.lines === undefined) return [];
73
+ const lines: RawLine[] = [];
74
+ for (const candidate of parsed.data.lines) {
75
+ const line = lineSubsetSchema.safeParse(candidate);
76
+ if (!line.success) continue;
77
+ if (line.data.productSlug === undefined && line.data.sku === undefined) continue;
78
+ lines.push({
79
+ label: null,
80
+ quantity: line.data.quantity,
81
+ lineSubtotalMinor: line.data.lineSubtotalGross?.amountMinor ?? null,
82
+ currency: line.data.lineSubtotalGross?.currency ?? null,
83
+ });
84
+ }
85
+ return lines;
86
+ }
87
+
88
+ function buildTotals<Locale extends string>(snapshot: unknown, runtime: Pick<CommerceEmailRuntime<Locale>, "formatMoney" | "genericItemLabel" | "mapSendOutcome" | "parseOutboxPayload" | "resolveLocale">): OrderEmailTotals | null {
89
+ const parsed = snapshotSubsetSchema.safeParse(snapshot);
90
+ if (!parsed.success || parsed.data.totals === undefined) return null;
91
+ const total = moneySubsetSchema.safeParse(parsed.data.totals.totalGross);
92
+ if (!total.success) return null;
93
+ const subtotal = moneySubsetSchema.safeParse(parsed.data.totals.subtotalGross);
94
+ const discount = moneySubsetSchema.safeParse(parsed.data.totals.discountTotalGross);
95
+ const currency = total.data.currency;
96
+ return {
97
+ // Omit the subtotal row when it cannot be parsed — do NOT fall back to the
98
+ // grand total, which would render "Subtotal = Total" alongside a separate
99
+ // discount line that no longer reconciles.
100
+ subtotalLabel: subtotal.success
101
+ ? runtime.formatMoney(subtotal.data.amountMinor, subtotal.data.currency)
102
+ : null,
103
+ discountLabel:
104
+ discount.success && discount.data.amountMinor > 0
105
+ ? `-${runtime.formatMoney(discount.data.amountMinor, discount.data.currency)}`
106
+ : null,
107
+ totalLabel: runtime.formatMoney(total.data.amountMinor, currency),
108
+ };
109
+ }
110
+
111
+ export function createOutboxOrderDraftEmailHandler<Locale extends string>(deps: {
112
+ runtime: Pick<CommerceEmailRuntime<Locale>, "formatMoney" | "genericItemLabel" | "mapSendOutcome" | "parseOutboxPayload" | "resolveLocale">;
113
+ emailPort: TransactionalEmailPort<Locale>;
114
+ recipientPort: OrderRecipientPort;
115
+ lifecyclePort: OrderPaymentLifecyclePort;
116
+ }): OutboxHandler {
117
+ return {
118
+ ...outboxOrderDraftEmailDescriptor,
119
+ async handle(row: OutboxEventRow, signal: AbortSignal): Promise<OutboxHandlerOutcome> {
120
+ const parsed = deps.runtime.parseOutboxPayload(payloadSubsetSchema, row.payload);
121
+ if (parsed.ok === false) {
122
+ return parsed.outcome;
123
+ }
124
+
125
+ const lifecycle = await deps.lifecyclePort.read(parsed.data.orderUuid, signal);
126
+ const skipReason = shouldSkipOrderDraftConfirmation(lifecycle);
127
+ if (skipReason) {
128
+ return { kind: "processed", detail: { skipped: skipReason } };
129
+ }
130
+
131
+ const recipient = await deps.recipientPort.resolve(parsed.data.orderUuid, signal);
132
+ if (recipient === null) {
133
+ return { kind: "processed", detail: { skipped: "recipient_unresolved" } };
134
+ }
135
+
136
+ const alreadySent = await deps.emailPort.findExistingSend(
137
+ OUTBOX_ORDER_CONFIRMATION_TEMPLATE_SLUG,
138
+ row.id,
139
+ );
140
+ if (alreadySent) {
141
+ return { kind: "processed", detail: { dedupe: "email_already_sent" } };
142
+ }
143
+
144
+ const lines = extractLines(parsed.data.orderDraftSnapshot);
145
+ const locale = deps.runtime.resolveLocale(recipient.country ?? null);
146
+
147
+ const outcome = await deps.emailPort.sendOrderConfirmation({
148
+ to: recipient.email,
149
+ firstName: recipient.firstName,
150
+ contextName: recipient.contextName ?? null,
151
+ orderId: parsed.data.orderId,
152
+ outboxEventId: row.id,
153
+ items: buildItems(lines, locale, deps.runtime),
154
+ totals: buildTotals(parsed.data.orderDraftSnapshot, deps.runtime),
155
+ locale,
156
+ signal,
157
+ });
158
+
159
+ return deps.runtime.mapSendOutcome(outcome);
160
+ },
161
+ };
162
+ }