@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,350 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ import { createGuardedEmailTransport, type EmailEgressPolicy, type EmailEgressContext } from "../emailEgress.js";
3
+ import type { EmailMessage, EmailSendResult, EmailTransport } from "../emailTransport.js";
4
+ import { processEmailObservation, type EmailObservationPort } from "../emailObservation.js";
5
+ import { runDeliveryReconciliation, type DeliveryReconciliationPort, type DeliveryReconciliationResult } from "../deliveryReconciliation.js";
6
+
7
+ /** Opt-in Node sender. Every send obtains a fresh validated host egress decision. */
8
+ export function createResendTransport(input: {
9
+ policy: EmailEgressPolicy;
10
+ environmentTagName: string;
11
+ fetch?: typeof fetch;
12
+ }): EmailTransport {
13
+ return createGuardedEmailTransport({
14
+ providerKind: "resend",
15
+ policy: input.policy,
16
+ send: (message, context) => postResendEmail(message, context, input.environmentTagName, input.fetch ?? fetch),
17
+ });
18
+ }
19
+
20
+ const RESEND_ENDPOINT = "https://api.resend.com/emails";
21
+ const PROVIDER_ERROR_MAX = 240;
22
+ const PROVIDER_ERROR_CODE_MAX = 80;
23
+ const EMAIL_ADDRESS_PATTERN = /[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi;
24
+
25
+ function truncate(value: string, max: number): string {
26
+ return value.length > max ? `${value.slice(0, max - 1)}...` : value;
27
+ }
28
+
29
+ function sanitizeProviderText(value: string): string {
30
+ return truncate(value.replace(EMAIL_ADDRESS_PATTERN, "[email]").replace(/\s+/g, " ").trim(), PROVIDER_ERROR_MAX);
31
+ }
32
+
33
+ function sanitizeProviderCode(value: string): string {
34
+ return truncate(value.replace(/[^A-Za-z0-9._:-]/g, "_"), PROVIDER_ERROR_CODE_MAX);
35
+ }
36
+
37
+ function providerErrorFromBody(body: Record<string, unknown>, httpStatus: number): string {
38
+ const errField = body.error;
39
+ if (typeof errField === "string") return sanitizeProviderText(errField);
40
+ if (errField && typeof errField === "object") {
41
+ const message = (errField as Record<string, unknown>).message;
42
+ if (typeof message === "string") return sanitizeProviderText(message);
43
+ }
44
+ if (typeof body.message === "string") return sanitizeProviderText(body.message);
45
+ return `Resend HTTP ${httpStatus}`;
46
+ }
47
+
48
+ function providerErrorCodeFromBody(body: Record<string, unknown>): string | null {
49
+ const errField = body.error;
50
+ const errorRecord = errField && typeof errField === "object"
51
+ ? (errField as Record<string, unknown>)
52
+ : {};
53
+ const candidates = [
54
+ errorRecord.code,
55
+ errorRecord.name,
56
+ errorRecord.type,
57
+ body.code,
58
+ body.name,
59
+ body.type,
60
+ ];
61
+ const value = candidates.find((candidate) => typeof candidate === "string" && candidate.trim() !== "");
62
+ return typeof value === "string" ? sanitizeProviderCode(value) : null;
63
+ }
64
+
65
+ function providerErrorResponse(body: Record<string, unknown>, httpStatus: number): Record<string, unknown> {
66
+ const providerErrorMessage = providerErrorFromBody(body, httpStatus);
67
+ const providerErrorCode = providerErrorCodeFromBody(body);
68
+ return {
69
+ http_status: httpStatus,
70
+ provider_error_message: providerErrorMessage,
71
+ ...(providerErrorCode ? { provider_error_code: providerErrorCode } : {}),
72
+ };
73
+ }
74
+
75
+ async function postResendEmail(
76
+ input: EmailMessage, context: Readonly<EmailEgressContext>, environmentTagName: string, fetcher: typeof fetch,
77
+ ): Promise<EmailSendResult> {
78
+ const headers: Record<string, string> = {
79
+ Authorization: `Bearer ${context.credential}`,
80
+ "Content-Type": "application/json",
81
+ };
82
+ if (input.idempotencyKey !== undefined) headers["Idempotency-Key"] = input.idempotencyKey;
83
+ if (context.mode === "sandbox_sink") headers["X-Original-Recipient"] = context.intendedRecipient;
84
+ try {
85
+ const res = await fetcher(RESEND_ENDPOINT, {
86
+ method: "POST",
87
+ headers,
88
+ body: JSON.stringify({
89
+ from: input.from,
90
+ to: [context.recipient],
91
+ subject: input.subject,
92
+ html: input.html,
93
+ ...(input.text === undefined ? {} : { text: input.text }),
94
+ ...(input.replyTo === undefined ? {} : { reply_to: input.replyTo }),
95
+ ...(input.attachments === undefined ? {} : { attachments: input.attachments }),
96
+ tags: [{
97
+ name: environmentTagName,
98
+ value: context.environmentTag,
99
+ }],
100
+ }),
101
+ signal: input.signal,
102
+ });
103
+ const parsed = await res.json().catch(() => ({}));
104
+ const body = typeof parsed === "object" && parsed !== null
105
+ ? (parsed as Record<string, unknown>)
106
+ : {};
107
+ if (!res.ok) {
108
+ const providerErrorMessage = providerErrorFromBody(body, res.status);
109
+ const providerErrorCode = providerErrorCodeFromBody(body);
110
+ return {
111
+ outcome: {
112
+ ok: false,
113
+ resendId: null,
114
+ httpStatus: res.status,
115
+ providerError: providerErrorMessage,
116
+ providerErrorCode,
117
+ aborted: false,
118
+ egressMode: context.mode,
119
+ egressReason: context.reason,
120
+ },
121
+ providerResponse: providerErrorResponse(body, res.status),
122
+ };
123
+ }
124
+ const id = typeof body.id === "string" ? body.id : null;
125
+ return {
126
+ outcome: {
127
+ ok: true,
128
+ resendId: id,
129
+ httpStatus: res.status,
130
+ providerError: null,
131
+ aborted: false,
132
+ egressMode: context.mode,
133
+ egressReason: context.reason,
134
+ },
135
+ providerResponse: { id },
136
+ };
137
+ } catch (err) {
138
+ const aborted =
139
+ input.signal?.aborted === true ||
140
+ (err instanceof Error && err.name === "AbortError");
141
+ const msg = err instanceof Error ? err.message : String(err);
142
+ return {
143
+ outcome: {
144
+ ok: false,
145
+ resendId: null,
146
+ httpStatus: 0,
147
+ providerError: sanitizeProviderText(msg),
148
+ aborted,
149
+ egressMode: context.mode,
150
+ egressReason: context.reason,
151
+ },
152
+ providerResponse: { exception: true, aborted },
153
+ };
154
+ }
155
+ }
156
+
157
+ interface ResendEmailReadResult {
158
+ ok: boolean;
159
+ httpStatus: number;
160
+ lastEvent: string | null;
161
+ recipientEmail: string | null;
162
+ providerError: string | null;
163
+ }
164
+
165
+ function readFirstRecipient(value: unknown): string | null {
166
+ if (typeof value === "string" && value.includes("@")) return value;
167
+ if (!Array.isArray(value)) return null;
168
+ return value.find((recipient): recipient is string =>
169
+ typeof recipient === "string" && recipient.includes("@")
170
+ ) ?? null;
171
+ }
172
+
173
+ // Read-only status retrieval for the delivery reconciliation poller
174
+ // (docs/platform/RUNTIME_AND_SELF_HOSTING.md Wave 4). No egress gating needed: this never
175
+ // sends mail — it only reads the provider's view of an already-sent message.
176
+ async function getResendEmail(input: {
177
+ apiKey: string;
178
+ resendId: string;
179
+ signal?: AbortSignal;
180
+ }, fetcher: typeof fetch): Promise<ResendEmailReadResult> {
181
+ try {
182
+ const res = await fetcher(`${RESEND_ENDPOINT}/${encodeURIComponent(input.resendId)}`, {
183
+ method: "GET",
184
+ headers: { Authorization: `Bearer ${input.apiKey}` },
185
+ signal: input.signal,
186
+ });
187
+ const parsed = (await res.json().catch(() => ({}))) as Record<string, unknown>;
188
+ if (!res.ok) {
189
+ return {
190
+ ok: false,
191
+ httpStatus: res.status,
192
+ lastEvent: null,
193
+ recipientEmail: null,
194
+ providerError: providerErrorFromBody(parsed, res.status),
195
+ };
196
+ }
197
+ const lastEvent = typeof parsed.last_event === "string" ? parsed.last_event : null;
198
+ return {
199
+ ok: true,
200
+ httpStatus: res.status,
201
+ lastEvent,
202
+ recipientEmail: readFirstRecipient(parsed.to),
203
+ providerError: null,
204
+ };
205
+ } catch (error) {
206
+ return {
207
+ ok: false,
208
+ httpStatus: 0,
209
+ lastEvent: null,
210
+ recipientEmail: null,
211
+ providerError: error instanceof Error ? sanitizeProviderCode(error.message) : "fetch_failed",
212
+ };
213
+ }
214
+ }
215
+
216
+ /** Poll existing IDs through the neutral runner; never retries a send. */
217
+ export function runResendDeliveryReconciliation(input: {
218
+ port: DeliveryReconciliationPort;
219
+ now: string;
220
+ graceMinutes: number;
221
+ limit: number;
222
+ apiKey: string;
223
+ fetch?: typeof fetch;
224
+ signal?: AbortSignal;
225
+ }): Promise<DeliveryReconciliationResult> {
226
+ return runDeliveryReconciliation({
227
+ port: input.port, now: input.now, graceMinutes: input.graceMinutes, limit: input.limit,
228
+ readEmail: messageId => getResendEmail({ apiKey: input.apiKey, resendId: messageId, signal: input.signal }, input.fetch ?? fetch),
229
+ observationFor: read => {
230
+ const status = read.lastEvent;
231
+ if (!status || !["delivered", "delivery_delayed", "bounced", "complained", "failed"].includes(status)) return null;
232
+ return { eventType: `email.${status}`, terminal: ["bounced", "complained", "failed"].includes(status) };
233
+ },
234
+ });
235
+ }
236
+
237
+ const eventKinds = new Map([
238
+ ["email.sent", "sent"], ["email.delivered", "delivered"], ["email.delivery_delayed", "delivery_delayed"],
239
+ ["email.opened", "open"], ["email.clicked", "click"], ["email.bounced", "bounce"],
240
+ ["email.complained", "complaint"], ["email.failed", "failed"], ["email.suppressed", "suppressed"], ["email.scheduled", "scheduled"],
241
+ ]);
242
+
243
+ /** Authenticate untouched bytes before decoding. now returns UTC seconds; receiver defaults to non-production. */
244
+ export function createResendWebhookHandler(input: {
245
+ port: EmailObservationPort;
246
+ webhookSecret: string;
247
+ environmentTagName: string;
248
+ resolveReceiverEnvironmentTag?: () => "production" | "non_production";
249
+ now?: () => number;
250
+ receivedAt?: () => Date;
251
+ }): (request: {
252
+ method?: string;
253
+ rawBody: string | Uint8Array;
254
+ headers: { webhookId?: string; timestamp?: string; signature?: string };
255
+ }) => Promise<{ status: number; body: Record<string, unknown> | "ok" }> {
256
+ return async request => {
257
+ if (request.method === "OPTIONS") return { status: 200, body: "ok" };
258
+ if (!verifyResendWebhookSignature({ ...request.headers, rawBody: request.rawBody, secret: input.webhookSecret, now: input.now })) {
259
+ return { status: 401, body: { error: "Invalid signature" } };
260
+ }
261
+ try {
262
+ const payload: unknown = JSON.parse(typeof request.rawBody === "string" ? request.rawBody : Buffer.from(request.rawBody).toString("utf8"));
263
+ const type = isRecord(payload) ? payload.type : null;
264
+ const data = isRecord(payload) ? payload.data : null;
265
+ if (typeof type !== "string" || !eventKinds.has(type)) return { status: 200, body: { ignored: true } };
266
+ const messageId = isRecord(data) && typeof data.email_id === "string" ? data.email_id : null;
267
+ if (!messageId) return { status: 200, body: { ignored: true, reason: "missing_email_id" } };
268
+ const tag = readEnvironmentTag(data, input.environmentTagName);
269
+ if (tag && tag !== (input.resolveReceiverEnvironmentTag?.() ?? "non_production")) {
270
+ return { status: 200, body: { ignored: true, reason: "foreign_send_not_found" } };
271
+ }
272
+ const disposition = await processEmailObservation({
273
+ port: input.port, messageId,
274
+ observation: () => {
275
+ const time = resolveEventTime(isRecord(payload) ? payload.created_at : null, request.headers.timestamp, input.receivedAt?.() ?? new Date());
276
+ return { eventType: type, eventAt: time.eventAt, metadata: {
277
+ providerEventId: request.headers.webhookId ?? null, eventMetadata: data ?? {},
278
+ link: isRecord(data) && isRecord(data.click) && typeof data.click.link === "string" ? data.click.link : null,
279
+ source: "resend-webhook", eventTimeSource: time.source,
280
+ } };
281
+ },
282
+ });
283
+ return disposition.outcome === "send_not_found"
284
+ ? { status: 200, body: { ignored: true, reason: "send_not_found" } }
285
+ : { status: 200, body: { success: true } };
286
+ } catch {
287
+ return { status: 500, body: { error: "Webhook processing failed" } };
288
+ }
289
+ };
290
+ }
291
+
292
+ function isRecord(value: unknown): value is Record<string, unknown> {
293
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
294
+ }
295
+ function readEnvironmentTag(data: unknown, tagName: string): "production" | "non_production" | null {
296
+ if (!isRecord(data)) return null;
297
+ const tags = data.tags;
298
+ const value = Array.isArray(tags)
299
+ ? tags.find(entry => isRecord(entry) && entry.name === tagName)?.value
300
+ : isRecord(tags) ? tags[tagName] : null;
301
+ return value === "production" || value === "non_production" ? value : null;
302
+ }
303
+ function resolveEventTime(createdAt: unknown, timestamp: string | undefined, receivedAt: Date) {
304
+ if (typeof createdAt === "string" && Number.isFinite(Date.parse(createdAt))) {
305
+ return { eventAt: new Date(createdAt).toISOString(), source: "provider_created_at" };
306
+ }
307
+ const seconds = Number(timestamp);
308
+ if (Number.isFinite(seconds) && seconds > 0) return { eventAt: new Date(seconds * 1000).toISOString(), source: "svix_timestamp" };
309
+ return { eventAt: receivedAt.toISOString(), source: "received_at" };
310
+ }
311
+
312
+ function verifyResendWebhookSignature(input: { rawBody: string | Uint8Array; webhookId?: string; timestamp?: string; signature?: string; secret: string; now?: () => number }): boolean {
313
+ const { webhookId, timestamp, signature, rawBody, secret } = input;
314
+ if (!webhookId || !timestamp || !signature || !secret) return false;
315
+
316
+ const timestampSeconds = Number(timestamp);
317
+ if (!Number.isFinite(timestampSeconds)) return false;
318
+ const now = input.now ?? (() => Math.floor(Date.now() / 1000));
319
+ if (Math.abs(now() - timestampSeconds) > 300) return false;
320
+
321
+ const secretKey = signingSecret(secret);
322
+ if (secretKey === null) return false;
323
+
324
+ const expected = createHmac("sha256", secretKey)
325
+ .update(`${webhookId}.${timestamp}.`).update(rawBody)
326
+ .digest("base64");
327
+ return signature.split(" ").some((member) =>
328
+ timingSafeStringEqual(member, `v1,${expected}`) || timingSafeStringEqual(member, expected));
329
+ }
330
+
331
+ function signingSecret(secret: string): Buffer | string | null {
332
+ if (!secret.startsWith("whsec_")) return secret;
333
+ // Svix encodes endpoint secrets after the whsec_ prefix. Buffer preserves the
334
+ // decoded bytes for node:crypto rather than treating the base64 text as a key.
335
+ // Buffer.from(base64) is deliberately permissive, so reject empty, malformed,
336
+ // or non-canonical input before it can silently become an empty/partial key.
337
+ const encoded = secret.slice("whsec_".length);
338
+ if (!encoded || !/^[A-Za-z0-9+/]+={0,2}$/.test(encoded) || encoded.length % 4 === 1) return null;
339
+ const decoded = Buffer.from(encoded, "base64");
340
+ if (decoded.length === 0) return null;
341
+ const canonicalPadded = decoded.toString("base64");
342
+ const canonicalUnpadded = canonicalPadded.replace(/=+$/, "");
343
+ return encoded === canonicalPadded || encoded === canonicalUnpadded ? decoded : null;
344
+ }
345
+
346
+ function timingSafeStringEqual(left: string, right: string): boolean {
347
+ const leftBuffer = Buffer.from(left);
348
+ const rightBuffer = Buffer.from(right);
349
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
350
+ }
@@ -0,0 +1,32 @@
1
+ import type { Contribution, RequiredSchema } from "@openlup/core/readiness";
2
+
3
+ /** Actual selected callables, supplied by the host; declarations never activate them. */
4
+ export interface CatalogContributionInput {
5
+ version: string;
6
+ requiredSchema: RequiredSchema;
7
+ catalog: Readonly<Record<string, (...args: never[]) => unknown>>;
8
+ pricing: Readonly<Record<string, (...args: never[]) => unknown>>;
9
+ promotions: Readonly<Record<string, (...args: never[]) => unknown>>;
10
+ routes: Contribution["routes"];
11
+ }
12
+
13
+ /** Thin structural declarations under the existing logical module identities. */
14
+ export function createCatalogContribution(input: CatalogContributionInput) {
15
+ const contributions: Contribution[] = [];
16
+ const ports: Record<string, unknown> = {};
17
+ for (const module of ["catalog", "pricing", "promotions"] as const) {
18
+ const selected = input[module];
19
+ for (const [name, callable] of Object.entries(selected)) ports[`@openlup/platform/${module}:${name}`] = callable;
20
+ contributions.push({
21
+ manifest: {
22
+ name: "@openlup/platform", version: input.version, kind: "rail",
23
+ module: { id: module, kind: "capability", provides: [`./${module}`], dependsOn: [] },
24
+ emits: [], handles: [], schedules: [], env: [], requiredPorts: Object.keys(selected),
25
+ routes: module === "catalog" ? input.routes.map(route => route.id) : [],
26
+ ...(module === "catalog" ? { requiredSchema: input.requiredSchema } : {}),
27
+ },
28
+ handlers: [], schedules: [], routes: module === "catalog" ? input.routes : [],
29
+ });
30
+ }
31
+ return { contributions, ports, routes: Object.fromEntries(input.routes.map(route => [`@openlup/platform/catalog:${route.id}`, route.handle])) };
32
+ }
@@ -15,3 +15,4 @@ export {
15
15
  type ServeNodeOptions,
16
16
  type ServeNodeResult,
17
17
  } from "./nodeHost.js";
18
+ export { createCatalogContribution, type CatalogContributionInput } from "./catalogContribution.js";
@@ -0,0 +1,251 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ import { calculateInventoryAtp, availableNow, INVENTORY_CONTRACT_VERSION, type InventoryStockLine } from "@openlup/core/inventory";
4
+ import type { z } from "zod";
5
+ import { createInventorySubscriptionForecastResponseSchema } from "./schemas.js";
6
+ import {
7
+ type InventorySubscriptionForecastSku,
8
+ } from "./contracts/index.js";
9
+ import {
10
+ InventoryPersistenceError,
11
+ type InventoryMutationPort,
12
+ type InventoryReadPort,
13
+ } from "./ports.js";
14
+ interface PgQueryExecutor {
15
+ query(text: string, values?: unknown[]): Promise<{ rows: Record<string, unknown>[] }>;
16
+ }
17
+
18
+ const STOCK_SQL = `SELECT sku.id AS sku_id, sku.sku, stock.source_key,
19
+ stock.for_sale_quantity AS on_hand,
20
+ COALESCE((SELECT sum(line.quantity)::integer
21
+ FROM public.fulfillment_inventory_reservation_lines line
22
+ JOIN public.fulfillment_inventory_reservations reservation ON reservation.id = line.reservation_id
23
+ WHERE line.sku = stock.sku AND reservation.source_key = stock.source_key
24
+ AND reservation.status IN ('held', 'committed')), 0) AS reserved,
25
+ stock.last_synced_at, stock.stale_after,
26
+ count(*) OVER() AS total_count
27
+ FROM public.fulfillment_stock_current stock
28
+ JOIN public.catalog_skus sku ON sku.sku = stock.sku
29
+ WHERE ($1::uuid IS NULL OR sku.id = $1::uuid)
30
+ AND ($2::boolean OR stock.for_sale_quantity > 0)
31
+ ORDER BY stock.updated_at DESC, stock.source_key, stock.sku
32
+ LIMIT $3::integer OFFSET $4::integer`;
33
+
34
+ const RESERVATIONS_SQL = `SELECT reservation.id, order_row.id AS order_id, cycle.id AS cycle_id,
35
+ CASE reservation.status
36
+ WHEN 'held' THEN 'reserved' WHEN 'committed' THEN 'reserved'
37
+ ELSE reservation.status END AS status,
38
+ reservation.expires_at, reservation.created_at,
39
+ operation.identity_key, operation.operation_fingerprint, operation.terminal_outcome,
40
+ count(*) OVER() AS total_count
41
+ FROM public.fulfillment_inventory_reservations reservation
42
+ JOIN public.subscription_renewal_operations operation ON operation.id = reservation.renewal_operation_id
43
+ LEFT JOIN public.subscription_cycles cycle ON cycle.renewal_operation_id = operation.id
44
+ LEFT JOIN public.commerce_orders order_row ON order_row.renewal_operation_id = operation.id
45
+ WHERE ($1::text IS NULL OR CASE reservation.status
46
+ WHEN 'held' THEN 'reserved' WHEN 'committed' THEN 'reserved'
47
+ ELSE reservation.status END = $1::text)
48
+ AND ($2::uuid IS NULL OR order_row.id = $2::uuid)
49
+ ORDER BY reservation.created_at DESC, reservation.id
50
+ LIMIT $3::integer OFFSET $4::integer`;
51
+
52
+ const ATP_SQL = `SELECT sku.id AS sku_id, sku.sku, stock.source_key,
53
+ stock.for_sale_quantity AS on_hand,
54
+ COALESCE((SELECT sum(line.quantity)::integer
55
+ FROM public.fulfillment_inventory_reservation_lines line
56
+ JOIN public.fulfillment_inventory_reservations reservation ON reservation.id = line.reservation_id
57
+ WHERE line.sku = stock.sku AND reservation.source_key = stock.source_key
58
+ AND reservation.status IN ('held', 'committed')), 0) AS reserved,
59
+ stock.last_synced_at, stock.stale_after, 0 AS total_count
60
+ FROM public.fulfillment_stock_current stock
61
+ JOIN public.catalog_skus sku ON sku.sku = stock.sku
62
+ WHERE sku.id = ANY($1::uuid[])
63
+ AND stock.inventory_class = 'sellable'
64
+ AND stock.stale_after > $2::timestamptz
65
+ ORDER BY stock.source_key, stock.sku`;
66
+
67
+ const FORECAST_SQL = `WITH forecast_skus AS (
68
+ SELECT line.sku
69
+ FROM public.subscription_renewal_term_lines line
70
+ JOIN public.subscriptions subscription ON subscription.id = line.subscription_id
71
+ WHERE subscription.status = 'active'
72
+ UNION
73
+ SELECT stock.sku
74
+ FROM public.fulfillment_stock_current stock
75
+ WHERE stock.inventory_class = 'sellable'
76
+ ), sellable_stock AS (
77
+ SELECT stock.sku,
78
+ sum(stock.for_sale_quantity)::integer AS for_sale_quantity,
79
+ max(stock.last_synced_at) AS last_synced_at,
80
+ min(stock.stale_after) AS stale_after
81
+ FROM public.fulfillment_stock_current stock
82
+ WHERE stock.inventory_class = 'sellable'
83
+ GROUP BY stock.sku
84
+ )
85
+ SELECT sku.id AS sku_id, sku.sku,
86
+ stock.for_sale_quantity, stock.last_synced_at, stock.stale_after,
87
+ COALESCE(reserved.quantity, 0)::integer AS reserved,
88
+ COALESCE(demand.days_30, 0)::integer AS days_30,
89
+ COALESCE(demand.days_45, 0)::integer AS days_45,
90
+ COALESCE(demand.days_60, 0)::integer AS days_60
91
+ FROM forecast_skus forecast
92
+ JOIN public.catalog_skus sku ON sku.sku = forecast.sku
93
+ LEFT JOIN sellable_stock stock ON stock.sku = forecast.sku
94
+ LEFT JOIN LATERAL (
95
+ SELECT sum(line.quantity)::integer AS quantity
96
+ FROM public.fulfillment_inventory_reservation_lines line
97
+ JOIN public.fulfillment_inventory_reservations reservation ON reservation.id = line.reservation_id
98
+ WHERE line.sku = forecast.sku
99
+ AND reservation.status IN ('held', 'committed')) reserved ON true
100
+ LEFT JOIN LATERAL (
101
+ SELECT
102
+ sum(CASE WHEN subscription.next_cycle_at <= $1::timestamptz + interval '30 days' THEN line.quantity ELSE 0 END)::integer AS days_30,
103
+ sum(CASE WHEN subscription.next_cycle_at <= $1::timestamptz + interval '45 days' THEN line.quantity ELSE 0 END)::integer AS days_45,
104
+ sum(CASE WHEN subscription.next_cycle_at <= $1::timestamptz + interval '60 days' THEN line.quantity ELSE 0 END)::integer AS days_60
105
+ FROM public.subscription_renewal_term_lines line
106
+ JOIN public.subscriptions subscription ON subscription.id = line.subscription_id
107
+ WHERE line.sku = forecast.sku AND subscription.status = 'active'
108
+ AND subscription.next_cycle_at <= $1::timestamptz + ($2::integer * interval '1 day')) demand ON true
109
+ ORDER BY sku.sku`;
110
+
111
+ const ACTIVE_SUBSCRIPTION_COUNT_SQL = `SELECT count(*)::integer AS active_subscription_count
112
+ FROM public.subscriptions WHERE status = 'active'`;
113
+
114
+ /** Operator readback over the same public stock and reservation rail used by renewal. */
115
+ export function createPostgresAdminInventoryPort(
116
+ executor: PgQueryExecutor,
117
+ config: { stockMasterProvider: string; providerSchema: z.ZodType<string> },
118
+ now: () => string = () => new Date().toISOString(),
119
+ ): InventoryReadPort & InventoryMutationPort {
120
+ const forecastResponseSchema = createInventorySubscriptionForecastResponseSchema(config.providerSchema);
121
+ return {
122
+ async listStock(request) {
123
+ const offset = (request.page - 1) * request.pageSize;
124
+ const result = await executor.query(STOCK_SQL, [
125
+ request.skuId ?? null, request.includeZero, request.pageSize, offset,
126
+ ]);
127
+ const stock = result.rows.map(mapStock);
128
+ return {
129
+ contractVersion: INVENTORY_CONTRACT_VERSION,
130
+ stock: stock.map((line) => ({ ...line, availableNow: availableNow(line) })),
131
+ totalCount: count(result.rows[0]), page: request.page, pageSize: request.pageSize,
132
+ };
133
+ },
134
+
135
+ async listReservations(request) {
136
+ const offset = (request.page - 1) * request.pageSize;
137
+ const result = await executor.query(RESERVATIONS_SQL, [
138
+ request.status ?? null, request.orderId ?? null, request.pageSize, offset,
139
+ ]);
140
+ return {
141
+ contractVersion: INVENTORY_CONTRACT_VERSION,
142
+ reservations: result.rows.map((row) => ({
143
+ id: text(row.id), orderId: text(row.order_id),
144
+ subscriptionCycleId: nullableText(row.cycle_id),
145
+ status: text(row.status) as "reserved" | "released" | "consumed" | "expired",
146
+ kind: "subscription_retry_window" as const,
147
+ expiresAt: timestampOrNull(row.expires_at), createdAt: timestamp(row.created_at),
148
+ })),
149
+ totalCount: count(result.rows[0]), page: request.page, pageSize: request.pageSize,
150
+ };
151
+ },
152
+
153
+ async checkAtp(request) {
154
+ const result = await executor.query(ATP_SQL, [
155
+ request.lines.map((line) => line.skuId), request.requestedAt,
156
+ ]);
157
+ return calculateInventoryAtp({ request, stock: result.rows.map(mapStock) });
158
+ },
159
+
160
+ async forecastSubscriptions(request) {
161
+ const generatedAt = now();
162
+ const result = await executor.query(FORECAST_SQL, [generatedAt, request.horizonDays]);
163
+ const activeSubscriptionResult = await executor.query(ACTIVE_SUBSCRIPTION_COUNT_SQL);
164
+ const skus = result.rows.map((row) => mapForecast(row, generatedAt, request));
165
+ return forecastResponseSchema.parse({
166
+ contractVersion: INVENTORY_CONTRACT_VERSION,
167
+ generatedAt,
168
+ // Compatibility projection required by the existing admin BFF contract;
169
+ // the public tables themselves keep source_key opaque.
170
+ stockMasterProvider: config.stockMasterProvider,
171
+ horizonDays: request.horizonDays,
172
+ protectionDays: request.protectionDays,
173
+ skus,
174
+ totals: {
175
+ activeSubscriptionCount: integer(
176
+ activeSubscriptionResult.rows[0]?.active_subscription_count,
177
+ ),
178
+ excludedSubscriptionCount: 0,
179
+ missingProviderStockSkuCount: skus.filter((sku) => sku.stockStatus === "missing").length,
180
+ staleProviderStockSkuCount: skus.filter((sku) => sku.stockStatus === "stale").length,
181
+ horizonShortageSkuCount: skus.filter((sku) => sku.horizonMissingQty > 0).length,
182
+ protectionShortageSkuCount: skus.filter((sku) => sku.protectionMissingQty > 0).length,
183
+ shortageSkuCount: skus.filter((sku) => sku.missingQty > 0).length,
184
+ recommendedProductionQty: skus.reduce((sum, sku) => sum + sku.recommendedProductionQty, 0),
185
+ },
186
+ });
187
+ },
188
+
189
+ async adjustStock() {
190
+ throw new InventoryPersistenceError(
191
+ "Direct stock is externally observed; use the configured stock-source adapter",
192
+ );
193
+ },
194
+ };
195
+ }
196
+
197
+ function mapStock(row: Record<string, unknown>): InventoryStockLine {
198
+ const source = text(row.source_key);
199
+ return {
200
+ skuId: text(row.sku_id), sku: text(row.sku),
201
+ locationId: stableUuid(source), locationCode: source,
202
+ locationKind: "third_party_logistics", locationStatus: "active", fulfillable: true,
203
+ lotId: null, lotCode: null, lotStatus: null, expiresAt: null,
204
+ onHand: integer(row.on_hand), reserved: integer(row.reserved), unavailable: 0,
205
+ incoming: 0, safetyStock: 0,
206
+ };
207
+ }
208
+
209
+ function mapForecast(
210
+ row: Record<string, unknown>,
211
+ generatedAt: string,
212
+ request: { horizonDays: number; protectionDays: number },
213
+ ): InventorySubscriptionForecastSku {
214
+ const sellable = Math.max(0, integer(row.for_sale_quantity) - integer(row.reserved));
215
+ const horizonDemand = request.horizonDays <= 30 ? integer(row.days_30)
216
+ : request.horizonDays <= 45 ? integer(row.days_45) : integer(row.days_60);
217
+ const protectionDemand = request.protectionDays <= 30 ? integer(row.days_30)
218
+ : request.protectionDays <= 45 ? integer(row.days_45) : integer(row.days_60);
219
+ const horizonMissingQty = Math.max(0, horizonDemand - sellable);
220
+ const protectionMissingQty = Math.max(0, protectionDemand - sellable);
221
+ const staleAfter = timestampOrNull(row.stale_after);
222
+ const stockStatus = row.for_sale_quantity === null || row.for_sale_quantity === undefined
223
+ ? "missing" as const
224
+ : staleAfter && staleAfter <= generatedAt ? "stale" as const : "fresh" as const;
225
+ return {
226
+ skuId: text(row.sku_id), sku: text(row.sku),
227
+ providerCurrentForSale: nullableInteger(row.for_sale_quantity),
228
+ providerStockLastSyncedAt: timestampOrNull(row.last_synced_at),
229
+ providerStockStaleAfter: staleAfter, stockStatus,
230
+ openHardReservations: integer(row.reserved), safetyStock: 0, sellableNow: sellable,
231
+ activeSubscriptionDemand30: integer(row.days_30),
232
+ activeSubscriptionDemand45: integer(row.days_45),
233
+ activeSubscriptionDemand60: integer(row.days_60), plannedSupply: 0,
234
+ coverageDays: null, shortageDate: null,
235
+ horizonMissingQty, protectionMissingQty,
236
+ missingQty: Math.max(horizonMissingQty, protectionMissingQty),
237
+ recommendedProductionQty: Math.max(horizonMissingQty, protectionMissingQty),
238
+ };
239
+ }
240
+
241
+ function stableUuid(value: string): string {
242
+ const hash = createHash("sha256").update(`inventory-source:${value}`).digest("hex");
243
+ return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-4${hash.slice(13, 16)}-8${hash.slice(17, 20)}-${hash.slice(20, 32)}`;
244
+ }
245
+ function text(value: unknown): string { return typeof value === "string" ? value : String(value ?? ""); }
246
+ function nullableText(value: unknown): string | null { const result = text(value).trim(); return result || null; }
247
+ function integer(value: unknown): number { const result = Number(value); return Number.isSafeInteger(result) && result >= 0 ? result : 0; }
248
+ function nullableInteger(value: unknown): number | null { return value === null || value === undefined ? null : integer(value); }
249
+ function count(row: Record<string, unknown> | undefined): number { return row ? integer(row.total_count) : 0; }
250
+ function timestamp(value: unknown): string { return value instanceof Date ? value.toISOString() : text(value); }
251
+ function timestampOrNull(value: unknown): string | null { return value === null || value === undefined ? null : timestamp(value); }