@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.
- package/CHANGELOG.md +96 -0
- package/README.md +284 -6
- package/dist/catalog/activeSkuSelection.d.ts +2 -0
- package/dist/catalog/activeSkuSelection.js +20 -0
- package/dist/catalog/adminBindings.d.ts +72 -0
- package/dist/catalog/adminBindings.js +1 -0
- package/dist/catalog/adminBundleHandler.d.ts +26 -0
- package/dist/catalog/adminBundleHandler.js +111 -0
- package/dist/catalog/adminBundleReadHandler.d.ts +20 -0
- package/dist/catalog/adminBundleReadHandler.js +185 -0
- package/dist/catalog/adminBundleWritePort.d.ts +85 -0
- package/dist/catalog/adminBundleWritePort.js +1 -0
- package/dist/catalog/bundleAvailability.d.ts +63 -0
- package/dist/catalog/bundleAvailability.js +78 -0
- package/dist/catalog/bundleAvailabilityService.d.ts +65 -0
- package/dist/catalog/bundleAvailabilityService.js +89 -0
- package/dist/catalog/bundleBindings.d.ts +33 -0
- package/dist/catalog/bundleBindings.js +1 -0
- package/dist/catalog/bundleCatalogQueries.d.ts +28 -0
- package/dist/catalog/bundleCatalogQueries.js +84 -0
- package/dist/catalog/bundleCatalogReadPort.d.ts +166 -0
- package/dist/catalog/bundleCatalogReadPort.js +36 -0
- package/dist/catalog/bundleCatalogStore.d.ts +3 -0
- package/dist/catalog/bundleCatalogStore.js +152 -0
- package/dist/catalog/bundleRuleCodes.d.ts +41 -0
- package/dist/catalog/bundleRuleCodes.js +110 -0
- package/dist/catalog/bundleRules.d.ts +69 -0
- package/dist/catalog/bundleRules.js +102 -0
- package/dist/catalog/catalogDraftStore.d.ts +4 -0
- package/dist/catalog/catalogDraftStore.js +63 -0
- package/dist/catalog/catalogSkuEnvelope.d.ts +7 -0
- package/dist/catalog/catalogSkuEnvelope.js +142 -0
- package/dist/catalog/catalogSkuEnvelopeMapper.d.ts +38 -0
- package/dist/catalog/catalogSkuEnvelopeMapper.js +88 -0
- package/dist/catalog/contracts/bundleAdmin.d.ts +143 -0
- package/dist/catalog/contracts/bundleAdmin.js +1 -0
- package/dist/catalog/contracts/bundleAvailability.d.ts +61 -0
- package/dist/catalog/contracts/bundleAvailability.js +1 -0
- package/dist/catalog/contracts/bundleRead.d.ts +223 -0
- package/dist/catalog/contracts/bundleRead.js +1 -0
- package/dist/catalog/contracts/draft.d.ts +79 -0
- package/dist/catalog/contracts/draft.js +47 -0
- package/dist/catalog/contracts/foundation.d.ts +78 -0
- package/dist/catalog/contracts/foundation.js +53 -0
- package/dist/catalog/contracts/index.d.ts +14 -0
- package/dist/catalog/contracts/index.js +6 -0
- package/dist/catalog/contracts/lifecycle.d.ts +77 -0
- package/dist/catalog/contracts/lifecycle.js +1 -0
- package/dist/catalog/contracts/productMediaContracts.d.ts +77 -0
- package/dist/catalog/contracts/productMediaContracts.js +149 -0
- package/dist/catalog/contracts/productTypes.d.ts +54 -0
- package/dist/catalog/contracts/productTypes.js +16 -0
- package/dist/catalog/contracts/resolveProductMedia.d.ts +3 -0
- package/dist/catalog/contracts/resolveProductMedia.js +78 -0
- package/dist/catalog/contracts/sellable.d.ts +23 -0
- package/dist/catalog/contracts/sellable.js +1 -0
- package/dist/catalog/draftHandler.d.ts +25 -0
- package/dist/catalog/draftHandler.js +118 -0
- package/dist/catalog/draftPorts.d.ts +18 -0
- package/dist/catalog/draftPorts.js +8 -0
- package/dist/catalog/draftSchemas.d.ts +423 -0
- package/dist/catalog/draftSchemas.js +89 -0
- package/dist/catalog/draftValidation.d.ts +21 -0
- package/dist/catalog/draftValidation.js +125 -0
- package/dist/catalog/foundationSchemas.d.ts +98 -0
- package/dist/catalog/foundationSchemas.js +110 -0
- package/dist/catalog/index.d.ts +43 -0
- package/dist/catalog/index.js +25 -0
- package/dist/catalog/mediaSourcePack.d.ts +51 -0
- package/dist/catalog/mediaSourcePack.js +247 -0
- package/dist/catalog/productTypeSchemas.d.ts +46 -0
- package/dist/catalog/productTypeSchemas.js +72 -0
- package/dist/catalog/publicationPorts.d.ts +42 -0
- package/dist/catalog/publicationPorts.js +1 -0
- package/dist/catalog/readPorts.d.ts +51 -0
- package/dist/catalog/readPorts.js +1 -0
- package/dist/catalog/referenceCatalogHandler.d.ts +7 -0
- package/dist/catalog/referenceCatalogHandler.js +27 -0
- package/dist/catalog/resolvedSkuProjection.d.ts +88 -0
- package/dist/catalog/resolvedSkuProjection.js +60 -0
- package/dist/catalog/sellableBundleHandler.d.ts +46 -0
- package/dist/catalog/sellableBundleHandler.js +84 -0
- package/dist/catalog/sellableSchemas.d.ts +46 -0
- package/dist/catalog/sellableSchemas.js +27 -0
- package/dist/catalog/sql.d.ts +46 -0
- package/dist/catalog/sql.js +13 -0
- package/dist/catalog/validation.d.ts +2 -0
- package/dist/catalog/validation.js +3 -0
- package/dist/communications/contracts/index.d.ts +1 -0
- package/dist/communications/contracts/marketingSync.d.ts +194 -0
- package/dist/communications/contracts/marketingSync.js +1 -0
- package/dist/communications/index.d.ts +1 -0
- package/dist/communications/klaviyo/consentPush.d.ts +51 -0
- package/dist/communications/klaviyo/consentPush.js +19 -0
- package/dist/communications/klaviyo/index.d.ts +110 -0
- package/dist/communications/klaviyo/index.js +860 -0
- package/dist/communications/newsletterIntegrationContracts.d.ts +3 -3
- package/dist/communications/resend/index.d.ts +40 -0
- package/dist/communications/resend/index.js +310 -0
- package/dist/composition/catalogContribution.d.ts +18 -0
- package/dist/composition/catalogContribution.js +21 -0
- package/dist/composition/index.d.ts +1 -0
- package/dist/composition/index.js +1 -0
- package/dist/identity/customerContracts.d.ts +2 -2
- package/dist/inventory/adminInventory.d.ts +13 -0
- package/dist/inventory/adminInventory.js +214 -0
- package/dist/inventory/contracts/forecast.d.ts +86 -0
- package/dist/inventory/contracts/forecast.js +1 -0
- package/dist/inventory/contracts/index.d.ts +4 -0
- package/dist/inventory/contracts/index.js +1 -0
- package/dist/inventory/contracts/inventory.d.ts +157 -0
- package/dist/inventory/contracts/inventory.js +1 -0
- package/dist/inventory/contracts/newPurchaseStock.d.ts +20 -0
- package/dist/inventory/contracts/newPurchaseStock.js +1 -0
- package/dist/inventory/contracts/reservation.d.ts +18 -0
- package/dist/inventory/contracts/reservation.js +1 -0
- package/dist/inventory/forecast.d.ts +24 -0
- package/dist/inventory/forecast.js +200 -0
- package/dist/inventory/index.d.ts +13 -0
- package/dist/inventory/index.js +10 -0
- package/dist/inventory/inventoryHandlers.d.ts +53 -0
- package/dist/inventory/inventoryHandlers.js +151 -0
- package/dist/inventory/newPurchaseStockAvailability.d.ts +47 -0
- package/dist/inventory/newPurchaseStockAvailability.js +75 -0
- package/dist/inventory/ports.d.ts +21 -0
- package/dist/inventory/ports.js +16 -0
- package/dist/inventory/renewalReservation.d.ts +27 -0
- package/dist/inventory/renewalReservation.js +61 -0
- package/dist/inventory/reservationPolicy.d.ts +6 -0
- package/dist/inventory/reservationPolicy.js +19 -0
- package/dist/inventory/schemas.d.ts +137 -0
- package/dist/inventory/schemas.js +112 -0
- package/dist/inventory/sql.d.ts +14 -0
- package/dist/inventory/sql.js +5 -0
- package/dist/inventory/subscriptionForecastCalculation.d.ts +4 -0
- package/dist/inventory/subscriptionForecastCalculation.js +133 -0
- package/dist/pricing/acquisitionFinancialPolicy.d.ts +26 -0
- package/dist/pricing/acquisitionFinancialPolicy.js +1 -0
- package/dist/pricing/acquisitionOfferPricing.d.ts +27 -0
- package/dist/pricing/acquisitionOfferPricing.js +25 -0
- package/dist/pricing/acquisitionOfferPricingHandler.d.ts +20 -0
- package/dist/pricing/acquisitionOfferPricingHandler.js +105 -0
- package/dist/pricing/acquisitionOfferPricingSchemas.d.ts +31 -0
- package/dist/pricing/acquisitionOfferPricingSchemas.js +33 -0
- package/dist/pricing/acquisitionQuoteBinding.d.ts +12 -0
- package/dist/pricing/acquisitionQuoteBinding.js +12 -0
- package/dist/pricing/acquisitionReadiness.d.ts +31 -0
- package/dist/pricing/acquisitionReadiness.js +117 -0
- package/dist/pricing/acquisitionTerms.d.ts +19 -0
- package/dist/pricing/acquisitionTerms.js +126 -0
- package/dist/pricing/acquisitionTermsSchemas.d.ts +59 -0
- package/dist/pricing/acquisitionTermsSchemas.js +42 -0
- package/dist/pricing/catalogFactsProvenance.d.ts +33 -0
- package/dist/pricing/catalogFactsProvenance.js +64 -0
- package/dist/pricing/catalogFactsSchemas.d.ts +45 -0
- package/dist/pricing/catalogFactsSchemas.js +58 -0
- package/dist/pricing/commerceOfferPricing.d.ts +25 -0
- package/dist/pricing/commerceOfferPricing.js +139 -0
- package/dist/pricing/commerceOfferPricingCache.d.ts +10 -0
- package/dist/pricing/commerceOfferPricingCache.js +44 -0
- package/dist/pricing/commerceOfferPricingHandler.d.ts +21 -0
- package/dist/pricing/commerceOfferPricingHandler.js +78 -0
- package/dist/pricing/commercePriceAuthority.d.ts +26 -0
- package/dist/pricing/commercePriceAuthority.js +79 -0
- package/dist/pricing/commercePricingPolicyErrors.d.ts +40 -0
- package/dist/pricing/commercePricingPolicyErrors.js +67 -0
- package/dist/pricing/commerceQuoteBatchHandler.d.ts +37 -0
- package/dist/pricing/commerceQuoteBatchHandler.js +83 -0
- package/dist/pricing/commerceQuoteCatalogReadPort.d.ts +27 -0
- package/dist/pricing/commerceQuoteCatalogReadPort.js +8 -0
- package/dist/pricing/commerceQuoteHandler.d.ts +31 -0
- package/dist/pricing/commerceQuoteHandler.js +110 -0
- package/dist/pricing/commerceQuoteSnapshotExpect.d.ts +9 -0
- package/dist/pricing/commerceQuoteSnapshotExpect.js +22 -0
- package/dist/pricing/contracts/acquisitionTerms.d.ts +32 -0
- package/dist/pricing/contracts/acquisitionTerms.js +2 -0
- package/dist/pricing/contracts/authority.d.ts +27 -0
- package/dist/pricing/contracts/authority.js +1 -0
- package/dist/pricing/contracts/evidence.d.ts +33 -0
- package/dist/pricing/contracts/evidence.js +1 -0
- package/dist/pricing/contracts/index.d.ts +17 -0
- package/dist/pricing/contracts/index.js +7 -0
- package/dist/pricing/contracts/money.d.ts +27 -0
- package/dist/pricing/contracts/money.js +1 -0
- package/dist/pricing/contracts/offer.d.ts +52 -0
- package/dist/pricing/contracts/offer.js +4 -0
- package/dist/pricing/contracts/offerPolicy.d.ts +16 -0
- package/dist/pricing/contracts/offerPolicy.js +5 -0
- package/dist/pricing/contracts/policy.d.ts +22 -0
- package/dist/pricing/contracts/policy.js +3 -0
- package/dist/pricing/contracts/provenance.d.ts +24 -0
- package/dist/pricing/contracts/provenance.js +2 -0
- package/dist/pricing/contracts/quote.d.ts +94 -0
- package/dist/pricing/contracts/quote.js +1 -0
- package/dist/pricing/dbBackedCommerceQuoteHelpers.d.ts +78 -0
- package/dist/pricing/dbBackedCommerceQuoteHelpers.js +146 -0
- package/dist/pricing/dbBackedCommerceQuotePort.d.ts +66 -0
- package/dist/pricing/dbBackedCommerceQuotePort.js +206 -0
- package/dist/pricing/dbBackedCommerceQuoteSnapshotVerifier.d.ts +4 -0
- package/dist/pricing/dbBackedCommerceQuoteSnapshotVerifier.js +25 -0
- package/dist/pricing/index.d.ts +51 -0
- package/dist/pricing/index.js +36 -0
- package/dist/pricing/offerPolicyReadiness.d.ts +21 -0
- package/dist/pricing/offerPolicyReadiness.js +79 -0
- package/dist/pricing/offerPolicySchemas.d.ts +25 -0
- package/dist/pricing/offerPolicySchemas.js +15 -0
- package/dist/pricing/offerPricingSchemas.d.ts +315 -0
- package/dist/pricing/offerPricingSchemas.js +65 -0
- package/dist/pricing/ports.d.ts +13 -0
- package/dist/pricing/ports.js +9 -0
- package/dist/pricing/pricingPolicyToken.d.ts +39 -0
- package/dist/pricing/pricingPolicyToken.js +84 -0
- package/dist/pricing/quoteAcquisitionTerms.d.ts +2 -0
- package/dist/pricing/quoteAcquisitionTerms.js +4 -0
- package/dist/pricing/quoteCodeRejectionDetails.d.ts +20 -0
- package/dist/pricing/quoteCodeRejectionDetails.js +22 -0
- package/dist/pricing/quotePorts.d.ts +30 -0
- package/dist/pricing/quotePorts.js +20 -0
- package/dist/pricing/quotePrimitives.d.ts +13 -0
- package/dist/pricing/quotePrimitives.js +18 -0
- package/dist/pricing/quotePromotionEvidence.d.ts +39 -0
- package/dist/pricing/quotePromotionEvidence.js +73 -0
- package/dist/pricing/quoteSchemas.d.ts +1176 -0
- package/dist/pricing/quoteSchemas.js +196 -0
- package/dist/pricing/subscriptionPricePolicy.d.ts +52 -0
- package/dist/pricing/subscriptionPricePolicy.js +96 -0
- package/dist/pricing/taxProfileGuard.d.ts +2 -0
- package/dist/pricing/taxProfileGuard.js +24 -0
- package/dist/pricing/validation.d.ts +2 -0
- package/dist/pricing/validation.js +3 -0
- package/dist/promotions/adminBindings.d.ts +72 -0
- package/dist/promotions/adminBindings.js +1 -0
- package/dist/promotions/adminPromotionCodesContracts.d.ts +498 -0
- package/dist/promotions/adminPromotionCodesContracts.js +211 -0
- package/dist/promotions/adminPromotionCodesDataPort.d.ts +47 -0
- package/dist/promotions/adminPromotionCodesDataPort.js +1 -0
- package/dist/promotions/adminPromotionCodesHandler.d.ts +14 -0
- package/dist/promotions/adminPromotionCodesHandler.js +194 -0
- package/dist/promotions/adminPromotionsContracts.d.ts +235 -0
- package/dist/promotions/adminPromotionsContracts.js +99 -0
- package/dist/promotions/adminPromotionsDataPort.d.ts +42 -0
- package/dist/promotions/adminPromotionsDataPort.js +1 -0
- package/dist/promotions/adminPromotionsHandler.d.ts +16 -0
- package/dist/promotions/adminPromotionsHandler.js +47 -0
- package/dist/promotions/contracts/index.d.ts +387 -0
- package/dist/promotions/contracts/index.js +1 -0
- package/dist/promotions/index.d.ts +22 -0
- package/dist/promotions/index.js +21 -0
- package/dist/promotions/promoDataPort.d.ts +47 -0
- package/dist/promotions/promoDataPort.js +14 -0
- package/dist/promotions/promotionAcceptanceTelemetry.d.ts +29 -0
- package/dist/promotions/promotionAcceptanceTelemetry.js +45 -0
- package/dist/promotions/promotionBenefitDefinition.d.ts +37 -0
- package/dist/promotions/promotionBenefitDefinition.js +33 -0
- package/dist/promotions/promotionClaimSweepPort.d.ts +14 -0
- package/dist/promotions/promotionClaimSweepPort.js +8 -0
- package/dist/promotions/promotionClaims.d.ts +5 -0
- package/dist/promotions/promotionClaims.js +57 -0
- package/dist/promotions/promotionCodePreview.d.ts +11 -0
- package/dist/promotions/promotionCodePreview.js +154 -0
- package/dist/promotions/promotionCodeQuotePort.d.ts +25 -0
- package/dist/promotions/promotionCodeQuotePort.js +1 -0
- package/dist/promotions/promotionQuoteAcceptance.d.ts +65 -0
- package/dist/promotions/promotionQuoteAcceptance.js +202 -0
- package/dist/promotions/promotionQuoteBinding.d.ts +71 -0
- package/dist/promotions/promotionQuoteBinding.js +85 -0
- package/dist/promotions/quoteAutomaticPromotions.d.ts +24 -0
- package/dist/promotions/quoteAutomaticPromotions.js +36 -0
- package/dist/promotions/quotePromoDiscounts.d.ts +32 -0
- package/dist/promotions/quotePromoDiscounts.js +140 -0
- package/dist/promotions/quotePromotionCodeAdjustments.d.ts +27 -0
- package/dist/promotions/quotePromotionCodeAdjustments.js +223 -0
- package/dist/promotions/quotePromotionV2.d.ts +29 -0
- package/dist/promotions/quotePromotionV2.js +174 -0
- package/dist/promotions/sql.d.ts +14 -0
- package/dist/promotions/sql.js +5 -0
- package/dist/promotions/validation.d.ts +2 -0
- package/dist/promotions/validation.js +3 -0
- package/dist/shipping/addressCanonLookupHandler.d.ts +7 -0
- package/dist/shipping/addressCanonLookupHandler.js +105 -0
- package/dist/shipping/addressCanonical.d.ts +2 -0
- package/dist/shipping/addressCanonical.js +26 -0
- package/dist/shipping/addressPorts.d.ts +39 -0
- package/dist/shipping/addressPorts.js +1 -0
- package/dist/shipping/addressSchemas.d.ts +30 -0
- package/dist/shipping/addressSchemas.js +1 -0
- package/dist/shipping/adminShippingRateHandler.d.ts +4 -0
- package/dist/shipping/adminShippingRateHandler.js +64 -0
- package/dist/shipping/contracts/address.d.ts +144 -0
- package/dist/shipping/contracts/address.js +1 -0
- package/dist/shipping/contracts/delivery.d.ts +110 -0
- package/dist/shipping/contracts/delivery.js +1 -0
- package/dist/shipping/contracts/index.d.ts +3 -0
- package/dist/shipping/contracts/index.js +1 -0
- package/dist/shipping/contracts/shippingRate.d.ts +11 -0
- package/dist/shipping/contracts/shippingRate.js +1 -0
- package/dist/shipping/customerAddressesHandler.d.ts +5 -0
- package/dist/shipping/customerAddressesHandler.js +80 -0
- package/dist/shipping/customerSelfServiceAddressModels.d.ts +27 -0
- package/dist/shipping/customerSelfServiceAddressModels.js +101 -0
- package/dist/shipping/deliverySelectionHandler.d.ts +10 -0
- package/dist/shipping/deliverySelectionHandler.js +77 -0
- package/dist/shipping/deliverySelectionPort.d.ts +8 -0
- package/dist/shipping/deliverySelectionPort.js +1 -0
- package/dist/shipping/deliverySelectionSummary.d.ts +2 -0
- package/dist/shipping/deliverySelectionSummary.js +53 -0
- package/dist/shipping/handlerSchemas.d.ts +21 -0
- package/dist/shipping/handlerSchemas.js +1 -0
- package/dist/shipping/index.d.ts +17 -0
- package/dist/shipping/index.js +8 -0
- package/dist/shipping/pickupPointSearchPort.d.ts +4 -0
- package/dist/shipping/pickupPointSearchPort.js +1 -0
- package/dist/shipping/readDeliverySelectionEvidence.d.ts +2 -0
- package/dist/shipping/readDeliverySelectionEvidence.js +44 -0
- package/dist/shipping/shippingRatePorts.d.ts +52 -0
- package/dist/shipping/shippingRatePorts.js +1 -0
- package/package.json +62 -2
- package/release-gates.json +218 -10
- package/sql/20260807140000_read_parity_catalog.sql +94 -0
- package/sql/20260810120000_storefront_catalog_attributes.sql +51 -0
- package/sql/20260812140000_sellable_bundle_catalog.sql +124 -0
- package/sql/20260812210000_bundle_operator_write_boundary.sql +567 -0
- package/sql/20260814000000_fulfillment_stock_evidence.sql +239 -0
- package/sql/20260815120000_automatic_subscription_renewal.sql +1083 -0
- package/sql/20260817180000_promotion_claim_lifecycle.sql +591 -0
- package/sql/20260817180100_promotion_claim_lifecycle_validation.sql +23 -0
- package/sql/20260827220000_catalog_document_revision_foundation.sql +226 -0
- package/sql/20260827220001_catalog_document_revision_foundation_validate.sql +10 -0
- package/sql/20260908100000_catalog_draft_foundation.sql +217 -0
- package/src/catalog/activeSkuSelection.ts +20 -0
- package/src/catalog/adminBindings.ts +59 -0
- package/src/catalog/adminBundleHandler.ts +165 -0
- package/src/catalog/adminBundleReadHandler.ts +261 -0
- package/src/catalog/adminBundleWritePort.ts +102 -0
- package/src/catalog/bundleAvailability.ts +129 -0
- package/src/catalog/bundleAvailabilityService.ts +171 -0
- package/src/catalog/bundleBindings.ts +29 -0
- package/src/catalog/bundleCatalogQueries.ts +98 -0
- package/src/catalog/bundleCatalogReadPort.ts +177 -0
- package/src/catalog/bundleCatalogStore.ts +269 -0
- package/src/catalog/bundleRuleCodes.ts +134 -0
- package/src/catalog/bundleRules.ts +153 -0
- package/src/catalog/catalogDraftStore.ts +71 -0
- package/src/catalog/catalogSkuEnvelope.ts +214 -0
- package/src/catalog/catalogSkuEnvelopeMapper.ts +160 -0
- package/src/catalog/contracts/bundleAdmin.ts +20 -0
- package/src/catalog/contracts/bundleAvailability.ts +5 -0
- package/src/catalog/contracts/bundleRead.ts +13 -0
- package/src/catalog/contracts/draft.ts +69 -0
- package/src/catalog/contracts/foundation.ts +104 -0
- package/src/catalog/contracts/index.ts +14 -0
- package/src/catalog/contracts/lifecycle.ts +74 -0
- package/src/catalog/contracts/productMediaContracts.ts +170 -0
- package/src/catalog/contracts/productTypes.ts +35 -0
- package/src/catalog/contracts/resolveProductMedia.ts +85 -0
- package/src/catalog/contracts/sellable.ts +12 -0
- package/src/catalog/draftHandler.ts +105 -0
- package/src/catalog/draftPorts.ts +18 -0
- package/src/catalog/draftSchemas.ts +88 -0
- package/src/catalog/draftValidation.ts +100 -0
- package/src/catalog/foundationSchemas.ts +123 -0
- package/src/catalog/index.ts +43 -0
- package/src/catalog/mediaSourcePack.ts +219 -0
- package/src/catalog/productTypeSchemas.ts +70 -0
- package/src/catalog/publicationPorts.ts +44 -0
- package/src/catalog/readPorts.ts +61 -0
- package/src/catalog/referenceCatalogHandler.ts +39 -0
- package/src/catalog/resolvedSkuProjection.ts +76 -0
- package/src/catalog/sellableBundleHandler.ts +133 -0
- package/src/catalog/sellableSchemas.ts +33 -0
- package/src/catalog/sql.ts +14 -0
- package/src/catalog/validation.ts +3 -0
- package/src/communications/contracts/index.ts +1 -0
- package/src/communications/contracts/marketingSync.ts +109 -0
- package/src/communications/index.ts +1 -0
- package/src/communications/klaviyo/consentPush.ts +19 -0
- package/src/communications/klaviyo/index.ts +649 -0
- package/src/communications/resend/index.ts +350 -0
- package/src/composition/catalogContribution.ts +32 -0
- package/src/composition/index.ts +1 -0
- package/src/inventory/adminInventory.ts +251 -0
- package/src/inventory/contracts/forecast.ts +96 -0
- package/src/inventory/contracts/index.ts +18 -0
- package/src/inventory/contracts/inventory.ts +14 -0
- package/src/inventory/contracts/newPurchaseStock.ts +23 -0
- package/src/inventory/contracts/reservation.ts +20 -0
- package/src/inventory/forecast.ts +239 -0
- package/src/inventory/index.ts +21 -0
- package/src/inventory/inventoryHandlers.ts +215 -0
- package/src/inventory/newPurchaseStockAvailability.ts +120 -0
- package/src/inventory/ports.ts +46 -0
- package/src/inventory/renewalReservation.ts +88 -0
- package/src/inventory/reservationPolicy.ts +27 -0
- package/src/inventory/schemas.ts +128 -0
- package/src/inventory/sql.ts +5 -0
- package/src/inventory/subscriptionForecastCalculation.ts +157 -0
- package/src/pricing/acquisitionFinancialPolicy.ts +26 -0
- package/src/pricing/acquisitionOfferPricing.ts +38 -0
- package/src/pricing/acquisitionOfferPricingHandler.ts +157 -0
- package/src/pricing/acquisitionOfferPricingSchemas.ts +35 -0
- package/src/pricing/acquisitionQuoteBinding.ts +12 -0
- package/src/pricing/acquisitionReadiness.ts +148 -0
- package/src/pricing/acquisitionTerms.ts +125 -0
- package/src/pricing/acquisitionTermsSchemas.ts +52 -0
- package/src/pricing/catalogFactsProvenance.ts +102 -0
- package/src/pricing/catalogFactsSchemas.ts +69 -0
- package/src/pricing/commerceOfferPricing.ts +224 -0
- package/src/pricing/commerceOfferPricingCache.ts +53 -0
- package/src/pricing/commerceOfferPricingHandler.ts +137 -0
- package/src/pricing/commercePriceAuthority.ts +104 -0
- package/src/pricing/commercePricingPolicyErrors.ts +88 -0
- package/src/pricing/commerceQuoteBatchHandler.ts +129 -0
- package/src/pricing/commerceQuoteCatalogReadPort.ts +42 -0
- package/src/pricing/commerceQuoteHandler.ts +184 -0
- package/src/pricing/commerceQuoteSnapshotExpect.ts +48 -0
- package/src/pricing/contracts/acquisitionTerms.ts +26 -0
- package/src/pricing/contracts/authority.ts +44 -0
- package/src/pricing/contracts/evidence.ts +25 -0
- package/src/pricing/contracts/index.ts +17 -0
- package/src/pricing/contracts/money.ts +32 -0
- package/src/pricing/contracts/offer.ts +42 -0
- package/src/pricing/contracts/offerPolicy.ts +17 -0
- package/src/pricing/contracts/policy.ts +26 -0
- package/src/pricing/contracts/provenance.ts +19 -0
- package/src/pricing/contracts/quote.ts +106 -0
- package/src/pricing/dbBackedCommerceQuoteHelpers.ts +256 -0
- package/src/pricing/dbBackedCommerceQuotePort.ts +260 -0
- package/src/pricing/dbBackedCommerceQuoteSnapshotVerifier.ts +49 -0
- package/src/pricing/index.ts +52 -0
- package/src/pricing/offerPolicyReadiness.ts +110 -0
- package/src/pricing/offerPolicySchemas.ts +23 -0
- package/src/pricing/offerPricingSchemas.ts +92 -0
- package/src/pricing/ports.ts +17 -0
- package/src/pricing/pricingPolicyToken.ts +119 -0
- package/src/pricing/quoteAcquisitionTerms.ts +10 -0
- package/src/pricing/quoteCodeRejectionDetails.ts +23 -0
- package/src/pricing/quotePorts.ts +58 -0
- package/src/pricing/quotePrimitives.ts +21 -0
- package/src/pricing/quotePromotionEvidence.ts +82 -0
- package/src/pricing/quoteSchemas.ts +230 -0
- package/src/pricing/subscriptionPricePolicy.ts +113 -0
- package/src/pricing/taxProfileGuard.ts +33 -0
- package/src/pricing/validation.ts +3 -0
- package/src/promotions/adminBindings.ts +53 -0
- package/src/promotions/adminPromotionCodesContracts.ts +244 -0
- package/src/promotions/adminPromotionCodesDataPort.ts +56 -0
- package/src/promotions/adminPromotionCodesHandler.ts +254 -0
- package/src/promotions/adminPromotionsContracts.ts +121 -0
- package/src/promotions/adminPromotionsDataPort.ts +48 -0
- package/src/promotions/adminPromotionsHandler.ts +72 -0
- package/src/promotions/contracts/index.ts +387 -0
- package/src/promotions/index.ts +22 -0
- package/src/promotions/promoDataPort.ts +65 -0
- package/src/promotions/promotionAcceptanceTelemetry.ts +73 -0
- package/src/promotions/promotionBenefitDefinition.ts +48 -0
- package/src/promotions/promotionClaimSweepPort.ts +23 -0
- package/src/promotions/promotionClaims.ts +70 -0
- package/src/promotions/promotionCodePreview.ts +183 -0
- package/src/promotions/promotionCodeQuotePort.ts +35 -0
- package/src/promotions/promotionQuoteAcceptance.ts +284 -0
- package/src/promotions/promotionQuoteBinding.ts +158 -0
- package/src/promotions/quoteAutomaticPromotions.ts +58 -0
- package/src/promotions/quotePromoDiscounts.ts +185 -0
- package/src/promotions/quotePromotionCodeAdjustments.ts +277 -0
- package/src/promotions/quotePromotionV2.ts +223 -0
- package/src/promotions/sql.ts +5 -0
- package/src/promotions/validation.ts +3 -0
- package/src/shipping/addressCanonLookupHandler.ts +114 -0
- package/src/shipping/addressCanonical.ts +31 -0
- package/src/shipping/addressPorts.ts +48 -0
- package/src/shipping/addressSchemas.ts +28 -0
- package/src/shipping/adminShippingRateHandler.ts +72 -0
- package/src/shipping/contracts/address.ts +71 -0
- package/src/shipping/contracts/delivery.ts +114 -0
- package/src/shipping/contracts/index.ts +18 -0
- package/src/shipping/contracts/shippingRate.ts +7 -0
- package/src/shipping/customerAddressesHandler.ts +100 -0
- package/src/shipping/customerSelfServiceAddressModels.ts +124 -0
- package/src/shipping/deliverySelectionHandler.ts +94 -0
- package/src/shipping/deliverySelectionPort.ts +10 -0
- package/src/shipping/deliverySelectionSummary.ts +56 -0
- package/src/shipping/handlerSchemas.ts +19 -0
- package/src/shipping/index.ts +23 -0
- package/src/shipping/pickupPointSearchPort.ts +8 -0
- package/src/shipping/readDeliverySelectionEvidence.ts +51 -0
- package/src/shipping/shippingRatePorts.ts +36 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { promotionRedemptionMap } from "./promoDataPort.js";
|
|
2
|
+
import { parsePromotionClaimSweepCounts, } from "./promotionClaimSweepPort.js";
|
|
3
|
+
const ACTIVE_SQL = "SELECT public.commerce_promotion_active_definitions() AS result";
|
|
4
|
+
const COUNTS_SQL = "SELECT public.commerce_promotion_paid_order_counts($1::uuid) AS result";
|
|
5
|
+
const REDEMPTIONS_SQL = "SELECT * FROM public.commerce_promotion_redemption_counts($1::uuid)";
|
|
6
|
+
const DEVICE_SQL = "SELECT public.commerce_promotion_device_first_order_redeemed($1) AS result";
|
|
7
|
+
const SWEEP_SQL = "SELECT public.commerce_sweep_stale_promotion_claims($1,$2,$3,$4) AS result";
|
|
8
|
+
export function createPostgresCommercePromoDataPort(executor) {
|
|
9
|
+
return {
|
|
10
|
+
async listActivePromotions() {
|
|
11
|
+
const value = scalar(await executor.query(ACTIVE_SQL));
|
|
12
|
+
if (!Array.isArray(value))
|
|
13
|
+
throw new Error("promotions_list_invalid_response");
|
|
14
|
+
return value;
|
|
15
|
+
},
|
|
16
|
+
async countPaidOrders(clientId) {
|
|
17
|
+
const counts = paidCounts(scalar(await executor.query(COUNTS_SQL, [clientId])));
|
|
18
|
+
return counts.oneTime + counts.subscription;
|
|
19
|
+
},
|
|
20
|
+
async countPaidOrdersByMode(clientId) {
|
|
21
|
+
return paidCounts(scalar(await executor.query(COUNTS_SQL, [clientId])));
|
|
22
|
+
},
|
|
23
|
+
async redemptionCounts(clientId) {
|
|
24
|
+
return promotionRedemptionMap((await executor.query(REDEMPTIONS_SQL, [clientId])).rows);
|
|
25
|
+
},
|
|
26
|
+
async deviceFirstOrderRedeemed(visitorId) {
|
|
27
|
+
if (!visitorId?.trim())
|
|
28
|
+
return false;
|
|
29
|
+
return scalar(await executor.query(DEVICE_SQL, [visitorId])) === true;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function createPostgresPromotionClaimSweepPort(executor) {
|
|
34
|
+
return {
|
|
35
|
+
async sweep(input) {
|
|
36
|
+
return parsePromotionClaimSweepCounts(scalar(await executor.query(SWEEP_SQL, [
|
|
37
|
+
input.now, input.limit, input.claimLeaseMinutes, input.graceMinutes,
|
|
38
|
+
])));
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function scalar(result) {
|
|
43
|
+
if (result.rows.length !== 1 || !("result" in result.rows[0])) {
|
|
44
|
+
throw new Error("promotion_claim_response_invalid");
|
|
45
|
+
}
|
|
46
|
+
return result.rows[0].result;
|
|
47
|
+
}
|
|
48
|
+
function paidCounts(value) {
|
|
49
|
+
const row = value && typeof value === "object" ? value : {};
|
|
50
|
+
const oneTime = Number(row.oneTime ?? -1);
|
|
51
|
+
const subscription = Number(row.subscription ?? -1);
|
|
52
|
+
if (!Number.isSafeInteger(oneTime) || oneTime < 0
|
|
53
|
+
|| !Number.isSafeInteger(subscription) || subscription < 0) {
|
|
54
|
+
throw new Error("promotion_paid_order_counts_invalid_response");
|
|
55
|
+
}
|
|
56
|
+
return { oneTime, subscription };
|
|
57
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PromotionCodePreviewRequest, PromotionCodePreviewResponse } from "./contracts/index.js";
|
|
2
|
+
export declare const PROMOTION_ENGINE_VERSION: "promotion-engine.v2";
|
|
3
|
+
export declare function createPromotionCodePreview({ minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR }: {
|
|
4
|
+
minimumProductPayableMinor: number;
|
|
5
|
+
}): {
|
|
6
|
+
generatePromotionCode: () => string;
|
|
7
|
+
normalizePromotionCode: (code: string) => string;
|
|
8
|
+
promotionPreviewProof: (input: PromotionCodePreviewRequest, secret: string) => string;
|
|
9
|
+
verifyPromotionPreviewProof: (input: PromotionCodePreviewRequest, proof: string, secret: string) => boolean;
|
|
10
|
+
previewPromotionCode: (input: PromotionCodePreviewRequest, secret: string) => PromotionCodePreviewResponse;
|
|
11
|
+
};
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { COMMERCE_CONTRACT_VERSION } from "../pricing/index.js";
|
|
3
|
+
import { evaluatePromotionAdjustmentsV2, } from "@openlup/core/promo";
|
|
4
|
+
const CROCKFORD_BASE32 = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
|
|
5
|
+
export const PROMOTION_ENGINE_VERSION = "promotion-engine.v2";
|
|
6
|
+
export function createPromotionCodePreview({ minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR }) {
|
|
7
|
+
function generatePromotionCode() {
|
|
8
|
+
const bytes = randomBytes(10);
|
|
9
|
+
let bits = 0;
|
|
10
|
+
let buffer = 0;
|
|
11
|
+
let code = "";
|
|
12
|
+
for (const byte of bytes) {
|
|
13
|
+
buffer = (buffer << 8) | byte;
|
|
14
|
+
bits += 8;
|
|
15
|
+
while (bits >= 5) {
|
|
16
|
+
bits -= 5;
|
|
17
|
+
code += CROCKFORD_BASE32[(buffer >>> bits) & 31];
|
|
18
|
+
buffer &= (1 << bits) - 1;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return code;
|
|
22
|
+
}
|
|
23
|
+
function normalizePromotionCode(code) {
|
|
24
|
+
return code.trim().toUpperCase();
|
|
25
|
+
}
|
|
26
|
+
function promotionPreviewProof(input, secret) {
|
|
27
|
+
assertPreviewSecret(secret);
|
|
28
|
+
assertSupportedPreviewBenefits(input);
|
|
29
|
+
return createHmac("sha256", secret).update(previewProofPayload(input)).digest("base64url");
|
|
30
|
+
}
|
|
31
|
+
function verifyPromotionPreviewProof(input, proof, secret) {
|
|
32
|
+
const expected = promotionPreviewProof(input, secret);
|
|
33
|
+
const expectedBytes = Buffer.from(expected);
|
|
34
|
+
const actualBytes = Buffer.from(proof);
|
|
35
|
+
return expectedBytes.length === actualBytes.length && timingSafeEqual(expectedBytes, actualBytes);
|
|
36
|
+
}
|
|
37
|
+
function previewPromotionCode(input, secret) {
|
|
38
|
+
assertSupportedPreviewBenefits(input);
|
|
39
|
+
const eligible = input.context.referenceProductMinor >= input.minimumReferenceMinor;
|
|
40
|
+
const results = ["one_time", "subscription_initial"].map((scope) => {
|
|
41
|
+
const currentProductMinor = scope === "one_time"
|
|
42
|
+
? input.context.oneTimeProductMinor
|
|
43
|
+
: input.context.subscriptionProductMinor;
|
|
44
|
+
const candidates = eligible
|
|
45
|
+
? input.benefits
|
|
46
|
+
.map((benefit) => toCandidate(benefit, input.scopes, currentProductMinor))
|
|
47
|
+
.filter((candidate) => candidate !== null)
|
|
48
|
+
: [];
|
|
49
|
+
const result = evaluatePromotionAdjustmentsV2({
|
|
50
|
+
purchaseScope: scope,
|
|
51
|
+
referenceProductMinor: input.context.referenceProductMinor,
|
|
52
|
+
currentProductMinor,
|
|
53
|
+
shippingMinor: input.context.shippingMinor,
|
|
54
|
+
minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR,
|
|
55
|
+
}, candidates);
|
|
56
|
+
return {
|
|
57
|
+
scope,
|
|
58
|
+
productPayableMinor: result.productPayableMinor,
|
|
59
|
+
shippingPayableMinor: result.shippingPayableMinor,
|
|
60
|
+
effectiveProductDiscountBps: result.effectiveProductDiscountBps,
|
|
61
|
+
winner: result.adjustments.some((adjustment) => adjustment.source === "code")
|
|
62
|
+
? "code"
|
|
63
|
+
: "automatic",
|
|
64
|
+
floorApplied: result.adjustments.some((adjustment) => adjustment.floorApplied),
|
|
65
|
+
adjustments: result.adjustments.map((adjustment) => ({
|
|
66
|
+
lane: adjustment.lane,
|
|
67
|
+
kind: input.promotionEngineVersion === "promotion-engine.v1"
|
|
68
|
+
&& adjustment.promotionId === "preview-product"
|
|
69
|
+
&& input.benefits.some((benefit) => benefit.lane === "product" && benefit.kind === "percentage")
|
|
70
|
+
? "percentage"
|
|
71
|
+
: adjustment.kind,
|
|
72
|
+
amountOffMinor: adjustment.amountOffMinor,
|
|
73
|
+
beforeMinor: adjustment.beforeMinor,
|
|
74
|
+
afterMinor: adjustment.afterMinor,
|
|
75
|
+
})),
|
|
76
|
+
};
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
contractVersion: COMMERCE_CONTRACT_VERSION,
|
|
80
|
+
promotionEngineVersion: input.promotionEngineVersion,
|
|
81
|
+
previewProof: promotionPreviewProof(input, secret),
|
|
82
|
+
results,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function previewProofPayload(input) {
|
|
86
|
+
return stableJson({
|
|
87
|
+
promotionEngineVersion: input.promotionEngineVersion,
|
|
88
|
+
minimumProductPayableMinor: MINIMUM_PRODUCT_PAYABLE_MINOR,
|
|
89
|
+
minimumReferenceMinor: input.minimumReferenceMinor,
|
|
90
|
+
benefits: input.benefits,
|
|
91
|
+
scopes: input.scopes,
|
|
92
|
+
context: input.context,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
function assertPreviewSecret(secret) {
|
|
96
|
+
if (Buffer.byteLength(secret, "utf8") < 32) {
|
|
97
|
+
throw new RangeError("promotion_preview_secret_too_short");
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function assertSupportedPreviewBenefits(input) {
|
|
101
|
+
if (input.benefits.some((benefit) => "validationState" in benefit)) {
|
|
102
|
+
throw new RangeError("promotion_preview_unsupported_legacy_value");
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function toCandidate(benefit, scopes, currentProductMinor) {
|
|
106
|
+
const base = {
|
|
107
|
+
promotionId: `preview-${benefit.lane}`,
|
|
108
|
+
codeId: "preview-code",
|
|
109
|
+
source: "code",
|
|
110
|
+
name: "Promotion code preview",
|
|
111
|
+
scopes,
|
|
112
|
+
};
|
|
113
|
+
if ("validationState" in benefit) {
|
|
114
|
+
throw new RangeError("promotion_preview_unsupported_legacy_value");
|
|
115
|
+
}
|
|
116
|
+
if (("valueBps" in benefit && benefit.valueBps === 0)
|
|
117
|
+
|| ("valueMinor" in benefit && benefit.valueMinor === 0))
|
|
118
|
+
return null;
|
|
119
|
+
if (benefit.kind === "target_percentage")
|
|
120
|
+
return { ...base, ...benefit };
|
|
121
|
+
if (benefit.kind === "percentage" && benefit.lane === "product") {
|
|
122
|
+
const valueMinor = benefit.valueBps >= 10_000
|
|
123
|
+
? currentProductMinor
|
|
124
|
+
: Number((BigInt(currentProductMinor) * BigInt(benefit.valueBps)) / 10000n);
|
|
125
|
+
return {
|
|
126
|
+
...base,
|
|
127
|
+
lane: "product",
|
|
128
|
+
kind: "fixed_amount",
|
|
129
|
+
valueMinor,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (benefit.kind === "percentage" && benefit.lane === "shipping" && benefit.valueBps >= 10_000) {
|
|
133
|
+
return { ...base, lane: "shipping", kind: "free_shipping" };
|
|
134
|
+
}
|
|
135
|
+
if (benefit.kind === "percentage")
|
|
136
|
+
return { ...base, ...benefit };
|
|
137
|
+
if (benefit.kind === "fixed_amount")
|
|
138
|
+
return { ...base, ...benefit };
|
|
139
|
+
return { ...base, ...benefit };
|
|
140
|
+
}
|
|
141
|
+
function stableJson(value) {
|
|
142
|
+
if (Array.isArray(value))
|
|
143
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
144
|
+
if (value && typeof value === "object") {
|
|
145
|
+
const record = value;
|
|
146
|
+
return `{${Object.keys(record)
|
|
147
|
+
.sort()
|
|
148
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(record[key])}`)
|
|
149
|
+
.join(",")}}`;
|
|
150
|
+
}
|
|
151
|
+
return JSON.stringify(value);
|
|
152
|
+
}
|
|
153
|
+
return { generatePromotionCode, normalizePromotionCode, promotionPreviewProof, verifyPromotionPreviewProof, previewPromotionCode };
|
|
154
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { PromotionAdjustmentCandidate, PromotionPurchaseScope } from "@openlup/core/promo";
|
|
2
|
+
import type { CommerceCodeRejection, CommerceCodeRejectionDetail } from "../pricing/index.js";
|
|
3
|
+
export type ResolvedPromotionCodeCandidate = PromotionAdjustmentCandidate & {
|
|
4
|
+
codeRevision: number;
|
|
5
|
+
definitionFingerprint: string;
|
|
6
|
+
minimumReferenceMinor: number;
|
|
7
|
+
validTo: string | null;
|
|
8
|
+
};
|
|
9
|
+
export interface ResolvePromotionCodeQuoteInput {
|
|
10
|
+
codes: readonly string[];
|
|
11
|
+
clientId: string | null;
|
|
12
|
+
purchaseScope: PromotionPurchaseScope;
|
|
13
|
+
referenceProductMinor: number;
|
|
14
|
+
atTime: string;
|
|
15
|
+
}
|
|
16
|
+
export interface PromotionCodeQuoteResolution {
|
|
17
|
+
candidates: ResolvedPromotionCodeCandidate[];
|
|
18
|
+
codeRejections: CommerceCodeRejection[];
|
|
19
|
+
/** Additive v2 RPC detail; old RPC fallback returns an empty array. */
|
|
20
|
+
codeRejectionDetails: CommerceCodeRejectionDetail[];
|
|
21
|
+
}
|
|
22
|
+
/** Advisory quote read only. Capacity is authoritatively claimed by the order transaction. */
|
|
23
|
+
export interface PromotionCodeQuotePort {
|
|
24
|
+
resolve(input: ResolvePromotionCodeQuoteInput): Promise<PromotionCodeQuoteResolution>;
|
|
25
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { ResolvedAcquisitionTerms } from "../pricing/index.js";
|
|
2
|
+
import type { CommerceQuote } from "../pricing/index.js";
|
|
3
|
+
import type { PromotionAcceptanceTelemetry } from "./promotionAcceptanceTelemetry.js";
|
|
4
|
+
import { type PromotionQuoteMismatchField } from "./promotionQuoteBinding.js";
|
|
5
|
+
export declare const PROMOTION_ACCEPTANCE_MAX_TTL_MS: number;
|
|
6
|
+
export declare const PROMOTION_ACCEPTANCE_CLOCK_SKEW_MS = 60000;
|
|
7
|
+
export interface PromotionAcceptanceKeyring {
|
|
8
|
+
current: string;
|
|
9
|
+
previous?: string | null;
|
|
10
|
+
}
|
|
11
|
+
export type PromotionAcceptanceDiagnostic = {
|
|
12
|
+
accepted: boolean;
|
|
13
|
+
reason: PromotionAcceptanceTelemetry["reason"];
|
|
14
|
+
keySlot: PromotionAcceptanceTelemetry["keySlot"];
|
|
15
|
+
expiryBucket: PromotionAcceptanceTelemetry["expiryBucket"];
|
|
16
|
+
/** Present only on `quote_mismatch`: which money section diverged. */
|
|
17
|
+
mismatchField?: PromotionQuoteMismatchField;
|
|
18
|
+
};
|
|
19
|
+
export declare function createPromotionQuoteAcceptance<Request extends {
|
|
20
|
+
promoCodes: string[];
|
|
21
|
+
}>({ projectRequest }: {
|
|
22
|
+
projectRequest(request: Request): unknown;
|
|
23
|
+
}): {
|
|
24
|
+
issuePromotionQuoteAcceptance: (input: {
|
|
25
|
+
request: Request;
|
|
26
|
+
quote: CommerceQuote;
|
|
27
|
+
acquisitionTerms?: ResolvedAcquisitionTerms;
|
|
28
|
+
keyring: PromotionAcceptanceKeyring;
|
|
29
|
+
now?: Date;
|
|
30
|
+
}) => string | null;
|
|
31
|
+
issuePromotionQuoteAcceptanceWithOutcome: (input: {
|
|
32
|
+
request: Request;
|
|
33
|
+
quote: CommerceQuote;
|
|
34
|
+
acquisitionTerms?: ResolvedAcquisitionTerms;
|
|
35
|
+
keyring: PromotionAcceptanceKeyring;
|
|
36
|
+
now?: Date;
|
|
37
|
+
}) => {
|
|
38
|
+
token: string | null;
|
|
39
|
+
diagnostic: PromotionAcceptanceDiagnostic;
|
|
40
|
+
};
|
|
41
|
+
verifyPromotionQuoteAcceptance: (input: {
|
|
42
|
+
token: string;
|
|
43
|
+
request: Request;
|
|
44
|
+
quote: CommerceQuote;
|
|
45
|
+
acquisitionTerms?: ResolvedAcquisitionTerms;
|
|
46
|
+
expectedTotal: {
|
|
47
|
+
amountMinor: number;
|
|
48
|
+
currency: string;
|
|
49
|
+
};
|
|
50
|
+
keyring: PromotionAcceptanceKeyring;
|
|
51
|
+
now?: Date;
|
|
52
|
+
}) => boolean;
|
|
53
|
+
verifyPromotionQuoteAcceptanceWithOutcome: (input: {
|
|
54
|
+
token: string;
|
|
55
|
+
request: Request;
|
|
56
|
+
quote: CommerceQuote;
|
|
57
|
+
acquisitionTerms?: ResolvedAcquisitionTerms;
|
|
58
|
+
expectedTotal: {
|
|
59
|
+
amountMinor: number;
|
|
60
|
+
currency: string;
|
|
61
|
+
};
|
|
62
|
+
keyring: PromotionAcceptanceKeyring;
|
|
63
|
+
now?: Date;
|
|
64
|
+
}) => PromotionAcceptanceDiagnostic;
|
|
65
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { createHash, createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { projectPromotionQuoteMoney, promotionQuoteMoneySections, PROMOTION_QUOTE_MONEY_SECTIONS, stableJson, } from "./promotionQuoteBinding.js";
|
|
4
|
+
export const PROMOTION_ACCEPTANCE_MAX_TTL_MS = 72 * 60 * 60 * 1_000;
|
|
5
|
+
export const PROMOTION_ACCEPTANCE_CLOCK_SKEW_MS = 60_000;
|
|
6
|
+
const payloadSchema = z.object({
|
|
7
|
+
v: z.literal(1),
|
|
8
|
+
kid: z.string().regex(/^[0-9a-f]{16}$/),
|
|
9
|
+
iat: z.number().int().nonnegative(),
|
|
10
|
+
exp: z.number().int().positive(),
|
|
11
|
+
engine: z.literal("promotion-engine.v2"),
|
|
12
|
+
requestBinding: z.string().regex(/^[0-9a-f]{64}$/),
|
|
13
|
+
codeBinding: z.string().regex(/^[0-9a-f]{64}$/),
|
|
14
|
+
quoteBinding: z.string().regex(/^[0-9a-f]{64}$/),
|
|
15
|
+
// Legacy payloads still parse, but their whole-quote binding is rejected.
|
|
16
|
+
// Quote refresh issues a new money-bound token; missing sections report other.
|
|
17
|
+
sectionBindings: z.object(Object.fromEntries(PROMOTION_QUOTE_MONEY_SECTIONS
|
|
18
|
+
.map((section) => [section, z.string().regex(/^[0-9a-f]{64}$/)]))).strict().optional(),
|
|
19
|
+
totalMinor: z.number().int().nonnegative(),
|
|
20
|
+
currency: z.string().length(3),
|
|
21
|
+
}).strict();
|
|
22
|
+
export function createPromotionQuoteAcceptance({ projectRequest }) {
|
|
23
|
+
function issuePromotionQuoteAcceptance(input) {
|
|
24
|
+
return issuePromotionQuoteAcceptanceWithOutcome(input).token;
|
|
25
|
+
}
|
|
26
|
+
function issuePromotionQuoteAcceptanceWithOutcome(input) {
|
|
27
|
+
const evidence = v2Discounts(input.quote);
|
|
28
|
+
if (evidence.length === 0 && !input.acquisitionTerms)
|
|
29
|
+
return {
|
|
30
|
+
token: null,
|
|
31
|
+
diagnostic: rejected("no_v2_adjustment"),
|
|
32
|
+
};
|
|
33
|
+
assertSecret(input.keyring.current);
|
|
34
|
+
const nowMs = (input.now ?? new Date()).getTime();
|
|
35
|
+
const codeExpiry = Math.min(...evidence.map((discount) => discount.promotionCodeValidTo ? Date.parse(discount.promotionCodeValidTo) : Number.POSITIVE_INFINITY));
|
|
36
|
+
const exp = Math.min(nowMs + PROMOTION_ACCEPTANCE_MAX_TTL_MS, codeExpiry);
|
|
37
|
+
if (!Number.isFinite(exp) || exp <= nowMs)
|
|
38
|
+
return {
|
|
39
|
+
token: null,
|
|
40
|
+
diagnostic: rejected("ttl_invalid"),
|
|
41
|
+
};
|
|
42
|
+
const payload = payloadSchema.parse({
|
|
43
|
+
v: 1,
|
|
44
|
+
kid: keyId(input.keyring.current),
|
|
45
|
+
iat: nowMs,
|
|
46
|
+
exp,
|
|
47
|
+
engine: "promotion-engine.v2",
|
|
48
|
+
requestBinding: requestBinding(input.request, input.keyring.current),
|
|
49
|
+
codeBinding: codeBinding(input.request.promoCodes, input.keyring.current),
|
|
50
|
+
quoteBinding: quoteBinding(input.quote, input.keyring.current, input.acquisitionTerms),
|
|
51
|
+
sectionBindings: sectionBindings(input.quote, input.keyring.current, input.acquisitionTerms),
|
|
52
|
+
totalMinor: input.quote.totalGross.amountMinor,
|
|
53
|
+
currency: input.quote.totalGross.currency,
|
|
54
|
+
});
|
|
55
|
+
const encoded = Buffer.from(JSON.stringify(payload)).toString("base64url");
|
|
56
|
+
return {
|
|
57
|
+
token: `${encoded}.${sign(encoded, input.keyring.current)}`,
|
|
58
|
+
diagnostic: {
|
|
59
|
+
accepted: true,
|
|
60
|
+
reason: "issued",
|
|
61
|
+
keySlot: "current",
|
|
62
|
+
expiryBucket: expiryBucket(exp, nowMs),
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function verifyPromotionQuoteAcceptance(input) {
|
|
67
|
+
return verifyPromotionQuoteAcceptanceWithOutcome(input).accepted;
|
|
68
|
+
}
|
|
69
|
+
function verifyPromotionQuoteAcceptanceWithOutcome(input) {
|
|
70
|
+
const verified = verifyEnvelopeWithOutcome(input.token, input.keyring, input.now ?? new Date());
|
|
71
|
+
if (verified.verified === false)
|
|
72
|
+
return verified.diagnostic;
|
|
73
|
+
const { payload, secret } = verified;
|
|
74
|
+
const base = { keySlot: verified.keySlot, expiryBucket: verified.expiryBucket };
|
|
75
|
+
if (payload.requestBinding !== requestBinding(input.request, secret)) {
|
|
76
|
+
return { accepted: false, reason: "request_mismatch", ...base };
|
|
77
|
+
}
|
|
78
|
+
if (payload.codeBinding !== codeBinding(input.request.promoCodes, secret)) {
|
|
79
|
+
return { accepted: false, reason: "code_mismatch", ...base };
|
|
80
|
+
}
|
|
81
|
+
if (payload.quoteBinding !== quoteBinding(input.quote, secret, input.acquisitionTerms)) {
|
|
82
|
+
const mismatchField = namedMismatchField(payload.sectionBindings, input.quote, secret, input.acquisitionTerms);
|
|
83
|
+
return { accepted: false, reason: "quote_mismatch", mismatchField, ...base };
|
|
84
|
+
}
|
|
85
|
+
if (payload.totalMinor !== input.expectedTotal.amountMinor) {
|
|
86
|
+
return { accepted: false, reason: "total_mismatch", ...base };
|
|
87
|
+
}
|
|
88
|
+
if (payload.currency !== input.expectedTotal.currency) {
|
|
89
|
+
return { accepted: false, reason: "currency_mismatch", ...base };
|
|
90
|
+
}
|
|
91
|
+
if (v2Discounts(input.quote).length === 0 && !input.acquisitionTerms) {
|
|
92
|
+
return { accepted: false, reason: "no_v2_adjustment_in_quote", ...base };
|
|
93
|
+
}
|
|
94
|
+
return { accepted: true, reason: "accepted", ...base };
|
|
95
|
+
}
|
|
96
|
+
function verifyEnvelopeWithOutcome(token, keyring, now) {
|
|
97
|
+
const [encoded, signature, extra] = token.split(".");
|
|
98
|
+
if (!encoded || !signature || extra || encoded.length > 8_192 || signature.length !== 43) {
|
|
99
|
+
return { verified: false, diagnostic: rejected("malformed") };
|
|
100
|
+
}
|
|
101
|
+
let parsed;
|
|
102
|
+
try {
|
|
103
|
+
parsed = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return { verified: false, diagnostic: rejected("malformed") };
|
|
107
|
+
}
|
|
108
|
+
const payload = payloadSchema.safeParse(parsed);
|
|
109
|
+
if (!payload.success)
|
|
110
|
+
return { verified: false, diagnostic: rejected("malformed") };
|
|
111
|
+
const candidates = [
|
|
112
|
+
{ slot: "current", secret: keyring.current },
|
|
113
|
+
{ slot: "previous", secret: keyring.previous },
|
|
114
|
+
].filter((candidate) => typeof candidate.secret === "string" && candidate.secret.length >= 32);
|
|
115
|
+
const candidate = candidates.find((value) => keyId(value.secret) === payload.data.kid);
|
|
116
|
+
if (!candidate)
|
|
117
|
+
return { verified: false, diagnostic: rejected("unknown_key") };
|
|
118
|
+
if (!safeEqual(signature, sign(encoded, candidate.secret))) {
|
|
119
|
+
return { verified: false, diagnostic: rejected("signature_invalid") };
|
|
120
|
+
}
|
|
121
|
+
const nowMs = now.getTime();
|
|
122
|
+
const safeBase = { keySlot: candidate.slot, expiryBucket: expiryBucket(payload.data.exp, nowMs) };
|
|
123
|
+
if (payload.data.exp <= payload.data.iat ||
|
|
124
|
+
payload.data.exp - payload.data.iat > PROMOTION_ACCEPTANCE_MAX_TTL_MS) {
|
|
125
|
+
return { verified: false, diagnostic: { accepted: false, reason: "ttl_invalid", ...safeBase } };
|
|
126
|
+
}
|
|
127
|
+
if (payload.data.iat > nowMs + PROMOTION_ACCEPTANCE_CLOCK_SKEW_MS) {
|
|
128
|
+
return { verified: false, diagnostic: { accepted: false, reason: "not_yet_valid", ...safeBase } };
|
|
129
|
+
}
|
|
130
|
+
if (nowMs >= payload.data.exp) {
|
|
131
|
+
return { verified: false, diagnostic: { accepted: false, reason: "expired", ...safeBase } };
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
verified: true,
|
|
135
|
+
payload: payload.data,
|
|
136
|
+
secret: candidate.secret,
|
|
137
|
+
keySlot: candidate.slot,
|
|
138
|
+
expiryBucket: expiryBucket(payload.data.exp, nowMs),
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function rejected(reason) {
|
|
142
|
+
return { accepted: false, reason, keySlot: "none", expiryBucket: "none" };
|
|
143
|
+
}
|
|
144
|
+
function expiryBucket(exp, now) {
|
|
145
|
+
const remaining = exp - now;
|
|
146
|
+
if (remaining <= 0)
|
|
147
|
+
return "expired";
|
|
148
|
+
if (remaining < 60 * 60 * 1_000)
|
|
149
|
+
return "lt_1h";
|
|
150
|
+
if (remaining < 24 * 60 * 60 * 1_000)
|
|
151
|
+
return "1_24h";
|
|
152
|
+
return "24_72h";
|
|
153
|
+
}
|
|
154
|
+
function v2Discounts(quote) {
|
|
155
|
+
return quote.discounts.filter((discount) => discount.promotionEngineVersion === "promotion-engine.v2" &&
|
|
156
|
+
discount.reasonCode === "promotion_code_v2");
|
|
157
|
+
}
|
|
158
|
+
function requestBinding(request, secret) {
|
|
159
|
+
return hmac(secret, stableJson(projectRequest(request)));
|
|
160
|
+
}
|
|
161
|
+
function codeBinding(codes, secret) {
|
|
162
|
+
return hmac(secret, stableJson([...new Set(codes.map((code) => code.trim().toUpperCase()))].sort()));
|
|
163
|
+
}
|
|
164
|
+
function quoteBinding(quote, secret, acquisitionTerms) {
|
|
165
|
+
return hmac(secret, stableJson(projectPromotionQuoteMoney(quote, acquisitionTerms)));
|
|
166
|
+
}
|
|
167
|
+
function sectionBindings(quote, secret, acquisitionTerms) {
|
|
168
|
+
const sections = promotionQuoteMoneySections(quote, acquisitionTerms);
|
|
169
|
+
return Object.fromEntries(PROMOTION_QUOTE_MONEY_SECTIONS
|
|
170
|
+
.map((section) => [section, hmac(secret, stableJson(sections[section]))]));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* The verifier holds only the issuer's HMACs, never the issued quote, so the
|
|
174
|
+
* differing section is found by comparing per-section hashes in a fixed order.
|
|
175
|
+
*/
|
|
176
|
+
function namedMismatchField(issued, quote, secret, acquisitionTerms) {
|
|
177
|
+
if (!issued)
|
|
178
|
+
return "other";
|
|
179
|
+
const verified = sectionBindings(quote, secret, acquisitionTerms);
|
|
180
|
+
return PROMOTION_QUOTE_MONEY_SECTIONS
|
|
181
|
+
.find((section) => issued[section] !== verified[section]) ?? "other";
|
|
182
|
+
}
|
|
183
|
+
function keyId(secret) {
|
|
184
|
+
return createHash("sha256").update(secret).digest("hex").slice(0, 16);
|
|
185
|
+
}
|
|
186
|
+
function hmac(secret, value) {
|
|
187
|
+
return createHmac("sha256", secret).update(value).digest("hex");
|
|
188
|
+
}
|
|
189
|
+
function sign(encoded, secret) {
|
|
190
|
+
return createHmac("sha256", secret).update(encoded).digest("base64url");
|
|
191
|
+
}
|
|
192
|
+
function safeEqual(left, right) {
|
|
193
|
+
const a = Buffer.from(left);
|
|
194
|
+
const b = Buffer.from(right);
|
|
195
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
196
|
+
}
|
|
197
|
+
function assertSecret(secret) {
|
|
198
|
+
if (secret.length < 32)
|
|
199
|
+
throw new Error("promotion_acceptance_secret_invalid");
|
|
200
|
+
}
|
|
201
|
+
return { issuePromotionQuoteAcceptance, issuePromotionQuoteAcceptanceWithOutcome, verifyPromotionQuoteAcceptance, verifyPromotionQuoteAcceptanceWithOutcome };
|
|
202
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { type ResolvedAcquisitionTerms } from "../pricing/index.js";
|
|
2
|
+
import type { CommerceMoney, CommerceQuote, CommerceQuoteDiscount } from "../pricing/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* The canonical money projection an acceptance token binds.
|
|
5
|
+
*
|
|
6
|
+
* ⛔ The token protects **what the customer pays**, never how it was presented
|
|
7
|
+
* or which server-only evidence produced it. Everything outside this projection
|
|
8
|
+
* is deliberately unbound: `label`, `reasonCode`, `customerSemantic`, `code`,
|
|
9
|
+
* `promotionCodeRevision`, `promotionCodeScopes`, `promotionMinimumReferenceMinor`,
|
|
10
|
+
* `promotionCodeValidTo`, `floorApplied`, `context`, `codeRejections`,
|
|
11
|
+
* `codeRejectionDetails`, `pricingComponents` and `catalogFacts`. Those differ
|
|
12
|
+
* legitimately between the two surfaces that share one token — the public quote
|
|
13
|
+
* route signs the contract-parsed public projection while the checkout guard
|
|
14
|
+
* verifies the raw server-authoritative quote, which carries `catalogFacts`
|
|
15
|
+
* (with a per-request `atTime`) that the public projection strips — so binding
|
|
16
|
+
* them turned every strict-path v2 code checkout into a permanent rejection.
|
|
17
|
+
*
|
|
18
|
+
* What stays bound is every amount and every discount identity: change a price,
|
|
19
|
+
* a quantity, a tax split, shipping, a discount amount, which promotion granted
|
|
20
|
+
* it, or what it applies to, and the token stops verifying.
|
|
21
|
+
*/
|
|
22
|
+
export interface PromotionQuoteMoneyLine {
|
|
23
|
+
sku: string;
|
|
24
|
+
quantity: number;
|
|
25
|
+
unitPriceGross: CommerceMoney;
|
|
26
|
+
lineSubtotalGross: CommerceMoney;
|
|
27
|
+
tax: {
|
|
28
|
+
vatRateBps: number;
|
|
29
|
+
netAmount: CommerceMoney;
|
|
30
|
+
vatAmount: CommerceMoney;
|
|
31
|
+
grossAmount: CommerceMoney;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface PromotionQuoteMoneyDiscount {
|
|
35
|
+
promotionId: string;
|
|
36
|
+
appliesTo: CommerceQuoteDiscount["appliesTo"];
|
|
37
|
+
amountOffMinor: number;
|
|
38
|
+
promotionCodeId: string | null;
|
|
39
|
+
promotionDefinitionFingerprint: string | null;
|
|
40
|
+
promotionBenefitKind: NonNullable<CommerceQuoteDiscount["promotionBenefitKind"]> | null;
|
|
41
|
+
promotionBenefitValueBps: number | null;
|
|
42
|
+
promotionBenefitValueMinor: number | null;
|
|
43
|
+
}
|
|
44
|
+
export interface PromotionQuoteMoneyProjection {
|
|
45
|
+
acquisitionTerms?: ResolvedAcquisitionTerms;
|
|
46
|
+
currency: string;
|
|
47
|
+
totalGross: CommerceMoney;
|
|
48
|
+
netTotal: CommerceMoney;
|
|
49
|
+
taxTotal: CommerceMoney;
|
|
50
|
+
subtotalGross: CommerceMoney;
|
|
51
|
+
discountTotalGross: CommerceMoney;
|
|
52
|
+
shippingGross: CommerceMoney | null;
|
|
53
|
+
shippingDiscountGross: CommerceMoney | null;
|
|
54
|
+
lines: PromotionQuoteMoneyLine[];
|
|
55
|
+
discounts: PromotionQuoteMoneyDiscount[];
|
|
56
|
+
}
|
|
57
|
+
/** The five sections the token hashes separately so a mismatch can name itself. */
|
|
58
|
+
export declare const PROMOTION_QUOTE_MONEY_SECTIONS: readonly ["lines", "discounts", "shipping", "totals", "currency"];
|
|
59
|
+
export type PromotionQuoteMoneySection = (typeof PROMOTION_QUOTE_MONEY_SECTIONS)[number];
|
|
60
|
+
/** `other` is the honest answer for a token minted before section bindings existed. */
|
|
61
|
+
export declare const PROMOTION_QUOTE_MISMATCH_FIELDS: readonly ["lines", "discounts", "shipping", "totals", "currency", "other"];
|
|
62
|
+
export type PromotionQuoteMismatchField = (typeof PROMOTION_QUOTE_MISMATCH_FIELDS)[number];
|
|
63
|
+
/**
|
|
64
|
+
* Discount order is preserved: the promotion engine emits its lanes
|
|
65
|
+
* deterministically, so a reordering is a real evaluation difference.
|
|
66
|
+
*/
|
|
67
|
+
export declare function projectPromotionQuoteMoney(quote: CommerceQuote, acquisitionTerms?: ResolvedAcquisitionTerms): PromotionQuoteMoneyProjection;
|
|
68
|
+
/** Same projection, partitioned. The union of these five sections is the whole projection. */
|
|
69
|
+
export declare function promotionQuoteMoneySections(quote: CommerceQuote, acquisitionTerms?: ResolvedAcquisitionTerms): Record<PromotionQuoteMoneySection, unknown>;
|
|
70
|
+
/** Key-sorted, `undefined`-dropping JSON. Every acceptance binding hashes this shape. */
|
|
71
|
+
export declare function stableJson(value: unknown): string;
|