@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,58 @@
1
+ import type { CreateQuoteRequest } from "../pricing/index.js";
2
+ import { PROMOTION_ENGINE_V2 } from "../pricing/index.js";
3
+ import type { CreateQuoteOptions } from "../pricing/index.js";
4
+ import type { CommercePromoDataPort } from "./promoDataPort.js";
5
+ import type { createQuotePromoDiscounts, QuoteDiscountResult } from "./quotePromoDiscounts.js";
6
+ import type { createQuotePromotionV2 } from "./quotePromotionV2.js";
7
+
8
+ export function createQuoteAutomaticPromotions<Request extends CreateQuoteRequest & { visitorId?: string }>({ resolveEmailEligibility, evaluateOrderTotalDiscounts, evaluateAutomaticPromotionV2, legacyPromotionDataPort }: {
9
+ resolveEmailEligibility(request: Request): boolean;
10
+ evaluateOrderTotalDiscounts: ReturnType<typeof createQuotePromoDiscounts>["evaluateOrderTotalDiscounts"];
11
+ evaluateAutomaticPromotionV2: ReturnType<typeof createQuotePromotionV2>["evaluateAutomaticPromotionV2"];
12
+ legacyPromotionDataPort: ReturnType<typeof createQuotePromotionV2>["legacyPromotionDataPort"];
13
+ }) {
14
+ async function resolveAutomaticQuotePromotions(input: {
15
+ promoDataPort?: CommercePromoDataPort;
16
+ request: Request;
17
+ options?: CreateQuoteOptions;
18
+ regionCode: string;
19
+ referenceProductMinor: number;
20
+ subtotalGrossMinor: number;
21
+ shippingGrossMinor: number;
22
+ atTime: string;
23
+ }): Promise<QuoteDiscountResult> {
24
+ if (!input.promoDataPort) return { discounts: [], codeRejections: [] };
25
+
26
+ const emailEligibilityConfirmed = resolveEmailEligibility(input.request);
27
+ if (input.options?.pricingPolicy?.promotionEngineVersion === PROMOTION_ENGINE_V2) {
28
+ return evaluateAutomaticPromotionV2({
29
+ promoDataPort: input.promoDataPort,
30
+ clientId: input.options.clientId ?? null,
31
+ visitorId: input.request.visitorId ?? null,
32
+ mode: input.request.mode,
33
+ promoCodes: input.request.promoCodes,
34
+ regionCode: input.regionCode,
35
+ referenceProductMinor: input.referenceProductMinor,
36
+ currentProductMinor: input.subtotalGrossMinor,
37
+ shippingMinor: input.shippingGrossMinor,
38
+ atTime: input.atTime,
39
+ emailEligibilityConfirmed,
40
+ });
41
+ }
42
+
43
+ return evaluateOrderTotalDiscounts({
44
+ promoDataPort: legacyPromotionDataPort(input.promoDataPort),
45
+ clientId: input.options?.clientId ?? null,
46
+ visitorId: input.request.visitorId ?? null,
47
+ mode: input.request.mode,
48
+ promoCodes: input.request.promoCodes,
49
+ regionCode: input.regionCode,
50
+ subtotalGrossMinor: input.subtotalGrossMinor,
51
+ shippingGrossMinor: input.shippingGrossMinor,
52
+ atTime: input.atTime,
53
+ emailEligibilityConfirmed,
54
+ });
55
+ }
56
+
57
+ return { resolveAutomaticQuotePromotions };
58
+ }
@@ -0,0 +1,185 @@
1
+ import type { CreateQuoteRequest } from "../pricing/index.js";
2
+ import type { CommerceCodeRejection, CommerceQuoteDiscount } from "../pricing/index.js";
3
+ import { evaluatePromos, promoEligibilityFailure } from "@openlup/core/promo";
4
+ import type { PromoEvaluationCart, PromotionRow } from "@openlup/core/promo";
5
+ import type { CommercePromoDataPort, PromotionRedemptionCount } from "./promoDataPort.js";
6
+ export interface QuoteDiscountResult {
7
+ /** Applied discounts: order_total (clamped to subtotal) + at most one shipping discount. */
8
+ discounts: CommerceQuoteDiscount[];
9
+ /** Submitted coupon codes that produced no discount, with the reason. */
10
+ codeRejections: CommerceCodeRejection[];
11
+ }
12
+
13
+ export function createQuotePromoDiscounts({ promotionCustomerSemantic }: { promotionCustomerSemantic: (row: PromotionRow) => CommerceQuoteDiscount["customerSemantic"] }) {
14
+ /**
15
+ * Evaluate active promotions for this cart and map them to quote discounts. Two lanes:
16
+ * `order_total` promos are clamped to the subtotal (so `discountTotal <= subtotal`), and
17
+ * a single `free_shipping` promo is emitted as an `appliesTo:"shipping"` discount clamped
18
+ * to `shippingGrossMinor` — it is NOT suppressed by an exclusive order-total promo and is
19
+ * NOT counted against the subtotal. Per-mode paid-order counts drive `first_subscription_purchase`
20
+ * and `first_onetime_purchase` independently; all counts are server-authoritative (resolved
21
+ * from the clientId, never client input), so eligibility cannot be forged.
22
+ */
23
+ async function evaluateOrderTotalDiscounts(input: {
24
+ promoDataPort: CommercePromoDataPort;
25
+ clientId: string | null;
26
+ visitorId?: string | null;
27
+ mode: CreateQuoteRequest["mode"];
28
+ promoCodes: string[];
29
+ regionCode: string;
30
+ subtotalGrossMinor: number;
31
+ shippingGrossMinor?: number;
32
+ atTime: string;
33
+ /**
34
+ * True when a customer email drove eligibility resolution (the request carried
35
+ * `customerEligibilityContext.email`). Demotes the device-redemption guard to
36
+ * advisory so an email-eligible customer on a used device still gets the
37
+ * first-order price. See {@link PromoEvaluationCart.email_eligibility_confirmed}.
38
+ */
39
+ emailEligibilityConfirmed?: boolean;
40
+ }): Promise<QuoteDiscountResult> {
41
+ if (input.subtotalGrossMinor <= 0) return { discounts: [], codeRejections: [] };
42
+
43
+ const byMode = input.clientId
44
+ ? await input.promoDataPort.countPaidOrdersByMode(input.clientId)
45
+ : { oneTime: 0, subscription: 0 };
46
+
47
+ // Cap pre-filter: drop a promo that has reached its global or per-customer redemption
48
+ // limit BEFORE evaluation, so caps are enforced at quote time and a paid order is never
49
+ // failed on a cap. A per-customer cap can only be checked for an authenticated client.
50
+ const [allCandidates, redemptionCounts, deviceFirstOrderRedeemed] = await Promise.all([
51
+ input.promoDataPort.listActivePromotions(),
52
+ input.promoDataPort.redemptionCounts(input.clientId),
53
+ input.promoDataPort.deviceFirstOrderRedeemed(input.visitorId ?? null),
54
+ ]);
55
+ const candidates = allCandidates.filter(
56
+ (promo) => !capReached(promo, redemptionCounts.get(promo.id), input.clientId),
57
+ );
58
+
59
+ const cart: PromoEvaluationCart = {
60
+ region_code: input.regionCode,
61
+ cart_mode: input.mode === "subscription" ? "subscription" : "one_time",
62
+ cart_subtotal_minor: input.subtotalGrossMinor,
63
+ shipping_amount_minor: input.shippingGrossMinor ?? 0,
64
+ client_orders_count: byMode.oneTime + byMode.subscription,
65
+ client_subscription_orders_count: byMode.subscription,
66
+ client_onetime_orders_count: byMode.oneTime,
67
+ device_first_order_redeemed: deviceFirstOrderRedeemed,
68
+ email_eligibility_confirmed: input.emailEligibilityConfirmed === true,
69
+ // The engine compares code literals. Preserve the customer's submitted spelling
70
+ // for the public rejection, but feed known legacy codes in their canonical
71
+ // spelling so eligibility and applied-discount matching are case-insensitive.
72
+ applied_codes: canonicalSubmittedCodes(input.promoCodes, allCandidates),
73
+ };
74
+
75
+ const applied = evaluatePromos({ cart, candidates, now: input.atTime });
76
+
77
+ const discounts: CommerceQuoteDiscount[] = [];
78
+ let running = 0;
79
+ for (const promo of applied) {
80
+ const source = candidates.find((candidate) => candidate.id === promo.promotion_id);
81
+ const customerSemantic = source ? promotionCustomerSemantic(source) : undefined;
82
+ const isShipping = promo.discount_type === "free_shipping";
83
+ const cap = isShipping ? cart.shipping_amount_minor : input.subtotalGrossMinor - running;
84
+ if (cap <= 0) {
85
+ if (isShipping) continue;
86
+ break;
87
+ }
88
+ const amountOffMinor = Math.min(promo.amount_off_minor, cap);
89
+ if (amountOffMinor <= 0) continue;
90
+ if (!isShipping) running += amountOffMinor;
91
+ discounts.push({
92
+ promotionId: promo.promotion_id,
93
+ ...(promo.code ? { code: promo.code } : {}),
94
+ // Human-facing label preserves the promotion name instead of its reason code.
95
+ ...(promo.name ? { label: promo.name } : {}),
96
+ appliesTo: isShipping ? "shipping" : "order_total",
97
+ amountOffMinor,
98
+ reasonCode: promo.reason_code,
99
+ ...(customerSemantic ? { customerSemantic } : {}),
100
+ });
101
+ }
102
+
103
+ return {
104
+ discounts,
105
+ codeRejections: classifyRejections(input, cart, allCandidates, redemptionCounts, discounts),
106
+ };
107
+ }
108
+
109
+ /**
110
+ * For each submitted coupon code that produced no discount, classify why. Uses the
111
+ * pre-cap candidate set so an exhausted per-customer cap reads as `already_used` rather
112
+ * than `not_recognized`. An eligible code that was merely outranked by a larger promo
113
+ * is a neutral `better_price_exists`: it was not applied because the customer already
114
+ * has an equally good or better automatic price. Applied shipping-code discounts count
115
+ * as applied too.
116
+ */
117
+ function classifyRejections(
118
+ input: { clientId: string | null; promoCodes: string[]; atTime: string },
119
+ cart: PromoEvaluationCart,
120
+ allCandidates: PromotionRow[],
121
+ redemptionCounts: Map<string, PromotionRedemptionCount>,
122
+ discounts: readonly CommerceQuoteDiscount[],
123
+ ): CommerceCodeRejection[] {
124
+ const rejections: CommerceCodeRejection[] = [];
125
+ for (const code of input.promoCodes) {
126
+ const promo = resolveSubmittedCoupon(code, allCandidates);
127
+ if (!promo) {
128
+ rejections.push({ code, reason: "not_recognized" });
129
+ continue;
130
+ }
131
+ // A legacy literal may be bound to more than one promotion/lane. A shipping
132
+ // benefit can legitimately apply even when the first matching product row
133
+ // fails scope, so the visible applied discount always wins over rejection.
134
+ if (discounts.some((discount) => sameCode(discount.code, code) || sameCode(discount.code, promo.code))) {
135
+ continue;
136
+ }
137
+ if (capReached(promo, redemptionCounts.get(promo.id), input.clientId)) {
138
+ rejections.push({ code, reason: "already_used" });
139
+ continue;
140
+ }
141
+ const failure = promoEligibilityFailure(promo, cart, input.atTime);
142
+ if (failure) rejections.push({ code, reason: failure });
143
+ else if (!discounts.some((discount) => discount.code === promo.code)) {
144
+ rejections.push({ code, reason: "better_price_exists" });
145
+ }
146
+ }
147
+ return rejections;
148
+ }
149
+
150
+ function canonicalSubmittedCodes(codes: readonly string[], candidates: readonly PromotionRow[]): string[] {
151
+ return codes.map((code) => resolveSubmittedCoupon(code, candidates)?.code ?? code);
152
+ }
153
+
154
+ /**
155
+ * Legacy permits normalized-code collisions. Preserve its exact-literal
156
+ * matching first; case-insensitive recovery is safe only for one identity.
157
+ */
158
+ function resolveSubmittedCoupon(code: string, candidates: readonly PromotionRow[]): PromotionRow | undefined {
159
+ const couponCandidates = candidates.filter((promo) => promo.trigger_type === "coupon_code" && promo.code !== null);
160
+ const exact = couponCandidates.find((promo) => promo.code === code);
161
+ if (exact) return exact;
162
+ const normalized = couponCandidates.filter((promo) => sameCode(promo.code, code));
163
+ return normalized.length === 1 ? normalized[0] : undefined;
164
+ }
165
+
166
+ function sameCode(left: string | null | undefined, right: string | null | undefined): boolean {
167
+ return typeof left === "string" && typeof right === "string" &&
168
+ left.trim().toUpperCase() === right.trim().toUpperCase();
169
+ }
170
+
171
+ function capReached(
172
+ promo: PromotionRow,
173
+ counts: PromotionRedemptionCount | undefined,
174
+ clientId: string | null,
175
+ ): boolean {
176
+ const { global, perCustomer } = counts ?? { global: 0, perCustomer: 0 };
177
+ if (promo.redemption_limit_global != null && global >= promo.redemption_limit_global) return true;
178
+ if (clientId && promo.redemption_limit_per_customer != null && perCustomer >= promo.redemption_limit_per_customer) {
179
+ return true;
180
+ }
181
+ return false;
182
+ }
183
+
184
+ return { evaluateOrderTotalDiscounts };
185
+ }
@@ -0,0 +1,277 @@
1
+ import { evaluatePromotionAdjustmentsV2 } from "@openlup/core/promo";
2
+ import type { PromotionCodeRejectionReason } from "@openlup/core/promo";
3
+ import type { CommerceCodeRejection, CommerceCodeRejectionDetail, CommerceCodeRejectionReason, CommerceQuoteDiscount } from "../pricing/index.js";
4
+ import type { PromotionCodeQuotePort } from "./promotionCodeQuotePort.js";
5
+ export interface ApplyPromotionCodesV2Input {
6
+ port?: PromotionCodeQuotePort;
7
+ preserveLegacyDiscountsWhenNoCandidates?: boolean;
8
+ clientId: string | null;
9
+ mode: "one_time" | "subscription";
10
+ promoCodes: readonly string[];
11
+ referenceProductMinor: number;
12
+ subtotalGrossMinor: number;
13
+ shippingGrossMinor: number;
14
+ discounts: CommerceQuoteDiscount[];
15
+ codeRejections: CommerceCodeRejection[];
16
+ codeRejectionDetails?: CommerceCodeRejectionDetail[];
17
+ atTime: string;
18
+ }
19
+
20
+ export function createQuotePromotionCodeAdjustments({ minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR }: { minimumProductPayableMinor: number }) {
21
+ async function applyPromotionCodesV2(input: ApplyPromotionCodesV2Input): Promise<{
22
+ discounts: CommerceQuoteDiscount[]; codeRejections: CommerceCodeRejection[]; codeRejectionDetails: CommerceCodeRejectionDetail[];
23
+ }> {
24
+ const submittedV2Codes = firstSubmittedCodes(input.promoCodes);
25
+ if (!input.port || submittedV2Codes.length === 0) {
26
+ return {
27
+ discounts: input.discounts,
28
+ codeRejections: input.codeRejections,
29
+ codeRejectionDetails: input.codeRejectionDetails ?? [],
30
+ };
31
+ }
32
+ const firstSubmittedByIdentity = new Map(submittedV2Codes.map((code) => [normalize(code), code]));
33
+ const resolution = await input.port.resolve({
34
+ codes: submittedV2Codes,
35
+ clientId: input.clientId,
36
+ purchaseScope: input.mode === "subscription" ? "subscription_initial" : "one_time",
37
+ referenceProductMinor: input.referenceProductMinor,
38
+ atTime: input.atTime,
39
+ });
40
+ if (resolution.candidates.length === 0) {
41
+ const discounts = input.preserveLegacyDiscountsWhenNoCandidates
42
+ ? input.discounts
43
+ : reduceProvisionalLegacyDiscounts(input.discounts, firstSubmittedByIdentity);
44
+ const appliedLegacyCodes = new Set(discounts.flatMap((discount) => discount.code ? [normalize(discount.code)] : []));
45
+ const codeRejections = dedupeRejections([
46
+ ...input.codeRejections,
47
+ ...resolution.codeRejections,
48
+ ], firstSubmittedByIdentity, input.codeRejections.length).filter((rejection) => !appliedLegacyCodes.has(normalize(rejection.code)));
49
+ return {
50
+ discounts: input.preserveLegacyDiscountsWhenNoCandidates
51
+ ? discounts
52
+ : orderSubmittedCodeSlots(projectSubmittedCodeSpelling(discounts, firstSubmittedByIdentity), firstSubmittedByIdentity),
53
+ codeRejections,
54
+ codeRejectionDetails: dedupeRejectionDetails([
55
+ ...selectedRejectionDetails(input.codeRejectionDetails ?? [], input.codeRejections, codeRejections),
56
+ ...selectedRejectionDetails(resolution.codeRejectionDetails, resolution.codeRejections, codeRejections),
57
+ ], firstSubmittedByIdentity),
58
+ };
59
+ }
60
+ const provisionalLegacyDiscounts = reduceProvisionalLegacyDiscounts(input.discounts, firstSubmittedByIdentity);
61
+ const appliedLegacyCodes = new Set(provisionalLegacyDiscounts.flatMap((discount) => discount.code ? [normalize(discount.code)] : []));
62
+ const result = evaluatePromotionAdjustmentsV2(
63
+ adjustmentContext(input, provisionalLegacyDiscounts),
64
+ resolution.candidates,
65
+ );
66
+ const byAdjustmentIdentity = new Map(
67
+ resolution.candidates.map((candidate) => [
68
+ `${candidate.promotionId}:${candidate.codeId}`,
69
+ candidate,
70
+ ]),
71
+ );
72
+ const replacedLegacyLanes = new Set<string>();
73
+ const effectiveAdjustments = result.adjustments.flatMap((adjustment) => {
74
+ const candidate = adjustment.codeId
75
+ ? byAdjustmentIdentity.get(`${adjustment.promotionId}:${adjustment.codeId}`)
76
+ : undefined;
77
+ if (!candidate || candidate.source !== "code" || !candidate.code) {
78
+ throw new Error("promotion_code_quote_candidate_identity_missing");
79
+ }
80
+ const appliesTo = adjustment.lane === "shipping" ? "shipping" : "order_total";
81
+ const identity = normalize(candidate.code);
82
+ const matchingLegacy = provisionalLegacyDiscounts.filter((discount) =>
83
+ discount.appliesTo === appliesTo && discount.code && normalize(discount.code) === identity,
84
+ );
85
+ if (matchingLegacy.length === 0) return [{ adjustment, candidate }];
86
+
87
+ // Re-score without the matching provisional legacy identity and retain the
88
+ // single adjustment that gives the customer the lower lane payable.
89
+ const withoutMatchingLegacy = provisionalLegacyDiscounts.filter((discount) =>
90
+ !(discount.appliesTo === appliesTo && discount.code && normalize(discount.code) === identity),
91
+ );
92
+ const replacement = evaluatePromotionAdjustmentsV2(
93
+ adjustmentContext(input, withoutMatchingLegacy),
94
+ [candidate],
95
+ ).adjustments[0];
96
+ const legacyAmount = matchingLegacy.reduce((sum, discount) => sum + discount.amountOffMinor, 0);
97
+ if (!replacement || replacement.amountOffMinor < legacyAmount) return [];
98
+ replacedLegacyLanes.add(`${identity}:${appliesTo}`);
99
+ return [{ adjustment: replacement, candidate }];
100
+ });
101
+ const retainedDiscounts = projectSubmittedCodeSpelling(
102
+ provisionalLegacyDiscounts.filter((discount) =>
103
+ !(discount.code && replacedLegacyLanes.has(`${normalize(discount.code)}:${discount.appliesTo}`)),
104
+ ),
105
+ firstSubmittedByIdentity,
106
+ );
107
+ const v2Discounts: CommerceQuoteDiscount[] = effectiveAdjustments.map(({ adjustment, candidate }) => ({
108
+ promotionId: adjustment.promotionId,
109
+ code: firstSubmittedByIdentity.get(normalize(candidate.code!)) ?? candidate.code!,
110
+ label: adjustment.name,
111
+ appliesTo: adjustment.lane === "shipping" ? "shipping" : "order_total",
112
+ amountOffMinor: adjustment.amountOffMinor,
113
+ reasonCode: "promotion_code_v2",
114
+ promotionEngineVersion: "promotion-engine.v2",
115
+ promotionCodeId: candidate.codeId,
116
+ promotionCodeRevision: candidate.codeRevision,
117
+ promotionDefinitionFingerprint: candidate.definitionFingerprint,
118
+ promotionCodeScopes: [...candidate.scopes],
119
+ promotionMinimumReferenceMinor: candidate.minimumReferenceMinor,
120
+ promotionCodeValidTo: candidate.validTo,
121
+ promotionBenefitKind: candidate.kind,
122
+ promotionBenefitValueBps: candidate.valueBps,
123
+ promotionBenefitValueMinor: candidate.valueMinor,
124
+ floorApplied: adjustment.floorApplied,
125
+ }));
126
+ const appliedV2Codes = new Set(v2Discounts.flatMap((discount) => discount.code ? [normalize(discount.code)] : []));
127
+ const engineRejections: CommerceCodeRejection[] = result.rejectedCodes.flatMap((rejection) =>
128
+ rejection.code && !appliedLegacyCodes.has(normalize(rejection.code))
129
+ ? [{
130
+ code: firstSubmittedByIdentity.get(normalize(rejection.code)) ?? rejection.code,
131
+ reason: mapEngineRejectionReason(rejection.reason),
132
+ }]
133
+ : [],
134
+ );
135
+ const appliedCodes = new Set([...retainedDiscounts.flatMap((discount) => discount.code ? [normalize(discount.code)] : []), ...appliedV2Codes]);
136
+ const mergedRejections = dedupeRejections([
137
+ ...input.codeRejections,
138
+ ...resolution.codeRejections,
139
+ ...engineRejections,
140
+ ], firstSubmittedByIdentity, input.codeRejections.length).filter((rejection) => !appliedCodes.has(normalize(rejection.code)));
141
+ const codeRejectionDetails = dedupeRejectionDetails([
142
+ ...selectedRejectionDetails(input.codeRejectionDetails ?? [], input.codeRejections, mergedRejections),
143
+ ...selectedRejectionDetails(resolution.codeRejectionDetails, resolution.codeRejections, mergedRejections),
144
+ ], firstSubmittedByIdentity);
145
+ return {
146
+ discounts: orderSubmittedCodeSlots([...retainedDiscounts, ...v2Discounts], firstSubmittedByIdentity),
147
+ codeRejections: mergedRejections,
148
+ codeRejectionDetails,
149
+ };
150
+ }
151
+
152
+ function adjustmentContext(input: ApplyPromotionCodesV2Input, discounts: readonly CommerceQuoteDiscount[]) {
153
+ return {
154
+ purchaseScope: input.mode === "subscription" ? "subscription_initial" : "one_time",
155
+ referenceProductMinor: input.referenceProductMinor,
156
+ currentProductMinor: input.subtotalGrossMinor - sumLane(discounts, false),
157
+ shippingMinor: input.shippingGrossMinor - sumLane(discounts, true),
158
+ minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR,
159
+ } as const;
160
+ }
161
+
162
+ function sumLane(discounts: readonly CommerceQuoteDiscount[], shipping: boolean): number {
163
+ return discounts
164
+ .filter((discount) => (discount.appliesTo === "shipping") === shipping)
165
+ .reduce((sum, discount) => sum + discount.amountOffMinor, 0);
166
+ }
167
+
168
+ function dedupeRejections(
169
+ rejections: readonly CommerceCodeRejection[],
170
+ firstSubmittedByIdentity: ReadonlyMap<string, string> = new Map(),
171
+ authoritativeFrom = rejections.length,
172
+ ): CommerceCodeRejection[] {
173
+ const byCode = new Map<string, CommerceCodeRejection>();
174
+ for (const [index, rejection] of rejections.entries()) {
175
+ const identity = normalize(rejection.code);
176
+ const current = byCode.get(identity);
177
+ if (!current || (index >= authoritativeFrom && current.reason === "not_recognized")) {
178
+ byCode.set(identity, { ...rejection, code: firstSubmittedByIdentity.get(identity) ?? rejection.code });
179
+ }
180
+ }
181
+ return orderSubmittedCodeSlots([...byCode.values()], firstSubmittedByIdentity);
182
+ }
183
+
184
+ function dedupeRejectionDetails(
185
+ details: readonly CommerceCodeRejectionDetail[],
186
+ firstSubmittedByIdentity: ReadonlyMap<string, string> = new Map(),
187
+ ): CommerceCodeRejectionDetail[] {
188
+ const byCode = new Map<string, CommerceCodeRejectionDetail>();
189
+ for (const detail of details) {
190
+ const identity = normalize(detail.code);
191
+ if (!byCode.has(identity)) {
192
+ byCode.set(identity, { ...detail, code: firstSubmittedByIdentity.get(identity) ?? detail.code });
193
+ }
194
+ }
195
+ return orderSubmittedCodeSlots([...byCode.values()], firstSubmittedByIdentity);
196
+ }
197
+
198
+ function selectedRejectionDetails(details: readonly CommerceCodeRejectionDetail[], sourceRejections: readonly CommerceCodeRejection[], selectedRejections: readonly CommerceCodeRejection[]): CommerceCodeRejectionDetail[] {
199
+ const selected = new Map(selectedRejections.map((rejection) => [normalize(rejection.code), rejection.reason]));
200
+ const matchingSources = new Set(sourceRejections.flatMap((rejection) =>
201
+ selected.get(normalize(rejection.code)) === rejection.reason ? [normalize(rejection.code)] : [],
202
+ ));
203
+ return details.filter((detail) => matchingSources.has(normalize(detail.code)));
204
+ }
205
+
206
+ function firstSubmittedCodes(codes: readonly string[]): string[] {
207
+ const seen = new Set<string>();
208
+ const first: string[] = [];
209
+ for (const code of codes) {
210
+ const identity = normalize(code);
211
+ if (!identity || seen.has(identity)) continue;
212
+ seen.add(identity);
213
+ first.push(code);
214
+ }
215
+ return first;
216
+ }
217
+
218
+ /** Collapse legacy exact-literal collisions to one submitted identity per lane. */
219
+ function reduceProvisionalLegacyDiscounts(discounts: readonly CommerceQuoteDiscount[], firstSubmittedByIdentity: ReadonlyMap<string, string>): CommerceQuoteDiscount[] {
220
+ const chosen = new Map<string, { index: number; exactFirstSpelling: boolean }>();
221
+ for (const [index, discount] of discounts.entries()) {
222
+ if (!discount.code) continue;
223
+ const identity = normalize(discount.code);
224
+ const firstSubmitted = firstSubmittedByIdentity.get(identity);
225
+ if (!firstSubmitted) continue;
226
+ const key = `${identity}:${discount.appliesTo}`;
227
+ const exactFirstSpelling = discount.code === firstSubmitted;
228
+ const current = chosen.get(key);
229
+ if (!current || (exactFirstSpelling && !current.exactFirstSpelling)) {
230
+ chosen.set(key, { index, exactFirstSpelling });
231
+ }
232
+ }
233
+ return discounts.filter((discount, index) => {
234
+ if (!discount.code) return true;
235
+ const identity = normalize(discount.code);
236
+ if (!firstSubmittedByIdentity.has(identity)) return true;
237
+ return chosen.get(`${identity}:${discount.appliesTo}`)?.index === index;
238
+ });
239
+ }
240
+
241
+ function projectSubmittedCodeSpelling(discounts: readonly CommerceQuoteDiscount[], firstSubmittedByIdentity: ReadonlyMap<string, string>): CommerceQuoteDiscount[] {
242
+ return discounts.map((discount) => {
243
+ if (!discount.code) return discount;
244
+ const code = firstSubmittedByIdentity.get(normalize(discount.code));
245
+ return code ? { ...discount, code } : discount;
246
+ });
247
+ }
248
+
249
+ function submittedRank(code: string, firstSubmittedByIdentity: ReadonlyMap<string, string>): number {
250
+ const rank = [...firstSubmittedByIdentity.keys()].indexOf(normalize(code));
251
+ return rank < 0 ? Number.MAX_SAFE_INTEGER : rank;
252
+ }
253
+
254
+ function orderSubmittedCodeSlots<T extends { code?: string | null }>(values: T[], firstSubmittedByIdentity: ReadonlyMap<string, string>): T[] {
255
+ const ordered = values.map((value, index) => ({ value, index })).filter(({ value }) => value.code && submittedRank(value.code, firstSubmittedByIdentity) < Number.MAX_SAFE_INTEGER)
256
+ .sort((left, right) => submittedRank(left.value.code!, firstSubmittedByIdentity) - submittedRank(right.value.code!, firstSubmittedByIdentity) || left.index - right.index).map(({ value }) => value);
257
+ let index = 0;
258
+ return values.map((value) => value.code && submittedRank(value.code, firstSubmittedByIdentity) < Number.MAX_SAFE_INTEGER ? ordered[index++]! : value);
259
+ }
260
+
261
+ function normalize(code: string): string {
262
+ return code.trim().toUpperCase();
263
+ }
264
+
265
+ function mapEngineRejectionReason(reason: PromotionCodeRejectionReason): CommerceCodeRejectionReason {
266
+ switch (reason) {
267
+ case "better_price_exists":
268
+ return "better_price_exists";
269
+ case "scope_not_applicable":
270
+ return "scope_not_applicable";
271
+ default:
272
+ return "not_eligible";
273
+ }
274
+ }
275
+
276
+ return { applyPromotionCodesV2, mapEngineRejectionReason };
277
+ }