@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,915 @@
|
|
|
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 { readActiveMemberSession } from "./membership.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
|
+
* Commerce Starter checkout (AGL-90): a site visitor buys a product. The
|
|
29
|
+
* price ALWAYS comes from the host's product doc (never the request), the
|
|
30
|
+
* money goes to the host owner's Connect account (AGL-46 onboarding) as a
|
|
31
|
+
* DESTINATION charge, and the webhook records the order under the host.
|
|
32
|
+
* 501 without Stripe env.
|
|
33
|
+
*
|
|
34
|
+
* Two claims in this comment were stale and are corrected here (AGL-2295),
|
|
35
|
+
* both in the direction that matters:
|
|
36
|
+
*
|
|
37
|
+
* - The fee is NOT "2%". It is `resolveTransactionFeePct(org, productType)`
|
|
38
|
+
* — a per-plan, per-product-type ladder with per-org staff overrides,
|
|
39
|
+
* resolved per request. 2% is one cell of sixteen (Starter, physical).
|
|
40
|
+
* - Selling is gated on `commerce`, not on `marketplaceSelling` (AGL-470).
|
|
41
|
+
* They are different entitlements on different plans: `marketplaceSelling`
|
|
42
|
+
* gates the marketplace, and reading this comment as current would gate a
|
|
43
|
+
* storefront on the wrong one.
|
|
44
|
+
*/ export const checkoutHandler = async (req, res)=>{
|
|
45
|
+
var _req_body, _body_hostId, _body_productId, _body_variantId, _body_quantity, _body_couponCode, _body_billing, _ref, _req_headers_idempotencykey;
|
|
46
|
+
if (req.method !== 'POST') {
|
|
47
|
+
return res.status(405).json({
|
|
48
|
+
error: 'Method not allowed'
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
if (!process.env.STRIPE_SECRET_KEY) {
|
|
52
|
+
return res.status(501).json({
|
|
53
|
+
error: 'Purchases are not configured on this site.'
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const body = typeof req.body === 'string' ? JSON.parse(req.body) : (_req_body = req.body) != null ? _req_body : {};
|
|
57
|
+
const hostId = String((_body_hostId = body.hostId) != null ? _body_hostId : '');
|
|
58
|
+
const productId = String((_body_productId = body.productId) != null ? _body_productId : '');
|
|
59
|
+
const variantId = String((_body_variantId = body.variantId) != null ? _body_variantId : '');
|
|
60
|
+
const quantity = Math.max(1, Math.min(99, Math.round(Number((_body_quantity = body.quantity) != null ? _body_quantity : 1))));
|
|
61
|
+
const couponCode = String((_body_couponCode = body.couponCode) != null ? _body_couponCode : '').trim().toUpperCase().slice(0, 40);
|
|
62
|
+
// Buyer-chosen billing (AGL-545): a request, never an instruction —
|
|
63
|
+
// the session mode re-resolves from the product doc below.
|
|
64
|
+
const billingRaw = String((_body_billing = body.billing) != null ? _body_billing : '');
|
|
65
|
+
const billing = billingRaw === 'once' || billingRaw === 'subscribe' ? billingRaw : undefined;
|
|
66
|
+
// One buy-now attempt, minted by the product page (AGL-1697). Node
|
|
67
|
+
// lowercases incoming headers, but read both spellings — the plugin API
|
|
68
|
+
// request type makes no promise about casing.
|
|
69
|
+
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);
|
|
70
|
+
if (!hostId || !productId) {
|
|
71
|
+
return res.status(400).json({
|
|
72
|
+
error: 'Missing hostId or productId'
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
let claim = null;
|
|
76
|
+
/**
|
|
77
|
+
* The coupon redemption slot this attempt reserved (AGL-2453), hoisted so
|
|
78
|
+
* every refusal below the claim — and the catch — can hand it back. A throw
|
|
79
|
+
* after the hold is placed is exactly the case where a slot would otherwise
|
|
80
|
+
* sit invisibly against the merchant's cap until the TTL lapsed.
|
|
81
|
+
*/ let couponSlot = null;
|
|
82
|
+
let couponHoldKey = '';
|
|
83
|
+
const releaseCouponSlot = async ()=>{
|
|
84
|
+
const slot = couponSlot;
|
|
85
|
+
couponSlot = null;
|
|
86
|
+
couponHoldKey = '';
|
|
87
|
+
if (slot) await slot.release();
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* The units this attempt reserved (AGL-2356), hoisted for the same reason the
|
|
91
|
+
* coupon slot is: every refusal below the claim — and the catch — has to hand
|
|
92
|
+
* them back, or an abandoned request stands on a merchant's last unit until
|
|
93
|
+
* the TTL lapses.
|
|
94
|
+
*/ let stockHold = null;
|
|
95
|
+
let heldStockKey = '';
|
|
96
|
+
const releaseStock = async ()=>{
|
|
97
|
+
const held = stockHold;
|
|
98
|
+
stockHold = null;
|
|
99
|
+
heldStockKey = '';
|
|
100
|
+
if (held) await held.release();
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
var _variant_priceUsd, _req_headers_referer, _storeSettings_get, _ref1, _lifted_type, _variant_weightGrams, _lifted_type1, _lifted_type2, _product_name, _ref2;
|
|
104
|
+
var _ownerOrg_org, _ownerOrg_org1, _ownerOrg_org2;
|
|
105
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
106
|
+
const hostRef = firestore.collection('hosts').doc(hostId);
|
|
107
|
+
const [hostSnapshot, productSnapshot] = await Promise.all([
|
|
108
|
+
hostRef.get(),
|
|
109
|
+
hostRef.collection('products').doc(productId).get()
|
|
110
|
+
]);
|
|
111
|
+
if (!hostSnapshot.exists) {
|
|
112
|
+
return res.status(404).json({
|
|
113
|
+
error: 'Unknown site'
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
const product = productSnapshot.data();
|
|
117
|
+
if (!product || product.deletedAt) {
|
|
118
|
+
return res.status(404).json({
|
|
119
|
+
error: 'Unknown product'
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
// Variant pricing (AGL-292): the buyer's selection maps to a variant;
|
|
123
|
+
// absent variantId keeps the legacy default-variant behavior.
|
|
124
|
+
const lifted = CommerceModel.liftLegacyProduct(product);
|
|
125
|
+
const variant = variantId ? lifted.variants.find((item)=>item.id === variantId) : lifted.variants[0];
|
|
126
|
+
if (!variant) return res.status(404).json({
|
|
127
|
+
error: 'Unknown variant'
|
|
128
|
+
});
|
|
129
|
+
const priceUsd = Number((_variant_priceUsd = variant.priceUsd) != null ? _variant_priceUsd : 0);
|
|
130
|
+
if (!(priceUsd > 0) || priceUsd > CommerceModel.COMMERCE_MAX_PRICE_USD) {
|
|
131
|
+
return res.status(400).json({
|
|
132
|
+
error: 'Product is not purchasable'
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Inventory (AGL-281): variant-aware, honoring the product's oversell
|
|
136
|
+
// policy (backorder products keep selling at zero). Enforced here (the
|
|
137
|
+
// block's display is cosmetic) and decremented by the webhook.
|
|
138
|
+
//
|
|
139
|
+
// DELIBERATELY UNCHANGED for subscription sessions. For a PHYSICAL
|
|
140
|
+
// subscription the gate is honest again since AGL-1750: every paid cycle
|
|
141
|
+
// mints an order and decrements the variant (the webhook's invoice
|
|
142
|
+
// branch), so the number this checks now moves as boxes ship. For a
|
|
143
|
+
// digital or service subscription nothing decrements, and the console
|
|
144
|
+
// withdraws the field there (`stockTrackingApplies`, AGL-1744) rather
|
|
145
|
+
// than skipping this gate — it is the one shared by every purchase path,
|
|
146
|
+
// and skipping it for subscription mode would silently RESUME selling
|
|
147
|
+
// for any merchant who set 0 to stop new subscribers.
|
|
148
|
+
if (!CommerceModel.canPurchase(lifted, variant.id, quantity)) {
|
|
149
|
+
return res.status(409).json({
|
|
150
|
+
error: 'Sold out'
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
// Seller resolution rides the owning org (AGL-238): plan gate from
|
|
154
|
+
// the org doc, Stripe account from the owner's marketplace profile.
|
|
155
|
+
const ownerOrg = await getOrgForHost(hostId);
|
|
156
|
+
const ownerId = ownerOrg == null ? void 0 : (_ownerOrg_org = ownerOrg.org) == null ? void 0 : _ownerOrg_org.ownerUid;
|
|
157
|
+
if (!ownerId) {
|
|
158
|
+
return res.status(409).json({
|
|
159
|
+
error: 'This site cannot sell yet'
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
const ownerProfile = await firestore.collection('profiles').doc(String(ownerId)).get();
|
|
163
|
+
// Storefront selling is the `commerce` entitlement (Starter+) — not
|
|
164
|
+
// `marketplaceSelling`, which gates the marketplace marketplace (AGL-470).
|
|
165
|
+
if (!Aglyn.checkEntitlement(ownerOrg.org, 'commerce')) {
|
|
166
|
+
return res.status(403).json({
|
|
167
|
+
error: 'Selling is not enabled'
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
// Session mode (AGL-545): 'subscribe' only takes effect on
|
|
171
|
+
// subscriptionOptional products, 'once' never applies to
|
|
172
|
+
// subscription-only products, and price/interval always come from
|
|
173
|
+
// the product doc — never the request.
|
|
174
|
+
const isSubscription = CommerceModel.resolveCheckoutBillingMode(lifted, billing) === 'subscription';
|
|
175
|
+
// Tiered product types (AGL-470): recurring subscriptions and gift
|
|
176
|
+
// cards are Business+ entitlements, checked per sale — the product doc
|
|
177
|
+
// alone must not unlock them. Gated whenever the session ends up in
|
|
178
|
+
// subscription mode (a subscriptionOptional one-time sale is a plain
|
|
179
|
+
// order, so it rides the base `commerce` entitlement).
|
|
180
|
+
if (isSubscription && !Aglyn.checkEntitlement(ownerOrg.org, 'storefrontSubscriptions')) {
|
|
181
|
+
return res.status(403).json({
|
|
182
|
+
error: 'Subscription products require a Business plan'
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
// ALREADY SUBSCRIBED? (AGL-1849)
|
|
186
|
+
//
|
|
187
|
+
// The idempotency key closes the RETRY shape: one attempt, one key, one
|
|
188
|
+
// subscription. What it cannot close is a deliberate second submit under a
|
|
189
|
+
// fresh key — `product-detail.tsx` mints a new UUID on every variant /
|
|
190
|
+
// quantity / billing change — which is two RECURRING charges on one card
|
|
191
|
+
// for one product, billed forever until someone notices.
|
|
192
|
+
//
|
|
193
|
+
// The issue filed this as impossible: the storefront buy-now path is
|
|
194
|
+
// anonymous, so there is no buyer to ask about. That was half right. The
|
|
195
|
+
// path admits anonymous buyers, but it is not ONLY anonymous — a signed-in
|
|
196
|
+
// site member's `aglyn_member_{hostId}` cookie is already on this request
|
|
197
|
+
// (same-origin `fetch` sends it, and `PluginApiRequest` exposes
|
|
198
|
+
// `cookies`), and the handler simply never read it. So the guard covers
|
|
199
|
+
// the case it can actually identify and, deliberately, no more.
|
|
200
|
+
//
|
|
201
|
+
// WHAT THIS DOES NOT COVER, stated rather than implied: a logged-out buyer
|
|
202
|
+
// who types any email at Stripe. There is no identity for them before the
|
|
203
|
+
// hosted page, and inventing one — forcing sign-in to buy — is a UX change
|
|
204
|
+
// that belongs to a product decision, not to a duplicate guard. The
|
|
205
|
+
// honest scope is "a member cannot silently double-subscribe", not "nobody
|
|
206
|
+
// can".
|
|
207
|
+
//
|
|
208
|
+
// Live-status, NOT existence. A member who cancelled must be able to
|
|
209
|
+
// re-subscribe; refusing on "a subscription row exists" would lock a
|
|
210
|
+
// returning customer out of paying us, which is the AGL-1715 lesson
|
|
211
|
+
// restated on the tenant side. `isTenantSubscriptionLive` is the shared
|
|
212
|
+
// tenant predicate, the same one `gate.ts` grants content access with.
|
|
213
|
+
if (isSubscription) {
|
|
214
|
+
var _session_member_get;
|
|
215
|
+
const session = await readActiveMemberSession(req, hostId);
|
|
216
|
+
const memberEmail = session.status === 'active' ? String((_session_member_get = session.member.get('email')) != null ? _session_member_get : '') : '';
|
|
217
|
+
if (memberEmail) {
|
|
218
|
+
// Scoped by email because that is the join `billing-webhook.ts` writes
|
|
219
|
+
// — `customerEmail` comes off Stripe's `customer_details` — and it is
|
|
220
|
+
// the same key `gate.ts` reads. The 10-doc cap matches gate.ts too: a
|
|
221
|
+
// member with more than ten subscriptions to ONE host is not the
|
|
222
|
+
// shape this refuses, and an unbounded scan on a checkout path is a
|
|
223
|
+
// cost with no matching benefit.
|
|
224
|
+
const existing = await hostRef.collection('subscriptions').where('customerEmail', '==', memberEmail).limit(10).get();
|
|
225
|
+
const alreadySubscribed = existing.docs.some((docSnapshot)=>docSnapshot.get('productId') === productId && CommerceModel.isTenantSubscriptionLive(docSnapshot.get('status')));
|
|
226
|
+
if (alreadySubscribed) {
|
|
227
|
+
// Above the claim, so the key is never burned: this is a
|
|
228
|
+
// deterministic refusal the buyer resolves by cancelling first, and
|
|
229
|
+
// a retry must get the same answer rather than a stale replay.
|
|
230
|
+
return res.status(409).json({
|
|
231
|
+
error: 'You already have an active subscription to this product — ' + 'manage it from your account.',
|
|
232
|
+
code: 'already_subscribed'
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
if (product.giftCard && !Aglyn.checkEntitlement(ownerOrg.org, 'giftCards')) {
|
|
238
|
+
return res.status(403).json({
|
|
239
|
+
error: 'Gift cards require a Business plan'
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
const accountId = ownerProfile.get('stripeAccountId');
|
|
243
|
+
if (!connectLinkageIsReady({
|
|
244
|
+
accountId,
|
|
245
|
+
chargesEnabled: ownerProfile.get('stripeChargesEnabled'),
|
|
246
|
+
accountLivemode: ownerProfile.get('stripeAccountLivemode')
|
|
247
|
+
}, {
|
|
248
|
+
subject: `checkout host ${hostId}`
|
|
249
|
+
})) {
|
|
250
|
+
return res.status(409).json({
|
|
251
|
+
error: 'This site has not enabled payments yet'
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
// The list price per unit, before any coupon (AGL-1711). This is what the
|
|
255
|
+
// order's line-item snapshot records, and it is carried in the session
|
|
256
|
+
// metadata so the webhook never has to re-price from the product doc.
|
|
257
|
+
const listUnitAmountCents = Math.round(priceUsd * 100);
|
|
258
|
+
let amountCents = listUnitAmountCents * quantity;
|
|
259
|
+
// THE DISCOUNTS HUB, ON THIS PATH TOO.
|
|
260
|
+
//
|
|
261
|
+
// `hosts/{hostId}/discounts` had exactly one reader — `cart-checkout.ts` —
|
|
262
|
+
// so a code from the Discounts card worked in the cart and was rejected as
|
|
263
|
+
// invalid on Buy now. Same goods, same code, two prices depending on which
|
|
264
|
+
// button the shopper pressed, which is the disagreement AGL-1953 already
|
|
265
|
+
// closed for TAX on these same two paths.
|
|
266
|
+
//
|
|
267
|
+
// Resolved through the SAME `resolveDiscount` the cart uses, so one code
|
|
268
|
+
// cannot mean two amounts. The legacy AGL-96 coupons below stay as the
|
|
269
|
+
// fallback for a code the hub does not know, exactly as the cart orders
|
|
270
|
+
// them.
|
|
271
|
+
//
|
|
272
|
+
// Applied by reducing the AMOUNT rather than by minting a session-level
|
|
273
|
+
// Stripe coupon, which is how this path already applies its legacy coupon.
|
|
274
|
+
// That difference is load-bearing: buy-now carries its manual tax as a
|
|
275
|
+
// real `line_items[1]` product line, and a session coupon spreads across
|
|
276
|
+
// every line — so minting one here would discount the tax line too.
|
|
277
|
+
const buyNowDiscountLines = [
|
|
278
|
+
{
|
|
279
|
+
productId,
|
|
280
|
+
amountCents: listUnitAmountCents * quantity
|
|
281
|
+
}
|
|
282
|
+
];
|
|
283
|
+
const hubDiscounts = await hostRef.collection('discounts').limit(100).get();
|
|
284
|
+
const resolvedDiscount = CommerceModel.resolveDiscount(hubDiscounts.docs.map((docSnapshot)=>_extends({}, docSnapshot.data(), {
|
|
285
|
+
$id: docSnapshot.id
|
|
286
|
+
})), _extends({}, couponCode ? {
|
|
287
|
+
code: couponCode
|
|
288
|
+
} : {}, {
|
|
289
|
+
subtotalCents: listUnitAmountCents * quantity,
|
|
290
|
+
productIds: [
|
|
291
|
+
productId
|
|
292
|
+
],
|
|
293
|
+
lines: buyNowDiscountLines
|
|
294
|
+
}));
|
|
295
|
+
if (resolvedDiscount == null ? void 0 : resolvedDiscount.codeProblem) {
|
|
296
|
+
return res.status(400).json({
|
|
297
|
+
error: resolvedDiscount.codeProblem
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
let appliedDiscountId = '';
|
|
301
|
+
if (resolvedDiscount && resolvedDiscount.benefit.kind !== 'none') {
|
|
302
|
+
// Stripe's charge minimum is 50c — never discount below it, the same
|
|
303
|
+
// floor the legacy coupon path applies.
|
|
304
|
+
amountCents = Math.max(50, amountCents - resolvedDiscount.discountCents);
|
|
305
|
+
appliedDiscountId = resolvedDiscount.discountId;
|
|
306
|
+
}
|
|
307
|
+
// Coupons (AGL-96): host-defined percent-off codes; invalid codes are
|
|
308
|
+
// a visible 400, never a silent full-price charge. Only consulted when the
|
|
309
|
+
// hub did not already answer for this code.
|
|
310
|
+
let appliedCoupon = '';
|
|
311
|
+
if (couponCode && !resolvedDiscount) {
|
|
312
|
+
var _ref3;
|
|
313
|
+
const couponSnapshot = await hostRef.collection('coupons').doc(couponCode).get();
|
|
314
|
+
const coupon = couponSnapshot.data();
|
|
315
|
+
const percentOff = Number((_ref3 = coupon == null ? void 0 : coupon.percentOff) != null ? _ref3 : 0);
|
|
316
|
+
const expired = (coupon == null ? void 0 : coupon.expiresAtMs) != null && Number(coupon.expiresAtMs) < Date.now();
|
|
317
|
+
// A PRE-FILTER, no longer the enforcement (AGL-2453). It answers early so
|
|
318
|
+
// an obviously spent code 400s before a claim is minted, and it counts
|
|
319
|
+
// live holds so it agrees with the transaction below — the authority is
|
|
320
|
+
// `holdPromotionSlot`, which re-asks this same question inside the write.
|
|
321
|
+
const exhausted = CommerceModel.promotionExhausted(coupon, Date.now());
|
|
322
|
+
if (!coupon || coupon.enabled === false || expired || !(percentOff > 0 && percentOff <= 100)) {
|
|
323
|
+
return res.status(400).json({
|
|
324
|
+
error: 'Invalid or expired coupon'
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
// Named separately from the invalid-code refusal (AGL-2453), and in the
|
|
328
|
+
// same words the transaction below uses. "Invalid or expired" is wrong
|
|
329
|
+
// about a code that is perfectly valid and simply spent — and once a live
|
|
330
|
+
// hold can produce this answer, a shopper told their code is invalid
|
|
331
|
+
// would stop trying rather than retry a moment later.
|
|
332
|
+
if (exhausted) {
|
|
333
|
+
return res.status(400).json({
|
|
334
|
+
error: CommerceModel.PROMOTION_EXHAUSTED_MESSAGE
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
// Stripe's charge minimum is 50¢ — never discount below it. The
|
|
338
|
+
// percentage comes off the ORDER total, matching `cart-checkout.ts`'s
|
|
339
|
+
// `Math.round(itemsCents * percentOff / 100)`, so the same code takes
|
|
340
|
+
// the same money whichever button the shopper pressed. What the SESSION
|
|
341
|
+
// is built from this figure is where the two used to diverge — see the
|
|
342
|
+
// line-item construction below (AGL-2159).
|
|
343
|
+
amountCents = Math.max(50, Math.round(amountCents * (100 - percentOff) / 100));
|
|
344
|
+
appliedCoupon = couponCode;
|
|
345
|
+
}
|
|
346
|
+
// The platform fee is NOT computed here (AGL-2152): Stripe's processing
|
|
347
|
+
// cost is charged on the whole card total, so the fee cannot be known
|
|
348
|
+
// until the manual tax and the shipping options are resolved below. The
|
|
349
|
+
// per-plan ladder (AGL-284, AGL-278) is read inside the two resolvers
|
|
350
|
+
// called after the shipping plan — `resolveTransactionFeeCents` for a
|
|
351
|
+
// one-time sale and `resolveSubscriptionFeePercent` for a subscription.
|
|
352
|
+
const referer = String((_req_headers_referer = req.headers.referer) != null ? _req_headers_referer : '');
|
|
353
|
+
const origin = `https://${req.headers.host}`;
|
|
354
|
+
const backUrl = referer.startsWith('http') ? referer : origin;
|
|
355
|
+
const separator = backUrl.includes('?') ? '&' : '?';
|
|
356
|
+
// Taxes (AGL-285): Stripe Tax when the host opted in; manual mode
|
|
357
|
+
// taxes by store origin here (destination-based arrives with our own
|
|
358
|
+
// checkout, AGL-296). Exempt products skip both.
|
|
359
|
+
const storeSettings = await hostRef.collection('settings').doc('store').get();
|
|
360
|
+
const taxSettings = (_storeSettings_get = storeSettings.get('tax')) != null ? _storeSettings_get : {};
|
|
361
|
+
// AGL-1999: an unset tax mode is NOT a decision, and it must not sell.
|
|
362
|
+
// `mode` was `undefined` for every store whose owner never opened the
|
|
363
|
+
// Taxes card — the default state of every new storefront — and each
|
|
364
|
+
// branch below tested for a specific string, so no branch was taken, no
|
|
365
|
+
// tax was computed, and the shopper was charged an untaxed total. See
|
|
366
|
+
// `commerce-tax-decision.ts` for why refusing beats defaulting to either
|
|
367
|
+
// mode. `mode: 'none'` is the explicit opt-out and passes straight
|
|
368
|
+
// through.
|
|
369
|
+
const taxDecision = CommerceModel.storefrontTaxDecision({
|
|
370
|
+
settings: taxSettings,
|
|
371
|
+
taxExempt: lifted.taxExempt
|
|
372
|
+
});
|
|
373
|
+
if (taxDecision.kind === 'undecided') {
|
|
374
|
+
// Above the attempt claim, like every other deterministic refusal on
|
|
375
|
+
// this path — there is no claim to release yet.
|
|
376
|
+
return res.status(409).json({
|
|
377
|
+
error: CommerceModel.STOREFRONT_TAX_UNDECIDED_MESSAGE
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
const taxMisconfigured = CommerceModel.storefrontTaxMisconfiguration(taxSettings);
|
|
381
|
+
if (taxMisconfigured) {
|
|
382
|
+
// AGL-2145: the merchant DECIDED to collect and the sale would have
|
|
383
|
+
// charged zero anyway — an unfinished settings card, not a
|
|
384
|
+
// jurisdiction miss. Refused in the same words and the same place as
|
|
385
|
+
// the undecided case above.
|
|
386
|
+
return res.status(409).json({
|
|
387
|
+
error: taxMisconfigured
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
const useStripeTax = taxDecision.kind === 'stripe-automatic';
|
|
391
|
+
let taxCents = 0;
|
|
392
|
+
let taxLabel = '';
|
|
393
|
+
let taxPct = 0;
|
|
394
|
+
if (taxSettings.mode === 'manual' && !lifted.taxExempt) {
|
|
395
|
+
var _taxSettings_origin;
|
|
396
|
+
const rate = CommerceModel.resolveTaxRate(taxSettings, (_taxSettings_origin = taxSettings.origin) != null ? _taxSettings_origin : {});
|
|
397
|
+
if (rate && !taxSettings.pricesIncludeTax) {
|
|
398
|
+
taxCents = CommerceModel.computeTaxCents(amountCents, rate.pct);
|
|
399
|
+
taxLabel = rate.label || `Tax (${rate.pct}%)`;
|
|
400
|
+
taxPct = rate.pct;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// Recurring manual tax (AGL-1751). The manual tax cannot ride the
|
|
404
|
+
// `line_items[1]` product line on a SUBSCRIPTION session: a non-recurring
|
|
405
|
+
// price in `mode: subscription` is a one-time item, and Stripe bills
|
|
406
|
+
// one-time items on the FIRST invoice only — so the merchant's tax was
|
|
407
|
+
// collected once and then, on every renewal the buyer had been shown a
|
|
408
|
+
// taxed price for, never again. A Stripe Tax Rate attached to the
|
|
409
|
+
// subscription line recurs: Checkout carries `line_items[][tax_rates]`
|
|
410
|
+
// onto the subscription item, every invoice applies it, and the figure
|
|
411
|
+
// lands where the records already look — the session's
|
|
412
|
+
// `total_details.amount_tax` for the sale (AGL-1732) and the invoice's
|
|
413
|
+
// own tax fields for each renewal (AGL-1743). No webhook change.
|
|
414
|
+
//
|
|
415
|
+
// The rate is created on the PLATFORM account — no Stripe-Account header,
|
|
416
|
+
// matching the session call below — because that is where the
|
|
417
|
+
// subscription lives (destination transfer); a connected-account rate id
|
|
418
|
+
// would not resolve. Tax rates are immutable, so one is minted per
|
|
419
|
+
// (percentage, label) and cached under the host; a merchant editing their
|
|
420
|
+
// zone rate misses the cache and mints a fresh object, while the old one
|
|
421
|
+
// stays behind on the subscriptions that already carry it, exactly as it
|
|
422
|
+
// should.
|
|
423
|
+
//
|
|
424
|
+
// A creation failure is a VISIBLE 502, never a fallback: the one-time
|
|
425
|
+
// line is this very bug, and a session with no tax at all would
|
|
426
|
+
// under-collect every invoice silently.
|
|
427
|
+
// Point of no return (AGL-1697): everything past here creates Stripe
|
|
428
|
+
// objects — possibly a Tax Rate, then the session. Server-side a retry
|
|
429
|
+
// looked free, but for `mode: subscription` two completed sessions are
|
|
430
|
+
// two RECURRING subscriptions for the same member and product. The
|
|
431
|
+
// shipping refusals BELOW this line release the claim: they are
|
|
432
|
+
// deterministic asks the shopper answers and retries under the same key,
|
|
433
|
+
// and the released key re-derives the same digests so Stripe replays the
|
|
434
|
+
// tax rate rather than minting a twin (AGL-1714's rule).
|
|
435
|
+
const claimed = await claimAttempt(firestore, {
|
|
436
|
+
kind: 'commerce-checkout',
|
|
437
|
+
scopeId: hostId,
|
|
438
|
+
orgId: String((_ref1 = ownerOrg == null ? void 0 : (_ownerOrg_org1 = ownerOrg.org) == null ? void 0 : _ownerOrg_org1.id) != null ? _ref1 : ''),
|
|
439
|
+
key: idempotencyKey,
|
|
440
|
+
busyMessage: 'This checkout is already being processed'
|
|
441
|
+
});
|
|
442
|
+
if ('replay' in claimed) {
|
|
443
|
+
return res.status(claimed.replay.status).json(claimed.replay.body);
|
|
444
|
+
}
|
|
445
|
+
claim = claimed.claim;
|
|
446
|
+
// Derived per OBJECT, never the raw digest (AGL-1714): Stripe
|
|
447
|
+
// parameter-compares a repeated key account-wide, so one digest sent to
|
|
448
|
+
// `/v1/tax_rates` and then `checkout/sessions` would error the second.
|
|
449
|
+
const stripeKeyHeader = (object)=>{
|
|
450
|
+
const derived = deriveStripeObjectKey(claimed.claim.stripeKey, object);
|
|
451
|
+
return derived ? {
|
|
452
|
+
'Idempotency-Key': derived
|
|
453
|
+
} : {};
|
|
454
|
+
};
|
|
455
|
+
// THE UNITS ARE RESERVED HERE (AGL-2356), not merely read above.
|
|
456
|
+
//
|
|
457
|
+
// The `canPurchase` gate at the top of this handler is a plain `.get()`
|
|
458
|
+
// compared against a number, and the webhook decremented minutes later
|
|
459
|
+
// without ever re-asking. Nothing was written, so there was no document to
|
|
460
|
+
// contend on: every shopper who reached this line while the shelf held one
|
|
461
|
+
// unit passed it, and they all paid. This is the write that makes them
|
|
462
|
+
// serialise — see `model/commerce-stock-holds.ts` for why the shelf count
|
|
463
|
+
// itself deliberately does not move.
|
|
464
|
+
//
|
|
465
|
+
// FIRST among the holds, before the coupon slot below: stock is the
|
|
466
|
+
// likeliest refusal on this path, and reserving a promotion slot for a
|
|
467
|
+
// basket the store cannot fill is a slot held against the merchant's cap
|
|
468
|
+
// for nothing.
|
|
469
|
+
{
|
|
470
|
+
const held = await holdStock({
|
|
471
|
+
firestore,
|
|
472
|
+
hostRef,
|
|
473
|
+
holdKey: stockHoldKey(claimed.claim.stripeKey),
|
|
474
|
+
// Empty when there is nothing to reserve ON — an untracked variant or
|
|
475
|
+
// a `backorder` product, whose merchant chose to sell past zero. See
|
|
476
|
+
// the note in `cart-checkout.ts`: the unlimited case stays off the
|
|
477
|
+
// write path entirely, and `holdStock` opens no transaction for it.
|
|
478
|
+
lines: CommerceModel.stockIsReservable(lifted, variant.id, Date.now()) ? [
|
|
479
|
+
{
|
|
480
|
+
productId,
|
|
481
|
+
variantId: variant.id,
|
|
482
|
+
quantity
|
|
483
|
+
}
|
|
484
|
+
] : [],
|
|
485
|
+
label: `buy-now ${hostId}/${productId}`
|
|
486
|
+
});
|
|
487
|
+
if (!held.ok) {
|
|
488
|
+
// Nothing has been minted yet, so the key goes back and the same
|
|
489
|
+
// button works when the units return (AGL-1697). `sold-out` is named
|
|
490
|
+
// separately from an empty shelf: the merchant HAS stock and someone
|
|
491
|
+
// else is in checkout with it, and a shopper told "sold out" leaves
|
|
492
|
+
// while a shopper told to wait a few minutes comes back.
|
|
493
|
+
await releaseStock();
|
|
494
|
+
await claim.release();
|
|
495
|
+
return res.status(409).json({
|
|
496
|
+
error: held.reason === 'sold-out' ? CommerceModel.STOCK_HELD_MESSAGE : 'Sold out'
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
if (held.holdKey) {
|
|
500
|
+
stockHold = held;
|
|
501
|
+
heldStockKey = held.holdKey;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
// THE COUPON SLOT IS RESERVED HERE (AGL-2453), not merely read above.
|
|
505
|
+
//
|
|
506
|
+
// The check at the top of this handler was a comparison against a plain
|
|
507
|
+
// `.get()`, and the webhook incremented `redemptions` minutes later without
|
|
508
|
+
// re-asking — so a cap of one admitted every shopper who reached checkout
|
|
509
|
+
// before the first of them paid. Placed BELOW the claim because the hold is
|
|
510
|
+
// keyed by the attempt: a retry of one attempt must re-claim its own slot
|
|
511
|
+
// rather than be refused by it.
|
|
512
|
+
//
|
|
513
|
+
// Buy-now runs no automatic-discount path — `resolveDiscount` is the cart's
|
|
514
|
+
// — so the typed coupon is the only counter this handler can burn.
|
|
515
|
+
if (appliedCoupon || appliedDiscountId) {
|
|
516
|
+
const slot = await holdPromotionSlot({
|
|
517
|
+
firestore,
|
|
518
|
+
// The hub discount and the legacy coupon are two counters and only one
|
|
519
|
+
// of them can be in play — the hub answers first and the legacy block
|
|
520
|
+
// is skipped when it did.
|
|
521
|
+
ref: appliedDiscountId ? hostRef.collection('discounts').doc(appliedDiscountId) : hostRef.collection('coupons').doc(appliedCoupon),
|
|
522
|
+
holdKey: promotionHoldKey(claimed.claim.stripeKey),
|
|
523
|
+
label: appliedDiscountId ? `discount ${appliedDiscountId}` : `coupon ${appliedCoupon}`
|
|
524
|
+
});
|
|
525
|
+
if (!slot.ok) {
|
|
526
|
+
// Nothing has been minted yet, so this is retryable and the key goes
|
|
527
|
+
// back (AGL-1697). Exhaustion is named; anything else reads as the
|
|
528
|
+
// same invalid-code refusal the pre-filter above would have given.
|
|
529
|
+
await releaseCouponSlot();
|
|
530
|
+
await releaseStock();
|
|
531
|
+
await claim.release();
|
|
532
|
+
return res.status(400).json({
|
|
533
|
+
error: slot.reason === 'exhausted' ? CommerceModel.PROMOTION_EXHAUSTED_MESSAGE : 'Invalid or expired coupon'
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
if (slot.holdKey) {
|
|
537
|
+
couponSlot = slot;
|
|
538
|
+
couponHoldKey = slot.holdKey;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
// Minting and caching now live in `manual-tax-rate.ts` (AGL-1953), which
|
|
542
|
+
// the cart path needs too — two copies of a money-shaped cache is how two
|
|
543
|
+
// paths quietly diverge. Behaviour here is unchanged.
|
|
544
|
+
let recurringTaxRateId = '';
|
|
545
|
+
if (isSubscription && taxCents > 0) {
|
|
546
|
+
var _ref4;
|
|
547
|
+
recurringTaxRateId = (_ref4 = await resolveManualTaxRateId({
|
|
548
|
+
hostRef,
|
|
549
|
+
taxPct,
|
|
550
|
+
taxLabel,
|
|
551
|
+
headers: stripeKeyHeader('tax-rate')
|
|
552
|
+
})) != null ? _ref4 : '';
|
|
553
|
+
if (!recurringTaxRateId) {
|
|
554
|
+
// Nothing was sold — hand the key back so the same button works
|
|
555
|
+
// once Stripe does (AGL-1697).
|
|
556
|
+
await releaseCouponSlot();
|
|
557
|
+
await releaseStock();
|
|
558
|
+
await claim.release();
|
|
559
|
+
return res.status(502).json({
|
|
560
|
+
error: 'Checkout failed'
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
// Shipping (AGL-1720), from the same settings document tax was just read
|
|
565
|
+
// from. Buy-now declared neither `shipping_address_collection` nor
|
|
566
|
+
// `shipping_options`, so a merchant who configured zones and rates in the
|
|
567
|
+
// console collected them on cart orders (AGL-1707) and nothing at all on
|
|
568
|
+
// buy-now — same merchant, same product, two totals depending on which
|
|
569
|
+
// button the shopper pressed.
|
|
570
|
+
//
|
|
571
|
+
// CONDITIONAL, unlike the cart's, on two things the cart cannot know:
|
|
572
|
+
//
|
|
573
|
+
// - PHYSICAL PRODUCTS ONLY. This is the PDP path for one product, and it
|
|
574
|
+
// sells digital downloads and services too. Asking a shopper buying a
|
|
575
|
+
// PDF for a shipping address, then charging them to ship it, would be
|
|
576
|
+
// a worse bug than the one being fixed. A missing `type` reads as
|
|
577
|
+
// physical, exactly as the fee ladder above already reads it —
|
|
578
|
+
// `liftLegacyProduct` only defaults the field on docs it synthesizes
|
|
579
|
+
// variants for, so a part-migrated doc can reach here without one, and
|
|
580
|
+
// the two reads must not disagree about what that doc is.
|
|
581
|
+
// - ONE-TIME SALES ONLY. Originally because the webhook's
|
|
582
|
+
// `commerce-subscription` branch never touched `total_details`, so
|
|
583
|
+
// shipping charged there would have been real money recorded NOWHERE.
|
|
584
|
+
// Both halves of that are now closed: AGL-1732 decomposed the initial
|
|
585
|
+
// charge onto the subscription document, `amount_shipping` included,
|
|
586
|
+
// and AGL-1743 records every renewal invoice — `shipping_cost` read
|
|
587
|
+
// the same way — so a recurring shipping charge would now land.
|
|
588
|
+
//
|
|
589
|
+
// What remains is the PRODUCT question, and only it: a rate editor
|
|
590
|
+
// written for one-time orders never asks the merchant whether a rate
|
|
591
|
+
// should be re-charged every cycle, and Stripe bills a subscription's
|
|
592
|
+
// one-time line items on the FIRST invoice only, so a shipping rate
|
|
593
|
+
// added as a session line item would be charged once and then silently
|
|
594
|
+
// stop. Answering that is AGL-1720's revisit, not wiring.
|
|
595
|
+
//
|
|
596
|
+
// Weight is per unit TIMES the quantity bought. `weightGrams` alone would
|
|
597
|
+
// quote the lightest tier on a multi-unit order — the same under-collection
|
|
598
|
+
// this issue is about, and adjacent to the hardcoded `quantity: 1` that
|
|
599
|
+
// AGL-1711 just removed from this function.
|
|
600
|
+
//
|
|
601
|
+
// The subtotal fed to the resolver is the PRE-discount list total, matching
|
|
602
|
+
// the cart's, so "free over $50" means what the shopper saw it mean.
|
|
603
|
+
//
|
|
604
|
+
// WHICH DESTINATIONS the session may serve is decided with the rates and
|
|
605
|
+
// not after them (AGL-1721): a session that declared every zone's rates
|
|
606
|
+
// while accepting an address in any country let a shopper pick a domestic
|
|
607
|
+
// rate for an international parcel, and Stripe re-checks nothing — a
|
|
608
|
+
// `shipping_rate_data` carries no country. `planCheckoutShipping` pairs
|
|
609
|
+
// the two lists, and refuses rather than guessing.
|
|
610
|
+
const shippingSettings = storeSettings.get('shipping');
|
|
611
|
+
const shipsPhysically = ((_lifted_type = lifted.type) != null ? _lifted_type : 'physical') === 'physical' && !isSubscription;
|
|
612
|
+
const shippingPlan = shipsPhysically ? CommerceModel.planCheckoutShipping(shippingSettings, {
|
|
613
|
+
subtotalCents: listUnitAmountCents * quantity,
|
|
614
|
+
totalGrams: Math.max(0, Number((_variant_weightGrams = variant.weightGrams) != null ? _variant_weightGrams : 0)) * quantity
|
|
615
|
+
}, body.shippingCountry) : {
|
|
616
|
+
countries: CommerceModel.CHECKOUT_SHIPPING_COUNTRIES,
|
|
617
|
+
options: []
|
|
618
|
+
};
|
|
619
|
+
if (shippingPlan.refusal === 'destination-required') {
|
|
620
|
+
// A deterministic ask the shopper answers and retries under the same
|
|
621
|
+
// key — released, not burned (AGL-1697). The tax rate possibly minted
|
|
622
|
+
// above replays from its cache (or its derived key) on the retry.
|
|
623
|
+
await releaseCouponSlot();
|
|
624
|
+
await releaseStock();
|
|
625
|
+
await claim.release();
|
|
626
|
+
return res.status(400).json({
|
|
627
|
+
error: 'Choose where you’re shipping to.',
|
|
628
|
+
needsShippingCountry: true,
|
|
629
|
+
shippingCountries: [
|
|
630
|
+
...CommerceModel.CHECKOUT_SHIPPING_COUNTRIES
|
|
631
|
+
]
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
if (shippingPlan.refusal === 'destination-unserved') {
|
|
635
|
+
var _CommerceModel_CHECKOUT_SHIPPING_COUNTRY_NAMES_declared;
|
|
636
|
+
const declared = CommerceModel.normalizeCheckoutShippingCountry(body.shippingCountry);
|
|
637
|
+
await releaseCouponSlot();
|
|
638
|
+
await releaseStock();
|
|
639
|
+
await claim.release();
|
|
640
|
+
return res.status(409).json({
|
|
641
|
+
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'}.`,
|
|
642
|
+
needsShippingCountry: true,
|
|
643
|
+
shippingCountries: [
|
|
644
|
+
...CommerceModel.CHECKOUT_SHIPPING_COUNTRIES
|
|
645
|
+
]
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
// The merchant priced shipping and no rate of theirs reaches this cart
|
|
649
|
+
// (AGL-2232). Released like every other deterministic refusal on this
|
|
650
|
+
// path — the shopper can change the basket and retry under the same key.
|
|
651
|
+
if (shippingPlan.refusal === 'cart-unpriceable') {
|
|
652
|
+
await releaseCouponSlot();
|
|
653
|
+
await releaseStock();
|
|
654
|
+
await claim.release();
|
|
655
|
+
return res.status(409).json({
|
|
656
|
+
error: CommerceModel.CART_UNPRICEABLE_SHIPPING_MESSAGE
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
// THE PLATFORM FEE (AGL-2152). Formerly `feePct` of the goods and nothing
|
|
660
|
+
// else, which on a DESTINATION charge meant Aglyn paid Stripe 2.9% + 30¢
|
|
661
|
+
// out of its own balance and — on every tier carrying
|
|
662
|
+
// `transactionFeePhysicalPct: 0` — collected nothing back. The advertised
|
|
663
|
+
// platform take is unchanged; the card cost is now passed through at cost
|
|
664
|
+
// so the net per order is the take rather than the take minus Stripe's fee.
|
|
665
|
+
//
|
|
666
|
+
// The charge base is everything Stripe will run the card for: the goods
|
|
667
|
+
// after the coupon, the manual tax line, and the DEAREST shipping option on
|
|
668
|
+
// the session. The dearest, not the cheapest, because the shopper picks
|
|
669
|
+
// after the session is created and `application_fee_amount` is a fixed
|
|
670
|
+
// number Stripe will not let us revise — under-reaching would put the
|
|
671
|
+
// difference back on Aglyn. A store on Stripe Tax is the one residual: its
|
|
672
|
+
// tax is computed by Stripe after this point and cannot be seen from here.
|
|
673
|
+
const shippingCeilingCents = shippingPlan.options.reduce((most, option)=>{
|
|
674
|
+
var _option_amountCents;
|
|
675
|
+
return Math.max(most, Math.max(0, Number((_option_amountCents = option.amountCents) != null ? _option_amountCents : 0)));
|
|
676
|
+
}, 0);
|
|
677
|
+
// A SUBSCRIPTION carries the same pass-through AS A PERCENT (AGL-2655).
|
|
678
|
+
// Stripe offers a Subscription no `application_fee_amount`, only
|
|
679
|
+
// `application_fee_percent`, which cannot carry a fixed 30¢ as cents — so
|
|
680
|
+
// the 30¢ is folded into the rate: `(rate × amount + fixed) ÷ amount`,
|
|
681
|
+
// rounded UP to the two decimals the parameter allows, on top of the
|
|
682
|
+
// plan's own percent. `resolveSubscriptionFeePercent` owns that
|
|
683
|
+
// arithmetic and its rounding. Sized on the recurring goods alone: Stripe
|
|
684
|
+
// applies the percent to the whole invoice, and the paid-invoice
|
|
685
|
+
// correction (`chargeSubscriptionFeeOnItemsOnly`, AGL-2317) scales it
|
|
686
|
+
// back to the items, so the goods are the base the recovery has to come
|
|
687
|
+
// out at cost on.
|
|
688
|
+
//
|
|
689
|
+
// This branch never reaches `payment_intent_data[application_fee_amount]`
|
|
690
|
+
// (the emission below is gated on `!isSubscription`); the percent goes
|
|
691
|
+
// out on `subscription_data` further down, and `feeCents` here is what
|
|
692
|
+
// that percent takes of the opening cycle's goods — the figure
|
|
693
|
+
// `metadata[feeCents]` records as what Aglyn keeps.
|
|
694
|
+
const subscriptionFeePercent = isSubscription ? Aglyn.resolveSubscriptionFeePercent(ownerOrg.org, (_lifted_type1 = lifted.type) != null ? _lifted_type1 : 'physical', amountCents) : 0;
|
|
695
|
+
const feeCents = isSubscription ? subscriptionFeePercent > 0 ? Math.max(1, Math.round(amountCents * subscriptionFeePercent / 100)) : 0 : Aglyn.resolveTransactionFeeCents(ownerOrg.org, (_lifted_type2 = lifted.type) != null ? _lifted_type2 : 'physical', amountCents, amountCents + taxCents + shippingCeilingCents);
|
|
696
|
+
// The unit price Stripe actually charges — the coupon is priced INTO it
|
|
697
|
+
// rather than sent as a Stripe discount, which is why `amount_discount` is
|
|
698
|
+
// 0 on every buy-now session and the discount has to ride the metadata
|
|
699
|
+
// (AGL-1711). Hoisted so the Stripe param and the metadata cannot drift.
|
|
700
|
+
// THE SESSION CHARGES `amountCents`, EXACTLY (AGL-2159).
|
|
701
|
+
//
|
|
702
|
+
// A Stripe line is `quantity` units at ONE integer `unit_amount`, so it can
|
|
703
|
+
// only express totals divisible by `quantity` — and a percentage coupon
|
|
704
|
+
// does not respect that. This used to be
|
|
705
|
+
// `Math.round(amountCents / quantity)` multiplied back by `quantity`, which
|
|
706
|
+
// missed `amountCents` by up to `quantity / 2` cents in either direction:
|
|
707
|
+
// the shopper was charged something the storefront never quoted, and the
|
|
708
|
+
// fee and the manual tax were computed on a base nothing collected.
|
|
709
|
+
// Rounding UP was worse still — `discountCents` below is
|
|
710
|
+
// `list - charged` under a `Math.max(0, …)`, so a charge that met or passed
|
|
711
|
+
// the list price clamped it to ZERO and the order recorded no discount on a
|
|
712
|
+
// sale the shopper had applied a coupon to. On a 99-unit order of a 50¢
|
|
713
|
+
// item with 1% off that is not a cent of drift: the shopper paid the full
|
|
714
|
+
// $49.50 and the promotion vanished from the merchant's own record.
|
|
715
|
+
//
|
|
716
|
+
// So when the total does not divide evenly, the line becomes ONE unit at
|
|
717
|
+
// the exact total, with the count moved into the display name. The
|
|
718
|
+
// shopper's real quantity was never carried by this field anyway —
|
|
719
|
+
// `metadata[quantity]` is what `computeBuyNowOrder` builds the order's line
|
|
720
|
+
// item from, and it is untouched — so nothing downstream can tell the
|
|
721
|
+
// difference except that the arithmetic now closes.
|
|
722
|
+
//
|
|
723
|
+
// The even case (every sale with no coupon, and most with one) keeps the
|
|
724
|
+
// per-unit shape byte for byte, so the ordinary Stripe checkout page is
|
|
725
|
+
// unchanged.
|
|
726
|
+
const evenUnitAmountCents = Math.floor(amountCents / quantity);
|
|
727
|
+
const remainderCents = amountCents - evenUnitAmountCents * quantity;
|
|
728
|
+
const chargedQuantity = remainderCents === 0 ? quantity : 1;
|
|
729
|
+
const chargedUnitAmountCents = remainderCents === 0 ? evenUnitAmountCents : amountCents;
|
|
730
|
+
const chargedLineName = `${String((_product_name = product.name) != null ? _product_name : 'Product')}${remainderCents === 0 ? '' : ` × ${quantity}`}`.slice(0, 120);
|
|
731
|
+
// What was ACTUALLY discounted, now that the charge and `amountCents` are
|
|
732
|
+
// the same number. `Math.max(0, …)` stays as a guard against a future floor
|
|
733
|
+
// lifting the charge above list; it no longer erases a real discount,
|
|
734
|
+
// because there is no longer a rounding that can push the charge past it.
|
|
735
|
+
const discountCents = Math.max(0, listUnitAmountCents * quantity - amountCents);
|
|
736
|
+
// Subscriptions (AGL-303): recurring prices bill on the platform with
|
|
737
|
+
// a destination transfer + percent fee; the webhook records the sub.
|
|
738
|
+
// `isSubscription` resolved above (AGL-545).
|
|
739
|
+
const params = new URLSearchParams(_extends({
|
|
740
|
+
mode: isSubscription ? 'subscription' : 'payment',
|
|
741
|
+
'line_items[0][quantity]': String(chargedQuantity),
|
|
742
|
+
'line_items[0][price_data][currency]': 'usd',
|
|
743
|
+
'line_items[0][price_data][unit_amount]': String(chargedUnitAmountCents),
|
|
744
|
+
'line_items[0][price_data][product_data][name]': chargedLineName
|
|
745
|
+
}, isSubscription ? _extends({
|
|
746
|
+
'line_items[0][price_data][recurring][interval]': lifted.subscription.interval
|
|
747
|
+
}, lifted.subscription.trialDays ? {
|
|
748
|
+
'subscription_data[trial_period_days]': String(lifted.subscription.trialDays)
|
|
749
|
+
} : {}, {
|
|
750
|
+
'subscription_data[transfer_data][destination]': String(accountId)
|
|
751
|
+
}, subscriptionFeePercent > 0 ? {
|
|
752
|
+
'subscription_data[application_fee_percent]': String(subscriptionFeePercent)
|
|
753
|
+
} : {}, {
|
|
754
|
+
'subscription_data[metadata][type]': 'commerce-subscription',
|
|
755
|
+
'subscription_data[metadata][hostId]': hostId,
|
|
756
|
+
'subscription_data[metadata][productId]': productId
|
|
757
|
+
}, recurringTaxRateId ? {
|
|
758
|
+
'subscription_data[metadata][taxRateId]': recurringTaxRateId,
|
|
759
|
+
'subscription_data[metadata][taxPct]': String(taxPct)
|
|
760
|
+
} : {}) : {}, taxCents > 0 && !isSubscription ? {
|
|
761
|
+
'line_items[1][quantity]': '1',
|
|
762
|
+
'line_items[1][price_data][currency]': 'usd',
|
|
763
|
+
'line_items[1][price_data][unit_amount]': String(taxCents),
|
|
764
|
+
'line_items[1][price_data][product_data][name]': taxLabel.slice(0, 120)
|
|
765
|
+
} : {}, recurringTaxRateId ? {
|
|
766
|
+
'line_items[0][tax_rates][0]': recurringTaxRateId
|
|
767
|
+
} : {}, useStripeTax ? {
|
|
768
|
+
'automatic_tax[enabled]': 'true'
|
|
769
|
+
} : {}, !isSubscription ? CommerceModel.destinationChargeParams({
|
|
770
|
+
accountId: String(accountId),
|
|
771
|
+
feeCents,
|
|
772
|
+
taxOwner: useStripeTax ? 'platform' : 'merchant',
|
|
773
|
+
merchantGoodsCents: amountCents,
|
|
774
|
+
shippingFloorCents: CommerceModel.shippingFloorCents(shippingPlan.options)
|
|
775
|
+
}) : {}, {
|
|
776
|
+
// Substituted by Stripe on redirect — see the note in `cart-checkout.ts`
|
|
777
|
+
// (AGL-1641). The single-product path returns the shopper to the product
|
|
778
|
+
// page, so the `purchase` is reported by the PDP rather than the cart.
|
|
779
|
+
success_url: `${backUrl}${separator}order=success&session_id={CHECKOUT_SESSION_ID}`,
|
|
780
|
+
cancel_url: `${backUrl}${separator}order=canceled`,
|
|
781
|
+
'metadata[type]': isSubscription ? 'commerce-subscription' : 'commerce-order',
|
|
782
|
+
'metadata[hostId]': hostId,
|
|
783
|
+
'metadata[productId]': productId
|
|
784
|
+
}, variantId ? {
|
|
785
|
+
'metadata[variantId]': variantId
|
|
786
|
+
} : {}, {
|
|
787
|
+
'metadata[quantity]': String(quantity),
|
|
788
|
+
'metadata[feeCents]': String(feeCents)
|
|
789
|
+
}, useStripeTax && !isSubscription ? {
|
|
790
|
+
'metadata[transferCents]': String(CommerceModel.platformLiableTransferCents({
|
|
791
|
+
feeCents,
|
|
792
|
+
merchantGoodsCents: amountCents,
|
|
793
|
+
shippingFloorCents: CommerceModel.shippingFloorCents(shippingPlan.options)
|
|
794
|
+
})),
|
|
795
|
+
'metadata[transferShippingCents]': String(CommerceModel.shippingFloorCents(shippingPlan.options))
|
|
796
|
+
} : {}, recurringTaxRateId ? {
|
|
797
|
+
'metadata[taxRateId]': recurringTaxRateId,
|
|
798
|
+
'metadata[taxPct]': String(taxPct)
|
|
799
|
+
} : {}, {
|
|
800
|
+
// The order's decomposition, snapshotted as we compute it (AGL-1711).
|
|
801
|
+
// Two of these three are invisible to Stripe's own `total_details` BY
|
|
802
|
+
// OUR OWN CONSTRUCTION — the manual tax goes over as an ordinary
|
|
803
|
+
// `line_items[1]` product line, and the coupon is priced into
|
|
804
|
+
// `line_items[0]`'s unit amount — so the webhook cannot recover them
|
|
805
|
+
// from the session. Frozen here rather than re-derived from the product
|
|
806
|
+
// doc at webhook time, which a price edit in between would falsify.
|
|
807
|
+
//
|
|
808
|
+
// EXCEPT the tax on a subscription (AGL-1751): there it is a real tax
|
|
809
|
+
// rate, `amount_tax` carries it, and this snapshot must say 0 —
|
|
810
|
+
// `computeBuyNowOrder` SUMS the metadata figure with Stripe's, on the
|
|
811
|
+
// documented ground that the two are mutually exclusive, so carrying
|
|
812
|
+
// both would double-count the tax on the recorded sale.
|
|
813
|
+
'metadata[unitAmountCents]': String(listUnitAmountCents),
|
|
814
|
+
'metadata[taxCents]': String(isSubscription ? 0 : taxCents),
|
|
815
|
+
'metadata[discountCents]': String(discountCents)
|
|
816
|
+
}, appliedCoupon ? {
|
|
817
|
+
'metadata[couponCode]': appliedCoupon
|
|
818
|
+
} : {}, appliedDiscountId ? {
|
|
819
|
+
'metadata[discountId]': appliedDiscountId
|
|
820
|
+
} : {}, couponHoldKey ? {
|
|
821
|
+
'metadata[couponHoldKey]': couponHoldKey
|
|
822
|
+
} : {}, heldStockKey ? {
|
|
823
|
+
'metadata[stockHoldKey]': heldStockKey
|
|
824
|
+
} : {}, {
|
|
825
|
+
// A HALF-HOUR SESSION, AND IT IS THE HOLD'S WHOLE SAFETY ARGUMENT
|
|
826
|
+
// (AGL-2356). Stripe's default is 24 hours, and a hold must outlive any
|
|
827
|
+
// session that can still be paid — so the default would mean one
|
|
828
|
+
// abandoned cart standing on a merchant's last unit for a day, which is
|
|
829
|
+
// exactly the unbounded, invisible UNDER-sell this issue was held back
|
|
830
|
+
// for. Shortening the SESSION is what bounds it; 30 minutes is Stripe's
|
|
831
|
+
// own minimum and the same figure the bookings plugin gives a held room.
|
|
832
|
+
// Sent on subscription sessions too, deliberately: a shopper who has been
|
|
833
|
+
// sitting on a payment page for half an hour is starting again either
|
|
834
|
+
// way, and one rule is one thing to reason about.
|
|
835
|
+
expires_at: String(Math.floor((Date.now() + CommerceModel.CHECKOUT_SESSION_TTL_MS) / 1000))
|
|
836
|
+
}));
|
|
837
|
+
// Both keys or neither (AGL-1720). Stripe will not apply a shipping rate
|
|
838
|
+
// without an address to ship to, and a merchant who configured nothing
|
|
839
|
+
// resolves to no options and so gets neither key — a session byte-identical
|
|
840
|
+
// to the one this handler built before shipping existed on it. Buy-now has
|
|
841
|
+
// never collected an address, so declaring one unconditionally would put a
|
|
842
|
+
// shipping form in front of every shopper on a store that charges none.
|
|
843
|
+
if (shippingPlan.options.length > 0) {
|
|
844
|
+
CommerceModel.appendShippingAddressCollectionParams(params, shippingPlan.countries);
|
|
845
|
+
CommerceModel.appendCheckoutShippingParams(params, // Free shipping is a zeroed rate, never a coupon — see the cart's note
|
|
846
|
+
// for why a session coupon cannot reach `shipping_options` at all.
|
|
847
|
+
(resolvedDiscount == null ? void 0 : resolvedDiscount.freeShipping) === true ? shippingPlan.options.map((option)=>_extends({}, option, {
|
|
848
|
+
amountCents: 0
|
|
849
|
+
})) : shippingPlan.options);
|
|
850
|
+
}
|
|
851
|
+
// The Payment Element (AGL-1944), and the LAST thing done to the params on
|
|
852
|
+
// purpose. Everything above — price, coupon, tax, shipping, the Connect
|
|
853
|
+
// destination, the fee, every metadata key the webhook reads — is computed
|
|
854
|
+
// identically for both checkout paths, and this rewrite touches none of it:
|
|
855
|
+
// it swaps the redirect's URL pair for `ui_mode` + `return_url` and stops.
|
|
856
|
+
// A native session that priced differently from the hosted one would be the
|
|
857
|
+
// under-collection AGL-1944 warns about, and the only defence that actually
|
|
858
|
+
// holds is that there is nowhere for the two to diverge.
|
|
859
|
+
//
|
|
860
|
+
// Off unless the flag is on for this org AND a publishable key exists, so
|
|
861
|
+
// the default remains the redirect — see `resolveNativeCheckoutMode`.
|
|
862
|
+
const nativeMode = await resolveNativeCheckoutMode(String((_ref2 = ownerOrg == null ? void 0 : (_ownerOrg_org2 = ownerOrg.org) == null ? void 0 : _ownerOrg_org2.id) != null ? _ref2 : ''));
|
|
863
|
+
if (nativeMode.native) {
|
|
864
|
+
applyNativeCheckoutParams(params, `${backUrl}${separator}order=success&session_id={CHECKOUT_SESSION_ID}`);
|
|
865
|
+
}
|
|
866
|
+
const response = await fetch('https://api.stripe.com/v1/checkout/sessions', {
|
|
867
|
+
method: 'POST',
|
|
868
|
+
headers: _extends({
|
|
869
|
+
Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,
|
|
870
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
871
|
+
}, stripeKeyHeader('session'), nativeCheckoutStripeHeaders(nativeMode)),
|
|
872
|
+
body: params.toString()
|
|
873
|
+
});
|
|
874
|
+
const session = await response.json();
|
|
875
|
+
// `!session.url` was the liveness check, and a `ui_mode` session HAS no
|
|
876
|
+
// url — so the check moves with the mode rather than staying behind and
|
|
877
|
+
// reading every successful native session as a Stripe failure (AGL-1944).
|
|
878
|
+
const payload = response.ok ? readCheckoutSessionPayload(session, nativeMode) : null;
|
|
879
|
+
if (!payload) {
|
|
880
|
+
console.error('Stripe checkout error', session.error);
|
|
881
|
+
// Nothing was sold — hand the key back so one flaky moment does not
|
|
882
|
+
// lock this product out for the shopper.
|
|
883
|
+
await releaseCouponSlot();
|
|
884
|
+
await releaseStock();
|
|
885
|
+
await claim.release();
|
|
886
|
+
return res.status(502).json({
|
|
887
|
+
error: 'Checkout failed'
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
// THE SESSION NOW EXISTS, SO THE SLOT BELONGS TO IT (AGL-2453). Past this
|
|
891
|
+
// line the hold must survive: the shopper can still pay, and the webhook
|
|
892
|
+
// settles against the reservation. A catch that released it here would
|
|
893
|
+
// leave a paid order whose redemption is never counted — the same
|
|
894
|
+
// over-redemption this fix closes, arriving through the release path.
|
|
895
|
+
couponSlot = null;
|
|
896
|
+
// And so do the units (AGL-2356), for the stronger version of the same
|
|
897
|
+
// reason: releasing here would hand the last unit to the next shopper while
|
|
898
|
+
// this one still holds a payable session, which is the oversell itself.
|
|
899
|
+
stockHold = null;
|
|
900
|
+
await claim.record(200, payload);
|
|
901
|
+
return res.status(200).json(payload);
|
|
902
|
+
} catch (error) {
|
|
903
|
+
console.error(error);
|
|
904
|
+
// Release on the way out so a transient failure does not strand the key
|
|
905
|
+
// (AGL-1691's rule).
|
|
906
|
+
await releaseCouponSlot();
|
|
907
|
+
await releaseStock();
|
|
908
|
+
await (claim == null ? void 0 : claim.release());
|
|
909
|
+
return res.status(500).json({
|
|
910
|
+
error: 'Checkout failed'
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
//# sourceMappingURL=checkout.js.map
|