@openlup/platform 0.17.0 → 0.19.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 +143 -0
- package/README.md +299 -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 +21 -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 +173 -0
- package/dist/catalog/bundleCatalogReadPort.js +36 -0
- package/dist/catalog/bundleCatalogStore.d.ts +5 -0
- package/dist/catalog/bundleCatalogStore.js +156 -0
- package/dist/catalog/bundlePriceSelection.d.ts +13 -0
- package/dist/catalog/bundlePriceSelection.js +26 -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 +241 -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/effectiveBundleCatalogQueries.d.ts +8 -0
- package/dist/catalog/effectiveBundleCatalogQueries.js +79 -0
- package/dist/catalog/effectiveBundleCatalogStore.d.ts +4 -0
- package/dist/catalog/effectiveBundleCatalogStore.js +156 -0
- package/dist/catalog/foundationSchemas.d.ts +98 -0
- package/dist/catalog/foundationSchemas.js +110 -0
- package/dist/catalog/index.d.ts +44 -0
- package/dist/catalog/index.js +26 -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 +3 -0
- package/dist/composition/index.js +3 -0
- package/dist/composition/inventoryContribution.d.ts +16 -0
- package/dist/composition/inventoryContribution.js +17 -0
- package/dist/composition/shippingContribution.d.ts +16 -0
- package/dist/composition/shippingContribution.js +17 -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/bindings.d.ts +35 -0
- package/dist/inventory/bindings.js +45 -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 +14 -0
- package/dist/inventory/index.js +11 -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 +28 -0
- package/dist/pricing/commercePriceAuthority.js +140 -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 +35 -0
- package/dist/pricing/commerceQuoteHandler.js +122 -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 +74 -0
- package/dist/pricing/dbBackedCommerceQuotePort.js +223 -0
- package/dist/pricing/dbBackedCommerceQuoteSnapshotVerifier.d.ts +4 -0
- package/dist/pricing/dbBackedCommerceQuoteSnapshotVerifier.js +25 -0
- package/dist/pricing/index.d.ts +52 -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/quoteShippingPromotions.d.ts +20 -0
- package/dist/pricing/quoteShippingPromotions.js +1 -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/commerceQuoteReadPreparation.d.ts +3 -0
- package/dist/promotions/commerceQuoteReadPreparation.js +27 -0
- package/dist/promotions/contracts/index.d.ts +387 -0
- package/dist/promotions/contracts/index.js +1 -0
- package/dist/promotions/index.d.ts +25 -0
- package/dist/promotions/index.js +22 -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 +70 -0
- package/dist/promotions/promotionQuoteAcceptance.js +202 -0
- package/dist/promotions/promotionQuoteBinding.d.ts +73 -0
- package/dist/promotions/promotionQuoteBinding.js +87 -0
- package/dist/promotions/quoteAcquisitionBinding.d.ts +6 -0
- package/dist/promotions/quoteAcquisitionBinding.js +1 -0
- package/dist/promotions/quoteAutomaticPromotions.d.ts +26 -0
- package/dist/promotions/quoteAutomaticPromotions.js +60 -0
- package/dist/promotions/quotePromoDiscounts.d.ts +36 -0
- package/dist/promotions/quotePromoDiscounts.js +146 -0
- package/dist/promotions/quotePromotionCodeAdjustments.d.ts +27 -0
- package/dist/promotions/quotePromotionCodeAdjustments.js +223 -0
- package/dist/promotions/quotePromotionV2.d.ts +30 -0
- package/dist/promotions/quotePromotionV2.js +186 -0
- package/dist/promotions/quoteShippingPolicy.d.ts +23 -0
- package/dist/promotions/quoteShippingPolicy.js +47 -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/bindings.d.ts +57 -0
- package/dist/shipping/bindings.js +46 -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 +4 -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/contracts/threshold.d.ts +22 -0
- package/dist/shipping/contracts/threshold.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 +20 -0
- package/dist/shipping/index.js +10 -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/dist/shipping/thresholdPolicy.d.ts +42 -0
- package/dist/shipping/thresholdPolicy.js +60 -0
- package/package.json +62 -2
- package/release-gates.json +263 -53
- 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 +263 -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 +181 -0
- package/src/catalog/bundleCatalogStore.ts +274 -0
- package/src/catalog/bundlePriceSelection.ts +25 -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/effectiveBundleCatalogQueries.ts +80 -0
- package/src/catalog/effectiveBundleCatalogStore.ts +208 -0
- package/src/catalog/foundationSchemas.ts +123 -0
- package/src/catalog/index.ts +44 -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 +3 -0
- package/src/composition/inventoryContribution.ts +24 -0
- package/src/composition/shippingContribution.ts +24 -0
- package/src/inventory/adminInventory.ts +251 -0
- package/src/inventory/bindings.ts +61 -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 +22 -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 +169 -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 +199 -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 +280 -0
- package/src/pricing/dbBackedCommerceQuoteSnapshotVerifier.ts +49 -0
- package/src/pricing/index.ts +53 -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/quoteShippingPromotions.ts +21 -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/commerceQuoteReadPreparation.ts +29 -0
- package/src/promotions/contracts/index.ts +387 -0
- package/src/promotions/index.ts +25 -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 +291 -0
- package/src/promotions/promotionQuoteBinding.ts +163 -0
- package/src/promotions/quoteAcquisitionBinding.ts +7 -0
- package/src/promotions/quoteAutomaticPromotions.ts +84 -0
- package/src/promotions/quotePromoDiscounts.ts +194 -0
- package/src/promotions/quotePromotionCodeAdjustments.ts +277 -0
- package/src/promotions/quotePromotionV2.ts +239 -0
- package/src/promotions/quoteShippingPolicy.ts +61 -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/bindings.ts +74 -0
- package/src/shipping/contracts/address.ts +71 -0
- package/src/shipping/contracts/delivery.ts +114 -0
- package/src/shipping/contracts/index.ts +19 -0
- package/src/shipping/contracts/shippingRate.ts +7 -0
- package/src/shipping/contracts/threshold.ts +22 -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 +26 -0
- package/src/shipping/pickupPointSearchPort.ts +8 -0
- package/src/shipping/readDeliverySelectionEvidence.ts +51 -0
- package/src/shipping/shippingRatePorts.ts +36 -0
- package/src/shipping/thresholdPolicy.ts +65 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { filterAcquisitionPromotions } from "../pricing/index.js";
|
|
2
|
+
import type { ResolvedAcquisitionTerms } from "../pricing/index.js";
|
|
3
|
+
import type { QuoteAcquisitionBinding } from "./quoteAcquisitionBinding.js";
|
|
4
|
+
import type { CreateQuoteRequest } from "../pricing/index.js";
|
|
5
|
+
import type { CommerceCodeRejection, CommerceQuoteDiscount } from "../pricing/index.js";
|
|
6
|
+
import { evaluatePromos, promoEligibilityFailure } from "@openlup/core/promo";
|
|
7
|
+
import type { PromoEvaluationCart, PromotionRow } from "@openlup/core/promo";
|
|
8
|
+
import type { CommercePromoDataPort, PromotionRedemptionCount } from "./promoDataPort.js";
|
|
9
|
+
export interface QuoteDiscountResult {
|
|
10
|
+
acquisitionTerms?: ResolvedAcquisitionTerms;
|
|
11
|
+
/** Applied discounts: order_total (clamped to subtotal) + at most one shipping discount. */
|
|
12
|
+
discounts: CommerceQuoteDiscount[];
|
|
13
|
+
/** Submitted coupon codes that produced no discount, with the reason. */
|
|
14
|
+
codeRejections: CommerceCodeRejection[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function createQuotePromoDiscounts({ promotionCustomerSemantic, acquisition }: { acquisition?: Pick<QuoteAcquisitionBinding, "financialPolicy">; promotionCustomerSemantic: (row: PromotionRow) => CommerceQuoteDiscount["customerSemantic"] }) {
|
|
18
|
+
/**
|
|
19
|
+
* Evaluate active promotions for this cart and map them to quote discounts. Two lanes:
|
|
20
|
+
* `order_total` promos are clamped to the subtotal (so `discountTotal <= subtotal`), and
|
|
21
|
+
* a single `free_shipping` promo is emitted as an `appliesTo:"shipping"` discount clamped
|
|
22
|
+
* to `shippingGrossMinor` — it is NOT suppressed by an exclusive order-total promo and is
|
|
23
|
+
* NOT counted against the subtotal. Per-mode paid-order counts drive `first_subscription_purchase`
|
|
24
|
+
* and `first_onetime_purchase` independently; all counts are server-authoritative (resolved
|
|
25
|
+
* from the clientId, never client input), so eligibility cannot be forged.
|
|
26
|
+
*/
|
|
27
|
+
async function evaluateOrderTotalDiscounts(input: {
|
|
28
|
+
promoDataPort: CommercePromoDataPort;
|
|
29
|
+
clientId: string | null;
|
|
30
|
+
visitorId?: string | null;
|
|
31
|
+
mode: CreateQuoteRequest["mode"];
|
|
32
|
+
promoCodes: string[];
|
|
33
|
+
regionCode: string;
|
|
34
|
+
subtotalGrossMinor: number;
|
|
35
|
+
shippingGrossMinor?: number;
|
|
36
|
+
atTime: string;
|
|
37
|
+
/**
|
|
38
|
+
* True when a customer email drove eligibility resolution (the request carried
|
|
39
|
+
* `customerEligibilityContext.email`). Demotes the device-redemption guard to
|
|
40
|
+
* advisory so an email-eligible customer on a used device still gets the
|
|
41
|
+
* first-order price. See {@link PromoEvaluationCart.email_eligibility_confirmed}.
|
|
42
|
+
*/
|
|
43
|
+
emailEligibilityConfirmed?: boolean;
|
|
44
|
+
}): Promise<QuoteDiscountResult> {
|
|
45
|
+
if (input.subtotalGrossMinor <= 0) return { discounts: [], codeRejections: [] };
|
|
46
|
+
|
|
47
|
+
const byMode = input.clientId
|
|
48
|
+
? await input.promoDataPort.countPaidOrdersByMode(input.clientId)
|
|
49
|
+
: { oneTime: 0, subscription: 0 };
|
|
50
|
+
|
|
51
|
+
// Cap pre-filter: drop a promo that has reached its global or per-customer redemption
|
|
52
|
+
// limit BEFORE evaluation, so caps are enforced at quote time and a paid order is never
|
|
53
|
+
// failed on a cap. A per-customer cap can only be checked for an authenticated client.
|
|
54
|
+
const [allCandidates, redemptionCounts, deviceFirstOrderRedeemed] = await Promise.all([
|
|
55
|
+
input.promoDataPort.listActivePromotions(),
|
|
56
|
+
input.promoDataPort.redemptionCounts(input.clientId),
|
|
57
|
+
input.promoDataPort.deviceFirstOrderRedeemed(input.visitorId ?? null),
|
|
58
|
+
]);
|
|
59
|
+
const customerCandidates = acquisition && input.mode === "subscription" && byMode.subscription > 0
|
|
60
|
+
? allCandidates.filter((row) => !acquisition.financialPolicy.facts(row).metadataPresent && row.eligibility.first_subscription_purchase !== true)
|
|
61
|
+
: allCandidates;
|
|
62
|
+
const effectiveCandidates = acquisition && input.mode === "subscription"
|
|
63
|
+
? filterAcquisitionPromotions(customerCandidates, input.atTime, input.regionCode, acquisition.financialPolicy) : customerCandidates;
|
|
64
|
+
const candidates = effectiveCandidates.filter(
|
|
65
|
+
(promo) => !capReached(promo, redemptionCounts.get(promo.id), input.clientId),
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
const cart: PromoEvaluationCart = {
|
|
69
|
+
region_code: input.regionCode,
|
|
70
|
+
cart_mode: input.mode === "subscription" ? "subscription" : "one_time",
|
|
71
|
+
cart_subtotal_minor: input.subtotalGrossMinor,
|
|
72
|
+
shipping_amount_minor: input.shippingGrossMinor ?? 0,
|
|
73
|
+
client_orders_count: byMode.oneTime + byMode.subscription,
|
|
74
|
+
client_subscription_orders_count: byMode.subscription,
|
|
75
|
+
client_onetime_orders_count: byMode.oneTime,
|
|
76
|
+
device_first_order_redeemed: deviceFirstOrderRedeemed,
|
|
77
|
+
email_eligibility_confirmed: input.emailEligibilityConfirmed === true,
|
|
78
|
+
// The engine compares code literals. Preserve the customer's submitted spelling
|
|
79
|
+
// for the public rejection, but feed known legacy codes in their canonical
|
|
80
|
+
// spelling so eligibility and applied-discount matching are case-insensitive.
|
|
81
|
+
applied_codes: canonicalSubmittedCodes(input.promoCodes, allCandidates),
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const applied = evaluatePromos({ cart, candidates, now: input.atTime });
|
|
85
|
+
|
|
86
|
+
const discounts: CommerceQuoteDiscount[] = [];
|
|
87
|
+
let running = 0;
|
|
88
|
+
for (const promo of applied) {
|
|
89
|
+
const source = candidates.find((candidate) => candidate.id === promo.promotion_id);
|
|
90
|
+
const customerSemantic = source ? promotionCustomerSemantic(source) : undefined;
|
|
91
|
+
const isShipping = promo.discount_type === "free_shipping";
|
|
92
|
+
const cap = isShipping ? cart.shipping_amount_minor : input.subtotalGrossMinor - running;
|
|
93
|
+
if (cap <= 0) {
|
|
94
|
+
if (isShipping) continue;
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
const amountOffMinor = Math.min(promo.amount_off_minor, cap);
|
|
98
|
+
if (amountOffMinor <= 0) continue;
|
|
99
|
+
if (!isShipping) running += amountOffMinor;
|
|
100
|
+
discounts.push({
|
|
101
|
+
promotionId: promo.promotion_id,
|
|
102
|
+
...(promo.code ? { code: promo.code } : {}),
|
|
103
|
+
// Human-facing label preserves the promotion name instead of its reason code.
|
|
104
|
+
...(promo.name ? { label: promo.name } : {}),
|
|
105
|
+
appliesTo: isShipping ? "shipping" : "order_total",
|
|
106
|
+
amountOffMinor,
|
|
107
|
+
reasonCode: promo.reason_code,
|
|
108
|
+
...(customerSemantic ? { customerSemantic } : {}),
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
discounts,
|
|
114
|
+
codeRejections: classifyRejections(input, cart, allCandidates, redemptionCounts, discounts),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* For each submitted coupon code that produced no discount, classify why. Uses the
|
|
120
|
+
* pre-cap candidate set so an exhausted per-customer cap reads as `already_used` rather
|
|
121
|
+
* than `not_recognized`. An eligible code that was merely outranked by a larger promo
|
|
122
|
+
* is a neutral `better_price_exists`: it was not applied because the customer already
|
|
123
|
+
* has an equally good or better automatic price. Applied shipping-code discounts count
|
|
124
|
+
* as applied too.
|
|
125
|
+
*/
|
|
126
|
+
function classifyRejections(
|
|
127
|
+
input: { clientId: string | null; promoCodes: string[]; atTime: string },
|
|
128
|
+
cart: PromoEvaluationCart,
|
|
129
|
+
allCandidates: PromotionRow[],
|
|
130
|
+
redemptionCounts: Map<string, PromotionRedemptionCount>,
|
|
131
|
+
discounts: readonly CommerceQuoteDiscount[],
|
|
132
|
+
): CommerceCodeRejection[] {
|
|
133
|
+
const rejections: CommerceCodeRejection[] = [];
|
|
134
|
+
for (const code of input.promoCodes) {
|
|
135
|
+
const promo = resolveSubmittedCoupon(code, allCandidates);
|
|
136
|
+
if (!promo) {
|
|
137
|
+
rejections.push({ code, reason: "not_recognized" });
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
// A legacy literal may be bound to more than one promotion/lane. A shipping
|
|
141
|
+
// benefit can legitimately apply even when the first matching product row
|
|
142
|
+
// fails scope, so the visible applied discount always wins over rejection.
|
|
143
|
+
if (discounts.some((discount) => sameCode(discount.code, code) || sameCode(discount.code, promo.code))) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (capReached(promo, redemptionCounts.get(promo.id), input.clientId)) {
|
|
147
|
+
rejections.push({ code, reason: "already_used" });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const failure = promoEligibilityFailure(promo, cart, input.atTime);
|
|
151
|
+
if (failure) rejections.push({ code, reason: failure });
|
|
152
|
+
else if (!discounts.some((discount) => discount.code === promo.code)) {
|
|
153
|
+
rejections.push({ code, reason: "better_price_exists" });
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return rejections;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function canonicalSubmittedCodes(codes: readonly string[], candidates: readonly PromotionRow[]): string[] {
|
|
160
|
+
return codes.map((code) => resolveSubmittedCoupon(code, candidates)?.code ?? code);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Legacy permits normalized-code collisions. Preserve its exact-literal
|
|
165
|
+
* matching first; case-insensitive recovery is safe only for one identity.
|
|
166
|
+
*/
|
|
167
|
+
function resolveSubmittedCoupon(code: string, candidates: readonly PromotionRow[]): PromotionRow | undefined {
|
|
168
|
+
const couponCandidates = candidates.filter((promo) => promo.trigger_type === "coupon_code" && promo.code !== null);
|
|
169
|
+
const exact = couponCandidates.find((promo) => promo.code === code);
|
|
170
|
+
if (exact) return exact;
|
|
171
|
+
const normalized = couponCandidates.filter((promo) => sameCode(promo.code, code));
|
|
172
|
+
return normalized.length === 1 ? normalized[0] : undefined;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function sameCode(left: string | null | undefined, right: string | null | undefined): boolean {
|
|
176
|
+
return typeof left === "string" && typeof right === "string" &&
|
|
177
|
+
left.trim().toUpperCase() === right.trim().toUpperCase();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function capReached(
|
|
181
|
+
promo: PromotionRow,
|
|
182
|
+
counts: PromotionRedemptionCount | undefined,
|
|
183
|
+
clientId: string | null,
|
|
184
|
+
): boolean {
|
|
185
|
+
const { global, perCustomer } = counts ?? { global: 0, perCustomer: 0 };
|
|
186
|
+
if (promo.redemption_limit_global != null && global >= promo.redemption_limit_global) return true;
|
|
187
|
+
if (clientId && promo.redemption_limit_per_customer != null && perCustomer >= promo.redemption_limit_per_customer) {
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return { evaluateOrderTotalDiscounts };
|
|
194
|
+
}
|
|
@@ -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
|
+
}
|