@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,976 @@
|
|
|
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
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import * as Aglyn from "@aglyn/aglyn/server";
|
|
18
|
+
import * as CommerceModel from "../model/index.js";
|
|
19
|
+
import { claimAttempt, deriveStripeObjectKey } from "@aglyn/aglyn/server";
|
|
20
|
+
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
21
|
+
import { connectLinkageIsReady } from "@aglyn/tenant-data-admin/server/stripe-account-mode";
|
|
22
|
+
import { readCartId } from "./cart-cookie.js";
|
|
23
|
+
import { resolveManualTaxRateId } from "./manual-tax-rate.js";
|
|
24
|
+
import { holdPromotionSlot, promotionHoldKey } from "./promotion-hold.js";
|
|
25
|
+
import { holdStock, stockHoldKey } from "./stock-hold.js";
|
|
26
|
+
import { applyNativeCheckoutParams, nativeCheckoutStripeHeaders, readCheckoutSessionPayload, resolveNativeCheckoutMode } from "./native-checkout.js";
|
|
27
|
+
/**
|
|
28
|
+
* Cart checkout (AGL-293): the whole cart in one Stripe Checkout
|
|
29
|
+
* session on the merchant's account. Every line re-prices from its
|
|
30
|
+
* product doc, unavailable lines block with a visible error, the
|
|
31
|
+
* platform fee sums per line by product type (AGL-278 ladder), and the
|
|
32
|
+
* webhook creates one multi-line order and clears the cart.
|
|
33
|
+
*/ export const cartCheckoutHandler = async (req, res)=>{
|
|
34
|
+
var _req_body, _body_hostId, _body_email, _body_giftCardCode, _body_couponCode, _ref, _req_headers_idempotencykey;
|
|
35
|
+
if (req.method !== 'POST') {
|
|
36
|
+
return res.status(405).json({
|
|
37
|
+
error: 'Method not allowed'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (!process.env.STRIPE_SECRET_KEY) {
|
|
41
|
+
return res.status(501).json({
|
|
42
|
+
error: 'Purchases are not configured on this site.'
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
const body = typeof req.body === 'string' ? JSON.parse(req.body) : (_req_body = req.body) != null ? _req_body : {};
|
|
46
|
+
const hostId = String((_body_hostId = body.hostId) != null ? _body_hostId : '');
|
|
47
|
+
const email = String((_body_email = body.email) != null ? _body_email : '').trim().toLowerCase().slice(0, 120);
|
|
48
|
+
const marketingOptIn = Boolean(body.marketingOptIn);
|
|
49
|
+
const giftCardCode = String((_body_giftCardCode = body.giftCardCode) != null ? _body_giftCardCode : '').trim().toUpperCase().slice(0, 24);
|
|
50
|
+
const couponCode = String((_body_couponCode = body.couponCode) != null ? _body_couponCode : '').trim().toUpperCase().slice(0, 40);
|
|
51
|
+
if (!hostId) return res.status(400).json({
|
|
52
|
+
error: 'Missing hostId'
|
|
53
|
+
});
|
|
54
|
+
// AGL-1769: validated here even though this handler only READS the cart,
|
|
55
|
+
// because it is where the raw cookie left the request — `:342` stamps it
|
|
56
|
+
// into `metadata[cartId]`, and the billing webhook builds a document path
|
|
57
|
+
// from that copy. Closing it at the source is what lets the webhook keep
|
|
58
|
+
// taking its metadata at face value.
|
|
59
|
+
const cartId = readCartId(req.cookies, hostId);
|
|
60
|
+
if (!cartId) return res.status(400).json({
|
|
61
|
+
error: 'Your cart is empty'
|
|
62
|
+
});
|
|
63
|
+
// One checkout attempt, minted by the cart drawer (AGL-1697). Node
|
|
64
|
+
// lowercases incoming headers, but read both spellings — the plugin API
|
|
65
|
+
// request type makes no promise about casing.
|
|
66
|
+
const idempotencyKey = String((_ref = (_req_headers_idempotencykey = req.headers['idempotency-key']) != null ? _req_headers_idempotencykey : req.headers['Idempotency-Key']) != null ? _ref : '').trim().slice(0, 200);
|
|
67
|
+
let claim = null;
|
|
68
|
+
/**
|
|
69
|
+
* Promotion slots this attempt reserved (AGL-2453), hoisted so the catch
|
|
70
|
+
* below can hand them back too. A throw after a hold is placed is exactly the
|
|
71
|
+
* case where an invisible slot would sit against a merchant's cap for a day.
|
|
72
|
+
*/ const heldPromotions = [];
|
|
73
|
+
const releasePromotionHolds = async ()=>{
|
|
74
|
+
for (const hold of heldPromotions.splice(0))await hold.release();
|
|
75
|
+
};
|
|
76
|
+
/**
|
|
77
|
+
* The units this attempt reserved (AGL-2356), hoisted for the same reason —
|
|
78
|
+
* a throw after the hold is placed is exactly the case where a merchant's
|
|
79
|
+
* last unit would sit invisibly spoken for until the TTL lapsed.
|
|
80
|
+
*/ let stockHold = null;
|
|
81
|
+
let heldStockKey = '';
|
|
82
|
+
const releaseStock = async ()=>{
|
|
83
|
+
const held = stockHold;
|
|
84
|
+
stockHold = null;
|
|
85
|
+
heldStockKey = '';
|
|
86
|
+
if (held) await held.release();
|
|
87
|
+
};
|
|
88
|
+
try {
|
|
89
|
+
var _cartSnapshot_data, _ref1, _storeSettings_get, _req_headers_referer, _ref2;
|
|
90
|
+
var _ownerOrg_org, _ownerOrg_org1, _ownerOrg_org2;
|
|
91
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
92
|
+
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
93
|
+
const cartSnapshot = await hostRef.collection('carts').doc(cartId).get();
|
|
94
|
+
const cart = (_cartSnapshot_data = cartSnapshot.data()) != null ? _cartSnapshot_data : {
|
|
95
|
+
lines: []
|
|
96
|
+
};
|
|
97
|
+
if (cart.lines.length === 0) {
|
|
98
|
+
return res.status(400).json({
|
|
99
|
+
error: 'Your cart is empty'
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
const ownerOrg = await getOrgForHost(hostId);
|
|
103
|
+
const ownerId = ownerOrg == null ? void 0 : (_ownerOrg_org = ownerOrg.org) == null ? void 0 : _ownerOrg_org.ownerUid;
|
|
104
|
+
if (!ownerId) {
|
|
105
|
+
return res.status(409).json({
|
|
106
|
+
error: 'This site cannot sell yet'
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// Storefront selling is the `commerce` entitlement (Starter+) — not
|
|
110
|
+
// `marketplaceSelling`, which gates the marketplace marketplace (AGL-470).
|
|
111
|
+
if (!Aglyn.checkEntitlement(ownerOrg.org, 'commerce')) {
|
|
112
|
+
return res.status(403).json({
|
|
113
|
+
error: 'Selling is not enabled'
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const ownerProfile = await firestore.collection('profiles').doc(String(ownerId)).get();
|
|
117
|
+
const accountId = ownerProfile.get('stripeAccountId');
|
|
118
|
+
if (!connectLinkageIsReady({
|
|
119
|
+
accountId,
|
|
120
|
+
chargesEnabled: ownerProfile.get('stripeChargesEnabled'),
|
|
121
|
+
accountLivemode: ownerProfile.get('stripeAccountLivemode')
|
|
122
|
+
}, {
|
|
123
|
+
subject: `cart checkout host ${hostId}`
|
|
124
|
+
})) {
|
|
125
|
+
return res.status(409).json({
|
|
126
|
+
error: 'This site has not enabled payments yet'
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
// Re-price every line from its doc; any unavailable line blocks.
|
|
130
|
+
const productSnapshots = await Promise.all([
|
|
131
|
+
...new Set(cart.lines.map((line)=>line.productId))
|
|
132
|
+
].map((id)=>hostRef.collection('products').doc(id).get()));
|
|
133
|
+
const productsById = new Map(productSnapshots.map((snapshot)=>[
|
|
134
|
+
snapshot.id,
|
|
135
|
+
snapshot.exists ? CommerceModel.liftLegacyProduct(snapshot.data()) : null
|
|
136
|
+
]));
|
|
137
|
+
let itemsCents = 0;
|
|
138
|
+
/** Per-line values for scoped discount pricing. */ const discountLines = [];
|
|
139
|
+
let feeCents = 0;
|
|
140
|
+
/** Whether any line carries a fee rate above zero (AGL-2232). */ let feeApplies = false;
|
|
141
|
+
/**
|
|
142
|
+
* The goods the shopper actually pays for, after any discount that reached
|
|
143
|
+
* the session (AGL-2232). Starts as the whole basket and is lowered only
|
|
144
|
+
* when a Stripe coupon is minted, which is the one reduction the fee is
|
|
145
|
+
* scaled by.
|
|
146
|
+
*/ let chargedItemsCents = 0;
|
|
147
|
+
// Weight-tier shipping rates price off this (AGL-1707). Without it every
|
|
148
|
+
// `weight_tiers` rate would resolve at 0g and quote the lightest tier.
|
|
149
|
+
let totalGrams = 0;
|
|
150
|
+
// Whether anything in this cart is posted at all (AGL-1721). Buy-now has
|
|
151
|
+
// always resolved shipping for physical products only; the cart never
|
|
152
|
+
// asked, so a cart of downloads could be charged to ship. It matters more
|
|
153
|
+
// now that an unresolvable destination REFUSES: a shopper buying two PDFs
|
|
154
|
+
// must not be asked which country to ship them to.
|
|
155
|
+
let hasPhysicalLine = false;
|
|
156
|
+
/**
|
|
157
|
+
* What this cart will RESERVE (AGL-2356), collected as the loop below
|
|
158
|
+
* resolves each line's variant rather than re-derived afterwards: the
|
|
159
|
+
* reservation and the price must be taken against the same variant, and a
|
|
160
|
+
* second resolution is a second chance to pick a different one.
|
|
161
|
+
*/ const reserveLines = [];
|
|
162
|
+
// Cart checkout never builds subscription sessions — every line bills
|
|
163
|
+
// one-time in `payment` mode (recurring products subscribe through the
|
|
164
|
+
// PDP's direct checkout, AGL-303) — so the buyer-chosen billing field
|
|
165
|
+
// (AGL-545) does not apply here.
|
|
166
|
+
const params = new URLSearchParams({
|
|
167
|
+
mode: 'payment'
|
|
168
|
+
});
|
|
169
|
+
cart.lines.forEach((line, index)=>{
|
|
170
|
+
var _variant_weightGrams, _product_type, _product_type1, _variant_options, _variant_options1;
|
|
171
|
+
const product = productsById.get(line.productId);
|
|
172
|
+
if (!product || product.deletedAt || product.status !== 'active') {
|
|
173
|
+
var _ref;
|
|
174
|
+
throw Object.assign(new Error('unavailable'), {
|
|
175
|
+
visible: `"${(_ref = product == null ? void 0 : product.name) != null ? _ref : 'A product'}" is no longer available`
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const variant = line.variantId ? product.variants.find((item)=>item.id === line.variantId) : product.variants[0];
|
|
179
|
+
if (!variant || !CommerceModel.canPurchase(product, variant.id, line.quantity)) {
|
|
180
|
+
throw Object.assign(new Error('unavailable'), {
|
|
181
|
+
visible: `"${product.name}" is sold out`
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
// A variant with no price yet (AGL-2916) is not for sale: pricing it
|
|
185
|
+
// would charge nothing for it, or send the processor no amount.
|
|
186
|
+
if (!CommerceModel.variantHasPrice(variant)) {
|
|
187
|
+
throw Object.assign(new Error('unavailable'), {
|
|
188
|
+
visible: `"${product.name}" is not available`
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
// Gift cards are a Business+ entitlement, checked per sale (AGL-470).
|
|
192
|
+
if (product.giftCard && !Aglyn.checkEntitlement(ownerOrg.org, 'giftCards')) {
|
|
193
|
+
throw Object.assign(new Error('unavailable'), {
|
|
194
|
+
visible: `"${product.name}" is not available on this store's plan`
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// Only the lines there is something to reserve ON. An untracked variant
|
|
198
|
+
// and a `backorder` product have no finite shelf, so a hold could never
|
|
199
|
+
// change what they answer — and collecting them would open a transaction
|
|
200
|
+
// over the products of a purely digital storefront to write nothing at
|
|
201
|
+
// all. The same decision `holdPromotionSlot` makes for an UNCAPPED
|
|
202
|
+
// promotion: the unlimited case stays off the write path entirely.
|
|
203
|
+
//
|
|
204
|
+
// A pre-filter, so the authority is still the in-transaction re-check in
|
|
205
|
+
// `holdStock`. The only thing it can get wrong is skipping a hold for a
|
|
206
|
+
// variant that became tracked between this read and that transaction,
|
|
207
|
+
// which is the behaviour that shipped before this issue.
|
|
208
|
+
if (CommerceModel.stockIsReservable(product, variant.id, Date.now())) {
|
|
209
|
+
reserveLines.push({
|
|
210
|
+
productId: line.productId,
|
|
211
|
+
variantId: variant.id,
|
|
212
|
+
quantity: line.quantity
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
const unitCents = Math.round(Number(variant.priceUsd) * 100);
|
|
216
|
+
itemsCents += unitCents * line.quantity;
|
|
217
|
+
// What THIS line is worth, so a product-scoped discount can be priced
|
|
218
|
+
// against the lines it actually covers rather than the whole basket
|
|
219
|
+
//.
|
|
220
|
+
discountLines.push({
|
|
221
|
+
productId: line.productId,
|
|
222
|
+
amountCents: unitCents * line.quantity
|
|
223
|
+
});
|
|
224
|
+
totalGrams += Math.max(0, Number((_variant_weightGrams = variant.weightGrams) != null ? _variant_weightGrams : 0)) * line.quantity;
|
|
225
|
+
// A missing `type` reads as physical, exactly as the fee ladder below
|
|
226
|
+
// reads it — `liftLegacyProduct` only defaults the field on docs it
|
|
227
|
+
// synthesizes variants for, so a part-migrated doc can reach here
|
|
228
|
+
// without one and the two reads must not disagree about what it is.
|
|
229
|
+
if (((_product_type = product.type) != null ? _product_type : 'physical') === 'physical') hasPhysicalLine = true;
|
|
230
|
+
// `?? 'physical'`, which the comment four lines up already promised and
|
|
231
|
+
// the code did not deliver (AGL-2251). `resolveTransactionFeePct` keys
|
|
232
|
+
// on `productType === 'physical'` and sends everything else — `undefined`
|
|
233
|
+
// included — to the DIGITAL rate, so a part-migrated product doc with no
|
|
234
|
+
// `type` was priced at 3%/2%/1% here while `checkout.ts` (`lifted.type ??
|
|
235
|
+
// 'physical'`) priced the identical product at the physical rate. The
|
|
236
|
+
// same basket cost the merchant a different fee depending on which
|
|
237
|
+
// button the shopper pressed.
|
|
238
|
+
const feePct = Aglyn.resolveTransactionFeePct(ownerOrg.org, (_product_type1 = product.type) != null ? _product_type1 : 'physical');
|
|
239
|
+
// WHETHER a rate applies is not the same fact as what it rounds to
|
|
240
|
+
// (AGL-2232), and only the sum was being kept. `checkout.ts`,
|
|
241
|
+
// `draft-order.ts` and `reserve.ts` all floor at `Math.max(1, …)` when
|
|
242
|
+
// `feePct > 0`; the cart floored nothing, so a Scale-plan cart of $0.30
|
|
243
|
+
// digital lines rounded every line to 0, the emission guard below read
|
|
244
|
+
// `feeCents > 0` as "this plan charges nothing", and the session went out
|
|
245
|
+
// with no application fee at all.
|
|
246
|
+
if (feePct > 0) feeApplies = true;
|
|
247
|
+
feeCents += Math.round(unitCents * line.quantity * feePct / 100);
|
|
248
|
+
params.set(`line_items[${index}][quantity]`, String(line.quantity));
|
|
249
|
+
params.set(`line_items[${index}][price_data][currency]`, 'usd');
|
|
250
|
+
params.set(`line_items[${index}][price_data][unit_amount]`, String(unitCents));
|
|
251
|
+
params.set(`line_items[${index}][price_data][product_data][name]`, `${product.name}${Object.keys((_variant_options = variant.options) != null ? _variant_options : {}).length ? ` (${Object.values((_variant_options1 = variant.options) != null ? _variant_options1 : {}).join(' / ')})` : ''}`.slice(0, 120));
|
|
252
|
+
});
|
|
253
|
+
// Shipping (AGL-1707), from the settings document tax is also read from.
|
|
254
|
+
// This is the ONLY thing that makes Stripe charge shipping: without
|
|
255
|
+
// `shipping_options` it presents no shipping choice,
|
|
256
|
+
// `total_details.amount_shipping` is 0, and the zones and rates the
|
|
257
|
+
// merchant configured in the console are silently worth nothing.
|
|
258
|
+
//
|
|
259
|
+
// RESOLVED BEFORE ANY STRIPE CALL, ahead of the discount block below,
|
|
260
|
+
// because the plan can refuse — and the discount block creates real coupon
|
|
261
|
+
// objects on the merchant's account. Refusing after them would leave one
|
|
262
|
+
// orphaned on every attempt.
|
|
263
|
+
//
|
|
264
|
+
// A merchant who has configured nothing plans to no options and gets a
|
|
265
|
+
// session byte-identical to today's: no shipping charged, exactly as
|
|
266
|
+
// before. Only a merchant who set rates up starts collecting them.
|
|
267
|
+
const storeSettings = await hostRef.collection('settings').doc('store').get();
|
|
268
|
+
const shippingPlan = hasPhysicalLine ? CommerceModel.planCheckoutShipping(storeSettings.get('shipping'), {
|
|
269
|
+
subtotalCents: itemsCents,
|
|
270
|
+
totalGrams
|
|
271
|
+
}, body.shippingCountry) : {
|
|
272
|
+
countries: CommerceModel.CHECKOUT_SHIPPING_COUNTRIES,
|
|
273
|
+
options: []
|
|
274
|
+
};
|
|
275
|
+
// The shopper is asked, then the answer is ENFORCED (AGL-1721): a declared
|
|
276
|
+
// destination narrows `allowed_countries` to itself, so the rate resolved
|
|
277
|
+
// for it is the only one on the session and no other address can be
|
|
278
|
+
// entered against it. Answering the 400 is not optional — a caller that
|
|
279
|
+
// skips the field is refused again rather than served the union.
|
|
280
|
+
if (shippingPlan.refusal === 'destination-required') {
|
|
281
|
+
return res.status(400).json({
|
|
282
|
+
error: 'Choose where you’re shipping to.',
|
|
283
|
+
needsShippingCountry: true,
|
|
284
|
+
shippingCountries: [
|
|
285
|
+
...CommerceModel.CHECKOUT_SHIPPING_COUNTRIES
|
|
286
|
+
]
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
if (shippingPlan.refusal === 'destination-unserved') {
|
|
290
|
+
var _CommerceModel_CHECKOUT_SHIPPING_COUNTRY_NAMES_declared;
|
|
291
|
+
const declared = CommerceModel.normalizeCheckoutShippingCountry(body.shippingCountry);
|
|
292
|
+
return res.status(409).json({
|
|
293
|
+
error: `This store does not ship to ${(_CommerceModel_CHECKOUT_SHIPPING_COUNTRY_NAMES_declared = CommerceModel.CHECKOUT_SHIPPING_COUNTRY_NAMES[declared]) != null ? _CommerceModel_CHECKOUT_SHIPPING_COUNTRY_NAMES_declared : 'that country'}.`,
|
|
294
|
+
needsShippingCountry: true,
|
|
295
|
+
shippingCountries: [
|
|
296
|
+
...CommerceModel.CHECKOUT_SHIPPING_COUNTRIES
|
|
297
|
+
]
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
// The merchant priced shipping and their table cannot reach this cart
|
|
301
|
+
// (AGL-2232) — a 3 kg basket at a store whose weight tiers stop at 2 kg.
|
|
302
|
+
// No `needsShippingCountry`: asking where it goes cannot price it, and a
|
|
303
|
+
// shopper handed the country select would answer it and be refused again.
|
|
304
|
+
if (shippingPlan.refusal === 'cart-unpriceable') {
|
|
305
|
+
return res.status(409).json({
|
|
306
|
+
error: CommerceModel.CART_UNPRICEABLE_SHIPPING_MESSAGE
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
chargedItemsCents = itemsCents;
|
|
310
|
+
// Discounts engine (AGL-305): entered codes and automatic
|
|
311
|
+
// promotions from hosts/{id}/discounts; the AGL-96 coupons remain a
|
|
312
|
+
// fallback for unknown codes.
|
|
313
|
+
const discountsSnapshot = await hostRef.collection('discounts').limit(100).get();
|
|
314
|
+
const resolvedDiscount = CommerceModel.resolveDiscount(discountsSnapshot.docs.map((docSnapshot)=>_extends({}, docSnapshot.data(), {
|
|
315
|
+
$id: docSnapshot.id
|
|
316
|
+
})), _extends({}, couponCode ? {
|
|
317
|
+
code: couponCode
|
|
318
|
+
} : {}, {
|
|
319
|
+
subtotalCents: itemsCents,
|
|
320
|
+
productIds: cart.lines.map((line)=>line.productId),
|
|
321
|
+
lines: discountLines
|
|
322
|
+
}));
|
|
323
|
+
if (resolvedDiscount == null ? void 0 : resolvedDiscount.codeProblem) {
|
|
324
|
+
return res.status(400).json({
|
|
325
|
+
error: resolvedDiscount.codeProblem
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
// Point of no return (AGL-1697): everything past here creates Stripe
|
|
329
|
+
// objects — up to two coupons and the session — on the merchant's live
|
|
330
|
+
// account. The cart is not consumed until the webhook clears it, so
|
|
331
|
+
// before the claim one cart could spawn unlimited sessions, each retry
|
|
332
|
+
// leaving an orphan `checkouts/{sessionId}` doc (which feeds the AGL-323
|
|
333
|
+
// abandoned-cart emails) plus a stray coupon.
|
|
334
|
+
//
|
|
335
|
+
// The two refusals that remain BELOW this line (invalid legacy coupon,
|
|
336
|
+
// empty gift card) each release the claim: they are deterministic, the
|
|
337
|
+
// shopper fixes the code and presses the same button, and because the
|
|
338
|
+
// retry re-derives the same digest Stripe replays any coupon the first
|
|
339
|
+
// run already minted rather than creating a twin (AGL-1714's rule).
|
|
340
|
+
const claimed = await claimAttempt(firestore, {
|
|
341
|
+
kind: 'commerce-cart',
|
|
342
|
+
scopeId: `${hostId}:${cartId}`,
|
|
343
|
+
orgId: String((_ref1 = ownerOrg == null ? void 0 : (_ownerOrg_org1 = ownerOrg.org) == null ? void 0 : _ownerOrg_org1.id) != null ? _ref1 : ''),
|
|
344
|
+
key: idempotencyKey,
|
|
345
|
+
busyMessage: 'This checkout is already being processed'
|
|
346
|
+
});
|
|
347
|
+
if ('replay' in claimed) {
|
|
348
|
+
return res.status(claimed.replay.status).json(claimed.replay.body);
|
|
349
|
+
}
|
|
350
|
+
claim = claimed.claim;
|
|
351
|
+
// Derived per OBJECT, never the raw digest (AGL-1714): Stripe
|
|
352
|
+
// parameter-compares a repeated key account-wide, so one digest sent to
|
|
353
|
+
// `/v1/coupons` and then `checkout/sessions` would error the second call.
|
|
354
|
+
const stripeKeyHeader = (object)=>{
|
|
355
|
+
const derived = deriveStripeObjectKey(claimed.claim.stripeKey, object);
|
|
356
|
+
return derived ? {
|
|
357
|
+
'Idempotency-Key': derived
|
|
358
|
+
} : {};
|
|
359
|
+
};
|
|
360
|
+
// THE UNITS ARE RESERVED HERE (AGL-2356), not merely read in the loop
|
|
361
|
+
// above.
|
|
362
|
+
//
|
|
363
|
+
// `canPurchase` up there is a plain `.get()` compared against a number, and
|
|
364
|
+
// the webhook decremented minutes later without ever re-asking. Nothing was
|
|
365
|
+
// written, so there was no document to contend on: every shopper who
|
|
366
|
+
// reached this line while the shelf held one unit passed it, and they all
|
|
367
|
+
// paid. This is the write that makes them serialise.
|
|
368
|
+
//
|
|
369
|
+
// ONE transaction across every product in the cart, so a two-product basket
|
|
370
|
+
// reserves both or neither — a partial reservation would charge a shopper
|
|
371
|
+
// for goods the store cannot fill, which is this issue's own defect
|
|
372
|
+
// arriving through its fix. See `stock-hold.ts`.
|
|
373
|
+
//
|
|
374
|
+
// FIRST among the three holds this handler takes, before the promotion
|
|
375
|
+
// slots and the gift card below: stock is the likeliest refusal, and
|
|
376
|
+
// reserving a merchant's capped discount for a basket the store cannot fill
|
|
377
|
+
// is a slot held for nothing.
|
|
378
|
+
{
|
|
379
|
+
const held = await holdStock({
|
|
380
|
+
firestore,
|
|
381
|
+
hostRef,
|
|
382
|
+
holdKey: stockHoldKey(claimed.claim.stripeKey),
|
|
383
|
+
lines: reserveLines,
|
|
384
|
+
label: `cart ${hostId}/${cartId}`
|
|
385
|
+
});
|
|
386
|
+
if (!held.ok) {
|
|
387
|
+
// Nothing has been minted yet, so the key goes back and the same
|
|
388
|
+
// button works when the units return (AGL-1697). `sold-out` names the
|
|
389
|
+
// shelf being SPOKEN FOR rather than empty — the merchant has stock and
|
|
390
|
+
// another shopper is in checkout with it, and telling this one to come
|
|
391
|
+
// back in a few minutes is the difference between a wait and a loss.
|
|
392
|
+
await releaseStock();
|
|
393
|
+
await claim.release();
|
|
394
|
+
return res.status(409).json({
|
|
395
|
+
error: held.reason === 'sold-out' ? held.productName ? `"${held.productName}" — ${CommerceModel.STOCK_HELD_MESSAGE}` : CommerceModel.STOCK_HELD_MESSAGE : 'Your cart is no longer available'
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
if (held.holdKey) {
|
|
399
|
+
stockHold = held;
|
|
400
|
+
heldStockKey = held.holdKey;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// ONE Stripe discount, and that is the whole shape of this block
|
|
404
|
+
// (AGL-2112).
|
|
405
|
+
//
|
|
406
|
+
// A Checkout Session takes exactly one entry in `discounts`. This used to
|
|
407
|
+
// be three independent blocks — an AGL-305 discount, a legacy AGL-96
|
|
408
|
+
// coupon code, and an AGL-322 gift card — each minting its own Stripe
|
|
409
|
+
// coupon and each doing `params.set('discounts[0][coupon]', …)`.
|
|
410
|
+
// `URLSearchParams.set` REPLACES, so a cart carrying a discount AND a gift
|
|
411
|
+
// card minted two real coupon objects on the merchant's account and sent
|
|
412
|
+
// only the LAST one. The shopper paid full price minus the gift card; the
|
|
413
|
+
// discount silently evaporated — and the webhook still burned its
|
|
414
|
+
// redemption against `maxRedemptions`, so the code was spent on an order
|
|
415
|
+
// that never got it.
|
|
416
|
+
//
|
|
417
|
+
// The fee was wrong in the same combination and in Aglyn's favour by
|
|
418
|
+
// accident, then against it: `feeCents` was scaled DOWN once per block, so
|
|
419
|
+
// two reductions compounded for one discount that was actually applied.
|
|
420
|
+
//
|
|
421
|
+
// So: resolve every reduction FIRST, add them up, and mint a single
|
|
422
|
+
// `amount_off` coupon for the sum. A percentage coupon becomes its cash
|
|
423
|
+
// equivalent against `itemsCents` — which is what the fee arithmetic here
|
|
424
|
+
// has always assumed it was, and what `checkout.ts` already does for
|
|
425
|
+
// buy-now (`amountCents * (100 - percentOff) / 100`).
|
|
426
|
+
//
|
|
427
|
+
// Validation still happens in the same order and still 400s the same way,
|
|
428
|
+
// BEFORE anything is minted — the shopper fixes the code and presses the
|
|
429
|
+
// same button, so the claim is released and the key survives (AGL-1697).
|
|
430
|
+
// REDEMPTION SLOTS ARE HELD, NOT MERELY READ (AGL-2453).
|
|
431
|
+
//
|
|
432
|
+
// `resolveDiscount` above and the coupon `.get()` below both answered "is
|
|
433
|
+
// there a slot left?" from a plain read, and the webhook incremented the
|
|
434
|
+
// counter minutes later without ever re-asking. The gap is the whole
|
|
435
|
+
// Checkout Session lifetime — up to 24 hours — so every shopper who reached
|
|
436
|
+
// this line while a cap of 100 sat at 99 passed it, and the counter
|
|
437
|
+
// finished at 99+N. Nothing was lost; nothing was ever CHECKED.
|
|
438
|
+
//
|
|
439
|
+
// The AGL-2449 shape, reused: the slot is claimed in a transaction here and
|
|
440
|
+
// settled in one at the webhook. Both redemption paths hold — the typed
|
|
441
|
+
// coupon and the automatic discount — because holding one of two doors onto
|
|
442
|
+
// the same counter fixes neither, and the automatic path is the one a
|
|
443
|
+
// shopper does not even have to opt into.
|
|
444
|
+
//
|
|
445
|
+
// A held slot COUNTS while its session is live and is released when the
|
|
446
|
+
// session expires, is cancelled, or this handler refuses below. See
|
|
447
|
+
// `model/commerce-promotions.ts` for why that is the right way round.
|
|
448
|
+
const slotKey = promotionHoldKey(claim.stripeKey);
|
|
449
|
+
let totalOffCents = 0;
|
|
450
|
+
if (resolvedDiscount && resolvedDiscount.benefit.kind !== 'none') {
|
|
451
|
+
const discountRef = hostRef.collection('discounts').doc(resolvedDiscount.discountId);
|
|
452
|
+
const slot = await holdPromotionSlot({
|
|
453
|
+
firestore,
|
|
454
|
+
ref: discountRef,
|
|
455
|
+
holdKey: slotKey,
|
|
456
|
+
label: `discount ${resolvedDiscount.discountId}`
|
|
457
|
+
});
|
|
458
|
+
if (!slot.ok) {
|
|
459
|
+
// Refused rather than silently dropped, and that is a deliberate
|
|
460
|
+
// choice for the AUTOMATIC case: this discount was in the price the
|
|
461
|
+
// storefront showed, so continuing without it would charge more than
|
|
462
|
+
// the shopper agreed to and tell them nothing. Nothing has been minted
|
|
463
|
+
// yet, so this is retryable — and the retry prices correctly, because
|
|
464
|
+
// `resolveDiscount` counts holds too and simply stops offering it.
|
|
465
|
+
await claim.release();
|
|
466
|
+
return res.status(409).json({
|
|
467
|
+
error: couponCode && resolvedDiscount.discount.code ? CommerceModel.PROMOTION_EXHAUSTED_MESSAGE : CommerceModel.PROMOTION_UNAVAILABLE_MESSAGE
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
if (slot.holdKey) heldPromotions.push(slot);
|
|
471
|
+
totalOffCents += resolvedDiscount.discountCents;
|
|
472
|
+
params.set('metadata[discountId]', resolvedDiscount.discountId);
|
|
473
|
+
// Only when a slot was actually reserved. Its ABSENCE is what tells the
|
|
474
|
+
// webhook to fall back to the unconditional increment, which is right for
|
|
475
|
+
// an uncapped promotion and for a session minted before this deploy.
|
|
476
|
+
if (slot.holdKey) {
|
|
477
|
+
params.set('metadata[discountHoldKey]', slot.holdKey);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
// FREE SHIPPING IS A ZEROED RATE, NOT A COUPON.
|
|
481
|
+
//
|
|
482
|
+
// A session-level Stripe coupon discounts LINE ITEMS; shipping is a
|
|
483
|
+
// separate `shipping_options` concept and a coupon never touches it. There
|
|
484
|
+
// is also no amount to discount at this point — the shopper picks their
|
|
485
|
+
// rate after the session is created — so the only construction that can be
|
|
486
|
+
// exact is to offer every rate at zero.
|
|
487
|
+
//
|
|
488
|
+
// Every rate, not just the cheapest: `free_shipping` carries no field
|
|
489
|
+
// scoping it to one, so the merchant asked for shipping to be free rather
|
|
490
|
+
// than for one particular rate to be. Zeroing the dearest too also keeps
|
|
491
|
+
// the fee and transfer arithmetic below honest, since both read these same
|
|
492
|
+
// options and the shopper is paying nothing for carriage either way.
|
|
493
|
+
const shippingOptions = (resolvedDiscount == null ? void 0 : resolvedDiscount.freeShipping) === true ? shippingPlan.options.map((option)=>_extends({}, option, {
|
|
494
|
+
amountCents: 0
|
|
495
|
+
})) : shippingPlan.options;
|
|
496
|
+
// Coupons (AGL-96 semantics): percent off the items total.
|
|
497
|
+
if (couponCode && !resolvedDiscount) {
|
|
498
|
+
var _ref3;
|
|
499
|
+
const couponRef = hostRef.collection('coupons').doc(couponCode);
|
|
500
|
+
const couponSnapshot = await couponRef.get();
|
|
501
|
+
const coupon = couponSnapshot.data();
|
|
502
|
+
const percentOff = Number((_ref3 = coupon == null ? void 0 : coupon.percentOff) != null ? _ref3 : 0);
|
|
503
|
+
const expired = (coupon == null ? void 0 : coupon.expiresAtMs) != null && Number(coupon.expiresAtMs) < Date.now();
|
|
504
|
+
if (!coupon || coupon.enabled === false || expired || !(percentOff > 0 && percentOff <= 100)) {
|
|
505
|
+
// Deterministic and retryable — the shopper fixes the code and
|
|
506
|
+
// presses the same button, so the key survives (AGL-1697).
|
|
507
|
+
await claim.release();
|
|
508
|
+
return res.status(400).json({
|
|
509
|
+
error: 'Invalid or expired coupon'
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
// The exhaustion test moved INTO a transaction that also writes
|
|
513
|
+
// (AGL-2453). It used to be a comparison against a plain `.get()` here,
|
|
514
|
+
// and the webhook incremented the counter minutes later without
|
|
515
|
+
// re-asking — so the check bounded nothing once two shoppers held
|
|
516
|
+
// sessions at the same time.
|
|
517
|
+
const slot = await holdPromotionSlot({
|
|
518
|
+
firestore,
|
|
519
|
+
ref: couponRef,
|
|
520
|
+
holdKey: slotKey,
|
|
521
|
+
label: `coupon ${couponCode}`
|
|
522
|
+
});
|
|
523
|
+
if (!slot.ok) {
|
|
524
|
+
await claim.release();
|
|
525
|
+
return res.status(400).json({
|
|
526
|
+
error: slot.reason === 'exhausted' ? CommerceModel.PROMOTION_EXHAUSTED_MESSAGE : 'Invalid or expired coupon'
|
|
527
|
+
});
|
|
528
|
+
}
|
|
529
|
+
if (slot.holdKey) {
|
|
530
|
+
heldPromotions.push(slot);
|
|
531
|
+
params.set('metadata[couponHoldKey]', slot.holdKey);
|
|
532
|
+
}
|
|
533
|
+
totalOffCents += Math.round(itemsCents * percentOff / 100);
|
|
534
|
+
params.set('metadata[couponCode]', couponCode);
|
|
535
|
+
}
|
|
536
|
+
// Gift cards (AGL-322): balance applies as amount-off; the webhook settles
|
|
537
|
+
// the hold this places on completion.
|
|
538
|
+
//
|
|
539
|
+
// The read is a TRANSACTION and it WRITES (AGL-2449). A gift card is cash,
|
|
540
|
+
// and a plain `.get()` here was the last unreserved limited resource in the
|
|
541
|
+
// order path: two checkouts entering the same code both read $50, both got
|
|
542
|
+
// $50 off, and the webhook's bare `increment(-N)` settled the card at
|
|
543
|
+
// -$50 — $100 of goods against a $50 card, and a negative row in the
|
|
544
|
+
// outstanding-liability total. The increment was atomic the whole time;
|
|
545
|
+
// nothing ever CHECKED.
|
|
546
|
+
//
|
|
547
|
+
// Unlike stock (AGL-2320), this door can still refuse: the card is applied
|
|
548
|
+
// BEFORE Stripe is contacted, so no money has moved yet and a shortfall is
|
|
549
|
+
// a 400 rather than a shipped parcel and an apology. That is why this
|
|
550
|
+
// reserves instead of merely reporting.
|
|
551
|
+
//
|
|
552
|
+
// `giftCardCents` stays in the session metadata because the webhook still
|
|
553
|
+
// needs to know what to settle, but it is no longer the AUTHORITY for the
|
|
554
|
+
// decrement — the hold is, and it is keyed by the session id below.
|
|
555
|
+
// Set only once a hold actually stands, so the refusals below can call it
|
|
556
|
+
// unconditionally without knowing whether there was a gift card at all.
|
|
557
|
+
let releaseGiftCardHold = async ()=>undefined;
|
|
558
|
+
if (giftCardCode) {
|
|
559
|
+
const cardRef = hostRef.collection('giftCards').doc(giftCardCode);
|
|
560
|
+
// Placed before the Stripe session exists, so it is keyed by the
|
|
561
|
+
// idempotency claim's derived key rather than by a session id we do not
|
|
562
|
+
// have yet — the same string the session is minted under below, so a
|
|
563
|
+
// retry of this attempt re-places the SAME hold rather than a second one.
|
|
564
|
+
const holdKey = claim.stripeKey;
|
|
565
|
+
const nowMs = Date.now();
|
|
566
|
+
const held = await firestore.runTransaction(async (transaction)=>{
|
|
567
|
+
var _fresh_data, _ref, _card_holds;
|
|
568
|
+
var _holds_holdKey;
|
|
569
|
+
const fresh = await transaction.get(cardRef);
|
|
570
|
+
if (!fresh.exists) return null;
|
|
571
|
+
const card = (_fresh_data = fresh.data()) != null ? _fresh_data : {};
|
|
572
|
+
const holds = CommerceModel.pruneGiftCardHolds(card.holds, nowMs);
|
|
573
|
+
// This attempt's own prior hold does not stand in its way — a retry
|
|
574
|
+
// must be able to re-claim what it already reserved, or the second
|
|
575
|
+
// press of the same button refuses the shopper their own money.
|
|
576
|
+
const mine = Number((_ref = (_holds_holdKey = holds[holdKey]) == null ? void 0 : _holds_holdKey.cents) != null ? _ref : 0);
|
|
577
|
+
delete holds[holdKey];
|
|
578
|
+
const available = CommerceModel.giftCardAvailableCents(_extends({}, card, {
|
|
579
|
+
holds
|
|
580
|
+
}), nowMs);
|
|
581
|
+
if (available <= 0 && mine <= 0) return null;
|
|
582
|
+
// Against what is LEFT after the discount, not against the whole
|
|
583
|
+
// basket (AGL-2112). The old cap was `min(balance, itemsCents)`, so a
|
|
584
|
+
// $50 card on a $100 basket already 30% off consumed $50 of card
|
|
585
|
+
// against $70 of goods — the webhook decrements by exactly this
|
|
586
|
+
// number, so the extra was burned off the customer's card for nothing.
|
|
587
|
+
const applyCents = Math.min(available + mine, Math.max(0, itemsCents - totalOffCents));
|
|
588
|
+
if (applyCents <= 0) return 0;
|
|
589
|
+
// Lapsed holds are deleted by SENTINEL, not by writing back a pruned
|
|
590
|
+
// copy of the map: `set(…, { merge: true })` merges nested maps
|
|
591
|
+
// rather than replacing them, so a locally-pruned object leaves every
|
|
592
|
+
// stale key exactly where it was. Correctness does not depend on this
|
|
593
|
+
// — every read prunes again — but the document would otherwise grow
|
|
594
|
+
// without bound, one dead key per abandoned checkout, forever.
|
|
595
|
+
const swept = {};
|
|
596
|
+
for (const stale of Object.keys((_card_holds = card.holds) != null ? _card_holds : {})){
|
|
597
|
+
if (stale !== holdKey && !holds[stale]) {
|
|
598
|
+
swept[stale] = firebaseAdmin.firestore.FieldValue.delete();
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
transaction.set(cardRef, {
|
|
602
|
+
holds: _extends({}, swept, {
|
|
603
|
+
[holdKey]: {
|
|
604
|
+
cents: applyCents,
|
|
605
|
+
expiresAtMs: nowMs + CommerceModel.GIFT_CARD_HOLD_TTL_MS
|
|
606
|
+
}
|
|
607
|
+
})
|
|
608
|
+
}, {
|
|
609
|
+
merge: true
|
|
610
|
+
});
|
|
611
|
+
return applyCents;
|
|
612
|
+
}).catch((error)=>{
|
|
613
|
+
console.error('Gift card hold failed', giftCardCode, error);
|
|
614
|
+
return null;
|
|
615
|
+
});
|
|
616
|
+
if (held == null) {
|
|
617
|
+
// Deterministic and retryable; nothing has been minted yet. Covers the
|
|
618
|
+
// unknown card, the empty card, and the card whose whole balance is
|
|
619
|
+
// already held by someone else's live checkout — the shopper is told
|
|
620
|
+
// the same thing in each case, because "someone is spending it right
|
|
621
|
+
// now" is not a distinction a storefront should draw.
|
|
622
|
+
await claim.release();
|
|
623
|
+
return res.status(400).json({
|
|
624
|
+
error: 'Gift card is empty or invalid'
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
if (held > 0) {
|
|
628
|
+
totalOffCents += held;
|
|
629
|
+
params.set('metadata[giftCardCode]', giftCardCode);
|
|
630
|
+
params.set('metadata[giftCardCents]', String(held));
|
|
631
|
+
params.set('metadata[giftCardHoldKey]', holdKey);
|
|
632
|
+
// Every refusal BELOW this point releases the claim so the shopper can
|
|
633
|
+
// retry; the hold has to come back with it or the retry finds the card
|
|
634
|
+
// empty and the shopper is locked out of their own balance until the
|
|
635
|
+
// TTL lapses. The expiry is the backstop for a crash, not the
|
|
636
|
+
// mechanism for a refusal the code can see happening.
|
|
637
|
+
releaseGiftCardHold = async ()=>{
|
|
638
|
+
await cardRef.set({
|
|
639
|
+
holds: {
|
|
640
|
+
[holdKey]: firebaseAdmin.firestore.FieldValue.delete()
|
|
641
|
+
}
|
|
642
|
+
}, {
|
|
643
|
+
merge: true
|
|
644
|
+
}).catch((error)=>{
|
|
645
|
+
// Best effort: the TTL still releases it. Never let tidying up a
|
|
646
|
+
// hold turn a clean 409 into a 500.
|
|
647
|
+
console.error('Gift card hold release failed', giftCardCode, error);
|
|
648
|
+
});
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (totalOffCents > 0) {
|
|
653
|
+
const stripeCoupon = await fetch('https://api.stripe.com/v1/coupons', {
|
|
654
|
+
method: 'POST',
|
|
655
|
+
headers: _extends({
|
|
656
|
+
Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
|
|
657
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
658
|
+
}, stripeKeyHeader('coupon')),
|
|
659
|
+
body: new URLSearchParams({
|
|
660
|
+
amount_off: String(Math.min(totalOffCents, itemsCents)),
|
|
661
|
+
currency: 'usd',
|
|
662
|
+
duration: 'once'
|
|
663
|
+
}).toString()
|
|
664
|
+
}).then((response)=>response.json());
|
|
665
|
+
if (stripeCoupon == null ? void 0 : stripeCoupon.id) {
|
|
666
|
+
params.set('discounts[0][coupon]', stripeCoupon.id);
|
|
667
|
+
// Scaled ONCE, by the reduction that actually reached the session.
|
|
668
|
+
feeCents = Math.round(feeCents * Math.max(0, itemsCents - totalOffCents) / Math.max(1, itemsCents));
|
|
669
|
+
chargedItemsCents = Math.max(0, itemsCents - totalOffCents);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// THE FLOOR (AGL-2232). A rate above zero on goods the shopper actually
|
|
673
|
+
// pays for is a fee, however small the arithmetic makes it — the same rule
|
|
674
|
+
// the other three doors already hold. Gated on `chargedItemsCents` so a
|
|
675
|
+
// basket a gift card covered entirely still takes nothing: the fee is a cut
|
|
676
|
+
// of the goods, and there are no goods left to cut.
|
|
677
|
+
if (feeApplies && chargedItemsCents > 0 && feeCents < 1) feeCents = 1;
|
|
678
|
+
// STRIPE'S CARD COST, PASSED THROUGH AT COST (AGL-2152). The loop above
|
|
679
|
+
// accumulates the platform's advertised TAKE per line and nothing else; on
|
|
680
|
+
// a destination charge Stripe debits 2.9% + 30¢ of the whole card total
|
|
681
|
+
// from the PLATFORM's balance, so a cart on any tier carrying a 0% rate
|
|
682
|
+
// cost Aglyn money on every single order — and Starter's 2% is below 2.9%,
|
|
683
|
+
// so it lost money too. Added ONCE per charge, not per line: the 30¢ is a
|
|
684
|
+
// per-charge cost.
|
|
685
|
+
//
|
|
686
|
+
// The base is everything the card will be run for that can be seen from
|
|
687
|
+
// here: the goods the shopper actually pays for, the DEAREST shipping
|
|
688
|
+
// option on the session (the shopper picks after this and
|
|
689
|
+
// `application_fee_amount` is fixed at creation), and the manual origin tax
|
|
690
|
+
// rate the lines carry. A store on Stripe Tax is the one residual — its tax
|
|
691
|
+
// is computed inside Stripe after the session is made.
|
|
692
|
+
if (chargedItemsCents > 0) {
|
|
693
|
+
var _storeSettings_get1, _cartTaxSettings_origin;
|
|
694
|
+
const shippingCeilingCents = shippingOptions.reduce((most, option)=>{
|
|
695
|
+
var _option_amountCents;
|
|
696
|
+
return Math.max(most, Math.max(0, Number((_option_amountCents = option.amountCents) != null ? _option_amountCents : 0)));
|
|
697
|
+
}, 0);
|
|
698
|
+
const cartTaxSettings = (_storeSettings_get1 = storeSettings.get('tax')) != null ? _storeSettings_get1 : {};
|
|
699
|
+
const manualRate = cartTaxSettings.mode === 'manual' && !cartTaxSettings.pricesIncludeTax ? CommerceModel.resolveTaxRate(cartTaxSettings, (_cartTaxSettings_origin = cartTaxSettings.origin) != null ? _cartTaxSettings_origin : {}) : null;
|
|
700
|
+
const manualTaxPct = manualRate && manualRate.pct > 0 ? manualRate.pct : 0;
|
|
701
|
+
const chargeCents = chargedItemsCents + Math.round(chargedItemsCents * manualTaxPct / 100) + shippingCeilingCents;
|
|
702
|
+
feeCents = Math.min(chargeCents, feeCents + Aglyn.storefrontProcessingCostCents(chargeCents));
|
|
703
|
+
}
|
|
704
|
+
// THE CONNECT SPLIT IS EMITTED BELOW, NOT HERE (AGL-1956). It used to be
|
|
705
|
+
// written at this point, which is ABOVE the tax decision — and the shape
|
|
706
|
+
// of the split now depends on that decision: a Stripe Tax sale must send a
|
|
707
|
+
// fixed `transfer_data[amount]` instead of `application_fee_amount`,
|
|
708
|
+
// because the fee form transfers `amount_total − fee` and `amount_total`
|
|
709
|
+
// carries tax that AGLYN owes. `feeCents` is final as of this line; only
|
|
710
|
+
// its emission moved. See `commerce-connect-transfer.ts`.
|
|
711
|
+
//
|
|
712
|
+
// Address collection feeds order shipping + destination tax later, and is
|
|
713
|
+
// the enforcing half of the plan above (AGL-1721) — emitted from the
|
|
714
|
+
// plan's own country list, never a default, so it cannot widen past the
|
|
715
|
+
// destinations the declared rates were resolved for. The emission is
|
|
716
|
+
// shared with buy-now (AGL-1720).
|
|
717
|
+
CommerceModel.appendShippingAddressCollectionParams(params, shippingPlan.countries);
|
|
718
|
+
CommerceModel.appendCheckoutShippingParams(params, shippingOptions);
|
|
719
|
+
// Taxes (AGL-1953). This block used to be the `stripe` line alone, and a
|
|
720
|
+
// `manual`-mode store — the mode the AGL-285 zone editor leaves a merchant
|
|
721
|
+
// in by default — collected tax on a buy-now sale and NOTHING on a cart
|
|
722
|
+
// sale of the same goods. Same store, same shopper, same items, two totals
|
|
723
|
+
// depending on which button was pressed, with the merchant left owing the
|
|
724
|
+
// destination tax they never charged.
|
|
725
|
+
//
|
|
726
|
+
// The manual tax rides a real Stripe Tax Rate on each taxable line rather
|
|
727
|
+
// than buy-now's `line_items[1]` product line (AGL-1711). That is not a
|
|
728
|
+
// gratuitous difference: THIS path applies its discounts, coupons and gift
|
|
729
|
+
// cards as session-level Stripe coupons, which spread across every line,
|
|
730
|
+
// so a fake tax line would be discounted along with the goods. Stripe
|
|
731
|
+
// applies a tax rate AFTER the discount — measured, see
|
|
732
|
+
// `manual-tax-rate.ts` — so the arithmetic stays right and
|
|
733
|
+
// `total_details.amount_tax` becomes real, which is what lets the webhook
|
|
734
|
+
// record the tax with no metadata snapshot to keep in step.
|
|
735
|
+
//
|
|
736
|
+
// `automatic_tax.enabled` stays false on a manual sale, so
|
|
737
|
+
// `storefront-tax.ts` still classifies it `manual` and it is never summed
|
|
738
|
+
// into the Aglyn-liable figure (AGL-1904).
|
|
739
|
+
const taxSettings = (_storeSettings_get = storeSettings.get('tax')) != null ? _storeSettings_get : {};
|
|
740
|
+
// AGL-1999: an unset tax mode is NOT a decision, and it must not sell.
|
|
741
|
+
// `mode` was `undefined` for every store whose owner never opened the
|
|
742
|
+
// Taxes card — the default state of every new storefront — and each
|
|
743
|
+
// branch below tested for a specific string, so no branch was taken, no
|
|
744
|
+
// tax was computed, and the shopper was charged an untaxed total. See
|
|
745
|
+
// `commerce-tax-decision.ts` for why refusing beats defaulting to either
|
|
746
|
+
// mode. `mode: 'none'` is the explicit opt-out and passes straight
|
|
747
|
+
// through.
|
|
748
|
+
const taxDecision = CommerceModel.storefrontTaxDecision({
|
|
749
|
+
settings: taxSettings
|
|
750
|
+
});
|
|
751
|
+
if (taxDecision.kind === 'undecided') {
|
|
752
|
+
// BELOW the claim on this path, so release it: the merchant fixes the
|
|
753
|
+
// setting and the shopper retries under the same key (AGL-1697).
|
|
754
|
+
await releaseGiftCardHold();
|
|
755
|
+
await releasePromotionHolds();
|
|
756
|
+
await releaseStock();
|
|
757
|
+
await claim.release();
|
|
758
|
+
return res.status(409).json({
|
|
759
|
+
error: CommerceModel.STOREFRONT_TAX_UNDECIDED_MESSAGE
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
const taxMisconfigured = CommerceModel.storefrontTaxMisconfiguration(taxSettings);
|
|
763
|
+
if (taxMisconfigured) {
|
|
764
|
+
// AGL-2145: the merchant DECIDED to collect and the sale would have
|
|
765
|
+
// charged zero anyway — an unfinished settings card, not a
|
|
766
|
+
// jurisdiction miss. Refused in the same words and the same place as
|
|
767
|
+
// the undecided case above.
|
|
768
|
+
// Below the claim here too, so release it for the same reason.
|
|
769
|
+
await releaseGiftCardHold();
|
|
770
|
+
await releasePromotionHolds();
|
|
771
|
+
await releaseStock();
|
|
772
|
+
await claim.release();
|
|
773
|
+
return res.status(409).json({
|
|
774
|
+
error: taxMisconfigured
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
if (taxDecision.kind === 'stripe-automatic') {
|
|
778
|
+
params.set('automatic_tax[enabled]', 'true');
|
|
779
|
+
} else if (taxSettings.mode === 'manual' && !taxSettings.pricesIncludeTax) {
|
|
780
|
+
var _taxSettings_origin;
|
|
781
|
+
// Origin-based, exactly as buy-now resolves it: the cart collects the
|
|
782
|
+
// shopper's address inside Stripe Checkout, so there is no destination
|
|
783
|
+
// to resolve against before the session exists. Destination-based manual
|
|
784
|
+
// tax is AGL-296's checkout, and taxing by origin here is what makes the
|
|
785
|
+
// two paths agree — which is the whole point of this issue.
|
|
786
|
+
//
|
|
787
|
+
// `pricesIncludeTax` stores charge no separate tax on ANY path (buy-now
|
|
788
|
+
// skips it the same way); the tax is already inside the displayed price.
|
|
789
|
+
const rate = CommerceModel.resolveTaxRate(taxSettings, (_taxSettings_origin = taxSettings.origin) != null ? _taxSettings_origin : {});
|
|
790
|
+
// Tax-exempt products are excluded per LINE, which a single session-wide
|
|
791
|
+
// figure could not express — a cart mixing a taxable chair with an exempt
|
|
792
|
+
// download must tax only the chair.
|
|
793
|
+
const taxableIndexes = cart.lines.map((line, index)=>({
|
|
794
|
+
line,
|
|
795
|
+
index
|
|
796
|
+
})).filter(({ line })=>{
|
|
797
|
+
var _productsById_get;
|
|
798
|
+
return !((_productsById_get = productsById.get(line.productId)) == null ? void 0 : _productsById_get.taxExempt);
|
|
799
|
+
}).map(({ index })=>index);
|
|
800
|
+
if (rate && rate.pct > 0 && taxableIndexes.length > 0) {
|
|
801
|
+
const taxRateId = await resolveManualTaxRateId({
|
|
802
|
+
hostRef,
|
|
803
|
+
taxPct: rate.pct,
|
|
804
|
+
taxLabel: rate.label || `Tax (${rate.pct}%)`,
|
|
805
|
+
headers: stripeKeyHeader('tax-rate')
|
|
806
|
+
});
|
|
807
|
+
if (!taxRateId) {
|
|
808
|
+
// A VISIBLE refusal, never a fallback to an untaxed session — that
|
|
809
|
+
// silent under-collection is this issue. Nothing was sold, so the
|
|
810
|
+
// key goes back and the same button works once Stripe does
|
|
811
|
+
// (AGL-1697); the retry re-derives the same digest, so Stripe
|
|
812
|
+
// replays any rate the first run did mint.
|
|
813
|
+
await releaseGiftCardHold();
|
|
814
|
+
await releasePromotionHolds();
|
|
815
|
+
await releaseStock();
|
|
816
|
+
await claim.release();
|
|
817
|
+
return res.status(502).json({
|
|
818
|
+
error: 'Checkout failed'
|
|
819
|
+
});
|
|
820
|
+
}
|
|
821
|
+
for (const index of taxableIndexes){
|
|
822
|
+
params.set(`line_items[${index}][tax_rates][0]`, taxRateId);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
const referer = String((_req_headers_referer = req.headers.referer) != null ? _req_headers_referer : '');
|
|
827
|
+
const origin = `https://${req.headers.host}`;
|
|
828
|
+
const backUrl = referer.startsWith('http') ? referer : origin;
|
|
829
|
+
const separator = backUrl.includes('?') ? '&' : '?';
|
|
830
|
+
// `{CHECKOUT_SESSION_ID}` is substituted by Stripe on redirect (AGL-1641).
|
|
831
|
+
// Without it the return URL said only that SOMETHING succeeded, so the
|
|
832
|
+
// storefront could not name the order it had just completed — which is why
|
|
833
|
+
// `purchase` could not be reported at all. It is also the order doc id and
|
|
834
|
+
// becomes the GA `transaction_id`, so GA4's own de-duplication makes a
|
|
835
|
+
// refresh of this page harmless.
|
|
836
|
+
params.set('success_url', `${backUrl}${separator}order=success&session_id={CHECKOUT_SESSION_ID}`);
|
|
837
|
+
params.set('cancel_url', `${backUrl}${separator}order=canceled`);
|
|
838
|
+
if (email) params.set('customer_email', email);
|
|
839
|
+
// THE CONNECT SPLIT (AGL-1956), emitted here because it needs the tax
|
|
840
|
+
// decision taken above. On a Stripe Tax cart the tax is computed against
|
|
841
|
+
// AGLYN's registrations and is Aglyn's to remit (AGL-1904), so the
|
|
842
|
+
// merchant is paid a FIXED `transfer_data[amount]` and no
|
|
843
|
+
// `application_fee_amount` is sent at all — the fee form would have Stripe
|
|
844
|
+
// transfer `amount_total − fee`, handing the state's money to the
|
|
845
|
+
// merchant. A manual-rate or untaxed cart keeps the fee form byte for
|
|
846
|
+
// byte. See `commerce-connect-transfer.ts` for why only one of the two
|
|
847
|
+
// knobs may ever be present.
|
|
848
|
+
const cartTaxOwner = taxDecision.kind === 'stripe-automatic' ? 'platform' : 'merchant';
|
|
849
|
+
const cartShippingFloorCents = CommerceModel.shippingFloorCents(shippingOptions);
|
|
850
|
+
Object.entries(CommerceModel.destinationChargeParams({
|
|
851
|
+
accountId: String(accountId),
|
|
852
|
+
feeCents,
|
|
853
|
+
taxOwner: cartTaxOwner,
|
|
854
|
+
merchantGoodsCents: chargedItemsCents,
|
|
855
|
+
shippingFloorCents: cartShippingFloorCents
|
|
856
|
+
})).forEach(([key, value])=>params.set(key, value));
|
|
857
|
+
params.set('metadata[type]', 'commerce-cart');
|
|
858
|
+
params.set('metadata[hostId]', hostId);
|
|
859
|
+
params.set('metadata[cartId]', cartId);
|
|
860
|
+
params.set('metadata[feeCents]', String(Math.max(0, feeCents)));
|
|
861
|
+
// Only on the shape where `transfer.amount` is no longer `charge.amount`,
|
|
862
|
+
// so the merchant's actual payout stops being derivable from the charge
|
|
863
|
+
// (AGL-1956). The shipping baseline is the other half: the difference
|
|
864
|
+
// between it and the option the shopper chose is owed back to the merchant.
|
|
865
|
+
if (cartTaxOwner === 'platform') {
|
|
866
|
+
params.set('metadata[transferCents]', String(CommerceModel.platformLiableTransferCents({
|
|
867
|
+
feeCents,
|
|
868
|
+
merchantGoodsCents: chargedItemsCents,
|
|
869
|
+
shippingFloorCents: cartShippingFloorCents
|
|
870
|
+
})));
|
|
871
|
+
params.set('metadata[transferShippingCents]', String(cartShippingFloorCents));
|
|
872
|
+
}
|
|
873
|
+
// Set only when units were actually reserved (AGL-2356). Its ABSENCE is
|
|
874
|
+
// what tells the webhook there is nothing to release — right for a cart of
|
|
875
|
+
// untracked or backorder lines, and right for a session minted before this
|
|
876
|
+
// deploy, neither of which reserved anything.
|
|
877
|
+
if (heldStockKey) params.set('metadata[stockHoldKey]', heldStockKey);
|
|
878
|
+
// A HALF-HOUR SESSION, AND IT IS THE HOLD'S WHOLE SAFETY ARGUMENT
|
|
879
|
+
// (AGL-2356). Stripe's default is 24 hours, and a hold must outlive any
|
|
880
|
+
// session that can still be paid — so the default would mean one abandoned
|
|
881
|
+
// cart standing on a merchant's last unit for a day, which is exactly the
|
|
882
|
+
// unbounded, invisible UNDER-sell this issue was held back for. Shortening
|
|
883
|
+
// the SESSION is what bounds it; see `CHECKOUT_SESSION_TTL_MS` for why the
|
|
884
|
+
// figure is 31 minutes and not Stripe's advertised 30-minute floor.
|
|
885
|
+
params.set('expires_at', String(Math.floor((Date.now() + CommerceModel.CHECKOUT_SESSION_TTL_MS) / 1000)));
|
|
886
|
+
// The Payment Element (AGL-1944), LAST and touching nothing above it. Every
|
|
887
|
+
// figure the shopper is charged — line prices, the discount coupon, the tax
|
|
888
|
+
// construction, the shipping rates, the fee — was decided before this line
|
|
889
|
+
// and is identical on both checkout paths. This swaps the redirect's URL
|
|
890
|
+
// pair for `ui_mode` + `return_url` and stops. Off unless the flag is on for
|
|
891
|
+
// this org AND a publishable key exists; see `resolveNativeCheckoutMode`.
|
|
892
|
+
const nativeMode = await resolveNativeCheckoutMode(String((_ref2 = ownerOrg == null ? void 0 : (_ownerOrg_org2 = ownerOrg.org) == null ? void 0 : _ownerOrg_org2.id) != null ? _ref2 : ''));
|
|
893
|
+
if (nativeMode.native) {
|
|
894
|
+
applyNativeCheckoutParams(params, `${backUrl}${separator}order=success&session_id={CHECKOUT_SESSION_ID}`);
|
|
895
|
+
}
|
|
896
|
+
const response = await fetch('https://api.stripe.com/v1/checkout/sessions', {
|
|
897
|
+
method: 'POST',
|
|
898
|
+
headers: _extends({
|
|
899
|
+
Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
|
|
900
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
901
|
+
}, stripeKeyHeader('session'), nativeCheckoutStripeHeaders(nativeMode)),
|
|
902
|
+
body: params.toString()
|
|
903
|
+
});
|
|
904
|
+
const session = await response.json();
|
|
905
|
+
// `!session.url` was the liveness check and a `ui_mode` session has no url,
|
|
906
|
+
// so it moves with the mode (AGL-1944) — otherwise every successful native
|
|
907
|
+
// session reads as a Stripe failure: a 502 at the shopper, a released claim,
|
|
908
|
+
// and a real Checkout Session left open on the merchant's account.
|
|
909
|
+
const payload = response.ok ? readCheckoutSessionPayload(session, nativeMode) : null;
|
|
910
|
+
if (!payload) {
|
|
911
|
+
console.error('Stripe cart checkout error', session.error);
|
|
912
|
+
// The checkout did not happen — hand the key back so one flaky moment
|
|
913
|
+
// does not lock this cart out. The retry re-derives the same derived
|
|
914
|
+
// keys, so Stripe replays whatever objects the first run did create.
|
|
915
|
+
await releaseGiftCardHold();
|
|
916
|
+
await releasePromotionHolds();
|
|
917
|
+
await releaseStock();
|
|
918
|
+
await claim.release();
|
|
919
|
+
return res.status(502).json({
|
|
920
|
+
error: 'Checkout failed'
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
// THE SESSION NOW EXISTS, SO THE SLOTS BELONG TO IT (AGL-2453). Past this
|
|
924
|
+
// line a hold must survive: the shopper can still pay, and the webhook
|
|
925
|
+
// settles against the reservation. A catch that released one here would
|
|
926
|
+
// leave a paid order whose redemption is never counted — the same
|
|
927
|
+
// over-redemption this fix closes, arriving through the release path.
|
|
928
|
+
heldPromotions.length = 0;
|
|
929
|
+
// And so do the units (AGL-2356), for the stronger version of the same
|
|
930
|
+
// reason: releasing here would hand the last unit to the next shopper while
|
|
931
|
+
// this one still holds a payable session, which is the oversell itself.
|
|
932
|
+
stockHold = null;
|
|
933
|
+
// Recoverable checkout (AGL-296): email captured before the redirect
|
|
934
|
+
// makes abandonment actionable (AGL-323 sends the recovery emails). Written
|
|
935
|
+
// for the native path too, and it matters MORE there: a shopper who
|
|
936
|
+
// abandons an in-page form never leaves the store, so this row is the only
|
|
937
|
+
// trace that a cart reached checkout at all.
|
|
938
|
+
await hostRef.collection('checkouts').doc(String(session.id)).set(_extends({
|
|
939
|
+
cartId
|
|
940
|
+
}, email ? {
|
|
941
|
+
email
|
|
942
|
+
} : {}, marketingOptIn ? {
|
|
943
|
+
marketingOptIn: true
|
|
944
|
+
} : {}, {
|
|
945
|
+
itemsCents,
|
|
946
|
+
resumeUrl: backUrl,
|
|
947
|
+
status: 'open',
|
|
948
|
+
createdAtMs: Date.now()
|
|
949
|
+
})).catch(()=>undefined);
|
|
950
|
+
await claim.record(200, payload);
|
|
951
|
+
return res.status(200).json(payload);
|
|
952
|
+
} catch (error) {
|
|
953
|
+
if (error == null ? void 0 : error.visible) {
|
|
954
|
+
// Line-availability refusals throw from the pricing loop, which sits
|
|
955
|
+
// ABOVE the claim — so there is nothing to release and the key
|
|
956
|
+
// survives, as a deterministic refusal should (AGL-1697).
|
|
957
|
+
await releasePromotionHolds();
|
|
958
|
+
await releaseStock();
|
|
959
|
+
await (claim == null ? void 0 : claim.release());
|
|
960
|
+
return res.status(409).json({
|
|
961
|
+
error: error.visible
|
|
962
|
+
});
|
|
963
|
+
}
|
|
964
|
+
console.error(error);
|
|
965
|
+
// Release on the way out so a transient failure does not strand the key
|
|
966
|
+
// (AGL-1691's rule) — nor a merchant's promotion slot (AGL-2453).
|
|
967
|
+
await releasePromotionHolds();
|
|
968
|
+
await releaseStock();
|
|
969
|
+
await (claim == null ? void 0 : claim.release());
|
|
970
|
+
return res.status(500).json({
|
|
971
|
+
error: 'Checkout failed'
|
|
972
|
+
});
|
|
973
|
+
}
|
|
974
|
+
};
|
|
975
|
+
|
|
976
|
+
//# sourceMappingURL=cart-checkout.js.map
|