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