@aglyn/plugins-commerce 1.0.0-beta.143
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/LICENSE +201 -0
- package/README.md +10 -0
- package/package.json +57 -0
- package/src/index.d.ts +38 -0
- package/src/index.js +38 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/account.d.ts +31 -0
- package/src/lib/components/account.js +645 -0
- package/src/lib/components/account.js.map +1 -0
- package/src/lib/components/cart.d.ts +38 -0
- package/src/lib/components/cart.js +851 -0
- package/src/lib/components/cart.js.map +1 -0
- package/src/lib/components/commerce-console-page.d.ts +32 -0
- package/src/lib/components/commerce-console-page.js +296 -0
- package/src/lib/components/commerce-console-page.js.map +1 -0
- package/src/lib/components/commerce-console-sections.d.ts +34 -0
- package/src/lib/components/commerce-console-sections.js +62 -0
- package/src/lib/components/commerce-console-sections.js.map +1 -0
- package/src/lib/components/console/catalog-organization-card.component.d.ts +14 -0
- package/src/lib/components/console/catalog-organization-card.component.js +1087 -0
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -0
- package/src/lib/components/console/commerce-analytics-card.component.d.ts +27 -0
- package/src/lib/components/console/commerce-analytics-card.component.js +310 -0
- package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -0
- package/src/lib/components/console/commerce-glance-card.component.d.ts +20 -0
- package/src/lib/components/console/commerce-glance-card.component.js +340 -0
- package/src/lib/components/console/commerce-glance-card.component.js.map +1 -0
- package/src/lib/components/console/commerce-stat-tile.component.d.ts +37 -0
- package/src/lib/components/console/commerce-stat-tile.component.js +72 -0
- package/src/lib/components/console/commerce-stat-tile.component.js.map +1 -0
- package/src/lib/components/console/discounts-card.component.d.ts +14 -0
- package/src/lib/components/console/discounts-card.component.js +443 -0
- package/src/lib/components/console/discounts-card.component.js.map +1 -0
- package/src/lib/components/console/entitlement-gate.component.d.ts +76 -0
- package/src/lib/components/console/entitlement-gate.component.js +115 -0
- package/src/lib/components/console/entitlement-gate.component.js.map +1 -0
- package/src/lib/components/console/gift-cards-card.component.d.ts +29 -0
- package/src/lib/components/console/gift-cards-card.component.js +379 -0
- package/src/lib/components/console/gift-cards-card.component.js.map +1 -0
- package/src/lib/components/console/host-coupons-card.component.d.ts +14 -0
- package/src/lib/components/console/host-coupons-card.component.js +252 -0
- package/src/lib/components/console/host-coupons-card.component.js.map +1 -0
- package/src/lib/components/console/host-orders-card.component.d.ts +13 -0
- package/src/lib/components/console/host-orders-card.component.js +861 -0
- package/src/lib/components/console/host-orders-card.component.js.map +1 -0
- package/src/lib/components/console/locations-card.component.d.ts +14 -0
- package/src/lib/components/console/locations-card.component.js +256 -0
- package/src/lib/components/console/locations-card.component.js.map +1 -0
- package/src/lib/components/console/member-posts-card.component.d.ts +8 -0
- package/src/lib/components/console/member-posts-card.component.js +283 -0
- package/src/lib/components/console/member-posts-card.component.js.map +1 -0
- package/src/lib/components/console/newest-site-users-card.component.d.ts +36 -0
- package/src/lib/components/console/newest-site-users-card.component.js +126 -0
- package/src/lib/components/console/newest-site-users-card.component.js.map +1 -0
- package/src/lib/components/console/order-detail-dialog.component.d.ts +28 -0
- package/src/lib/components/console/order-detail-dialog.component.js +976 -0
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -0
- package/src/lib/components/console/paid-media.d.ts +127 -0
- package/src/lib/components/console/paid-media.js +397 -0
- package/src/lib/components/console/paid-media.js.map +1 -0
- package/src/lib/components/console/payments-settings-card.component.d.ts +13 -0
- package/src/lib/components/console/payments-settings-card.component.js +295 -0
- package/src/lib/components/console/payments-settings-card.component.js.map +1 -0
- package/src/lib/components/console/pos-page.component.d.ts +13 -0
- package/src/lib/components/console/pos-page.component.js +916 -0
- package/src/lib/components/console/pos-page.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.d.ts +32 -0
- package/src/lib/components/console/product-editor-dialog.component.js +1374 -0
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -0
- package/src/lib/components/console/products-hub-card.component.d.ts +16 -0
- package/src/lib/components/console/products-hub-card.component.js +1280 -0
- package/src/lib/components/console/products-hub-card.component.js.map +1 -0
- package/src/lib/components/console/products-hub-zone.component.d.ts +50 -0
- package/src/lib/components/console/products-hub-zone.component.js +200 -0
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -0
- package/src/lib/components/console/recovery-queue-card.component.d.ts +39 -0
- package/src/lib/components/console/recovery-queue-card.component.js +262 -0
- package/src/lib/components/console/recovery-queue-card.component.js.map +1 -0
- package/src/lib/components/console/registers-card.component.d.ts +21 -0
- package/src/lib/components/console/registers-card.component.js +296 -0
- package/src/lib/components/console/registers-card.component.js.map +1 -0
- package/src/lib/components/console/reservations-card.component.d.ts +14 -0
- package/src/lib/components/console/reservations-card.component.js +650 -0
- package/src/lib/components/console/reservations-card.component.js.map +1 -0
- package/src/lib/components/console/reviews-moderation-card.component.d.ts +8 -0
- package/src/lib/components/console/reviews-moderation-card.component.js +235 -0
- package/src/lib/components/console/reviews-moderation-card.component.js.map +1 -0
- package/src/lib/components/console/shipping-settings-card.component.d.ts +38 -0
- package/src/lib/components/console/shipping-settings-card.component.js +560 -0
- package/src/lib/components/console/shipping-settings-card.component.js.map +1 -0
- package/src/lib/components/console/stock-movements-card.component.d.ts +31 -0
- package/src/lib/components/console/stock-movements-card.component.js +333 -0
- package/src/lib/components/console/stock-movements-card.component.js.map +1 -0
- package/src/lib/components/console/store-settings-card.component.d.ts +14 -0
- package/src/lib/components/console/store-settings-card.component.js +300 -0
- package/src/lib/components/console/store-settings-card.component.js.map +1 -0
- package/src/lib/components/console/storefront-tax-summary-card.component.d.ts +73 -0
- package/src/lib/components/console/storefront-tax-summary-card.component.js +408 -0
- package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -0
- package/src/lib/components/console/suppliers-card.component.d.ts +8 -0
- package/src/lib/components/console/suppliers-card.component.js +328 -0
- package/src/lib/components/console/suppliers-card.component.js.map +1 -0
- package/src/lib/components/console/tax-settings-card.component.d.ts +14 -0
- package/src/lib/components/console/tax-settings-card.component.js +473 -0
- package/src/lib/components/console/tax-settings-card.component.js.map +1 -0
- package/src/lib/components/gate.d.ts +39 -0
- package/src/lib/components/gate.js +202 -0
- package/src/lib/components/gate.js.map +1 -0
- package/src/lib/components/gated-video.d.ts +40 -0
- package/src/lib/components/gated-video.js +366 -0
- package/src/lib/components/gated-video.js.map +1 -0
- package/src/lib/components/member-feed.d.ts +32 -0
- package/src/lib/components/member-feed.js +198 -0
- package/src/lib/components/member-feed.js.map +1 -0
- package/src/lib/components/member-recovery.d.ts +35 -0
- package/src/lib/components/member-recovery.js +313 -0
- package/src/lib/components/member-recovery.js.map +1 -0
- package/src/lib/components/member-signin.d.ts +35 -0
- package/src/lib/components/member-signin.js +257 -0
- package/src/lib/components/member-signin.js.map +1 -0
- package/src/lib/components/member-signup.d.ts +34 -0
- package/src/lib/components/member-signup.js +287 -0
- package/src/lib/components/member-signup.js.map +1 -0
- package/src/lib/components/newsletter-signup.d.ts +39 -0
- package/src/lib/components/newsletter-signup.js +229 -0
- package/src/lib/components/newsletter-signup.js.map +1 -0
- package/src/lib/components/product-detail.d.ts +38 -0
- package/src/lib/components/product-detail.js +1022 -0
- package/src/lib/components/product-detail.js.map +1 -0
- package/src/lib/components/product-grid.d.ts +67 -0
- package/src/lib/components/product-grid.js +909 -0
- package/src/lib/components/product-grid.js.map +1 -0
- package/src/lib/components/product-reviews.d.ts +32 -0
- package/src/lib/components/product-reviews.js +349 -0
- package/src/lib/components/product-reviews.js.map +1 -0
- package/src/lib/components/related-products.d.ts +33 -0
- package/src/lib/components/related-products.js +232 -0
- package/src/lib/components/related-products.js.map +1 -0
- package/src/lib/components/reservation-widget.d.ts +33 -0
- package/src/lib/components/reservation-widget.js +418 -0
- package/src/lib/components/reservation-widget.js.map +1 -0
- package/src/lib/components/storefront-payment-element-fallback.d.ts +18 -0
- package/src/lib/components/storefront-payment-element-fallback.js +51 -0
- package/src/lib/components/storefront-payment-element-fallback.js.map +1 -0
- package/src/lib/components/storefront-payment-element.d.ts +78 -0
- package/src/lib/components/storefront-payment-element.js +156 -0
- package/src/lib/components/storefront-payment-element.js.map +1 -0
- package/src/lib/components/wishlist.d.ts +46 -0
- package/src/lib/components/wishlist.js +304 -0
- package/src/lib/components/wishlist.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +18 -0
- package/src/lib/constants/bundle-common.js +18 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/constants/product-filters.d.ts +35 -0
- package/src/lib/constants/product-filters.js +109 -0
- package/src/lib/constants/product-filters.js.map +1 -0
- package/src/lib/model/commerce-cart.d.ts +52 -0
- package/src/lib/model/commerce-cart.js +102 -0
- package/src/lib/model/commerce-cart.js.map +1 -0
- package/src/lib/model/commerce-connect-transfer.d.ts +243 -0
- package/src/lib/model/commerce-connect-transfer.js +231 -0
- package/src/lib/model/commerce-connect-transfer.js.map +1 -0
- package/src/lib/model/commerce-discounts.d.ts +164 -0
- package/src/lib/model/commerce-discounts.js +231 -0
- package/src/lib/model/commerce-discounts.js.map +1 -0
- package/src/lib/model/commerce-dispute.d.ts +124 -0
- package/src/lib/model/commerce-dispute.js +225 -0
- package/src/lib/model/commerce-dispute.js.map +1 -0
- package/src/lib/model/commerce-gift-cards.d.ts +107 -0
- package/src/lib/model/commerce-gift-cards.js +121 -0
- package/src/lib/model/commerce-gift-cards.js.map +1 -0
- package/src/lib/model/commerce-io.d.ts +36 -0
- package/src/lib/model/commerce-io.js +245 -0
- package/src/lib/model/commerce-io.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +1040 -0
- package/src/lib/model/commerce-orders.js +1022 -0
- package/src/lib/model/commerce-orders.js.map +1 -0
- package/src/lib/model/commerce-promotions.d.ts +188 -0
- package/src/lib/model/commerce-promotions.js +199 -0
- package/src/lib/model/commerce-promotions.js.map +1 -0
- package/src/lib/model/commerce-reservations.d.ts +175 -0
- package/src/lib/model/commerce-reservations.js +157 -0
- package/src/lib/model/commerce-reservations.js.map +1 -0
- package/src/lib/model/commerce-shipping.d.ts +316 -0
- package/src/lib/model/commerce-shipping.js +484 -0
- package/src/lib/model/commerce-shipping.js.map +1 -0
- package/src/lib/model/commerce-stock-holds.d.ts +199 -0
- package/src/lib/model/commerce-stock-holds.js +190 -0
- package/src/lib/model/commerce-stock-holds.js.map +1 -0
- package/src/lib/model/commerce-subscription-status.d.ts +67 -0
- package/src/lib/model/commerce-subscription-status.js +76 -0
- package/src/lib/model/commerce-subscription-status.js.map +1 -0
- package/src/lib/model/commerce-tax-decision.d.ts +222 -0
- package/src/lib/model/commerce-tax-decision.js +165 -0
- package/src/lib/model/commerce-tax-decision.js.map +1 -0
- package/src/lib/model/commerce-tax.d.ts +176 -0
- package/src/lib/model/commerce-tax.js +92 -0
- package/src/lib/model/commerce-tax.js.map +1 -0
- package/src/lib/model/commerce.d.ts +551 -0
- package/src/lib/model/commerce.js +739 -0
- package/src/lib/model/commerce.js.map +1 -0
- package/src/lib/model/index.d.ts +40 -0
- package/src/lib/model/index.js +40 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/order-figures.d.ts +48 -0
- package/src/lib/model/order-figures.js +74 -0
- package/src/lib/model/order-figures.js.map +1 -0
- package/src/lib/model/plugin-permissions.d.ts +23 -0
- package/src/lib/model/plugin-permissions.js +34 -0
- package/src/lib/model/plugin-permissions.js.map +1 -0
- package/src/lib/model/purchase-analytics.d.ts +186 -0
- package/src/lib/model/purchase-analytics.js +216 -0
- package/src/lib/model/purchase-analytics.js.map +1 -0
- package/src/lib/plugin-config.d.ts +62 -0
- package/src/lib/plugin-config.js +78 -0
- package/src/lib/plugin-config.js.map +1 -0
- package/src/lib/plugin.d.ts +28 -0
- package/src/lib/plugin.js +147 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server/billing-webhook.d.ts +26 -0
- package/src/lib/server/billing-webhook.js +4011 -0
- package/src/lib/server/billing-webhook.js.map +1 -0
- package/src/lib/server/cancel-order.d.ts +87 -0
- package/src/lib/server/cancel-order.js +368 -0
- package/src/lib/server/cancel-order.js.map +1 -0
- package/src/lib/server/cart-checkout.d.ts +25 -0
- package/src/lib/server/cart-checkout.js +976 -0
- package/src/lib/server/cart-checkout.js.map +1 -0
- package/src/lib/server/cart-cookie.d.ts +32 -0
- package/src/lib/server/cart-cookie.js +77 -0
- package/src/lib/server/cart-cookie.js.map +1 -0
- package/src/lib/server/cart.d.ts +34 -0
- package/src/lib/server/cart.js +181 -0
- package/src/lib/server/cart.js.map +1 -0
- package/src/lib/server/catalog.d.ts +127 -0
- package/src/lib/server/catalog.js +277 -0
- package/src/lib/server/catalog.js.map +1 -0
- package/src/lib/server/checkout.d.ts +36 -0
- package/src/lib/server/checkout.js +915 -0
- package/src/lib/server/checkout.js.map +1 -0
- package/src/lib/server/connect.d.ts +26 -0
- package/src/lib/server/connect.js +238 -0
- package/src/lib/server/connect.js.map +1 -0
- package/src/lib/server/contact-refund.d.ts +117 -0
- package/src/lib/server/contact-refund.js +196 -0
- package/src/lib/server/contact-refund.js.map +1 -0
- package/src/lib/server/download.d.ts +39 -0
- package/src/lib/server/download.js +206 -0
- package/src/lib/server/download.js.map +1 -0
- package/src/lib/server/draft-order.d.ts +26 -0
- package/src/lib/server/draft-order.js +706 -0
- package/src/lib/server/draft-order.js.map +1 -0
- package/src/lib/server/feed.d.ts +24 -0
- package/src/lib/server/feed.js +59 -0
- package/src/lib/server/feed.js.map +1 -0
- package/src/lib/server/fulfill-order.d.ts +97 -0
- package/src/lib/server/fulfill-order.js +235 -0
- package/src/lib/server/fulfill-order.js.map +1 -0
- package/src/lib/server/gate.d.ts +36 -0
- package/src/lib/server/gate.js +112 -0
- package/src/lib/server/gate.js.map +1 -0
- package/src/lib/server/gift-cards.d.ts +42 -0
- package/src/lib/server/gift-cards.js +219 -0
- package/src/lib/server/gift-cards.js.map +1 -0
- package/src/lib/server/low-stock.d.ts +49 -0
- package/src/lib/server/low-stock.js +60 -0
- package/src/lib/server/low-stock.js.map +1 -0
- package/src/lib/server/manual-tax-rate.d.ts +82 -0
- package/src/lib/server/manual-tax-rate.js +113 -0
- package/src/lib/server/manual-tax-rate.js.map +1 -0
- package/src/lib/server/member-feed.d.ts +24 -0
- package/src/lib/server/member-feed.js +72 -0
- package/src/lib/server/member-feed.js.map +1 -0
- package/src/lib/server/member-name-search.d.ts +37 -0
- package/src/lib/server/member-name-search.js +42 -0
- package/src/lib/server/member-name-search.js.map +1 -0
- package/src/lib/server/member-post.d.ts +23 -0
- package/src/lib/server/member-post.js +166 -0
- package/src/lib/server/member-post.js.map +1 -0
- package/src/lib/server/membership-account.d.ts +37 -0
- package/src/lib/server/membership-account.js +148 -0
- package/src/lib/server/membership-account.js.map +1 -0
- package/src/lib/server/membership-admin-password.d.ts +33 -0
- package/src/lib/server/membership-admin-password.js +202 -0
- package/src/lib/server/membership-admin-password.js.map +1 -0
- package/src/lib/server/membership-content.d.ts +23 -0
- package/src/lib/server/membership-content.js +105 -0
- package/src/lib/server/membership-content.js.map +1 -0
- package/src/lib/server/membership-login.d.ts +19 -0
- package/src/lib/server/membership-login.js +137 -0
- package/src/lib/server/membership-login.js.map +1 -0
- package/src/lib/server/membership-logout.d.ts +19 -0
- package/src/lib/server/membership-logout.js +35 -0
- package/src/lib/server/membership-logout.js.map +1 -0
- package/src/lib/server/membership-recover.d.ts +60 -0
- package/src/lib/server/membership-recover.js +244 -0
- package/src/lib/server/membership-recover.js.map +1 -0
- package/src/lib/server/membership-register.d.ts +22 -0
- package/src/lib/server/membership-register.js +241 -0
- package/src/lib/server/membership-register.js.map +1 -0
- package/src/lib/server/membership-reset.d.ts +26 -0
- package/src/lib/server/membership-reset.js +88 -0
- package/src/lib/server/membership-reset.js.map +1 -0
- package/src/lib/server/membership-wishlist.d.ts +23 -0
- package/src/lib/server/membership-wishlist.js +89 -0
- package/src/lib/server/membership-wishlist.js.map +1 -0
- package/src/lib/server/membership.d.ts +96 -0
- package/src/lib/server/membership.js +222 -0
- package/src/lib/server/membership.js.map +1 -0
- package/src/lib/server/native-checkout.d.ts +170 -0
- package/src/lib/server/native-checkout.js +193 -0
- package/src/lib/server/native-checkout.js.map +1 -0
- package/src/lib/server/newsletter.d.ts +23 -0
- package/src/lib/server/newsletter.js +323 -0
- package/src/lib/server/newsletter.js.map +1 -0
- package/src/lib/server/notify-restock.d.ts +22 -0
- package/src/lib/server/notify-restock.js +84 -0
- package/src/lib/server/notify-restock.js.map +1 -0
- package/src/lib/server/order-analytics.d.ts +134 -0
- package/src/lib/server/order-analytics.js +263 -0
- package/src/lib/server/order-analytics.js.map +1 -0
- package/src/lib/server/order-figures.d.ts +33 -0
- package/src/lib/server/order-figures.js +220 -0
- package/src/lib/server/order-figures.js.map +1 -0
- package/src/lib/server/paid-media-delivery.d.ts +173 -0
- package/src/lib/server/paid-media-delivery.js +212 -0
- package/src/lib/server/paid-media-delivery.js.map +1 -0
- package/src/lib/server/pos-order.d.ts +54 -0
- package/src/lib/server/pos-order.js +1137 -0
- package/src/lib/server/pos-order.js.map +1 -0
- package/src/lib/server/process-abandoned.d.ts +54 -0
- package/src/lib/server/process-abandoned.js +222 -0
- package/src/lib/server/process-abandoned.js.map +1 -0
- package/src/lib/server/process-restock.d.ts +42 -0
- package/src/lib/server/process-restock.js +219 -0
- package/src/lib/server/process-restock.js.map +1 -0
- package/src/lib/server/product-card.d.ts +29 -0
- package/src/lib/server/product-card.js +54 -0
- package/src/lib/server/product-card.js.map +1 -0
- package/src/lib/server/product.d.ts +70 -0
- package/src/lib/server/product.js +105 -0
- package/src/lib/server/product.js.map +1 -0
- package/src/lib/server/promotion-hold.d.ts +144 -0
- package/src/lib/server/promotion-hold.js +174 -0
- package/src/lib/server/promotion-hold.js.map +1 -0
- package/src/lib/server/reconcile-stock.d.ts +106 -0
- package/src/lib/server/reconcile-stock.js +349 -0
- package/src/lib/server/reconcile-stock.js.map +1 -0
- package/src/lib/server/refund.d.ts +45 -0
- package/src/lib/server/refund.js +607 -0
- package/src/lib/server/refund.js.map +1 -0
- package/src/lib/server/related.d.ts +24 -0
- package/src/lib/server/related.js +104 -0
- package/src/lib/server/related.js.map +1 -0
- package/src/lib/server/reservation-availability.d.ts +23 -0
- package/src/lib/server/reservation-availability.js +111 -0
- package/src/lib/server/reservation-availability.js.map +1 -0
- package/src/lib/server/reserve-stock.d.ts +156 -0
- package/src/lib/server/reserve-stock.js +146 -0
- package/src/lib/server/reserve-stock.js.map +1 -0
- package/src/lib/server/reserve.d.ts +25 -0
- package/src/lib/server/reserve.js +426 -0
- package/src/lib/server/reserve.js.map +1 -0
- package/src/lib/server/restock-flag.d.ts +202 -0
- package/src/lib/server/restock-flag.js +449 -0
- package/src/lib/server/restock-flag.js.map +1 -0
- package/src/lib/server/reviews.d.ts +51 -0
- package/src/lib/server/reviews.js +132 -0
- package/src/lib/server/reviews.js.map +1 -0
- package/src/lib/server/site-page-enricher.d.ts +42 -0
- package/src/lib/server/site-page-enricher.js +149 -0
- package/src/lib/server/site-page-enricher.js.map +1 -0
- package/src/lib/server/site-page-resolver.d.ts +26 -0
- package/src/lib/server/site-page-resolver.js +223 -0
- package/src/lib/server/site-page-resolver.js.map +1 -0
- package/src/lib/server/stock-hold.d.ts +127 -0
- package/src/lib/server/stock-hold.js +258 -0
- package/src/lib/server/stock-hold.js.map +1 -0
- package/src/lib/server/storefront-tax-record.d.ts +22 -0
- package/src/lib/server/storefront-tax-record.js +218 -0
- package/src/lib/server/storefront-tax-record.js.map +1 -0
- package/src/lib/server/storefront-tax.d.ts +197 -0
- package/src/lib/server/storefront-tax.js +245 -0
- package/src/lib/server/storefront-tax.js.map +1 -0
- package/src/lib/server/stream.d.ts +25 -0
- package/src/lib/server/stream.js +191 -0
- package/src/lib/server/stream.js.map +1 -0
- package/src/lib/server/subscription-portal.d.ts +24 -0
- package/src/lib/server/subscription-portal.js +91 -0
- package/src/lib/server/subscription-portal.js.map +1 -0
- package/src/lib/server/supplier-outbox.d.ts +190 -0
- package/src/lib/server/supplier-outbox.js +342 -0
- package/src/lib/server/supplier-outbox.js.map +1 -0
- package/src/lib/server/supplier-update.d.ts +23 -0
- package/src/lib/server/supplier-update.js +289 -0
- package/src/lib/server/supplier-update.js.map +1 -0
- package/src/lib/server/tax-profile.d.ts +29 -0
- package/src/lib/server/tax-profile.js +40 -0
- package/src/lib/server/tax-profile.js.map +1 -0
- package/src/lib/server.d.ts +32 -0
- package/src/lib/server.js +311 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +27 -0
- package/src/lib/site.js +134 -0
- package/src/lib/site.js.map +1 -0
- package/src/lib/utils/escape-html.d.ts +41 -0
- package/src/lib/utils/escape-html.js +41 -0
- package/src/lib/utils/escape-html.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +19 -0
- package/src/lib/utils/generate-preset-id.js +25 -0
- package/src/lib/utils/generate-preset-id.js.map +1 -0
- package/src/lib/utils/member-continue.d.ts +45 -0
- package/src/lib/utils/member-continue.js +49 -0
- package/src/lib/utils/member-continue.js.map +1 -0
- package/src/lib/utils/use-storefront-purchase-event.d.ts +60 -0
- package/src/lib/utils/use-storefront-purchase-event.js +142 -0
- package/src/lib/utils/use-storefront-purchase-event.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/site-page-resolver.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { SitePageResolver } from '@aglyn/aglyn/server'\nimport { hostCollectionKind } from '@aglyn/aglyn/server'\nimport composeScreenNodes from '@aglyn/tenant-runtime/compose-screen-nodes'\nimport getScreen from '@aglyn/tenant-runtime/get-screen'\nimport { collectSocialImageFacts } from '@aglyn/tenant-runtime/social-image-facts'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { toPublicProductDetail } from './product'\nimport { readProductReviews } from './reviews'\n\n/**\n * Commerce page resolver (AGL-292/298/418), relocated verbatim from the\n * tenant loader: /products/{slug} renders the host's designated PDP\n * template with product tokens, /collections/{slug} the collection\n * template — both compose server-side and return complete page props.\n * Runs only for paths that didn't match a published screen; returns\n * undefined for everything else so the loader falls through.\n */\nexport const commerceSitePageResolver: SitePageResolver = async ({\n hostId,\n host,\n path,\n}) => {\n const hostRes = { host }\n // Product detail routes (AGL-292): /products/{slug} renders the\n // host's designated PDP template screen (settings/store\n // `pdpScreenId`, AGL-295) with product tokens; the product-detail\n // block hydrates variants client-side from the same slug. Same\n // mechanism as entry-template screens below.\n const pdpSegments = path.split('/').filter(Boolean)\n if (pdpSegments.length === 2 && pdpSegments[0] === 'products') {\n const adminFirestore = firebaseAdmin.app().firestore()\n const hostDocRef = adminFirestore.collection('hosts').doc(hostId)\n const [storeSettings, productSnapshot] = await Promise.all([\n hostDocRef.collection('settings').doc('store').get(),\n hostDocRef\n .collection('products')\n .where('slug', '==', pdpSegments[1])\n .limit(1)\n .get(),\n ])\n const pdpScreenId = storeSettings.get('pdpScreenId')\n const productRaw = productSnapshot.docs[0]?.data() as any\n if (\n pdpScreenId &&\n productRaw &&\n !productRaw.deletedAt &&\n productRaw.status === 'active'\n ) {\n const product = CommerceModel.liftLegacyProduct(productRaw)\n const templateRes = await getScreen({\n hostId,\n screenId: pdpScreenId,\n // A PDP template renders against a routed PRODUCT, never at an address\n // of its own, so it is served here and 404s everywhere else (AGL-1400).\n allowTemplate: true,\n })\n if (templateRes.screen) {\n const [minPrice, maxPrice] = CommerceModel.productPriceRange(product)\n // Best-effort: a reviews read that fails costs the page its rating\n // snippet, not the page.\n const productReviews = await readProductReviews(\n hostId,\n productSnapshot.docs[0].id,\n ).catch((error) => {\n console.error('product review aggregate failed', error)\n return { reviews: [], aggregate: { count: 0, average: 0 } }\n })\n // The head shares this page as the template's image, then the site\n // default, so their documents are read in the template's own batch\n // (AGL-2850).\n const card = collectSocialImageFacts([\n (templateRes.screen as any).seo?.image,\n hostRes.host?.seo?.image,\n ])\n const templateNodes = await composeScreenNodes({\n hostId,\n screenId: pdpScreenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // The template's host variables, and its layout's, fill in from\n // this site as they do on every other page (AGL-2883).\n host: hostRes.host,\n tokens: {\n 'product.name': product.name,\n 'product.description': product.description ?? '',\n 'product.price':\n minPrice === maxPrice\n ? `$${minPrice}`\n : `From $${minPrice}`,\n 'product.image':\n product.mediaUrls?.[0] ?? product.imageUrl ?? '',\n 'product.slug': product.slug,\n },\n })\n if (templateNodes) {\n return {\n props: JSON.parse(\n JSON.stringify({\n // The PDP block used to fetch this in an effect, so the\n // server rendered a skeleton and crawlers saw a product page\n // with no product in it (AGL-659). The product is already\n // loaded here — hand it down so it renders server-side.\n pageData: {\n commerce: {\n product: toPublicProductDetail(\n productSnapshot.docs[0].id,\n product,\n ),\n // Rating aggregate (AGL-686): needed HERE because\n // `aggregateRating` belongs nested inside the Product\n // structured data, and that is emitted server-side.\n // The reviews block used to publish a free-standing\n // AggregateRating node, which schema.org ignores.\n ...(productReviews.aggregate.count\n ? { reviews: productReviews }\n : {}),\n },\n },\n data: {\n host: hostRes.host,\n screen: {\n data: {\n ...templateRes.screen,\n // The product NAMES this page; the template's own name\n // (\"Product detail\") describes none of the products it\n // renders. Kept distinct from the SEO title because the\n // head treats an authored title as verbatim and a name\n // as the side the site title joins (AGL-1341) — so a\n // product with its own SEO title gets exactly that, and\n // one without gets \"Blue Widget – Acme Store\".\n displayName: product.name,\n seo: {\n ...((templateRes.screen as any).seo ?? {}),\n // Only when the PRODUCT authored one: inheriting the\n // template screen's title would give every product\n // page in the store the same `<title>`.\n title: product.seo?.title ?? undefined,\n description:\n product.seo?.description ??\n product.description ??\n undefined,\n },\n },\n },\n },\n nodes: templateNodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n }\n }\n }\n }\n // Commerce collection routes (AGL-298): /collections/{slug} renders\n // the designated collection template with collection tokens; the\n // product-grid block derives the same slug from the URL.\n if (pdpSegments.length === 2 && pdpSegments[0] === 'collections') {\n const adminFirestore = firebaseAdmin.app().firestore()\n const hostDocRef = adminFirestore.collection('hosts').doc(hostId)\n const [storeSettings, collectionSnapshot] = await Promise.all([\n hostDocRef.collection('settings').doc('store').get(),\n hostDocRef\n .collection('collections')\n .where('slug', '==', pdpSegments[1])\n .limit(5)\n .get(),\n ])\n const collectionScreenId = storeSettings.get('collectionScreenId')\n // Content collections share this path and a slug is only unique within a\n // kind (AGL-954), so take the first CATALOG match rather than the first\n // doc — otherwise a blog with the same slug renders as a product page.\n const shopCollection = collectionSnapshot.docs\n .find((docSnapshot) => hostCollectionKind(docSnapshot.data()) === 'catalog')\n ?.data() as CommerceModel.HostCollection | undefined\n if (collectionScreenId && shopCollection) {\n const templateRes = await getScreen({\n hostId,\n screenId: collectionScreenId,\n allowTemplate: true,\n })\n if (templateRes.screen) {\n // As on the PDP above: the template's image, then the site default\n // (AGL-2850).\n const card = collectSocialImageFacts([\n (templateRes.screen as any).seo?.image,\n hostRes.host?.seo?.image,\n ])\n const templateNodes = await composeScreenNodes({\n hostId,\n screenId: collectionScreenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // As on the PDP above (AGL-2883).\n host: hostRes.host,\n tokens: {\n 'collection.name': shopCollection.name,\n 'collection.description': shopCollection.description ?? '',\n 'collection.image': shopCollection.imageUrl ?? '',\n 'collection.slug': shopCollection.slug,\n },\n })\n if (templateNodes) {\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host: hostRes.host,\n screen: {\n data: {\n ...templateRes.screen,\n // As on the PDP above: the catalog collection names the\n // page, and a name takes the site title after it while\n // an authored SEO title would not (AGL-1341). A shop\n // collection has no SEO title field of its own, so the\n // template's must not stand in for one.\n displayName: shopCollection.name,\n seo: {\n ...((templateRes.screen as any).seo ?? {}),\n title: undefined,\n description:\n shopCollection.description ?? undefined,\n },\n },\n },\n },\n nodes: templateNodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n }\n }\n }\n }\n return undefined\n}\n"],"names":["hostCollectionKind","composeScreenNodes","getScreen","collectSocialImageFacts","firebaseAdmin","CommerceModel","toPublicProductDetail","readProductReviews","commerceSitePageResolver","hostId","host","path","hostRes","pdpSegments","split","filter","Boolean","length","productSnapshot","adminFirestore","app","firestore","hostDocRef","collection","doc","storeSettings","Promise","all","get","where","limit","pdpScreenId","productRaw","docs","data","deletedAt","status","product","liftLegacyProduct","templateRes","screenId","allowTemplate","screen","minPrice","maxPrice","productPriceRange","productReviews","id","catch","error","console","reviews","aggregate","count","average","card","seo","image","templateNodes","socialImages","tokens","name","description","mediaUrls","imageUrl","slug","props","JSON","parse","stringify","pageData","commerce","displayName","title","undefined","nodes","collected","revalidate","collectionSnapshot","collectionScreenId","shopCollection","find","docSnapshot"],"mappings":"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"}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Reserve-then-settle for STOCK (AGL-2356).
|
|
19
|
+
*
|
|
20
|
+
* The I/O half of `model/commerce-stock-holds.ts` — read that file first; it
|
|
21
|
+
* carries the decision this one implements, including why the hold does not
|
|
22
|
+
* touch `inventory` and why a lapsed hold is a release.
|
|
23
|
+
*
|
|
24
|
+
* This is the AGL-2453 promotion-slot shape applied to the one counter that
|
|
25
|
+
* moves real goods, with two differences that matter:
|
|
26
|
+
*
|
|
27
|
+
* 1. **A cart holds N products, so the hold is ONE transaction over all of
|
|
28
|
+
* them.** Firestore serialises the whole set, so a two-product cart either
|
|
29
|
+
* reserves both or reserves neither — a partial reservation would charge a
|
|
30
|
+
* shopper for a basket the store cannot fill, which is the failure this
|
|
31
|
+
* issue exists to prevent, arriving through the fix.
|
|
32
|
+
* 2. **The release needs an index.** A promotion hold is released against the
|
|
33
|
+
* one document named in the session metadata; a cart hold spans documents
|
|
34
|
+
* Stripe never hears about. `hosts/{hostId}/stockHolds/{holdKey}` is
|
|
35
|
+
* written INSIDE the same transaction as the holds it names, so the index
|
|
36
|
+
* cannot disagree with the reservations, and the session carries only the
|
|
37
|
+
* key.
|
|
38
|
+
*/
|
|
39
|
+
/** One reserved line, already resolved to a real variant. */
|
|
40
|
+
export interface StockHoldLine {
|
|
41
|
+
productId: string;
|
|
42
|
+
variantId: string;
|
|
43
|
+
quantity: number;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Why a reservation could not be taken.
|
|
47
|
+
*
|
|
48
|
+
* - `sold-out` — the units are not there, or are spoken for by another live
|
|
49
|
+
* checkout. The one refusal a shopper is told about by name.
|
|
50
|
+
* - `missing` — the product document is gone, deleted between the pricing
|
|
51
|
+
* loop's read and this transaction.
|
|
52
|
+
* - `error` — Firestore refused. The checkout is REFUSED rather than allowed
|
|
53
|
+
* through unreserved: passing a shopper to Stripe on a reservation that
|
|
54
|
+
* could not be taken is exactly the defect this file closes.
|
|
55
|
+
*/
|
|
56
|
+
export type StockHoldRefusal = 'sold-out' | 'missing' | 'error';
|
|
57
|
+
/**
|
|
58
|
+
* A two-member union with `reason` and `productName` present on BOTH members
|
|
59
|
+
* rather than an intersection, for the reason `PromotionSlotOutcome` records:
|
|
60
|
+
* discriminated-union narrowing does not reach through an intersection under
|
|
61
|
+
* this repo's compiler settings (`strictNullChecks` is off repo-wide), and the
|
|
62
|
+
* intersection form fails to compile at every call site.
|
|
63
|
+
*/
|
|
64
|
+
export type StockHoldOutcome = {
|
|
65
|
+
ok: true;
|
|
66
|
+
reason?: undefined;
|
|
67
|
+
productName?: undefined;
|
|
68
|
+
/**
|
|
69
|
+
* The key the hold was stored under, or `''` when nothing needed
|
|
70
|
+
* reserving (every line untracked or backorder). Carried in the session
|
|
71
|
+
* metadata so the webhook can release exactly this reservation; its
|
|
72
|
+
* absence is what tells the webhook there is nothing to release.
|
|
73
|
+
*/
|
|
74
|
+
holdKey: string;
|
|
75
|
+
/** Drop the reservation. Best-effort and never throws. */
|
|
76
|
+
release: () => Promise<void>;
|
|
77
|
+
} | {
|
|
78
|
+
ok: false;
|
|
79
|
+
reason: StockHoldRefusal;
|
|
80
|
+
productName: string;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* A stable per-attempt hold key — the same rule as `promotionHoldKey`.
|
|
84
|
+
*
|
|
85
|
+
* `claim.stripeKey` when the client sent an Idempotency-Key, so a retry of one
|
|
86
|
+
* attempt re-claims its OWN units rather than being refused by them. When it
|
|
87
|
+
* did not, a fresh random key per request — NOT `String(claim.stripeKey)`,
|
|
88
|
+
* which yields the literal `'null'` and would make every keyless shopper on the
|
|
89
|
+
* site share one reservation, each reading the other's hold as their own retry
|
|
90
|
+
* and both passing a shelf of one.
|
|
91
|
+
*/
|
|
92
|
+
export declare function stockHoldKey(stripeKey: string | null): string;
|
|
93
|
+
/**
|
|
94
|
+
* Reserve every line, or refuse.
|
|
95
|
+
*
|
|
96
|
+
* The re-check and the write are ONE transaction over the SAME documents, which
|
|
97
|
+
* is the whole fix: the old code read the shelf with a plain `.get()` at
|
|
98
|
+
* session creation and the webhook decremented it minutes later without ever
|
|
99
|
+
* re-asking, so N shoppers all read `inventory: 1` and all passed.
|
|
100
|
+
* `Transaction.get()` takes a lock on each product document, so contending
|
|
101
|
+
* checkouts SERIALISE — the loser re-runs its callback against the winner's
|
|
102
|
+
* committed hold and is refused.
|
|
103
|
+
*
|
|
104
|
+
* Lines with nothing to reserve — an untracked variant, a `backorder` product —
|
|
105
|
+
* write nothing at all, exactly as an uncapped promotion does. A cart made
|
|
106
|
+
* entirely of those returns an empty `holdKey` and the session carries no
|
|
107
|
+
* reservation, so the digital-goods storefront is byte-identical to what it was
|
|
108
|
+
* before this issue.
|
|
109
|
+
*/
|
|
110
|
+
export declare function holdStock(options: {
|
|
111
|
+
firestore: any;
|
|
112
|
+
hostRef: any;
|
|
113
|
+
holdKey: string;
|
|
114
|
+
lines: StockHoldLine[];
|
|
115
|
+
nowMs?: number;
|
|
116
|
+
/** Label for the failure log; never shown to a shopper. */
|
|
117
|
+
label: string;
|
|
118
|
+
}): Promise<StockHoldOutcome>;
|
|
119
|
+
/**
|
|
120
|
+
* Release a reservation named only by its key — the webhook's door.
|
|
121
|
+
*
|
|
122
|
+
* Reads the index written by `holdStock`, so `checkout.session.expired` needs
|
|
123
|
+
* nothing in the session but `metadata[stockHoldKey]`. A missing index doc is
|
|
124
|
+
* NOT an error: it means the hold was already released, or the session predates
|
|
125
|
+
* this deploy, and in both cases there is nothing to do.
|
|
126
|
+
*/
|
|
127
|
+
export declare function releaseStockHold(hostRef: any, holdKey: string): Promise<void>;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import * as CommerceModel from "../model/index.js";
|
|
18
|
+
import { firebaseAdmin } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { randomUUID } from "crypto";
|
|
20
|
+
/**
|
|
21
|
+
* A stable per-attempt hold key — the same rule as `promotionHoldKey`.
|
|
22
|
+
*
|
|
23
|
+
* `claim.stripeKey` when the client sent an Idempotency-Key, so a retry of one
|
|
24
|
+
* attempt re-claims its OWN units rather than being refused by them. When it
|
|
25
|
+
* did not, a fresh random key per request — NOT `String(claim.stripeKey)`,
|
|
26
|
+
* which yields the literal `'null'` and would make every keyless shopper on the
|
|
27
|
+
* site share one reservation, each reading the other's hold as their own retry
|
|
28
|
+
* and both passing a shelf of one.
|
|
29
|
+
*/ export function stockHoldKey(stripeKey) {
|
|
30
|
+
return stripeKey || `anon-${randomUUID()}`;
|
|
31
|
+
}
|
|
32
|
+
/** Two lines of one product COMPOUND — a cart holds variants, not products. */ function groupLines(lines) {
|
|
33
|
+
const byProduct = new Map();
|
|
34
|
+
for (const line of lines){
|
|
35
|
+
var _byProduct_get, _variants_get;
|
|
36
|
+
const quantity = Math.round(Number(line == null ? void 0 : line.quantity) || 0);
|
|
37
|
+
if (!(line == null ? void 0 : line.productId) || !(line == null ? void 0 : line.variantId) || quantity <= 0) continue;
|
|
38
|
+
const variants = (_byProduct_get = byProduct.get(line.productId)) != null ? _byProduct_get : new Map();
|
|
39
|
+
variants.set(line.variantId, ((_variants_get = variants.get(line.variantId)) != null ? _variants_get : 0) + quantity);
|
|
40
|
+
byProduct.set(line.productId, variants);
|
|
41
|
+
}
|
|
42
|
+
return byProduct;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Reserve every line, or refuse.
|
|
46
|
+
*
|
|
47
|
+
* The re-check and the write are ONE transaction over the SAME documents, which
|
|
48
|
+
* is the whole fix: the old code read the shelf with a plain `.get()` at
|
|
49
|
+
* session creation and the webhook decremented it minutes later without ever
|
|
50
|
+
* re-asking, so N shoppers all read `inventory: 1` and all passed.
|
|
51
|
+
* `Transaction.get()` takes a lock on each product document, so contending
|
|
52
|
+
* checkouts SERIALISE — the loser re-runs its callback against the winner's
|
|
53
|
+
* committed hold and is refused.
|
|
54
|
+
*
|
|
55
|
+
* Lines with nothing to reserve — an untracked variant, a `backorder` product —
|
|
56
|
+
* write nothing at all, exactly as an uncapped promotion does. A cart made
|
|
57
|
+
* entirely of those returns an empty `holdKey` and the session carries no
|
|
58
|
+
* reservation, so the digital-goods storefront is byte-identical to what it was
|
|
59
|
+
* before this issue.
|
|
60
|
+
*/ export async function holdStock(options) {
|
|
61
|
+
var _options_nowMs, _outcome_productName;
|
|
62
|
+
const { firestore, hostRef, holdKey, lines, label } = options;
|
|
63
|
+
const nowMs = (_options_nowMs = options.nowMs) != null ? _options_nowMs : Date.now();
|
|
64
|
+
const expiresAtMs = nowMs + CommerceModel.STOCK_HOLD_TTL_MS;
|
|
65
|
+
const byProduct = groupLines(lines);
|
|
66
|
+
const noop = {
|
|
67
|
+
ok: true,
|
|
68
|
+
holdKey: '',
|
|
69
|
+
release: async ()=>undefined
|
|
70
|
+
};
|
|
71
|
+
if (byProduct.size === 0) return noop;
|
|
72
|
+
// try/catch and NOT a trailing `.catch()`. A `runTransaction` that is
|
|
73
|
+
// missing or throws synchronously — a broken client, or a test double that
|
|
74
|
+
// never modelled one — never returns a promise for a `.catch()` to attach to,
|
|
75
|
+
// so the throw escapes the guard and becomes a 500 from the caller's own
|
|
76
|
+
// catch. That is still a refusal and never an unreserved sale, but it is a
|
|
77
|
+
// refusal this function was supposed to have named.
|
|
78
|
+
let outcome;
|
|
79
|
+
try {
|
|
80
|
+
outcome = await firestore.runTransaction(async (transaction)=>{
|
|
81
|
+
const productIds = [
|
|
82
|
+
...byProduct.keys()
|
|
83
|
+
];
|
|
84
|
+
// ALL READS BEFORE ANY WRITE, which Firestore requires and which is also
|
|
85
|
+
// what makes the set atomic: every document is locked before the first
|
|
86
|
+
// decision is taken against any of them.
|
|
87
|
+
const snapshots = await Promise.all(productIds.map((productId)=>transaction.get(hostRef.collection('products').doc(productId))));
|
|
88
|
+
const writes = [];
|
|
89
|
+
const heldProductIds = [];
|
|
90
|
+
for(let index = 0; index < productIds.length; index++){
|
|
91
|
+
var _ref;
|
|
92
|
+
var _snapshot_data, _snapshot_data1;
|
|
93
|
+
const productId = productIds[index];
|
|
94
|
+
const snapshot = snapshots[index];
|
|
95
|
+
if (!(snapshot == null ? void 0 : snapshot.exists)) {
|
|
96
|
+
return {
|
|
97
|
+
kind: 'missing',
|
|
98
|
+
productName: ''
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const product = CommerceModel.liftLegacyProduct(snapshot.data());
|
|
102
|
+
const wanted = byProduct.get(productId);
|
|
103
|
+
const units = {};
|
|
104
|
+
for (const [variantId, quantity] of wanted){
|
|
105
|
+
// `== null` is the test, never `!available` — `0` is a legitimate
|
|
106
|
+
// answer here and `strictNullChecks` is off repo-wide.
|
|
107
|
+
if (!CommerceModel.stockIsReservable(product, variantId, nowMs)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!CommerceModel.canReserveStock(product, variantId, quantity, nowMs, // This attempt's own prior hold does not stand in its way: a
|
|
111
|
+
// retry must re-claim what it already reserved, or the second
|
|
112
|
+
// press of the same button refuses the shopper their own units.
|
|
113
|
+
holdKey)) {
|
|
114
|
+
var _ref1;
|
|
115
|
+
return {
|
|
116
|
+
kind: 'sold-out',
|
|
117
|
+
productName: String((_ref1 = product == null ? void 0 : product.name) != null ? _ref1 : '')
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
units[variantId] = quantity;
|
|
121
|
+
}
|
|
122
|
+
if (Object.keys(units).length === 0) continue;
|
|
123
|
+
// Lapsed holds are removed by SENTINEL, never by writing back a locally
|
|
124
|
+
// pruned copy of the map: `set(…, { merge: true })` DEEP-merges nested
|
|
125
|
+
// maps, so an object with the key removed leaves the stored key exactly
|
|
126
|
+
// where it was. Correctness does not depend on this sweep — every read
|
|
127
|
+
// prunes again — but without it the document grows one dead key per
|
|
128
|
+
// abandoned checkout, forever, on the one document class in this plugin
|
|
129
|
+
// that is already the largest.
|
|
130
|
+
const live = CommerceModel.pruneStockHolds((_snapshot_data = snapshot.data()) == null ? void 0 : _snapshot_data.stockHolds, nowMs);
|
|
131
|
+
const swept = {};
|
|
132
|
+
for (const stale of Object.keys((_ref = (_snapshot_data1 = snapshot.data()) == null ? void 0 : _snapshot_data1.stockHolds) != null ? _ref : {})){
|
|
133
|
+
if (stale !== holdKey && !live[stale]) {
|
|
134
|
+
swept[stale] = firebaseAdmin.firestore.FieldValue.delete();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
writes.push({
|
|
138
|
+
ref: hostRef.collection('products').doc(productId),
|
|
139
|
+
value: {
|
|
140
|
+
stockHolds: _extends({}, swept, {
|
|
141
|
+
// A whole-object write for THIS key, not a nested merge: the
|
|
142
|
+
// retry of an attempt whose cart shrank must not keep reserving
|
|
143
|
+
// the line it dropped, and a deep merge of `units` would.
|
|
144
|
+
[holdKey]: {
|
|
145
|
+
expiresAtMs,
|
|
146
|
+
units
|
|
147
|
+
}
|
|
148
|
+
})
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
heldProductIds.push(productId);
|
|
152
|
+
}
|
|
153
|
+
if (writes.length === 0) return {
|
|
154
|
+
kind: 'nothing-to-hold'
|
|
155
|
+
};
|
|
156
|
+
for (const write of writes){
|
|
157
|
+
transaction.set(write.ref, write.value, {
|
|
158
|
+
merge: true
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
// THE RELEASE INDEX, in the same transaction as the reservations it
|
|
162
|
+
// names. Written last so it can never name a product whose hold did not
|
|
163
|
+
// commit; read by id (never queried) on `checkout.session.expired` and at
|
|
164
|
+
// settlement, so it needs no index of its own.
|
|
165
|
+
transaction.set(hostRef.collection('stockHolds').doc(holdKey), {
|
|
166
|
+
productIds: heldProductIds,
|
|
167
|
+
lines: heldProductIds.flatMap((productId)=>[
|
|
168
|
+
...byProduct.get(productId)
|
|
169
|
+
].map(([variantId, quantity])=>({
|
|
170
|
+
productId,
|
|
171
|
+
variantId,
|
|
172
|
+
quantity
|
|
173
|
+
}))),
|
|
174
|
+
expiresAtMs,
|
|
175
|
+
createdAtMs: nowMs
|
|
176
|
+
}, {
|
|
177
|
+
merge: false
|
|
178
|
+
});
|
|
179
|
+
return {
|
|
180
|
+
kind: 'held',
|
|
181
|
+
productIds: heldProductIds
|
|
182
|
+
};
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
console.error('Stock hold failed', label, error);
|
|
186
|
+
outcome = {
|
|
187
|
+
kind: 'error',
|
|
188
|
+
productName: ''
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
if (outcome.kind === 'nothing-to-hold') return noop;
|
|
192
|
+
if (outcome.kind === 'held') {
|
|
193
|
+
var _outcome_productIds;
|
|
194
|
+
const productIds = (_outcome_productIds = outcome.productIds) != null ? _outcome_productIds : [];
|
|
195
|
+
return {
|
|
196
|
+
ok: true,
|
|
197
|
+
holdKey,
|
|
198
|
+
// Captures the ids, so an in-request release costs no read. The webhook's
|
|
199
|
+
// release goes through `releaseStockHold` and reads the index instead.
|
|
200
|
+
release: ()=>dropStockHold(hostRef, holdKey, productIds)
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
reason: outcome.kind,
|
|
206
|
+
productName: String((_outcome_productName = outcome.productName) != null ? _outcome_productName : '')
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Give the units back, given the products to give them back on.
|
|
211
|
+
*
|
|
212
|
+
* Bare merge-sets with the delete sentinel rather than a transaction: removing
|
|
213
|
+
* one key is not a read-modify-write, and a release must not be able to fail
|
|
214
|
+
* the response it is tidying up after. Swallows its own errors for the same
|
|
215
|
+
* reason — the TTL still releases the units.
|
|
216
|
+
*/ async function dropStockHold(hostRef, holdKey, productIds) {
|
|
217
|
+
if (!holdKey) return;
|
|
218
|
+
// ONE try/catch around the whole thing, and not a `.catch()` per call. A
|
|
219
|
+
// release runs on the refusal paths — it is the last thing a 409 does on its
|
|
220
|
+
// way out — so anything it throws lands in the CALLER's catch and turns a
|
|
221
|
+
// clean, deliberate refusal into a 500 the shopper cannot act on. A trailing
|
|
222
|
+
// `.catch()` does not cover that: a method that is missing or throws
|
|
223
|
+
// synchronously never returns a promise for one to attach to.
|
|
224
|
+
//
|
|
225
|
+
// Swallowed rather than surfaced because the TTL still releases the units.
|
|
226
|
+
// Tidying up a reservation must not be able to fail the response it is
|
|
227
|
+
// tidying up after.
|
|
228
|
+
try {
|
|
229
|
+
for (const productId of productIds){
|
|
230
|
+
await hostRef.collection('products').doc(productId).set({
|
|
231
|
+
stockHolds: {
|
|
232
|
+
[holdKey]: firebaseAdmin.firestore.FieldValue.delete()
|
|
233
|
+
}
|
|
234
|
+
}, {
|
|
235
|
+
merge: true
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
await hostRef.collection('stockHolds').doc(holdKey).delete();
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error('Stock hold release failed', holdKey, error);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Release a reservation named only by its key — the webhook's door.
|
|
245
|
+
*
|
|
246
|
+
* Reads the index written by `holdStock`, so `checkout.session.expired` needs
|
|
247
|
+
* nothing in the session but `metadata[stockHoldKey]`. A missing index doc is
|
|
248
|
+
* NOT an error: it means the hold was already released, or the session predates
|
|
249
|
+
* this deploy, and in both cases there is nothing to do.
|
|
250
|
+
*/ export async function releaseStockHold(hostRef, holdKey) {
|
|
251
|
+
var _snapshot_get;
|
|
252
|
+
if (!holdKey) return;
|
|
253
|
+
const snapshot = await Promise.resolve().then(()=>hostRef.collection('stockHolds').doc(holdKey).get()).catch(()=>null);
|
|
254
|
+
const productIds = Array.isArray(snapshot == null ? void 0 : (_snapshot_get = snapshot.get) == null ? void 0 : _snapshot_get.call(snapshot, 'productIds')) ? snapshot.get('productIds') : [];
|
|
255
|
+
await dropStockHold(hostRef, holdKey, productIds);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
//# sourceMappingURL=stock-hold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/stock-hold.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as CommerceModel from '../model'\nimport { firebaseAdmin } from '@aglyn/tenant-data-admin'\nimport { randomUUID } from 'crypto'\n\n/**\n * Reserve-then-settle for STOCK (AGL-2356).\n *\n * The I/O half of `model/commerce-stock-holds.ts` — read that file first; it\n * carries the decision this one implements, including why the hold does not\n * touch `inventory` and why a lapsed hold is a release.\n *\n * This is the AGL-2453 promotion-slot shape applied to the one counter that\n * moves real goods, with two differences that matter:\n *\n * 1. **A cart holds N products, so the hold is ONE transaction over all of\n * them.** Firestore serialises the whole set, so a two-product cart either\n * reserves both or reserves neither — a partial reservation would charge a\n * shopper for a basket the store cannot fill, which is the failure this\n * issue exists to prevent, arriving through the fix.\n * 2. **The release needs an index.** A promotion hold is released against the\n * one document named in the session metadata; a cart hold spans documents\n * Stripe never hears about. `hosts/{hostId}/stockHolds/{holdKey}` is\n * written INSIDE the same transaction as the holds it names, so the index\n * cannot disagree with the reservations, and the session carries only the\n * key.\n */\n\n/** One reserved line, already resolved to a real variant. */\nexport interface StockHoldLine {\n productId: string\n variantId: string\n quantity: number\n}\n\n/**\n * Why a reservation could not be taken.\n *\n * - `sold-out` — the units are not there, or are spoken for by another live\n * checkout. The one refusal a shopper is told about by name.\n * - `missing` — the product document is gone, deleted between the pricing\n * loop's read and this transaction.\n * - `error` — Firestore refused. The checkout is REFUSED rather than allowed\n * through unreserved: passing a shopper to Stripe on a reservation that\n * could not be taken is exactly the defect this file closes.\n */\nexport type StockHoldRefusal = 'sold-out' | 'missing' | 'error'\n\n/**\n * A two-member union with `reason` and `productName` present on BOTH members\n * rather than an intersection, for the reason `PromotionSlotOutcome` records:\n * discriminated-union narrowing does not reach through an intersection under\n * this repo's compiler settings (`strictNullChecks` is off repo-wide), and the\n * intersection form fails to compile at every call site.\n */\nexport type StockHoldOutcome =\n | {\n ok: true\n reason?: undefined\n productName?: undefined\n /**\n * The key the hold was stored under, or `''` when nothing needed\n * reserving (every line untracked or backorder). Carried in the session\n * metadata so the webhook can release exactly this reservation; its\n * absence is what tells the webhook there is nothing to release.\n */\n holdKey: string\n /** Drop the reservation. Best-effort and never throws. */\n release: () => Promise<void>\n }\n | { ok: false; reason: StockHoldRefusal; productName: string }\n\n/**\n * A stable per-attempt hold key — the same rule as `promotionHoldKey`.\n *\n * `claim.stripeKey` when the client sent an Idempotency-Key, so a retry of one\n * attempt re-claims its OWN units rather than being refused by them. When it\n * did not, a fresh random key per request — NOT `String(claim.stripeKey)`,\n * which yields the literal `'null'` and would make every keyless shopper on the\n * site share one reservation, each reading the other's hold as their own retry\n * and both passing a shelf of one.\n */\nexport function stockHoldKey(stripeKey: string | null): string {\n return stripeKey || `anon-${randomUUID()}`\n}\n\n/** Two lines of one product COMPOUND — a cart holds variants, not products. */\nfunction groupLines(lines: StockHoldLine[]): Map<string, Map<string, number>> {\n const byProduct = new Map<string, Map<string, number>>()\n for (const line of lines) {\n const quantity = Math.round(Number(line?.quantity) || 0)\n if (!line?.productId || !line?.variantId || quantity <= 0) continue\n const variants = byProduct.get(line.productId) ?? new Map<string, number>()\n variants.set(line.variantId, (variants.get(line.variantId) ?? 0) + quantity)\n byProduct.set(line.productId, variants)\n }\n return byProduct\n}\n\n/**\n * Reserve every line, or refuse.\n *\n * The re-check and the write are ONE transaction over the SAME documents, which\n * is the whole fix: the old code read the shelf with a plain `.get()` at\n * session creation and the webhook decremented it minutes later without ever\n * re-asking, so N shoppers all read `inventory: 1` and all passed.\n * `Transaction.get()` takes a lock on each product document, so contending\n * checkouts SERIALISE — the loser re-runs its callback against the winner's\n * committed hold and is refused.\n *\n * Lines with nothing to reserve — an untracked variant, a `backorder` product —\n * write nothing at all, exactly as an uncapped promotion does. A cart made\n * entirely of those returns an empty `holdKey` and the session carries no\n * reservation, so the digital-goods storefront is byte-identical to what it was\n * before this issue.\n */\nexport async function holdStock(options: {\n firestore: any\n hostRef: any\n holdKey: string\n lines: StockHoldLine[]\n nowMs?: number\n /** Label for the failure log; never shown to a shopper. */\n label: string\n}): Promise<StockHoldOutcome> {\n const { firestore, hostRef, holdKey, lines, label } = options\n const nowMs = options.nowMs ?? Date.now()\n const expiresAtMs = nowMs + CommerceModel.STOCK_HOLD_TTL_MS\n const byProduct = groupLines(lines)\n const noop: StockHoldOutcome = {\n ok: true,\n holdKey: '',\n release: async () => undefined,\n }\n if (byProduct.size === 0) return noop\n\n // try/catch and NOT a trailing `.catch()`. A `runTransaction` that is\n // missing or throws synchronously — a broken client, or a test double that\n // never modelled one — never returns a promise for a `.catch()` to attach to,\n // so the throw escapes the guard and becomes a 500 from the caller's own\n // catch. That is still a refusal and never an unreserved sale, but it is a\n // refusal this function was supposed to have named.\n let outcome: any\n try {\n outcome = await firestore.runTransaction(async (transaction: any) => {\n const productIds = [...byProduct.keys()]\n // ALL READS BEFORE ANY WRITE, which Firestore requires and which is also\n // what makes the set atomic: every document is locked before the first\n // decision is taken against any of them.\n const snapshots = await Promise.all(\n productIds.map((productId) =>\n transaction.get(hostRef.collection('products').doc(productId)),\n ),\n )\n const writes: Array<{ ref: any; value: Record<string, unknown> }> = []\n const heldProductIds: string[] = []\n for (let index = 0; index < productIds.length; index++) {\n const productId = productIds[index]\n const snapshot = snapshots[index]\n if (!snapshot?.exists) {\n return { kind: 'missing' as const, productName: '' }\n }\n const product = CommerceModel.liftLegacyProduct(snapshot.data() as any)\n const wanted = byProduct.get(productId) as Map<string, number>\n const units: Record<string, number> = {}\n for (const [variantId, quantity] of wanted) {\n // `== null` is the test, never `!available` — `0` is a legitimate\n // answer here and `strictNullChecks` is off repo-wide.\n if (!CommerceModel.stockIsReservable(product, variantId, nowMs)) {\n continue\n }\n if (\n !CommerceModel.canReserveStock(\n product,\n variantId,\n quantity,\n nowMs,\n // This attempt's own prior hold does not stand in its way: a\n // retry must re-claim what it already reserved, or the second\n // press of the same button refuses the shopper their own units.\n holdKey,\n )\n ) {\n return {\n kind: 'sold-out' as const,\n productName: String(product?.name ?? ''),\n }\n }\n units[variantId] = quantity\n }\n if (Object.keys(units).length === 0) continue\n // Lapsed holds are removed by SENTINEL, never by writing back a locally\n // pruned copy of the map: `set(…, { merge: true })` DEEP-merges nested\n // maps, so an object with the key removed leaves the stored key exactly\n // where it was. Correctness does not depend on this sweep — every read\n // prunes again — but without it the document grows one dead key per\n // abandoned checkout, forever, on the one document class in this plugin\n // that is already the largest.\n const live = CommerceModel.pruneStockHolds(\n (snapshot.data() as any)?.stockHolds,\n nowMs,\n )\n const swept: Record<string, unknown> = {}\n for (const stale of Object.keys(\n (snapshot.data() as any)?.stockHolds ?? {},\n )) {\n if (stale !== holdKey && !live[stale]) {\n swept[stale] = firebaseAdmin.firestore.FieldValue.delete()\n }\n }\n writes.push({\n ref: hostRef.collection('products').doc(productId),\n value: {\n stockHolds: {\n ...swept,\n // A whole-object write for THIS key, not a nested merge: the\n // retry of an attempt whose cart shrank must not keep reserving\n // the line it dropped, and a deep merge of `units` would.\n [holdKey]: { expiresAtMs, units },\n },\n },\n })\n heldProductIds.push(productId)\n }\n if (writes.length === 0) return { kind: 'nothing-to-hold' as const }\n for (const write of writes) {\n transaction.set(write.ref, write.value, { merge: true })\n }\n // THE RELEASE INDEX, in the same transaction as the reservations it\n // names. Written last so it can never name a product whose hold did not\n // commit; read by id (never queried) on `checkout.session.expired` and at\n // settlement, so it needs no index of its own.\n transaction.set(\n hostRef.collection('stockHolds').doc(holdKey),\n {\n productIds: heldProductIds,\n lines: heldProductIds.flatMap((productId) =>\n [...(byProduct.get(productId) as Map<string, number>)].map(\n ([variantId, quantity]) => ({ productId, variantId, quantity }),\n ),\n ),\n expiresAtMs,\n createdAtMs: nowMs,\n },\n { merge: false },\n )\n return { kind: 'held' as const, productIds: heldProductIds }\n })\n } catch (error: unknown) {\n console.error('Stock hold failed', label, error)\n outcome = { kind: 'error' as const, productName: '' }\n }\n\n if (outcome.kind === 'nothing-to-hold') return noop\n if (outcome.kind === 'held') {\n const productIds: string[] = outcome.productIds ?? []\n return {\n ok: true,\n holdKey,\n // Captures the ids, so an in-request release costs no read. The webhook's\n // release goes through `releaseStockHold` and reads the index instead.\n release: () => dropStockHold(hostRef, holdKey, productIds),\n }\n }\n return {\n ok: false,\n reason: outcome.kind as StockHoldRefusal,\n productName: String(outcome.productName ?? ''),\n }\n}\n\n/**\n * Give the units back, given the products to give them back on.\n *\n * Bare merge-sets with the delete sentinel rather than a transaction: removing\n * one key is not a read-modify-write, and a release must not be able to fail\n * the response it is tidying up after. Swallows its own errors for the same\n * reason — the TTL still releases the units.\n */\nasync function dropStockHold(\n hostRef: any,\n holdKey: string,\n productIds: string[],\n): Promise<void> {\n if (!holdKey) return\n // ONE try/catch around the whole thing, and not a `.catch()` per call. A\n // release runs on the refusal paths — it is the last thing a 409 does on its\n // way out — so anything it throws lands in the CALLER's catch and turns a\n // clean, deliberate refusal into a 500 the shopper cannot act on. A trailing\n // `.catch()` does not cover that: a method that is missing or throws\n // synchronously never returns a promise for one to attach to.\n //\n // Swallowed rather than surfaced because the TTL still releases the units.\n // Tidying up a reservation must not be able to fail the response it is\n // tidying up after.\n try {\n for (const productId of productIds) {\n await hostRef\n .collection('products')\n .doc(productId)\n .set(\n {\n stockHolds: {\n [holdKey]: firebaseAdmin.firestore.FieldValue.delete(),\n },\n },\n { merge: true },\n )\n }\n await hostRef.collection('stockHolds').doc(holdKey).delete()\n } catch (error: unknown) {\n console.error('Stock hold release failed', holdKey, error)\n }\n}\n\n/**\n * Release a reservation named only by its key — the webhook's door.\n *\n * Reads the index written by `holdStock`, so `checkout.session.expired` needs\n * nothing in the session but `metadata[stockHoldKey]`. A missing index doc is\n * NOT an error: it means the hold was already released, or the session predates\n * this deploy, and in both cases there is nothing to do.\n */\nexport async function releaseStockHold(\n hostRef: any,\n holdKey: string,\n): Promise<void> {\n if (!holdKey) return\n const snapshot = await Promise.resolve()\n .then(() => hostRef.collection('stockHolds').doc(holdKey).get())\n .catch(() => null)\n const productIds: string[] = Array.isArray(snapshot?.get?.('productIds'))\n ? snapshot.get('productIds')\n : []\n await dropStockHold(hostRef, holdKey, productIds)\n}\n"],"names":["CommerceModel","firebaseAdmin","randomUUID","stockHoldKey","stripeKey","groupLines","lines","byProduct","Map","line","variants","quantity","Math","round","Number","productId","variantId","get","set","holdStock","options","outcome","firestore","hostRef","holdKey","label","nowMs","Date","now","expiresAtMs","STOCK_HOLD_TTL_MS","noop","ok","release","undefined","size","runTransaction","transaction","productIds","keys","snapshots","Promise","all","map","collection","doc","writes","heldProductIds","index","length","snapshot","exists","kind","productName","product","liftLegacyProduct","data","wanted","units","stockIsReservable","canReserveStock","String","name","Object","live","pruneStockHolds","stockHolds","swept","stale","FieldValue","delete","push","ref","value","write","merge","flatMap","createdAtMs","error","console","dropStockHold","reason","releaseStockHold","resolve","then","catch","Array","isArray"],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type StorefrontTaxRow } from './storefront-tax';
|
|
18
|
+
/**
|
|
19
|
+
* Records one storefront sale's tax, or does nothing when this event is not
|
|
20
|
+
* one. Returns the row written, for the specs — never for control flow.
|
|
21
|
+
*/
|
|
22
|
+
export declare function recordStorefrontTax(type: string, object: any): Promise<StorefrontTaxRow | null>;
|