@openlup/platform 0.17.0 → 0.18.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 (486) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/README.md +284 -6
  3. package/dist/catalog/activeSkuSelection.d.ts +2 -0
  4. package/dist/catalog/activeSkuSelection.js +20 -0
  5. package/dist/catalog/adminBindings.d.ts +72 -0
  6. package/dist/catalog/adminBindings.js +1 -0
  7. package/dist/catalog/adminBundleHandler.d.ts +26 -0
  8. package/dist/catalog/adminBundleHandler.js +111 -0
  9. package/dist/catalog/adminBundleReadHandler.d.ts +20 -0
  10. package/dist/catalog/adminBundleReadHandler.js +185 -0
  11. package/dist/catalog/adminBundleWritePort.d.ts +85 -0
  12. package/dist/catalog/adminBundleWritePort.js +1 -0
  13. package/dist/catalog/bundleAvailability.d.ts +63 -0
  14. package/dist/catalog/bundleAvailability.js +78 -0
  15. package/dist/catalog/bundleAvailabilityService.d.ts +65 -0
  16. package/dist/catalog/bundleAvailabilityService.js +89 -0
  17. package/dist/catalog/bundleBindings.d.ts +33 -0
  18. package/dist/catalog/bundleBindings.js +1 -0
  19. package/dist/catalog/bundleCatalogQueries.d.ts +28 -0
  20. package/dist/catalog/bundleCatalogQueries.js +84 -0
  21. package/dist/catalog/bundleCatalogReadPort.d.ts +166 -0
  22. package/dist/catalog/bundleCatalogReadPort.js +36 -0
  23. package/dist/catalog/bundleCatalogStore.d.ts +3 -0
  24. package/dist/catalog/bundleCatalogStore.js +152 -0
  25. package/dist/catalog/bundleRuleCodes.d.ts +41 -0
  26. package/dist/catalog/bundleRuleCodes.js +110 -0
  27. package/dist/catalog/bundleRules.d.ts +69 -0
  28. package/dist/catalog/bundleRules.js +102 -0
  29. package/dist/catalog/catalogDraftStore.d.ts +4 -0
  30. package/dist/catalog/catalogDraftStore.js +63 -0
  31. package/dist/catalog/catalogSkuEnvelope.d.ts +7 -0
  32. package/dist/catalog/catalogSkuEnvelope.js +142 -0
  33. package/dist/catalog/catalogSkuEnvelopeMapper.d.ts +38 -0
  34. package/dist/catalog/catalogSkuEnvelopeMapper.js +88 -0
  35. package/dist/catalog/contracts/bundleAdmin.d.ts +143 -0
  36. package/dist/catalog/contracts/bundleAdmin.js +1 -0
  37. package/dist/catalog/contracts/bundleAvailability.d.ts +61 -0
  38. package/dist/catalog/contracts/bundleAvailability.js +1 -0
  39. package/dist/catalog/contracts/bundleRead.d.ts +223 -0
  40. package/dist/catalog/contracts/bundleRead.js +1 -0
  41. package/dist/catalog/contracts/draft.d.ts +79 -0
  42. package/dist/catalog/contracts/draft.js +47 -0
  43. package/dist/catalog/contracts/foundation.d.ts +78 -0
  44. package/dist/catalog/contracts/foundation.js +53 -0
  45. package/dist/catalog/contracts/index.d.ts +14 -0
  46. package/dist/catalog/contracts/index.js +6 -0
  47. package/dist/catalog/contracts/lifecycle.d.ts +77 -0
  48. package/dist/catalog/contracts/lifecycle.js +1 -0
  49. package/dist/catalog/contracts/productMediaContracts.d.ts +77 -0
  50. package/dist/catalog/contracts/productMediaContracts.js +149 -0
  51. package/dist/catalog/contracts/productTypes.d.ts +54 -0
  52. package/dist/catalog/contracts/productTypes.js +16 -0
  53. package/dist/catalog/contracts/resolveProductMedia.d.ts +3 -0
  54. package/dist/catalog/contracts/resolveProductMedia.js +78 -0
  55. package/dist/catalog/contracts/sellable.d.ts +23 -0
  56. package/dist/catalog/contracts/sellable.js +1 -0
  57. package/dist/catalog/draftHandler.d.ts +25 -0
  58. package/dist/catalog/draftHandler.js +118 -0
  59. package/dist/catalog/draftPorts.d.ts +18 -0
  60. package/dist/catalog/draftPorts.js +8 -0
  61. package/dist/catalog/draftSchemas.d.ts +423 -0
  62. package/dist/catalog/draftSchemas.js +89 -0
  63. package/dist/catalog/draftValidation.d.ts +21 -0
  64. package/dist/catalog/draftValidation.js +125 -0
  65. package/dist/catalog/foundationSchemas.d.ts +98 -0
  66. package/dist/catalog/foundationSchemas.js +110 -0
  67. package/dist/catalog/index.d.ts +43 -0
  68. package/dist/catalog/index.js +25 -0
  69. package/dist/catalog/mediaSourcePack.d.ts +51 -0
  70. package/dist/catalog/mediaSourcePack.js +247 -0
  71. package/dist/catalog/productTypeSchemas.d.ts +46 -0
  72. package/dist/catalog/productTypeSchemas.js +72 -0
  73. package/dist/catalog/publicationPorts.d.ts +42 -0
  74. package/dist/catalog/publicationPorts.js +1 -0
  75. package/dist/catalog/readPorts.d.ts +51 -0
  76. package/dist/catalog/readPorts.js +1 -0
  77. package/dist/catalog/referenceCatalogHandler.d.ts +7 -0
  78. package/dist/catalog/referenceCatalogHandler.js +27 -0
  79. package/dist/catalog/resolvedSkuProjection.d.ts +88 -0
  80. package/dist/catalog/resolvedSkuProjection.js +60 -0
  81. package/dist/catalog/sellableBundleHandler.d.ts +46 -0
  82. package/dist/catalog/sellableBundleHandler.js +84 -0
  83. package/dist/catalog/sellableSchemas.d.ts +46 -0
  84. package/dist/catalog/sellableSchemas.js +27 -0
  85. package/dist/catalog/sql.d.ts +46 -0
  86. package/dist/catalog/sql.js +13 -0
  87. package/dist/catalog/validation.d.ts +2 -0
  88. package/dist/catalog/validation.js +3 -0
  89. package/dist/communications/contracts/index.d.ts +1 -0
  90. package/dist/communications/contracts/marketingSync.d.ts +194 -0
  91. package/dist/communications/contracts/marketingSync.js +1 -0
  92. package/dist/communications/index.d.ts +1 -0
  93. package/dist/communications/klaviyo/consentPush.d.ts +51 -0
  94. package/dist/communications/klaviyo/consentPush.js +19 -0
  95. package/dist/communications/klaviyo/index.d.ts +110 -0
  96. package/dist/communications/klaviyo/index.js +860 -0
  97. package/dist/communications/newsletterIntegrationContracts.d.ts +3 -3
  98. package/dist/communications/resend/index.d.ts +40 -0
  99. package/dist/communications/resend/index.js +310 -0
  100. package/dist/composition/catalogContribution.d.ts +18 -0
  101. package/dist/composition/catalogContribution.js +21 -0
  102. package/dist/composition/index.d.ts +1 -0
  103. package/dist/composition/index.js +1 -0
  104. package/dist/identity/customerContracts.d.ts +2 -2
  105. package/dist/inventory/adminInventory.d.ts +13 -0
  106. package/dist/inventory/adminInventory.js +214 -0
  107. package/dist/inventory/contracts/forecast.d.ts +86 -0
  108. package/dist/inventory/contracts/forecast.js +1 -0
  109. package/dist/inventory/contracts/index.d.ts +4 -0
  110. package/dist/inventory/contracts/index.js +1 -0
  111. package/dist/inventory/contracts/inventory.d.ts +157 -0
  112. package/dist/inventory/contracts/inventory.js +1 -0
  113. package/dist/inventory/contracts/newPurchaseStock.d.ts +20 -0
  114. package/dist/inventory/contracts/newPurchaseStock.js +1 -0
  115. package/dist/inventory/contracts/reservation.d.ts +18 -0
  116. package/dist/inventory/contracts/reservation.js +1 -0
  117. package/dist/inventory/forecast.d.ts +24 -0
  118. package/dist/inventory/forecast.js +200 -0
  119. package/dist/inventory/index.d.ts +13 -0
  120. package/dist/inventory/index.js +10 -0
  121. package/dist/inventory/inventoryHandlers.d.ts +53 -0
  122. package/dist/inventory/inventoryHandlers.js +151 -0
  123. package/dist/inventory/newPurchaseStockAvailability.d.ts +47 -0
  124. package/dist/inventory/newPurchaseStockAvailability.js +75 -0
  125. package/dist/inventory/ports.d.ts +21 -0
  126. package/dist/inventory/ports.js +16 -0
  127. package/dist/inventory/renewalReservation.d.ts +27 -0
  128. package/dist/inventory/renewalReservation.js +61 -0
  129. package/dist/inventory/reservationPolicy.d.ts +6 -0
  130. package/dist/inventory/reservationPolicy.js +19 -0
  131. package/dist/inventory/schemas.d.ts +137 -0
  132. package/dist/inventory/schemas.js +112 -0
  133. package/dist/inventory/sql.d.ts +14 -0
  134. package/dist/inventory/sql.js +5 -0
  135. package/dist/inventory/subscriptionForecastCalculation.d.ts +4 -0
  136. package/dist/inventory/subscriptionForecastCalculation.js +133 -0
  137. package/dist/pricing/acquisitionFinancialPolicy.d.ts +26 -0
  138. package/dist/pricing/acquisitionFinancialPolicy.js +1 -0
  139. package/dist/pricing/acquisitionOfferPricing.d.ts +27 -0
  140. package/dist/pricing/acquisitionOfferPricing.js +25 -0
  141. package/dist/pricing/acquisitionOfferPricingHandler.d.ts +20 -0
  142. package/dist/pricing/acquisitionOfferPricingHandler.js +105 -0
  143. package/dist/pricing/acquisitionOfferPricingSchemas.d.ts +31 -0
  144. package/dist/pricing/acquisitionOfferPricingSchemas.js +33 -0
  145. package/dist/pricing/acquisitionQuoteBinding.d.ts +12 -0
  146. package/dist/pricing/acquisitionQuoteBinding.js +12 -0
  147. package/dist/pricing/acquisitionReadiness.d.ts +31 -0
  148. package/dist/pricing/acquisitionReadiness.js +117 -0
  149. package/dist/pricing/acquisitionTerms.d.ts +19 -0
  150. package/dist/pricing/acquisitionTerms.js +126 -0
  151. package/dist/pricing/acquisitionTermsSchemas.d.ts +59 -0
  152. package/dist/pricing/acquisitionTermsSchemas.js +42 -0
  153. package/dist/pricing/catalogFactsProvenance.d.ts +33 -0
  154. package/dist/pricing/catalogFactsProvenance.js +64 -0
  155. package/dist/pricing/catalogFactsSchemas.d.ts +45 -0
  156. package/dist/pricing/catalogFactsSchemas.js +58 -0
  157. package/dist/pricing/commerceOfferPricing.d.ts +25 -0
  158. package/dist/pricing/commerceOfferPricing.js +139 -0
  159. package/dist/pricing/commerceOfferPricingCache.d.ts +10 -0
  160. package/dist/pricing/commerceOfferPricingCache.js +44 -0
  161. package/dist/pricing/commerceOfferPricingHandler.d.ts +21 -0
  162. package/dist/pricing/commerceOfferPricingHandler.js +78 -0
  163. package/dist/pricing/commercePriceAuthority.d.ts +26 -0
  164. package/dist/pricing/commercePriceAuthority.js +79 -0
  165. package/dist/pricing/commercePricingPolicyErrors.d.ts +40 -0
  166. package/dist/pricing/commercePricingPolicyErrors.js +67 -0
  167. package/dist/pricing/commerceQuoteBatchHandler.d.ts +37 -0
  168. package/dist/pricing/commerceQuoteBatchHandler.js +83 -0
  169. package/dist/pricing/commerceQuoteCatalogReadPort.d.ts +27 -0
  170. package/dist/pricing/commerceQuoteCatalogReadPort.js +8 -0
  171. package/dist/pricing/commerceQuoteHandler.d.ts +31 -0
  172. package/dist/pricing/commerceQuoteHandler.js +110 -0
  173. package/dist/pricing/commerceQuoteSnapshotExpect.d.ts +9 -0
  174. package/dist/pricing/commerceQuoteSnapshotExpect.js +22 -0
  175. package/dist/pricing/contracts/acquisitionTerms.d.ts +32 -0
  176. package/dist/pricing/contracts/acquisitionTerms.js +2 -0
  177. package/dist/pricing/contracts/authority.d.ts +27 -0
  178. package/dist/pricing/contracts/authority.js +1 -0
  179. package/dist/pricing/contracts/evidence.d.ts +33 -0
  180. package/dist/pricing/contracts/evidence.js +1 -0
  181. package/dist/pricing/contracts/index.d.ts +17 -0
  182. package/dist/pricing/contracts/index.js +7 -0
  183. package/dist/pricing/contracts/money.d.ts +27 -0
  184. package/dist/pricing/contracts/money.js +1 -0
  185. package/dist/pricing/contracts/offer.d.ts +52 -0
  186. package/dist/pricing/contracts/offer.js +4 -0
  187. package/dist/pricing/contracts/offerPolicy.d.ts +16 -0
  188. package/dist/pricing/contracts/offerPolicy.js +5 -0
  189. package/dist/pricing/contracts/policy.d.ts +22 -0
  190. package/dist/pricing/contracts/policy.js +3 -0
  191. package/dist/pricing/contracts/provenance.d.ts +24 -0
  192. package/dist/pricing/contracts/provenance.js +2 -0
  193. package/dist/pricing/contracts/quote.d.ts +94 -0
  194. package/dist/pricing/contracts/quote.js +1 -0
  195. package/dist/pricing/dbBackedCommerceQuoteHelpers.d.ts +78 -0
  196. package/dist/pricing/dbBackedCommerceQuoteHelpers.js +146 -0
  197. package/dist/pricing/dbBackedCommerceQuotePort.d.ts +66 -0
  198. package/dist/pricing/dbBackedCommerceQuotePort.js +206 -0
  199. package/dist/pricing/dbBackedCommerceQuoteSnapshotVerifier.d.ts +4 -0
  200. package/dist/pricing/dbBackedCommerceQuoteSnapshotVerifier.js +25 -0
  201. package/dist/pricing/index.d.ts +51 -0
  202. package/dist/pricing/index.js +36 -0
  203. package/dist/pricing/offerPolicyReadiness.d.ts +21 -0
  204. package/dist/pricing/offerPolicyReadiness.js +79 -0
  205. package/dist/pricing/offerPolicySchemas.d.ts +25 -0
  206. package/dist/pricing/offerPolicySchemas.js +15 -0
  207. package/dist/pricing/offerPricingSchemas.d.ts +315 -0
  208. package/dist/pricing/offerPricingSchemas.js +65 -0
  209. package/dist/pricing/ports.d.ts +13 -0
  210. package/dist/pricing/ports.js +9 -0
  211. package/dist/pricing/pricingPolicyToken.d.ts +39 -0
  212. package/dist/pricing/pricingPolicyToken.js +84 -0
  213. package/dist/pricing/quoteAcquisitionTerms.d.ts +2 -0
  214. package/dist/pricing/quoteAcquisitionTerms.js +4 -0
  215. package/dist/pricing/quoteCodeRejectionDetails.d.ts +20 -0
  216. package/dist/pricing/quoteCodeRejectionDetails.js +22 -0
  217. package/dist/pricing/quotePorts.d.ts +30 -0
  218. package/dist/pricing/quotePorts.js +20 -0
  219. package/dist/pricing/quotePrimitives.d.ts +13 -0
  220. package/dist/pricing/quotePrimitives.js +18 -0
  221. package/dist/pricing/quotePromotionEvidence.d.ts +39 -0
  222. package/dist/pricing/quotePromotionEvidence.js +73 -0
  223. package/dist/pricing/quoteSchemas.d.ts +1176 -0
  224. package/dist/pricing/quoteSchemas.js +196 -0
  225. package/dist/pricing/subscriptionPricePolicy.d.ts +52 -0
  226. package/dist/pricing/subscriptionPricePolicy.js +96 -0
  227. package/dist/pricing/taxProfileGuard.d.ts +2 -0
  228. package/dist/pricing/taxProfileGuard.js +24 -0
  229. package/dist/pricing/validation.d.ts +2 -0
  230. package/dist/pricing/validation.js +3 -0
  231. package/dist/promotions/adminBindings.d.ts +72 -0
  232. package/dist/promotions/adminBindings.js +1 -0
  233. package/dist/promotions/adminPromotionCodesContracts.d.ts +498 -0
  234. package/dist/promotions/adminPromotionCodesContracts.js +211 -0
  235. package/dist/promotions/adminPromotionCodesDataPort.d.ts +47 -0
  236. package/dist/promotions/adminPromotionCodesDataPort.js +1 -0
  237. package/dist/promotions/adminPromotionCodesHandler.d.ts +14 -0
  238. package/dist/promotions/adminPromotionCodesHandler.js +194 -0
  239. package/dist/promotions/adminPromotionsContracts.d.ts +235 -0
  240. package/dist/promotions/adminPromotionsContracts.js +99 -0
  241. package/dist/promotions/adminPromotionsDataPort.d.ts +42 -0
  242. package/dist/promotions/adminPromotionsDataPort.js +1 -0
  243. package/dist/promotions/adminPromotionsHandler.d.ts +16 -0
  244. package/dist/promotions/adminPromotionsHandler.js +47 -0
  245. package/dist/promotions/contracts/index.d.ts +387 -0
  246. package/dist/promotions/contracts/index.js +1 -0
  247. package/dist/promotions/index.d.ts +22 -0
  248. package/dist/promotions/index.js +21 -0
  249. package/dist/promotions/promoDataPort.d.ts +47 -0
  250. package/dist/promotions/promoDataPort.js +14 -0
  251. package/dist/promotions/promotionAcceptanceTelemetry.d.ts +29 -0
  252. package/dist/promotions/promotionAcceptanceTelemetry.js +45 -0
  253. package/dist/promotions/promotionBenefitDefinition.d.ts +37 -0
  254. package/dist/promotions/promotionBenefitDefinition.js +33 -0
  255. package/dist/promotions/promotionClaimSweepPort.d.ts +14 -0
  256. package/dist/promotions/promotionClaimSweepPort.js +8 -0
  257. package/dist/promotions/promotionClaims.d.ts +5 -0
  258. package/dist/promotions/promotionClaims.js +57 -0
  259. package/dist/promotions/promotionCodePreview.d.ts +11 -0
  260. package/dist/promotions/promotionCodePreview.js +154 -0
  261. package/dist/promotions/promotionCodeQuotePort.d.ts +25 -0
  262. package/dist/promotions/promotionCodeQuotePort.js +1 -0
  263. package/dist/promotions/promotionQuoteAcceptance.d.ts +65 -0
  264. package/dist/promotions/promotionQuoteAcceptance.js +202 -0
  265. package/dist/promotions/promotionQuoteBinding.d.ts +71 -0
  266. package/dist/promotions/promotionQuoteBinding.js +85 -0
  267. package/dist/promotions/quoteAutomaticPromotions.d.ts +24 -0
  268. package/dist/promotions/quoteAutomaticPromotions.js +36 -0
  269. package/dist/promotions/quotePromoDiscounts.d.ts +32 -0
  270. package/dist/promotions/quotePromoDiscounts.js +140 -0
  271. package/dist/promotions/quotePromotionCodeAdjustments.d.ts +27 -0
  272. package/dist/promotions/quotePromotionCodeAdjustments.js +223 -0
  273. package/dist/promotions/quotePromotionV2.d.ts +29 -0
  274. package/dist/promotions/quotePromotionV2.js +174 -0
  275. package/dist/promotions/sql.d.ts +14 -0
  276. package/dist/promotions/sql.js +5 -0
  277. package/dist/promotions/validation.d.ts +2 -0
  278. package/dist/promotions/validation.js +3 -0
  279. package/dist/shipping/addressCanonLookupHandler.d.ts +7 -0
  280. package/dist/shipping/addressCanonLookupHandler.js +105 -0
  281. package/dist/shipping/addressCanonical.d.ts +2 -0
  282. package/dist/shipping/addressCanonical.js +26 -0
  283. package/dist/shipping/addressPorts.d.ts +39 -0
  284. package/dist/shipping/addressPorts.js +1 -0
  285. package/dist/shipping/addressSchemas.d.ts +30 -0
  286. package/dist/shipping/addressSchemas.js +1 -0
  287. package/dist/shipping/adminShippingRateHandler.d.ts +4 -0
  288. package/dist/shipping/adminShippingRateHandler.js +64 -0
  289. package/dist/shipping/contracts/address.d.ts +144 -0
  290. package/dist/shipping/contracts/address.js +1 -0
  291. package/dist/shipping/contracts/delivery.d.ts +110 -0
  292. package/dist/shipping/contracts/delivery.js +1 -0
  293. package/dist/shipping/contracts/index.d.ts +3 -0
  294. package/dist/shipping/contracts/index.js +1 -0
  295. package/dist/shipping/contracts/shippingRate.d.ts +11 -0
  296. package/dist/shipping/contracts/shippingRate.js +1 -0
  297. package/dist/shipping/customerAddressesHandler.d.ts +5 -0
  298. package/dist/shipping/customerAddressesHandler.js +80 -0
  299. package/dist/shipping/customerSelfServiceAddressModels.d.ts +27 -0
  300. package/dist/shipping/customerSelfServiceAddressModels.js +101 -0
  301. package/dist/shipping/deliverySelectionHandler.d.ts +10 -0
  302. package/dist/shipping/deliverySelectionHandler.js +77 -0
  303. package/dist/shipping/deliverySelectionPort.d.ts +8 -0
  304. package/dist/shipping/deliverySelectionPort.js +1 -0
  305. package/dist/shipping/deliverySelectionSummary.d.ts +2 -0
  306. package/dist/shipping/deliverySelectionSummary.js +53 -0
  307. package/dist/shipping/handlerSchemas.d.ts +21 -0
  308. package/dist/shipping/handlerSchemas.js +1 -0
  309. package/dist/shipping/index.d.ts +17 -0
  310. package/dist/shipping/index.js +8 -0
  311. package/dist/shipping/pickupPointSearchPort.d.ts +4 -0
  312. package/dist/shipping/pickupPointSearchPort.js +1 -0
  313. package/dist/shipping/readDeliverySelectionEvidence.d.ts +2 -0
  314. package/dist/shipping/readDeliverySelectionEvidence.js +44 -0
  315. package/dist/shipping/shippingRatePorts.d.ts +52 -0
  316. package/dist/shipping/shippingRatePorts.js +1 -0
  317. package/package.json +62 -2
  318. package/release-gates.json +218 -10
  319. package/sql/20260807140000_read_parity_catalog.sql +94 -0
  320. package/sql/20260810120000_storefront_catalog_attributes.sql +51 -0
  321. package/sql/20260812140000_sellable_bundle_catalog.sql +124 -0
  322. package/sql/20260812210000_bundle_operator_write_boundary.sql +567 -0
  323. package/sql/20260814000000_fulfillment_stock_evidence.sql +239 -0
  324. package/sql/20260815120000_automatic_subscription_renewal.sql +1083 -0
  325. package/sql/20260817180000_promotion_claim_lifecycle.sql +591 -0
  326. package/sql/20260817180100_promotion_claim_lifecycle_validation.sql +23 -0
  327. package/sql/20260827220000_catalog_document_revision_foundation.sql +226 -0
  328. package/sql/20260827220001_catalog_document_revision_foundation_validate.sql +10 -0
  329. package/sql/20260908100000_catalog_draft_foundation.sql +217 -0
  330. package/src/catalog/activeSkuSelection.ts +20 -0
  331. package/src/catalog/adminBindings.ts +59 -0
  332. package/src/catalog/adminBundleHandler.ts +165 -0
  333. package/src/catalog/adminBundleReadHandler.ts +261 -0
  334. package/src/catalog/adminBundleWritePort.ts +102 -0
  335. package/src/catalog/bundleAvailability.ts +129 -0
  336. package/src/catalog/bundleAvailabilityService.ts +171 -0
  337. package/src/catalog/bundleBindings.ts +29 -0
  338. package/src/catalog/bundleCatalogQueries.ts +98 -0
  339. package/src/catalog/bundleCatalogReadPort.ts +177 -0
  340. package/src/catalog/bundleCatalogStore.ts +269 -0
  341. package/src/catalog/bundleRuleCodes.ts +134 -0
  342. package/src/catalog/bundleRules.ts +153 -0
  343. package/src/catalog/catalogDraftStore.ts +71 -0
  344. package/src/catalog/catalogSkuEnvelope.ts +214 -0
  345. package/src/catalog/catalogSkuEnvelopeMapper.ts +160 -0
  346. package/src/catalog/contracts/bundleAdmin.ts +20 -0
  347. package/src/catalog/contracts/bundleAvailability.ts +5 -0
  348. package/src/catalog/contracts/bundleRead.ts +13 -0
  349. package/src/catalog/contracts/draft.ts +69 -0
  350. package/src/catalog/contracts/foundation.ts +104 -0
  351. package/src/catalog/contracts/index.ts +14 -0
  352. package/src/catalog/contracts/lifecycle.ts +74 -0
  353. package/src/catalog/contracts/productMediaContracts.ts +170 -0
  354. package/src/catalog/contracts/productTypes.ts +35 -0
  355. package/src/catalog/contracts/resolveProductMedia.ts +85 -0
  356. package/src/catalog/contracts/sellable.ts +12 -0
  357. package/src/catalog/draftHandler.ts +105 -0
  358. package/src/catalog/draftPorts.ts +18 -0
  359. package/src/catalog/draftSchemas.ts +88 -0
  360. package/src/catalog/draftValidation.ts +100 -0
  361. package/src/catalog/foundationSchemas.ts +123 -0
  362. package/src/catalog/index.ts +43 -0
  363. package/src/catalog/mediaSourcePack.ts +219 -0
  364. package/src/catalog/productTypeSchemas.ts +70 -0
  365. package/src/catalog/publicationPorts.ts +44 -0
  366. package/src/catalog/readPorts.ts +61 -0
  367. package/src/catalog/referenceCatalogHandler.ts +39 -0
  368. package/src/catalog/resolvedSkuProjection.ts +76 -0
  369. package/src/catalog/sellableBundleHandler.ts +133 -0
  370. package/src/catalog/sellableSchemas.ts +33 -0
  371. package/src/catalog/sql.ts +14 -0
  372. package/src/catalog/validation.ts +3 -0
  373. package/src/communications/contracts/index.ts +1 -0
  374. package/src/communications/contracts/marketingSync.ts +109 -0
  375. package/src/communications/index.ts +1 -0
  376. package/src/communications/klaviyo/consentPush.ts +19 -0
  377. package/src/communications/klaviyo/index.ts +649 -0
  378. package/src/communications/resend/index.ts +350 -0
  379. package/src/composition/catalogContribution.ts +32 -0
  380. package/src/composition/index.ts +1 -0
  381. package/src/inventory/adminInventory.ts +251 -0
  382. package/src/inventory/contracts/forecast.ts +96 -0
  383. package/src/inventory/contracts/index.ts +18 -0
  384. package/src/inventory/contracts/inventory.ts +14 -0
  385. package/src/inventory/contracts/newPurchaseStock.ts +23 -0
  386. package/src/inventory/contracts/reservation.ts +20 -0
  387. package/src/inventory/forecast.ts +239 -0
  388. package/src/inventory/index.ts +21 -0
  389. package/src/inventory/inventoryHandlers.ts +215 -0
  390. package/src/inventory/newPurchaseStockAvailability.ts +120 -0
  391. package/src/inventory/ports.ts +46 -0
  392. package/src/inventory/renewalReservation.ts +88 -0
  393. package/src/inventory/reservationPolicy.ts +27 -0
  394. package/src/inventory/schemas.ts +128 -0
  395. package/src/inventory/sql.ts +5 -0
  396. package/src/inventory/subscriptionForecastCalculation.ts +157 -0
  397. package/src/pricing/acquisitionFinancialPolicy.ts +26 -0
  398. package/src/pricing/acquisitionOfferPricing.ts +38 -0
  399. package/src/pricing/acquisitionOfferPricingHandler.ts +157 -0
  400. package/src/pricing/acquisitionOfferPricingSchemas.ts +35 -0
  401. package/src/pricing/acquisitionQuoteBinding.ts +12 -0
  402. package/src/pricing/acquisitionReadiness.ts +148 -0
  403. package/src/pricing/acquisitionTerms.ts +125 -0
  404. package/src/pricing/acquisitionTermsSchemas.ts +52 -0
  405. package/src/pricing/catalogFactsProvenance.ts +102 -0
  406. package/src/pricing/catalogFactsSchemas.ts +69 -0
  407. package/src/pricing/commerceOfferPricing.ts +224 -0
  408. package/src/pricing/commerceOfferPricingCache.ts +53 -0
  409. package/src/pricing/commerceOfferPricingHandler.ts +137 -0
  410. package/src/pricing/commercePriceAuthority.ts +104 -0
  411. package/src/pricing/commercePricingPolicyErrors.ts +88 -0
  412. package/src/pricing/commerceQuoteBatchHandler.ts +129 -0
  413. package/src/pricing/commerceQuoteCatalogReadPort.ts +42 -0
  414. package/src/pricing/commerceQuoteHandler.ts +184 -0
  415. package/src/pricing/commerceQuoteSnapshotExpect.ts +48 -0
  416. package/src/pricing/contracts/acquisitionTerms.ts +26 -0
  417. package/src/pricing/contracts/authority.ts +44 -0
  418. package/src/pricing/contracts/evidence.ts +25 -0
  419. package/src/pricing/contracts/index.ts +17 -0
  420. package/src/pricing/contracts/money.ts +32 -0
  421. package/src/pricing/contracts/offer.ts +42 -0
  422. package/src/pricing/contracts/offerPolicy.ts +17 -0
  423. package/src/pricing/contracts/policy.ts +26 -0
  424. package/src/pricing/contracts/provenance.ts +19 -0
  425. package/src/pricing/contracts/quote.ts +106 -0
  426. package/src/pricing/dbBackedCommerceQuoteHelpers.ts +256 -0
  427. package/src/pricing/dbBackedCommerceQuotePort.ts +260 -0
  428. package/src/pricing/dbBackedCommerceQuoteSnapshotVerifier.ts +49 -0
  429. package/src/pricing/index.ts +52 -0
  430. package/src/pricing/offerPolicyReadiness.ts +110 -0
  431. package/src/pricing/offerPolicySchemas.ts +23 -0
  432. package/src/pricing/offerPricingSchemas.ts +92 -0
  433. package/src/pricing/ports.ts +17 -0
  434. package/src/pricing/pricingPolicyToken.ts +119 -0
  435. package/src/pricing/quoteAcquisitionTerms.ts +10 -0
  436. package/src/pricing/quoteCodeRejectionDetails.ts +23 -0
  437. package/src/pricing/quotePorts.ts +58 -0
  438. package/src/pricing/quotePrimitives.ts +21 -0
  439. package/src/pricing/quotePromotionEvidence.ts +82 -0
  440. package/src/pricing/quoteSchemas.ts +230 -0
  441. package/src/pricing/subscriptionPricePolicy.ts +113 -0
  442. package/src/pricing/taxProfileGuard.ts +33 -0
  443. package/src/pricing/validation.ts +3 -0
  444. package/src/promotions/adminBindings.ts +53 -0
  445. package/src/promotions/adminPromotionCodesContracts.ts +244 -0
  446. package/src/promotions/adminPromotionCodesDataPort.ts +56 -0
  447. package/src/promotions/adminPromotionCodesHandler.ts +254 -0
  448. package/src/promotions/adminPromotionsContracts.ts +121 -0
  449. package/src/promotions/adminPromotionsDataPort.ts +48 -0
  450. package/src/promotions/adminPromotionsHandler.ts +72 -0
  451. package/src/promotions/contracts/index.ts +387 -0
  452. package/src/promotions/index.ts +22 -0
  453. package/src/promotions/promoDataPort.ts +65 -0
  454. package/src/promotions/promotionAcceptanceTelemetry.ts +73 -0
  455. package/src/promotions/promotionBenefitDefinition.ts +48 -0
  456. package/src/promotions/promotionClaimSweepPort.ts +23 -0
  457. package/src/promotions/promotionClaims.ts +70 -0
  458. package/src/promotions/promotionCodePreview.ts +183 -0
  459. package/src/promotions/promotionCodeQuotePort.ts +35 -0
  460. package/src/promotions/promotionQuoteAcceptance.ts +284 -0
  461. package/src/promotions/promotionQuoteBinding.ts +158 -0
  462. package/src/promotions/quoteAutomaticPromotions.ts +58 -0
  463. package/src/promotions/quotePromoDiscounts.ts +185 -0
  464. package/src/promotions/quotePromotionCodeAdjustments.ts +277 -0
  465. package/src/promotions/quotePromotionV2.ts +223 -0
  466. package/src/promotions/sql.ts +5 -0
  467. package/src/promotions/validation.ts +3 -0
  468. package/src/shipping/addressCanonLookupHandler.ts +114 -0
  469. package/src/shipping/addressCanonical.ts +31 -0
  470. package/src/shipping/addressPorts.ts +48 -0
  471. package/src/shipping/addressSchemas.ts +28 -0
  472. package/src/shipping/adminShippingRateHandler.ts +72 -0
  473. package/src/shipping/contracts/address.ts +71 -0
  474. package/src/shipping/contracts/delivery.ts +114 -0
  475. package/src/shipping/contracts/index.ts +18 -0
  476. package/src/shipping/contracts/shippingRate.ts +7 -0
  477. package/src/shipping/customerAddressesHandler.ts +100 -0
  478. package/src/shipping/customerSelfServiceAddressModels.ts +124 -0
  479. package/src/shipping/deliverySelectionHandler.ts +94 -0
  480. package/src/shipping/deliverySelectionPort.ts +10 -0
  481. package/src/shipping/deliverySelectionSummary.ts +56 -0
  482. package/src/shipping/handlerSchemas.ts +19 -0
  483. package/src/shipping/index.ts +23 -0
  484. package/src/shipping/pickupPointSearchPort.ts +8 -0
  485. package/src/shipping/readDeliverySelectionEvidence.ts +51 -0
  486. package/src/shipping/shippingRatePorts.ts +36 -0
@@ -0,0 +1,57 @@
1
+ import { promotionRedemptionMap } from "./promoDataPort.js";
2
+ import { parsePromotionClaimSweepCounts, } from "./promotionClaimSweepPort.js";
3
+ const ACTIVE_SQL = "SELECT public.commerce_promotion_active_definitions() AS result";
4
+ const COUNTS_SQL = "SELECT public.commerce_promotion_paid_order_counts($1::uuid) AS result";
5
+ const REDEMPTIONS_SQL = "SELECT * FROM public.commerce_promotion_redemption_counts($1::uuid)";
6
+ const DEVICE_SQL = "SELECT public.commerce_promotion_device_first_order_redeemed($1) AS result";
7
+ const SWEEP_SQL = "SELECT public.commerce_sweep_stale_promotion_claims($1,$2,$3,$4) AS result";
8
+ export function createPostgresCommercePromoDataPort(executor) {
9
+ return {
10
+ async listActivePromotions() {
11
+ const value = scalar(await executor.query(ACTIVE_SQL));
12
+ if (!Array.isArray(value))
13
+ throw new Error("promotions_list_invalid_response");
14
+ return value;
15
+ },
16
+ async countPaidOrders(clientId) {
17
+ const counts = paidCounts(scalar(await executor.query(COUNTS_SQL, [clientId])));
18
+ return counts.oneTime + counts.subscription;
19
+ },
20
+ async countPaidOrdersByMode(clientId) {
21
+ return paidCounts(scalar(await executor.query(COUNTS_SQL, [clientId])));
22
+ },
23
+ async redemptionCounts(clientId) {
24
+ return promotionRedemptionMap((await executor.query(REDEMPTIONS_SQL, [clientId])).rows);
25
+ },
26
+ async deviceFirstOrderRedeemed(visitorId) {
27
+ if (!visitorId?.trim())
28
+ return false;
29
+ return scalar(await executor.query(DEVICE_SQL, [visitorId])) === true;
30
+ },
31
+ };
32
+ }
33
+ export function createPostgresPromotionClaimSweepPort(executor) {
34
+ return {
35
+ async sweep(input) {
36
+ return parsePromotionClaimSweepCounts(scalar(await executor.query(SWEEP_SQL, [
37
+ input.now, input.limit, input.claimLeaseMinutes, input.graceMinutes,
38
+ ])));
39
+ },
40
+ };
41
+ }
42
+ function scalar(result) {
43
+ if (result.rows.length !== 1 || !("result" in result.rows[0])) {
44
+ throw new Error("promotion_claim_response_invalid");
45
+ }
46
+ return result.rows[0].result;
47
+ }
48
+ function paidCounts(value) {
49
+ const row = value && typeof value === "object" ? value : {};
50
+ const oneTime = Number(row.oneTime ?? -1);
51
+ const subscription = Number(row.subscription ?? -1);
52
+ if (!Number.isSafeInteger(oneTime) || oneTime < 0
53
+ || !Number.isSafeInteger(subscription) || subscription < 0) {
54
+ throw new Error("promotion_paid_order_counts_invalid_response");
55
+ }
56
+ return { oneTime, subscription };
57
+ }
@@ -0,0 +1,11 @@
1
+ import type { PromotionCodePreviewRequest, PromotionCodePreviewResponse } from "./contracts/index.js";
2
+ export declare const PROMOTION_ENGINE_VERSION: "promotion-engine.v2";
3
+ export declare function createPromotionCodePreview({ minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR }: {
4
+ minimumProductPayableMinor: number;
5
+ }): {
6
+ generatePromotionCode: () => string;
7
+ normalizePromotionCode: (code: string) => string;
8
+ promotionPreviewProof: (input: PromotionCodePreviewRequest, secret: string) => string;
9
+ verifyPromotionPreviewProof: (input: PromotionCodePreviewRequest, proof: string, secret: string) => boolean;
10
+ previewPromotionCode: (input: PromotionCodePreviewRequest, secret: string) => PromotionCodePreviewResponse;
11
+ };
@@ -0,0 +1,154 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
2
+ import { COMMERCE_CONTRACT_VERSION } from "../pricing/index.js";
3
+ import { evaluatePromotionAdjustmentsV2, } from "@openlup/core/promo";
4
+ const CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
5
+ export const PROMOTION_ENGINE_VERSION = "promotion-engine.v2";
6
+ export function createPromotionCodePreview({ minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR }) {
7
+ function generatePromotionCode() {
8
+ const bytes = randomBytes(10);
9
+ let bits = 0;
10
+ let buffer = 0;
11
+ let code = "";
12
+ for (const byte of bytes) {
13
+ buffer = (buffer << 8) | byte;
14
+ bits += 8;
15
+ while (bits >= 5) {
16
+ bits -= 5;
17
+ code += CROCKFORD_BASE32[(buffer >>> bits) & 31];
18
+ buffer &= (1 << bits) - 1;
19
+ }
20
+ }
21
+ return code;
22
+ }
23
+ function normalizePromotionCode(code) {
24
+ return code.trim().toUpperCase();
25
+ }
26
+ function promotionPreviewProof(input, secret) {
27
+ assertPreviewSecret(secret);
28
+ assertSupportedPreviewBenefits(input);
29
+ return createHmac("sha256", secret).update(previewProofPayload(input)).digest("base64url");
30
+ }
31
+ function verifyPromotionPreviewProof(input, proof, secret) {
32
+ const expected = promotionPreviewProof(input, secret);
33
+ const expectedBytes = Buffer.from(expected);
34
+ const actualBytes = Buffer.from(proof);
35
+ return expectedBytes.length === actualBytes.length && timingSafeEqual(expectedBytes, actualBytes);
36
+ }
37
+ function previewPromotionCode(input, secret) {
38
+ assertSupportedPreviewBenefits(input);
39
+ const eligible = input.context.referenceProductMinor >= input.minimumReferenceMinor;
40
+ const results = ["one_time", "subscription_initial"].map((scope) => {
41
+ const currentProductMinor = scope === "one_time"
42
+ ? input.context.oneTimeProductMinor
43
+ : input.context.subscriptionProductMinor;
44
+ const candidates = eligible
45
+ ? input.benefits
46
+ .map((benefit) => toCandidate(benefit, input.scopes, currentProductMinor))
47
+ .filter((candidate) => candidate !== null)
48
+ : [];
49
+ const result = evaluatePromotionAdjustmentsV2({
50
+ purchaseScope: scope,
51
+ referenceProductMinor: input.context.referenceProductMinor,
52
+ currentProductMinor,
53
+ shippingMinor: input.context.shippingMinor,
54
+ minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR,
55
+ }, candidates);
56
+ return {
57
+ scope,
58
+ productPayableMinor: result.productPayableMinor,
59
+ shippingPayableMinor: result.shippingPayableMinor,
60
+ effectiveProductDiscountBps: result.effectiveProductDiscountBps,
61
+ winner: result.adjustments.some((adjustment) => adjustment.source === "code")
62
+ ? "code"
63
+ : "automatic",
64
+ floorApplied: result.adjustments.some((adjustment) => adjustment.floorApplied),
65
+ adjustments: result.adjustments.map((adjustment) => ({
66
+ lane: adjustment.lane,
67
+ kind: input.promotionEngineVersion === "promotion-engine.v1"
68
+ && adjustment.promotionId === "preview-product"
69
+ && input.benefits.some((benefit) => benefit.lane === "product" && benefit.kind === "percentage")
70
+ ? "percentage"
71
+ : adjustment.kind,
72
+ amountOffMinor: adjustment.amountOffMinor,
73
+ beforeMinor: adjustment.beforeMinor,
74
+ afterMinor: adjustment.afterMinor,
75
+ })),
76
+ };
77
+ });
78
+ return {
79
+ contractVersion: COMMERCE_CONTRACT_VERSION,
80
+ promotionEngineVersion: input.promotionEngineVersion,
81
+ previewProof: promotionPreviewProof(input, secret),
82
+ results,
83
+ };
84
+ }
85
+ function previewProofPayload(input) {
86
+ return stableJson({
87
+ promotionEngineVersion: input.promotionEngineVersion,
88
+ minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR,
89
+ minimumReferenceMinor: input.minimumReferenceMinor,
90
+ benefits: input.benefits,
91
+ scopes: input.scopes,
92
+ context: input.context,
93
+ });
94
+ }
95
+ function assertPreviewSecret(secret) {
96
+ if (Buffer.byteLength(secret, "utf8") < 32) {
97
+ throw new RangeError("promotion_preview_secret_too_short");
98
+ }
99
+ }
100
+ function assertSupportedPreviewBenefits(input) {
101
+ if (input.benefits.some((benefit) => "validationState" in benefit)) {
102
+ throw new RangeError("promotion_preview_unsupported_legacy_value");
103
+ }
104
+ }
105
+ function toCandidate(benefit, scopes, currentProductMinor) {
106
+ const base = {
107
+ promotionId: `preview-${benefit.lane}`,
108
+ codeId: "preview-code",
109
+ source: "code",
110
+ name: "Promotion code preview",
111
+ scopes,
112
+ };
113
+ if ("validationState" in benefit) {
114
+ throw new RangeError("promotion_preview_unsupported_legacy_value");
115
+ }
116
+ if (("valueBps" in benefit && benefit.valueBps === 0)
117
+ || ("valueMinor" in benefit && benefit.valueMinor === 0))
118
+ return null;
119
+ if (benefit.kind === "target_percentage")
120
+ return { ...base, ...benefit };
121
+ if (benefit.kind === "percentage" && benefit.lane === "product") {
122
+ const valueMinor = benefit.valueBps >= 10_000
123
+ ? currentProductMinor
124
+ : Number((BigInt(currentProductMinor) * BigInt(benefit.valueBps)) / 10000n);
125
+ return {
126
+ ...base,
127
+ lane: "product",
128
+ kind: "fixed_amount",
129
+ valueMinor,
130
+ };
131
+ }
132
+ if (benefit.kind === "percentage" && benefit.lane === "shipping" && benefit.valueBps >= 10_000) {
133
+ return { ...base, lane: "shipping", kind: "free_shipping" };
134
+ }
135
+ if (benefit.kind === "percentage")
136
+ return { ...base, ...benefit };
137
+ if (benefit.kind === "fixed_amount")
138
+ return { ...base, ...benefit };
139
+ return { ...base, ...benefit };
140
+ }
141
+ function stableJson(value) {
142
+ if (Array.isArray(value))
143
+ return `[${value.map(stableJson).join(",")}]`;
144
+ if (value && typeof value === "object") {
145
+ const record = value;
146
+ return `{${Object.keys(record)
147
+ .sort()
148
+ .map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
149
+ .join(",")}}`;
150
+ }
151
+ return JSON.stringify(value);
152
+ }
153
+ return { generatePromotionCode, normalizePromotionCode, promotionPreviewProof, verifyPromotionPreviewProof, previewPromotionCode };
154
+ }
@@ -0,0 +1,25 @@
1
+ import type { PromotionAdjustmentCandidate, PromotionPurchaseScope } from "@openlup/core/promo";
2
+ import type { CommerceCodeRejection, CommerceCodeRejectionDetail } from "../pricing/index.js";
3
+ export type ResolvedPromotionCodeCandidate = PromotionAdjustmentCandidate & {
4
+ codeRevision: number;
5
+ definitionFingerprint: string;
6
+ minimumReferenceMinor: number;
7
+ validTo: string | null;
8
+ };
9
+ export interface ResolvePromotionCodeQuoteInput {
10
+ codes: readonly string[];
11
+ clientId: string | null;
12
+ purchaseScope: PromotionPurchaseScope;
13
+ referenceProductMinor: number;
14
+ atTime: string;
15
+ }
16
+ export interface PromotionCodeQuoteResolution {
17
+ candidates: ResolvedPromotionCodeCandidate[];
18
+ codeRejections: CommerceCodeRejection[];
19
+ /** Additive v2 RPC detail; old RPC fallback returns an empty array. */
20
+ codeRejectionDetails: CommerceCodeRejectionDetail[];
21
+ }
22
+ /** Advisory quote read only. Capacity is authoritatively claimed by the order transaction. */
23
+ export interface PromotionCodeQuotePort {
24
+ resolve(input: ResolvePromotionCodeQuoteInput): Promise<PromotionCodeQuoteResolution>;
25
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ import type { ResolvedAcquisitionTerms } from "../pricing/index.js";
2
+ import type { CommerceQuote } from "../pricing/index.js";
3
+ import type { PromotionAcceptanceTelemetry } from "./promotionAcceptanceTelemetry.js";
4
+ import { type PromotionQuoteMismatchField } from "./promotionQuoteBinding.js";
5
+ export declare const PROMOTION_ACCEPTANCE_MAX_TTL_MS: number;
6
+ export declare const PROMOTION_ACCEPTANCE_CLOCK_SKEW_MS = 60000;
7
+ export interface PromotionAcceptanceKeyring {
8
+ current: string;
9
+ previous?: string | null;
10
+ }
11
+ export type PromotionAcceptanceDiagnostic = {
12
+ accepted: boolean;
13
+ reason: PromotionAcceptanceTelemetry["reason"];
14
+ keySlot: PromotionAcceptanceTelemetry["keySlot"];
15
+ expiryBucket: PromotionAcceptanceTelemetry["expiryBucket"];
16
+ /** Present only on `quote_mismatch`: which money section diverged. */
17
+ mismatchField?: PromotionQuoteMismatchField;
18
+ };
19
+ export declare function createPromotionQuoteAcceptance<Request extends {
20
+ promoCodes: string[];
21
+ }>({ projectRequest }: {
22
+ projectRequest(request: Request): unknown;
23
+ }): {
24
+ issuePromotionQuoteAcceptance: (input: {
25
+ request: Request;
26
+ quote: CommerceQuote;
27
+ acquisitionTerms?: ResolvedAcquisitionTerms;
28
+ keyring: PromotionAcceptanceKeyring;
29
+ now?: Date;
30
+ }) => string | null;
31
+ issuePromotionQuoteAcceptanceWithOutcome: (input: {
32
+ request: Request;
33
+ quote: CommerceQuote;
34
+ acquisitionTerms?: ResolvedAcquisitionTerms;
35
+ keyring: PromotionAcceptanceKeyring;
36
+ now?: Date;
37
+ }) => {
38
+ token: string | null;
39
+ diagnostic: PromotionAcceptanceDiagnostic;
40
+ };
41
+ verifyPromotionQuoteAcceptance: (input: {
42
+ token: string;
43
+ request: Request;
44
+ quote: CommerceQuote;
45
+ acquisitionTerms?: ResolvedAcquisitionTerms;
46
+ expectedTotal: {
47
+ amountMinor: number;
48
+ currency: string;
49
+ };
50
+ keyring: PromotionAcceptanceKeyring;
51
+ now?: Date;
52
+ }) => boolean;
53
+ verifyPromotionQuoteAcceptanceWithOutcome: (input: {
54
+ token: string;
55
+ request: Request;
56
+ quote: CommerceQuote;
57
+ acquisitionTerms?: ResolvedAcquisitionTerms;
58
+ expectedTotal: {
59
+ amountMinor: number;
60
+ currency: string;
61
+ };
62
+ keyring: PromotionAcceptanceKeyring;
63
+ now?: Date;
64
+ }) => PromotionAcceptanceDiagnostic;
65
+ };
@@ -0,0 +1,202 @@
1
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
2
+ import { z } from "zod";
3
+ import { projectPromotionQuoteMoney, promotionQuoteMoneySections, PROMOTION_QUOTE_MONEY_SECTIONS, stableJson, } from "./promotionQuoteBinding.js";
4
+ export const PROMOTION_ACCEPTANCE_MAX_TTL_MS = 72 * 60 * 60 * 1_000;
5
+ export const PROMOTION_ACCEPTANCE_CLOCK_SKEW_MS = 60_000;
6
+ const payloadSchema = z.object({
7
+ v: z.literal(1),
8
+ kid: z.string().regex(/^[0-9a-f]{16}$/),
9
+ iat: z.number().int().nonnegative(),
10
+ exp: z.number().int().positive(),
11
+ engine: z.literal("promotion-engine.v2"),
12
+ requestBinding: z.string().regex(/^[0-9a-f]{64}$/),
13
+ codeBinding: z.string().regex(/^[0-9a-f]{64}$/),
14
+ quoteBinding: z.string().regex(/^[0-9a-f]{64}$/),
15
+ // Legacy payloads still parse, but their whole-quote binding is rejected.
16
+ // Quote refresh issues a new money-bound token; missing sections report other.
17
+ sectionBindings: z.object(Object.fromEntries(PROMOTION_QUOTE_MONEY_SECTIONS
18
+ .map((section) => [section, z.string().regex(/^[0-9a-f]{64}$/)]))).strict().optional(),
19
+ totalMinor: z.number().int().nonnegative(),
20
+ currency: z.string().length(3),
21
+ }).strict();
22
+ export function createPromotionQuoteAcceptance({ projectRequest }) {
23
+ function issuePromotionQuoteAcceptance(input) {
24
+ return issuePromotionQuoteAcceptanceWithOutcome(input).token;
25
+ }
26
+ function issuePromotionQuoteAcceptanceWithOutcome(input) {
27
+ const evidence = v2Discounts(input.quote);
28
+ if (evidence.length === 0 && !input.acquisitionTerms)
29
+ return {
30
+ token: null,
31
+ diagnostic: rejected("no_v2_adjustment"),
32
+ };
33
+ assertSecret(input.keyring.current);
34
+ const nowMs = (input.now ?? new Date()).getTime();
35
+ const codeExpiry = Math.min(...evidence.map((discount) => discount.promotionCodeValidTo ? Date.parse(discount.promotionCodeValidTo) : Number.POSITIVE_INFINITY));
36
+ const exp = Math.min(nowMs + PROMOTION_ACCEPTANCE_MAX_TTL_MS, codeExpiry);
37
+ if (!Number.isFinite(exp) || exp <= nowMs)
38
+ return {
39
+ token: null,
40
+ diagnostic: rejected("ttl_invalid"),
41
+ };
42
+ const payload = payloadSchema.parse({
43
+ v: 1,
44
+ kid: keyId(input.keyring.current),
45
+ iat: nowMs,
46
+ exp,
47
+ engine: "promotion-engine.v2",
48
+ requestBinding: requestBinding(input.request, input.keyring.current),
49
+ codeBinding: codeBinding(input.request.promoCodes, input.keyring.current),
50
+ quoteBinding: quoteBinding(input.quote, input.keyring.current, input.acquisitionTerms),
51
+ sectionBindings: sectionBindings(input.quote, input.keyring.current, input.acquisitionTerms),
52
+ totalMinor: input.quote.totalGross.amountMinor,
53
+ currency: input.quote.totalGross.currency,
54
+ });
55
+ const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
56
+ return {
57
+ token: `${encoded}.${sign(encoded, input.keyring.current)}`,
58
+ diagnostic: {
59
+ accepted: true,
60
+ reason: "issued",
61
+ keySlot: "current",
62
+ expiryBucket: expiryBucket(exp, nowMs),
63
+ },
64
+ };
65
+ }
66
+ function verifyPromotionQuoteAcceptance(input) {
67
+ return verifyPromotionQuoteAcceptanceWithOutcome(input).accepted;
68
+ }
69
+ function verifyPromotionQuoteAcceptanceWithOutcome(input) {
70
+ const verified = verifyEnvelopeWithOutcome(input.token, input.keyring, input.now ?? new Date());
71
+ if (verified.verified === false)
72
+ return verified.diagnostic;
73
+ const { payload, secret } = verified;
74
+ const base = { keySlot: verified.keySlot, expiryBucket: verified.expiryBucket };
75
+ if (payload.requestBinding !== requestBinding(input.request, secret)) {
76
+ return { accepted: false, reason: "request_mismatch", ...base };
77
+ }
78
+ if (payload.codeBinding !== codeBinding(input.request.promoCodes, secret)) {
79
+ return { accepted: false, reason: "code_mismatch", ...base };
80
+ }
81
+ if (payload.quoteBinding !== quoteBinding(input.quote, secret, input.acquisitionTerms)) {
82
+ const mismatchField = namedMismatchField(payload.sectionBindings, input.quote, secret, input.acquisitionTerms);
83
+ return { accepted: false, reason: "quote_mismatch", mismatchField, ...base };
84
+ }
85
+ if (payload.totalMinor !== input.expectedTotal.amountMinor) {
86
+ return { accepted: false, reason: "total_mismatch", ...base };
87
+ }
88
+ if (payload.currency !== input.expectedTotal.currency) {
89
+ return { accepted: false, reason: "currency_mismatch", ...base };
90
+ }
91
+ if (v2Discounts(input.quote).length === 0 && !input.acquisitionTerms) {
92
+ return { accepted: false, reason: "no_v2_adjustment_in_quote", ...base };
93
+ }
94
+ return { accepted: true, reason: "accepted", ...base };
95
+ }
96
+ function verifyEnvelopeWithOutcome(token, keyring, now) {
97
+ const [encoded, signature, extra] = token.split(".");
98
+ if (!encoded || !signature || extra || encoded.length > 8_192 || signature.length !== 43) {
99
+ return { verified: false, diagnostic: rejected("malformed") };
100
+ }
101
+ let parsed;
102
+ try {
103
+ parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
104
+ }
105
+ catch {
106
+ return { verified: false, diagnostic: rejected("malformed") };
107
+ }
108
+ const payload = payloadSchema.safeParse(parsed);
109
+ if (!payload.success)
110
+ return { verified: false, diagnostic: rejected("malformed") };
111
+ const candidates = [
112
+ { slot: "current", secret: keyring.current },
113
+ { slot: "previous", secret: keyring.previous },
114
+ ].filter((candidate) => typeof candidate.secret === "string" && candidate.secret.length >= 32);
115
+ const candidate = candidates.find((value) => keyId(value.secret) === payload.data.kid);
116
+ if (!candidate)
117
+ return { verified: false, diagnostic: rejected("unknown_key") };
118
+ if (!safeEqual(signature, sign(encoded, candidate.secret))) {
119
+ return { verified: false, diagnostic: rejected("signature_invalid") };
120
+ }
121
+ const nowMs = now.getTime();
122
+ const safeBase = { keySlot: candidate.slot, expiryBucket: expiryBucket(payload.data.exp, nowMs) };
123
+ if (payload.data.exp <= payload.data.iat ||
124
+ payload.data.exp - payload.data.iat > PROMOTION_ACCEPTANCE_MAX_TTL_MS) {
125
+ return { verified: false, diagnostic: { accepted: false, reason: "ttl_invalid", ...safeBase } };
126
+ }
127
+ if (payload.data.iat > nowMs + PROMOTION_ACCEPTANCE_CLOCK_SKEW_MS) {
128
+ return { verified: false, diagnostic: { accepted: false, reason: "not_yet_valid", ...safeBase } };
129
+ }
130
+ if (nowMs >= payload.data.exp) {
131
+ return { verified: false, diagnostic: { accepted: false, reason: "expired", ...safeBase } };
132
+ }
133
+ return {
134
+ verified: true,
135
+ payload: payload.data,
136
+ secret: candidate.secret,
137
+ keySlot: candidate.slot,
138
+ expiryBucket: expiryBucket(payload.data.exp, nowMs),
139
+ };
140
+ }
141
+ function rejected(reason) {
142
+ return { accepted: false, reason, keySlot: "none", expiryBucket: "none" };
143
+ }
144
+ function expiryBucket(exp, now) {
145
+ const remaining = exp - now;
146
+ if (remaining <= 0)
147
+ return "expired";
148
+ if (remaining < 60 * 60 * 1_000)
149
+ return "lt_1h";
150
+ if (remaining < 24 * 60 * 60 * 1_000)
151
+ return "1_24h";
152
+ return "24_72h";
153
+ }
154
+ function v2Discounts(quote) {
155
+ return quote.discounts.filter((discount) => discount.promotionEngineVersion === "promotion-engine.v2" &&
156
+ discount.reasonCode === "promotion_code_v2");
157
+ }
158
+ function requestBinding(request, secret) {
159
+ return hmac(secret, stableJson(projectRequest(request)));
160
+ }
161
+ function codeBinding(codes, secret) {
162
+ return hmac(secret, stableJson([...new Set(codes.map((code) => code.trim().toUpperCase()))].sort()));
163
+ }
164
+ function quoteBinding(quote, secret, acquisitionTerms) {
165
+ return hmac(secret, stableJson(projectPromotionQuoteMoney(quote, acquisitionTerms)));
166
+ }
167
+ function sectionBindings(quote, secret, acquisitionTerms) {
168
+ const sections = promotionQuoteMoneySections(quote, acquisitionTerms);
169
+ return Object.fromEntries(PROMOTION_QUOTE_MONEY_SECTIONS
170
+ .map((section) => [section, hmac(secret, stableJson(sections[section]))]));
171
+ }
172
+ /**
173
+ * The verifier holds only the issuer's HMACs, never the issued quote, so the
174
+ * differing section is found by comparing per-section hashes in a fixed order.
175
+ */
176
+ function namedMismatchField(issued, quote, secret, acquisitionTerms) {
177
+ if (!issued)
178
+ return "other";
179
+ const verified = sectionBindings(quote, secret, acquisitionTerms);
180
+ return PROMOTION_QUOTE_MONEY_SECTIONS
181
+ .find((section) => issued[section] !== verified[section]) ?? "other";
182
+ }
183
+ function keyId(secret) {
184
+ return createHash("sha256").update(secret).digest("hex").slice(0, 16);
185
+ }
186
+ function hmac(secret, value) {
187
+ return createHmac("sha256", secret).update(value).digest("hex");
188
+ }
189
+ function sign(encoded, secret) {
190
+ return createHmac("sha256", secret).update(encoded).digest("base64url");
191
+ }
192
+ function safeEqual(left, right) {
193
+ const a = Buffer.from(left);
194
+ const b = Buffer.from(right);
195
+ return a.length === b.length && timingSafeEqual(a, b);
196
+ }
197
+ function assertSecret(secret) {
198
+ if (secret.length < 32)
199
+ throw new Error("promotion_acceptance_secret_invalid");
200
+ }
201
+ return { issuePromotionQuoteAcceptance, issuePromotionQuoteAcceptanceWithOutcome, verifyPromotionQuoteAcceptance, verifyPromotionQuoteAcceptanceWithOutcome };
202
+ }
@@ -0,0 +1,71 @@
1
+ import { type ResolvedAcquisitionTerms } from "../pricing/index.js";
2
+ import type { CommerceMoney, CommerceQuote, CommerceQuoteDiscount } from "../pricing/index.js";
3
+ /**
4
+ * The canonical money projection an acceptance token binds.
5
+ *
6
+ * ⛔ The token protects **what the customer pays**, never how it was presented
7
+ * or which server-only evidence produced it. Everything outside this projection
8
+ * is deliberately unbound: `label`, `reasonCode`, `customerSemantic`, `code`,
9
+ * `promotionCodeRevision`, `promotionCodeScopes`, `promotionMinimumReferenceMinor`,
10
+ * `promotionCodeValidTo`, `floorApplied`, `context`, `codeRejections`,
11
+ * `codeRejectionDetails`, `pricingComponents` and `catalogFacts`. Those differ
12
+ * legitimately between the two surfaces that share one token — the public quote
13
+ * route signs the contract-parsed public projection while the checkout guard
14
+ * verifies the raw server-authoritative quote, which carries `catalogFacts`
15
+ * (with a per-request `atTime`) that the public projection strips — so binding
16
+ * them turned every strict-path v2 code checkout into a permanent rejection.
17
+ *
18
+ * What stays bound is every amount and every discount identity: change a price,
19
+ * a quantity, a tax split, shipping, a discount amount, which promotion granted
20
+ * it, or what it applies to, and the token stops verifying.
21
+ */
22
+ export interface PromotionQuoteMoneyLine {
23
+ sku: string;
24
+ quantity: number;
25
+ unitPriceGross: CommerceMoney;
26
+ lineSubtotalGross: CommerceMoney;
27
+ tax: {
28
+ vatRateBps: number;
29
+ netAmount: CommerceMoney;
30
+ vatAmount: CommerceMoney;
31
+ grossAmount: CommerceMoney;
32
+ };
33
+ }
34
+ export interface PromotionQuoteMoneyDiscount {
35
+ promotionId: string;
36
+ appliesTo: CommerceQuoteDiscount["appliesTo"];
37
+ amountOffMinor: number;
38
+ promotionCodeId: string | null;
39
+ promotionDefinitionFingerprint: string | null;
40
+ promotionBenefitKind: NonNullable<CommerceQuoteDiscount["promotionBenefitKind"]> | null;
41
+ promotionBenefitValueBps: number | null;
42
+ promotionBenefitValueMinor: number | null;
43
+ }
44
+ export interface PromotionQuoteMoneyProjection {
45
+ acquisitionTerms?: ResolvedAcquisitionTerms;
46
+ currency: string;
47
+ totalGross: CommerceMoney;
48
+ netTotal: CommerceMoney;
49
+ taxTotal: CommerceMoney;
50
+ subtotalGross: CommerceMoney;
51
+ discountTotalGross: CommerceMoney;
52
+ shippingGross: CommerceMoney | null;
53
+ shippingDiscountGross: CommerceMoney | null;
54
+ lines: PromotionQuoteMoneyLine[];
55
+ discounts: PromotionQuoteMoneyDiscount[];
56
+ }
57
+ /** The five sections the token hashes separately so a mismatch can name itself. */
58
+ export declare const PROMOTION_QUOTE_MONEY_SECTIONS: readonly ["lines", "discounts", "shipping", "totals", "currency"];
59
+ export type PromotionQuoteMoneySection = (typeof PROMOTION_QUOTE_MONEY_SECTIONS)[number];
60
+ /** `other` is the honest answer for a token minted before section bindings existed. */
61
+ export declare const PROMOTION_QUOTE_MISMATCH_FIELDS: readonly ["lines", "discounts", "shipping", "totals", "currency", "other"];
62
+ export type PromotionQuoteMismatchField = (typeof PROMOTION_QUOTE_MISMATCH_FIELDS)[number];
63
+ /**
64
+ * Discount order is preserved: the promotion engine emits its lanes
65
+ * deterministically, so a reordering is a real evaluation difference.
66
+ */
67
+ export declare function projectPromotionQuoteMoney(quote: CommerceQuote, acquisitionTerms?: ResolvedAcquisitionTerms): PromotionQuoteMoneyProjection;
68
+ /** Same projection, partitioned. The union of these five sections is the whole projection. */
69
+ export declare function promotionQuoteMoneySections(quote: CommerceQuote, acquisitionTerms?: ResolvedAcquisitionTerms): Record<PromotionQuoteMoneySection, unknown>;
70
+ /** Key-sorted, `undefined`-dropping JSON. Every acceptance binding hashes this shape. */
71
+ export declare function stableJson(value: unknown): string;