@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,219 @@
|
|
|
1
|
+
/** Local, bounded source-resource transfer. Uploading bytes never activates associations. */
|
|
2
|
+
import { constants, promises as fs } from "node:fs";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { isAbsolute, resolve, sep } from "node:path";
|
|
5
|
+
import { resolveProductMedia } from "./contracts/resolveProductMedia.js";
|
|
6
|
+
import type { BlobStoragePort } from "@openlup/core/platform-runtime";
|
|
7
|
+
import {
|
|
8
|
+
validateProductMediaManifest,
|
|
9
|
+
PRODUCT_MEDIA_LIMITS,
|
|
10
|
+
type ProductMediaManifest,
|
|
11
|
+
type ProductMediaAsset,
|
|
12
|
+
} from "./contracts/productMediaContracts.js";
|
|
13
|
+
|
|
14
|
+
export const SOURCE_PACK_LIMITS = PRODUCT_MEDIA_LIMITS;
|
|
15
|
+
export interface CatalogMediaSourcePack {
|
|
16
|
+
schemaVersion: "catalog.media-source-pack.v1";
|
|
17
|
+
manifest: ProductMediaManifest;
|
|
18
|
+
}
|
|
19
|
+
export class CatalogMediaSourcePackError extends Error {
|
|
20
|
+
constructor(public readonly code: string, public readonly reference?: string) {
|
|
21
|
+
super(reference ? `${code}:${reference}` : code);
|
|
22
|
+
this.name = "CatalogMediaSourcePackError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
const refuse = (code: string, reference?: string): never => {
|
|
26
|
+
throw new CatalogMediaSourcePackError(code, reference);
|
|
27
|
+
};
|
|
28
|
+
const digest = (bytes: Uint8Array): string => createHash("sha256").update(bytes).digest("hex");
|
|
29
|
+
const extensions: Record<string, string> = { "image/png": "png", "image/jpeg": "jpg", "image/webp": "webp" };
|
|
30
|
+
const keyFor = (asset: ProductMediaAsset): string => `assets/${asset.sha256}.${extensions[asset.contentType]}`;
|
|
31
|
+
|
|
32
|
+
function normalized(manifest: ProductMediaManifest): ProductMediaManifest {
|
|
33
|
+
return {
|
|
34
|
+
schemaVersion: manifest.schemaVersion,
|
|
35
|
+
products: manifest.products.map(({ productId, variantIds, primaryVariantId }) => ({ productId, variantIds: [...variantIds], primaryVariantId })),
|
|
36
|
+
assets: manifest.assets.map(({ assetRef, source, contentType, byteLength, sha256, artworkLocale }) => ({ assetRef, source, contentType, byteLength, sha256, artworkLocale })),
|
|
37
|
+
associations: manifest.associations.map(({ itemId, assetRef, productId, scope, variantId, locale, role, viewKey, order, label }) => ({
|
|
38
|
+
itemId, assetRef, productId, scope, ...(variantId === undefined ? {} : { variantId }),
|
|
39
|
+
locale, role, ...(viewKey === undefined ? {} : { viewKey }), order, label,
|
|
40
|
+
})),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function checkedManifest(value: unknown): ProductMediaManifest {
|
|
44
|
+
const manifest = normalized(validateProductMediaManifest(value));
|
|
45
|
+
const envelope = { schemaVersion: "catalog.media-source-pack.v1", manifest: {
|
|
46
|
+
...manifest, assets: manifest.assets.map((asset) => ({ ...asset, source: keyFor(asset) })),
|
|
47
|
+
} };
|
|
48
|
+
if (Buffer.byteLength(JSON.stringify(manifest)) > SOURCE_PACK_LIMITS.manifestBytes ||
|
|
49
|
+
Buffer.byteLength(JSON.stringify(envelope, null, 2) + "\n") > SOURCE_PACK_LIMITS.manifestBytes) refuse("pack_limit_exceeded");
|
|
50
|
+
const checks = manifest.products.map((product) => {
|
|
51
|
+
const items = manifest.associations.filter((item) => item.productId === product.productId);
|
|
52
|
+
const locales = new Set(items.flatMap((item) => item.locale === null ? [] : [item.locale]));
|
|
53
|
+
let neutralIndex = 0;
|
|
54
|
+
while (locales.has(`source-pack-neutral-${neutralIndex}`)) neutralIndex++;
|
|
55
|
+
locales.add(`source-pack-neutral-${neutralIndex}`);
|
|
56
|
+
return { product, locales };
|
|
57
|
+
});
|
|
58
|
+
// Bound processing before any full resolver pass, resource read or upload.
|
|
59
|
+
const expandedChecks = checks.reduce((total, { product, locales }) => total + product.variantIds.length * locales.size, 0);
|
|
60
|
+
if (expandedChecks > SOURCE_PACK_LIMITS.associations) refuse("pack_limit_exceeded");
|
|
61
|
+
const urls = Object.fromEntries(manifest.assets.map((asset) => [asset.assetRef, asset.source]));
|
|
62
|
+
for (const { product, locales } of checks) {
|
|
63
|
+
for (const variantId of product.variantIds) for (const locale of locales) {
|
|
64
|
+
resolveProductMedia(manifest, { productId: product.productId, variantId, locale }, urls);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return manifest;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function contained(root: string, key: string, reference?: string): Promise<string> {
|
|
71
|
+
if (!key || isAbsolute(key) || key.includes("\\") || key.includes(":" ) || key.includes("%") ||
|
|
72
|
+
key.split("/").some((part) => !part || part === "." || part === "..")) refuse("unsafe_path", reference);
|
|
73
|
+
try {
|
|
74
|
+
const canonicalRoot = await fs.realpath(root);
|
|
75
|
+
const target = await fs.realpath(resolve(canonicalRoot, key));
|
|
76
|
+
if (!target.startsWith(canonicalRoot + sep)) refuse("unsafe_path", reference);
|
|
77
|
+
return target;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (error instanceof CatalogMediaSourcePackError) throw error;
|
|
80
|
+
return refuse("missing_resource", reference);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** Open only contained regular files, allocate a fixed bound and read at most limit + 1. */
|
|
84
|
+
async function boundedRead(path: string, limit: number, reference?: string): Promise<Buffer> {
|
|
85
|
+
const handle = await fs.open(path, constants.O_RDONLY | constants.O_NOFOLLOW).catch(() => refuse("missing_resource", reference));
|
|
86
|
+
try {
|
|
87
|
+
const stat = await handle.stat();
|
|
88
|
+
if (!stat.isFile()) refuse("unsafe_path", reference);
|
|
89
|
+
if (stat.size > limit) refuse("pack_limit_exceeded", reference);
|
|
90
|
+
const bytes = Buffer.alloc(limit + 1);
|
|
91
|
+
let length = 0;
|
|
92
|
+
while (length < bytes.length) {
|
|
93
|
+
const { bytesRead } = await handle.read(bytes, length, bytes.length - length, null);
|
|
94
|
+
if (!bytesRead) break;
|
|
95
|
+
length += bytesRead;
|
|
96
|
+
}
|
|
97
|
+
if (length > limit) refuse("pack_limit_exceeded", reference);
|
|
98
|
+
return bytes.subarray(0, length);
|
|
99
|
+
} finally { await handle.close(); }
|
|
100
|
+
}
|
|
101
|
+
function matchesMime(bytes: Buffer, mime: string): boolean {
|
|
102
|
+
if (mime === "image/png") return bytes.subarray(0, 8).equals(Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
103
|
+
if (mime === "image/jpeg") return bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255;
|
|
104
|
+
return bytes.length >= 16 && bytes.toString("ascii", 0, 4) === "RIFF" &&
|
|
105
|
+
bytes.toString("ascii", 8, 12) === "WEBP" && bytes.readUInt32LE(4) + 8 === bytes.length &&
|
|
106
|
+
["VP8 ", "VP8L", "VP8X"].includes(bytes.toString("ascii", 12, 16));
|
|
107
|
+
}
|
|
108
|
+
async function loadResources(manifest: ProductMediaManifest, root: string): Promise<Map<string, Buffer>> {
|
|
109
|
+
const paths = new Map<string, string>();
|
|
110
|
+
// Stat every resource before reading any payload; declared and actual totals are bounded.
|
|
111
|
+
for (const asset of manifest.assets) {
|
|
112
|
+
const path = await contained(root, asset.source, asset.assetRef);
|
|
113
|
+
const stat = await fs.stat(path);
|
|
114
|
+
if (!stat.isFile()) refuse("unsafe_path", asset.assetRef);
|
|
115
|
+
if (stat.size > SOURCE_PACK_LIMITS.imageBytes) refuse("pack_limit_exceeded", asset.assetRef);
|
|
116
|
+
if (stat.size !== asset.byteLength) refuse("size_mismatch", asset.assetRef);
|
|
117
|
+
paths.set(asset.assetRef, path);
|
|
118
|
+
}
|
|
119
|
+
const resources = new Map<string, Buffer>();
|
|
120
|
+
for (const asset of manifest.assets) {
|
|
121
|
+
const bytes = await boundedRead(paths.get(asset.assetRef)!, asset.byteLength, asset.assetRef);
|
|
122
|
+
if (bytes.length !== asset.byteLength) refuse("size_mismatch", asset.assetRef);
|
|
123
|
+
if (!matchesMime(bytes, asset.contentType)) refuse("mime_mismatch", asset.assetRef);
|
|
124
|
+
if (digest(bytes) !== asset.sha256) refuse("digest_mismatch", asset.assetRef);
|
|
125
|
+
resources.set(asset.assetRef, bytes);
|
|
126
|
+
}
|
|
127
|
+
return resources;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function readCatalogMediaSourceManifest(path: string): Promise<unknown> {
|
|
131
|
+
const bytes = await boundedRead(path, SOURCE_PACK_LIMITS.manifestBytes);
|
|
132
|
+
try { return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); }
|
|
133
|
+
catch { return refuse("invalid_schema"); }
|
|
134
|
+
}
|
|
135
|
+
async function checkInventory(root: string, manifest: ProductMediaManifest) {
|
|
136
|
+
const expected = new Set(manifest.assets.map((asset) => keyFor(asset).slice(7)));
|
|
137
|
+
const rootEntries = await fs.opendir(root);
|
|
138
|
+
let rootCount = 0;
|
|
139
|
+
for await (const entry of rootEntries) {
|
|
140
|
+
if (++rootCount > 2 || !["assets", "manifest.json"].includes(entry.name)) refuse("reference_mismatch");
|
|
141
|
+
}
|
|
142
|
+
const assetRoot = await contained(root, "assets");
|
|
143
|
+
const entries = await fs.opendir(assetRoot);
|
|
144
|
+
let count = 0;
|
|
145
|
+
for await (const entry of entries) {
|
|
146
|
+
if (++count > SOURCE_PACK_LIMITS.assets) refuse("pack_limit_exceeded");
|
|
147
|
+
if (!expected.delete(entry.name)) refuse("reference_mismatch");
|
|
148
|
+
}
|
|
149
|
+
if (expected.size) refuse("missing_resource");
|
|
150
|
+
}
|
|
151
|
+
export async function validateCatalogMediaSourcePack(root: string) {
|
|
152
|
+
const input = await readCatalogMediaSourceManifest(await contained(root, "manifest.json"));
|
|
153
|
+
if (!input || typeof input !== "object" || (input as CatalogMediaSourcePack).schemaVersion !== "catalog.media-source-pack.v1") return refuse("invalid_schema");
|
|
154
|
+
if (Object.keys(input).some((key) => !["schemaVersion", "manifest"].includes(key))) refuse("invalid_schema");
|
|
155
|
+
const manifest = checkedManifest((input as CatalogMediaSourcePack).manifest);
|
|
156
|
+
for (const asset of manifest.assets) if (asset.source !== keyFor(asset)) refuse("unsafe_path", asset.assetRef);
|
|
157
|
+
await checkInventory(root, manifest);
|
|
158
|
+
const resources = await loadResources(manifest, root);
|
|
159
|
+
return { pack: { schemaVersion: "catalog.media-source-pack.v1", manifest } as CatalogMediaSourcePack, resources };
|
|
160
|
+
}
|
|
161
|
+
async function writeNewPack(pack: CatalogMediaSourcePack, resources: Map<string, Buffer>, targetRoot: string) {
|
|
162
|
+
const encoded = JSON.stringify(pack, null, 2) + "\n";
|
|
163
|
+
if (Buffer.byteLength(encoded) > SOURCE_PACK_LIMITS.manifestBytes) refuse("pack_limit_exceeded");
|
|
164
|
+
// mkdir without recursive is the atomic no-overwrite boundary.
|
|
165
|
+
await fs.mkdir(targetRoot).catch((error: NodeJS.ErrnoException) => {
|
|
166
|
+
if (error.code === "EEXIST") refuse("target_exists");
|
|
167
|
+
refuse("dependency_unavailable");
|
|
168
|
+
});
|
|
169
|
+
const root = await fs.realpath(targetRoot);
|
|
170
|
+
await fs.mkdir(resolve(root, "assets"));
|
|
171
|
+
const written = new Set<string>();
|
|
172
|
+
for (const asset of pack.manifest.assets) {
|
|
173
|
+
if (written.has(asset.source)) continue;
|
|
174
|
+
await fs.writeFile(resolve(root, asset.source), resources.get(asset.assetRef)!, { flag: "wx" });
|
|
175
|
+
written.add(asset.source);
|
|
176
|
+
}
|
|
177
|
+
await fs.writeFile(resolve(root, "manifest.json"), encoded, { flag: "wx" });
|
|
178
|
+
return pack;
|
|
179
|
+
}
|
|
180
|
+
export async function exportCatalogMediaSourcePack(options: { manifest: unknown; sourceRoot: string; targetRoot: string }) {
|
|
181
|
+
const manifest = checkedManifest(options.manifest);
|
|
182
|
+
const resources = await loadResources(manifest, options.sourceRoot);
|
|
183
|
+
const portable = { ...manifest, assets: manifest.assets.map((asset) => ({ ...asset, source: keyFor(asset) })) };
|
|
184
|
+
return writeNewPack({ schemaVersion: "catalog.media-source-pack.v1", manifest: portable }, resources, options.targetRoot);
|
|
185
|
+
}
|
|
186
|
+
export async function restoreCatalogMediaSourcePack(packRoot: string, targetRoot: string) {
|
|
187
|
+
const { pack, resources } = await validateCatalogMediaSourcePack(packRoot);
|
|
188
|
+
return writeNewPack(pack, resources, targetRoot);
|
|
189
|
+
}
|
|
190
|
+
export type CatalogMediaImportOutcome =
|
|
191
|
+
| { status: "complete"; successfulKeys: string[]; assets: Array<{ assetRef: string; key: string; url: string }> }
|
|
192
|
+
| { status: "validation_refusal"; code: string; reference?: string }
|
|
193
|
+
| { status: "dependency_unavailable" | "partial_upload"; successfulKeys: string[]; failedAssetRef: string; failedKey: string };
|
|
194
|
+
export async function importCatalogMediaSourcePack(root: string, storage: BlobStoragePort): Promise<CatalogMediaImportOutcome> {
|
|
195
|
+
let validated: Awaited<ReturnType<typeof validateCatalogMediaSourcePack>>;
|
|
196
|
+
try { validated = await validateCatalogMediaSourcePack(root); }
|
|
197
|
+
catch (error) {
|
|
198
|
+
const refusal = error as { code?: string; reference?: string };
|
|
199
|
+
return { status: "validation_refusal", code: refusal.code ?? "invalid_schema", ...(refusal.reference ? { reference: refusal.reference } : {}) };
|
|
200
|
+
}
|
|
201
|
+
const successfulKeys: string[] = [];
|
|
202
|
+
const assets: Array<{ assetRef: string; key: string; url: string }> = [];
|
|
203
|
+
const urlsByKey = new Map<string, string>();
|
|
204
|
+
for (const asset of validated.pack.manifest.assets) {
|
|
205
|
+
const key = keyFor(asset);
|
|
206
|
+
try {
|
|
207
|
+
if (!urlsByKey.has(key)) {
|
|
208
|
+
const result = await storage.upload(key, validated.resources.get(asset.assetRef)!, { contentType: asset.contentType });
|
|
209
|
+
if (result.pathname !== key) refuse("storage_path_mismatch", asset.assetRef);
|
|
210
|
+
successfulKeys.push(key);
|
|
211
|
+
urlsByKey.set(key, storage.urlFor(key));
|
|
212
|
+
}
|
|
213
|
+
assets.push({ assetRef: asset.assetRef, key, url: urlsByKey.get(key)! });
|
|
214
|
+
} catch {
|
|
215
|
+
return { status: successfulKeys.length ? "partial_upload" : "dependency_unavailable", successfulKeys, failedAssetRef: asset.assetRef, failedKey: key };
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return { status: "complete", successfulKeys, assets };
|
|
219
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { z } from "./validation.js";
|
|
2
|
+
import type { CatalogProductTypeDefinition, CatalogProductTypeRegistry } from "./contracts/productTypes.js";
|
|
3
|
+
export { canonicalCatalogOption, validateCatalogQuantity } from "./contracts/productTypes.js";
|
|
4
|
+
export type { CatalogQuantity, CatalogOptionValue, CatalogDraftIssue, CatalogProductTypeDefinition, CatalogProductTypeRegistry } from "./contracts/productTypes.js";
|
|
5
|
+
|
|
6
|
+
export const catalogNamespacedKeySchema = z.string().max(120).regex(/^[a-z][a-z0-9.-]*:[a-z][a-z0-9._-]*$/u);
|
|
7
|
+
export const catalogDimensionKeySchema = z.string().max(64).regex(/^[a-z][a-z0-9_-]*$/u)
|
|
8
|
+
.refine((key) => !["__proto__", "constructor", "prototype"].includes(key));
|
|
9
|
+
export const catalogQuantityDimensionSchema = z.enum(["mass", "volume", "count"]);
|
|
10
|
+
export const catalogQuantitySchema = z.object({
|
|
11
|
+
dimension: catalogQuantityDimensionSchema,
|
|
12
|
+
unscaled: z.string().regex(/^[1-9][0-9]{0,39}$/u),
|
|
13
|
+
scale: z.number().int().min(0).max(12),
|
|
14
|
+
unit: catalogNamespacedKeySchema,
|
|
15
|
+
}).strict();
|
|
16
|
+
export const catalogOptionValueSchema = z.discriminatedUnion("kind", [
|
|
17
|
+
z.object({ kind: z.literal("choice"), value: z.string().min(1).max(120) }).strict(),
|
|
18
|
+
z.object({ kind: z.literal("quantity"), value: catalogQuantitySchema }).strict(),
|
|
19
|
+
z.object({ kind: z.literal("boolean"), value: z.boolean() }).strict(),
|
|
20
|
+
]);
|
|
21
|
+
export const catalogProductTypeRefSchema = z.object({
|
|
22
|
+
key: catalogNamespacedKeySchema,
|
|
23
|
+
version: z.number().int().positive().max(2_147_483_647),
|
|
24
|
+
}).strict();
|
|
25
|
+
const unitSchema = z.object({
|
|
26
|
+
code: catalogNamespacedKeySchema,
|
|
27
|
+
dimension: catalogQuantityDimensionSchema,
|
|
28
|
+
integral: z.boolean(),
|
|
29
|
+
}).strict();
|
|
30
|
+
const dimensionSchema = z.discriminatedUnion("kind", [
|
|
31
|
+
z.object({ key: catalogDimensionKeySchema, kind: z.literal("choice"), values: z.array(z.string().min(1).max(120)).min(1).max(1000) }).strict(),
|
|
32
|
+
z.object({ key: catalogDimensionKeySchema, kind: z.literal("quantity"), dimension: catalogQuantityDimensionSchema,
|
|
33
|
+
units: z.array(catalogNamespacedKeySchema).min(1).max(32), role: z.literal("net_content").optional() }).strict(),
|
|
34
|
+
z.object({ key: catalogDimensionKeySchema, kind: z.literal("boolean") }).strict(),
|
|
35
|
+
]);
|
|
36
|
+
const definitionSchema = catalogProductTypeRefSchema.extend({
|
|
37
|
+
units: z.array(unitSchema).max(64),
|
|
38
|
+
netContentUnits: z.array(catalogNamespacedKeySchema).min(1).max(64),
|
|
39
|
+
dimensions: z.array(dimensionSchema).max(16),
|
|
40
|
+
}).strict();
|
|
41
|
+
/** Definitions are installed by composition, never supplied as executable draft data. */
|
|
42
|
+
export function createCatalogProductTypeRegistry(definitions: readonly CatalogProductTypeDefinition[]): CatalogProductTypeRegistry {
|
|
43
|
+
const installed = new Map<string, CatalogProductTypeDefinition>();
|
|
44
|
+
for (const definition of definitions) {
|
|
45
|
+
const { validateContent, ...metadata } = definition;
|
|
46
|
+
const parsed = definitionSchema.parse(metadata);
|
|
47
|
+
const units = new Map(parsed.units.map((unit) => [unit.code, unit]));
|
|
48
|
+
const identity = `${parsed.key}@${parsed.version}`;
|
|
49
|
+
if (typeof validateContent !== "function" || installed.has(identity)
|
|
50
|
+
|| units.size !== parsed.units.length
|
|
51
|
+
|| new Set(parsed.dimensions.map(({ key }) => key)).size !== parsed.dimensions.length
|
|
52
|
+
|| new Set(parsed.netContentUnits).size !== parsed.netContentUnits.length
|
|
53
|
+
|| parsed.netContentUnits.some((code) => !units.has(code))) throw new Error("invalid_catalog_type_definition");
|
|
54
|
+
for (const dimension of parsed.dimensions) {
|
|
55
|
+
if (dimension.kind === "choice" && new Set(dimension.values).size !== dimension.values.length) throw new Error("invalid_catalog_type_definition");
|
|
56
|
+
if (dimension.kind === "quantity" && (new Set(dimension.units).size !== dimension.units.length
|
|
57
|
+
|| dimension.units.some((code) => units.get(code)?.dimension !== dimension.dimension)
|
|
58
|
+
|| (dimension.role === "net_content" && dimension.units.some((code) => !parsed.netContentUnits.includes(code))))) throw new Error("invalid_catalog_type_definition");
|
|
59
|
+
}
|
|
60
|
+
if (parsed.dimensions.filter((dimension) => dimension.kind === "quantity" && dimension.role === "net_content").length > 1) throw new Error("invalid_catalog_type_definition");
|
|
61
|
+
function freeze(value: object): void {
|
|
62
|
+
for (const child of Object.values(value)) if (child && typeof child === "object") freeze(child);
|
|
63
|
+
Object.freeze(value);
|
|
64
|
+
}
|
|
65
|
+
freeze(parsed);
|
|
66
|
+
installed.set(identity, Object.freeze({ ...parsed, validateContent }));
|
|
67
|
+
}
|
|
68
|
+
return Object.freeze({ resolve: (ref: z.infer<typeof catalogProductTypeRefSchema>) => installed.get(`${ref.key}@${ref.version}`) });
|
|
69
|
+
}
|
|
70
|
+
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CatalogHeadRef, CatalogHistoryResult, CatalogPublicationReadResult,
|
|
3
|
+
CatalogPublicationReceipt, CatalogPublicationReceiptResult,
|
|
4
|
+
CatalogPublicationRefusal, CatalogPublishCommand,
|
|
5
|
+
} from "./contracts/lifecycle.js";
|
|
6
|
+
|
|
7
|
+
declare const verifiedCatalogPublisher: unique symbol;
|
|
8
|
+
/** Server authorization result. P1 exposes no constructor or actor-string conversion. */
|
|
9
|
+
export interface VerifiedCatalogPublisher {
|
|
10
|
+
readonly [verifiedCatalogPublisher]: true;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CatalogValidationPort {
|
|
14
|
+
validate(candidateRef: string): Promise<{
|
|
15
|
+
candidateDigest: string;
|
|
16
|
+
issues: readonly { code: string; path: string }[];
|
|
17
|
+
}>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** No default binding: missing authority is unsupported; configured outage is dependency_unavailable. */
|
|
21
|
+
export interface CatalogPublicationReadPort {
|
|
22
|
+
current(productId: string): Promise<CatalogPublicationReadResult>;
|
|
23
|
+
readback(head: CatalogHeadRef): Promise<CatalogPublicationReadResult>;
|
|
24
|
+
receipt(scope: { productId: string; commandKey: string }): Promise<CatalogPublicationReceiptResult>;
|
|
25
|
+
/** Null starts a bounded history page; cursors retain the authority's stable ordering. */
|
|
26
|
+
history(productId: string, cursor: string | null): Promise<CatalogHistoryResult>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Types only: proposal submission does not implement publication or lifecycle writes. */
|
|
30
|
+
export interface CatalogPublicationPort {
|
|
31
|
+
publish(command: CatalogPublishCommand, actor: VerifiedCatalogPublisher): Promise<
|
|
32
|
+
| { outcome: "committed" | "replayed"; receipt: CatalogPublicationReceipt }
|
|
33
|
+
| CatalogPublicationRefusal
|
|
34
|
+
>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Describes a host's actual support; exporting ports supplies no capabilities. */
|
|
38
|
+
export interface CatalogCapabilityDescriptor {
|
|
39
|
+
read: boolean;
|
|
40
|
+
draft: boolean;
|
|
41
|
+
publicationRead: boolean;
|
|
42
|
+
publish: boolean;
|
|
43
|
+
lifecycleWrite: boolean;
|
|
44
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { SellableCatalogItem, SellableCatalogProfile, CatalogSkuEnvelopeSelector, NeutralSkuEnvelopeV1 } from "./contracts/index.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal neutral catalog seam used by the local reference storefront. It is
|
|
5
|
+
* independent of richer application catalog read ports.
|
|
6
|
+
*/
|
|
7
|
+
export interface SellableCatalogPort {
|
|
8
|
+
getStorefrontProfile(): Promise<SellableCatalogProfile>;
|
|
9
|
+
listSellableItems(): Promise<SellableCatalogItem[]>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Dark W2a read seam. Its row-safe return value deliberately contains document
|
|
14
|
+
* references, never a document payload or a SKU-level document override.
|
|
15
|
+
*/
|
|
16
|
+
export interface CatalogSkuEnvelopeReadPort {
|
|
17
|
+
listSkuEnvelopes(query: CatalogSkuEnvelopeListQuery): Promise<CatalogSkuEnvelopePage>;
|
|
18
|
+
/**
|
|
19
|
+
* Page only active trade items that are sellable in at least one commerce
|
|
20
|
+
* channel. This is intentionally distinct from the neutral inventory-style
|
|
21
|
+
* listing above: consumers such as compatibility must not discover dormant
|
|
22
|
+
* SKUs and then try to filter them with a second authority.
|
|
23
|
+
*/
|
|
24
|
+
listActiveSkuEnvelopes(query: CatalogActiveSkuEnvelopeListQuery): Promise<CatalogSkuEnvelopePage>;
|
|
25
|
+
getSkuEnvelope(selector: CatalogSkuEnvelopeSelector): Promise<NeutralSkuEnvelopeV1 | null>;
|
|
26
|
+
/** Compatibility lookup for callers that already persist the neutral SKU UUID. */
|
|
27
|
+
getSkuEnvelopeById(skuId: string): Promise<NeutralSkuEnvelopeV1 | null>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Additive discovery capability. Existing commerce consumers remain typed to
|
|
32
|
+
* {@link CatalogSkuEnvelopeReadPort} and therefore cannot accidentally use it.
|
|
33
|
+
*/
|
|
34
|
+
export interface PublicCatalogSkuEnvelopeReadPort extends CatalogSkuEnvelopeReadPort {
|
|
35
|
+
/**
|
|
36
|
+
* Public discovery's active-only page. Unlike the commerce method above it
|
|
37
|
+
* intentionally includes temporarily unsellable active variants.
|
|
38
|
+
*/
|
|
39
|
+
listPublicActiveSkuEnvelopes(query: CatalogSkuEnvelopeListQuery): Promise<CatalogSkuEnvelopePage>;
|
|
40
|
+
/** Active-only cursor page restricted to one already-authoritative product. */
|
|
41
|
+
listPublicActiveProductSkuEnvelopes(
|
|
42
|
+
productId: string,
|
|
43
|
+
query: CatalogSkuEnvelopeListQuery,
|
|
44
|
+
): Promise<CatalogSkuEnvelopePage>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface CatalogSkuEnvelopeListQuery {
|
|
48
|
+
cursor: string | null;
|
|
49
|
+
limit: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface CatalogActiveSkuEnvelopeListQuery extends CatalogSkuEnvelopeListQuery {
|
|
53
|
+
/** Optional server-selected scope; supplied selections contain 1–100 unique SKU codes. */
|
|
54
|
+
skuCodes?: readonly string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CatalogSkuEnvelopePage {
|
|
58
|
+
items: NeutralSkuEnvelopeV1[];
|
|
59
|
+
nextCursor: string | null;
|
|
60
|
+
}
|
|
61
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { HttpRequest, HttpResponse } from "../http/index.js";
|
|
2
|
+
import type { BffResponder } from "../bff/index.js";
|
|
3
|
+
import {
|
|
4
|
+
SELLABLE_CATALOG_CONTRACT_VERSION,
|
|
5
|
+
sellableCatalogListResponseSchema,
|
|
6
|
+
} from "./sellableSchemas.js";
|
|
7
|
+
import type { SellableCatalogPort } from "./readPorts.js";
|
|
8
|
+
|
|
9
|
+
export function createReferenceCatalogItemsHandler({
|
|
10
|
+
catalogPort, responder,
|
|
11
|
+
}: {
|
|
12
|
+
catalogPort: SellableCatalogPort;
|
|
13
|
+
responder: BffResponder;
|
|
14
|
+
}) {
|
|
15
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = responder;
|
|
16
|
+
return async function handler(req: HttpRequest, res: HttpResponse): Promise<void> {
|
|
17
|
+
if (req.method !== "GET") {
|
|
18
|
+
sendMethodNotAllowed(res, ["GET"]);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const response = sellableCatalogListResponseSchema.safeParse({
|
|
24
|
+
contractVersion: SELLABLE_CATALOG_CONTRACT_VERSION,
|
|
25
|
+
profile: await catalogPort.getStorefrontProfile(),
|
|
26
|
+
items: await catalogPort.listSellableItems(),
|
|
27
|
+
});
|
|
28
|
+
if (!response.success) {
|
|
29
|
+
sendBffError(res, "INVALID_RESPONSE", "Reference catalog returned invalid items");
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
sendBffSuccess(res, response.data, { contractVersion: SELLABLE_CATALOG_CONTRACT_VERSION }, {
|
|
33
|
+
cacheControl: "no-store",
|
|
34
|
+
});
|
|
35
|
+
} catch {
|
|
36
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Reference catalog is unavailable");
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from "./validation.js";
|
|
2
|
+
import {
|
|
3
|
+
CatalogSkuAuthorityError,
|
|
4
|
+
assertPrimaryUnitGtinAuthority,
|
|
5
|
+
neutralSkuEnvelopeV1Schema,
|
|
6
|
+
type NeutralSkuEnvelopeV1,
|
|
7
|
+
} from "./foundationSchemas.js";
|
|
8
|
+
|
|
9
|
+
const uuidSchema = z.guid();
|
|
10
|
+
const digestSchema = z.string().regex(/^[a-f0-9]{64}$/u);
|
|
11
|
+
const currencySchema = z.string().regex(/^[A-Z]{3}$/u);
|
|
12
|
+
const moneyMinorSchema = z.number().int().nonnegative();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Server-only, read-safe projection. It intentionally composes opaque document
|
|
16
|
+
* references with resolved prices and never selects or exposes document payload.
|
|
17
|
+
*/
|
|
18
|
+
export const RESOLVED_SKU_PROJECTION_VERSION = "catalog.resolved-sku.v1";
|
|
19
|
+
|
|
20
|
+
export const resolvedSkuProjectionSchema = z.object({
|
|
21
|
+
contractVersion: z.literal(RESOLVED_SKU_PROJECTION_VERSION),
|
|
22
|
+
envelope: neutralSkuEnvelopeV1Schema,
|
|
23
|
+
priceContext: z.object({
|
|
24
|
+
priceListId: uuidSchema,
|
|
25
|
+
regionCode: z.string().regex(/^[A-Z]{2,3}$/u),
|
|
26
|
+
currency: currencySchema,
|
|
27
|
+
channel: z.string().trim().min(1).max(64),
|
|
28
|
+
atTime: z.string().datetime({ offset: true }),
|
|
29
|
+
}).strict(),
|
|
30
|
+
pricing: z.object({
|
|
31
|
+
base: z.object({
|
|
32
|
+
priceEntryId: uuidSchema,
|
|
33
|
+
amountMinor: moneyMinorSchema,
|
|
34
|
+
}).strict(),
|
|
35
|
+
subscription: z.object({
|
|
36
|
+
policyRevisionId: uuidSchema,
|
|
37
|
+
policyDigest: digestSchema,
|
|
38
|
+
amountMinor: moneyMinorSchema,
|
|
39
|
+
}).strict().nullable(),
|
|
40
|
+
}).strict(),
|
|
41
|
+
}).strict().superRefine((projection, context) => {
|
|
42
|
+
try {
|
|
43
|
+
assertPrimaryUnitGtinAuthority(projection.envelope.identifiers);
|
|
44
|
+
} catch (error) {
|
|
45
|
+
if (!(error instanceof CatalogSkuAuthorityError)) throw error;
|
|
46
|
+
context.addIssue({
|
|
47
|
+
code: z.ZodIssueCode.custom,
|
|
48
|
+
path: ["envelope", "identifiers"],
|
|
49
|
+
message: error.code,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const subscriptionSellable = projection.envelope.sku.sellability.subscription;
|
|
53
|
+
if (subscriptionSellable && projection.pricing.subscription === null) {
|
|
54
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["pricing", "subscription"], message: "subscription-sellable SKUs require a subscription price" });
|
|
55
|
+
}
|
|
56
|
+
if (!subscriptionSellable && projection.pricing.subscription !== null) {
|
|
57
|
+
context.addIssue({ code: z.ZodIssueCode.custom, path: ["pricing", "subscription"], message: "one-time-only SKUs must not carry a subscription price" });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export type ResolvedSkuProjection = z.infer<typeof resolvedSkuProjectionSchema>;
|
|
62
|
+
|
|
63
|
+
/** Row-safe constructor boundary for adapters; it accepts no raw document row/payload. */
|
|
64
|
+
export interface ResolvedSkuProjectionInput {
|
|
65
|
+
envelope: NeutralSkuEnvelopeV1;
|
|
66
|
+
priceContext: ResolvedSkuProjection["priceContext"];
|
|
67
|
+
pricing: ResolvedSkuProjection["pricing"];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildResolvedSkuProjection(input: ResolvedSkuProjectionInput): ResolvedSkuProjection {
|
|
71
|
+
assertPrimaryUnitGtinAuthority(input.envelope.identifiers);
|
|
72
|
+
return resolvedSkuProjectionSchema.parse({
|
|
73
|
+
contractVersion: RESOLVED_SKU_PROJECTION_VERSION,
|
|
74
|
+
...input,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { allocateBundleTargetPrice } from "@openlup/core/pricing";
|
|
2
|
+
import type { HttpRequest, HttpResponse } from "../http/index.js";
|
|
3
|
+
import type { BffResponder } from "../bff/index.js";
|
|
4
|
+
import type { BundleResponseSchema } from "./adminBindings.js";
|
|
5
|
+
import type { SellableBundle } from "./contracts/bundleAvailability.js";
|
|
6
|
+
import type { BundleAvailability } from "./bundleAvailability.js";
|
|
7
|
+
import type { ActiveBundleComposition } from "./bundleCatalogReadPort.js";
|
|
8
|
+
import type { BundleAvailabilityService } from "./bundleAvailabilityService.js";
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The public sellable-bundle feed handler, shaped after the reference catalog
|
|
13
|
+
* handler: GET-only, one
|
|
14
|
+
* port call, validate the body before sending, and never serve an unvalidated
|
|
15
|
+
* shape.
|
|
16
|
+
*
|
|
17
|
+
* It composes rather than computes twice: the availability service already folds
|
|
18
|
+
* one batched stock answer into the live compositions, and the pricing kernel
|
|
19
|
+
* turns the stored target price into per-component money. This module only decides
|
|
20
|
+
* what a storefront is allowed to see.
|
|
21
|
+
*
|
|
22
|
+
* A bundle whose money cannot be derived is DROPPED from the feed, not published
|
|
23
|
+
* with a guessed figure. Three cases reach that door — an unpriced component, an
|
|
24
|
+
* empty composition, and a target price the kernel refuses — and all three mean
|
|
25
|
+
* the same thing to a storefront: this is not sellable today. Publishing it with a
|
|
26
|
+
* partial price would put a number on a shelf that no order line could reproduce.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
export interface SellableBundleHandlerDeps {
|
|
30
|
+
availabilityService: BundleAvailabilityService;
|
|
31
|
+
/** Narrow the feed to one currency; omitted means every currency an operator priced. */
|
|
32
|
+
currency?: string;
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createSellableBundleHandlers({ responder, sellableBundleListResponseSchema, SELLABLE_BUNDLE_CONTRACT_VERSION }: { responder: BffResponder; sellableBundleListResponseSchema: BundleResponseSchema; SELLABLE_BUNDLE_CONTRACT_VERSION: "catalog.sellable-bundle.v1" }) {
|
|
37
|
+
const { sendBffError, sendBffSuccess, sendMethodNotAllowed } = responder;
|
|
38
|
+
function createSellableBundleHandler(deps: SellableBundleHandlerDeps) {
|
|
39
|
+
return async function handler(req: HttpRequest, res: HttpResponse): Promise<void> {
|
|
40
|
+
if (req.method !== "GET") {
|
|
41
|
+
sendMethodNotAllowed(res, ["GET"]);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!deps.enabled) {
|
|
45
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Bundle reads are disabled", {
|
|
46
|
+
details: { reason: "feature_flag_disabled", featureFlag: "COMMERCE_BUNDLE_READ_ENABLED" },
|
|
47
|
+
});
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const rows = await deps.availabilityService.describeActiveBundles(
|
|
53
|
+
deps.currency ? { currency: deps.currency } : {},
|
|
54
|
+
);
|
|
55
|
+
const response = sellableBundleListResponseSchema.safeParse({
|
|
56
|
+
contractVersion: SELLABLE_BUNDLE_CONTRACT_VERSION,
|
|
57
|
+
bundles: rows
|
|
58
|
+
.map((row) => toSellableBundle(row.composition, row.availability))
|
|
59
|
+
.filter((bundle): bundle is SellableBundle => bundle !== null),
|
|
60
|
+
});
|
|
61
|
+
if (!response.success) {
|
|
62
|
+
sendBffError(res, "INVALID_RESPONSE", "Sellable bundle feed returned an invalid shape");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
sendBffSuccess(
|
|
66
|
+
res,
|
|
67
|
+
response.data,
|
|
68
|
+
{ contractVersion: SELLABLE_BUNDLE_CONTRACT_VERSION },
|
|
69
|
+
{ cacheControl: "no-store" },
|
|
70
|
+
);
|
|
71
|
+
} catch {
|
|
72
|
+
sendBffError(res, "UPSTREAM_UNAVAILABLE", "Sellable bundles are unavailable");
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return { createSellableBundleHandler };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Derive the row a storefront sees, or `null` when the money cannot be stated.
|
|
82
|
+
*
|
|
83
|
+
* The allocator emits TWO lines for a quantity whose subtotal does not divide
|
|
84
|
+
* evenly, which is exactly why the feed carries lines rather than a unit price per
|
|
85
|
+
* component: `unitPrice x quantity === lineSubtotal` holds on every line, and the
|
|
86
|
+
* lines sum to the bundle's price with no remainder.
|
|
87
|
+
*/
|
|
88
|
+
export function toSellableBundle(
|
|
89
|
+
composition: ActiveBundleComposition,
|
|
90
|
+
availability: BundleAvailability,
|
|
91
|
+
): SellableBundle | null {
|
|
92
|
+
if (composition.components.length === 0) return null;
|
|
93
|
+
if (composition.components.some((component) => component.referenceUnitPriceMinor === null)) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const allocation = allocateBundleTargetPrice({
|
|
98
|
+
components: composition.components.map((component) => ({
|
|
99
|
+
sku: component.sku,
|
|
100
|
+
quantity: component.quantity,
|
|
101
|
+
unitPriceMinor: component.referenceUnitPriceMinor as number,
|
|
102
|
+
})),
|
|
103
|
+
targetPriceMinor: composition.targetPriceMinor,
|
|
104
|
+
});
|
|
105
|
+
if (!allocation.ok) return null;
|
|
106
|
+
|
|
107
|
+
// Basis points come from the per-component allocation, so a component split
|
|
108
|
+
// across two lines reports the same discount on both halves rather than two
|
|
109
|
+
// rounded figures that disagree.
|
|
110
|
+
const discountBpsBySku = new Map(
|
|
111
|
+
allocation.components.map((component) => [component.sku, component.discountBps]),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
code: composition.code,
|
|
116
|
+
title: composition.title,
|
|
117
|
+
fulfillmentMode: composition.fulfillmentMode,
|
|
118
|
+
price: { amountMinor: composition.targetPriceMinor, currency: composition.currency },
|
|
119
|
+
components: allocation.lines.map((line) => ({
|
|
120
|
+
sku: line.sku,
|
|
121
|
+
quantity: line.quantity,
|
|
122
|
+
effectiveUnitPriceMinor: line.effectiveUnitPriceMinor,
|
|
123
|
+
lineSubtotalMinor: line.lineSubtotalMinor,
|
|
124
|
+
discountBps: discountBpsBySku.get(line.sku) ?? 0,
|
|
125
|
+
})),
|
|
126
|
+
availability: {
|
|
127
|
+
status: availability.status,
|
|
128
|
+
sellableNow: availability.sellableNow,
|
|
129
|
+
limitingSku: availability.limitingSku,
|
|
130
|
+
reasonCode: availability.reasonCode,
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
}
|