@aglyn/plugins-commerce 1.0.0-beta.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +10 -0
- package/package.json +57 -0
- package/src/index.d.ts +38 -0
- package/src/index.js +38 -0
- package/src/index.js.map +1 -0
- package/src/lib/components/account.d.ts +31 -0
- package/src/lib/components/account.js +645 -0
- package/src/lib/components/account.js.map +1 -0
- package/src/lib/components/cart.d.ts +38 -0
- package/src/lib/components/cart.js +851 -0
- package/src/lib/components/cart.js.map +1 -0
- package/src/lib/components/commerce-console-page.d.ts +32 -0
- package/src/lib/components/commerce-console-page.js +296 -0
- package/src/lib/components/commerce-console-page.js.map +1 -0
- package/src/lib/components/commerce-console-sections.d.ts +34 -0
- package/src/lib/components/commerce-console-sections.js +62 -0
- package/src/lib/components/commerce-console-sections.js.map +1 -0
- package/src/lib/components/console/catalog-organization-card.component.d.ts +14 -0
- package/src/lib/components/console/catalog-organization-card.component.js +1087 -0
- package/src/lib/components/console/catalog-organization-card.component.js.map +1 -0
- package/src/lib/components/console/commerce-analytics-card.component.d.ts +27 -0
- package/src/lib/components/console/commerce-analytics-card.component.js +310 -0
- package/src/lib/components/console/commerce-analytics-card.component.js.map +1 -0
- package/src/lib/components/console/commerce-glance-card.component.d.ts +20 -0
- package/src/lib/components/console/commerce-glance-card.component.js +340 -0
- package/src/lib/components/console/commerce-glance-card.component.js.map +1 -0
- package/src/lib/components/console/commerce-stat-tile.component.d.ts +37 -0
- package/src/lib/components/console/commerce-stat-tile.component.js +72 -0
- package/src/lib/components/console/commerce-stat-tile.component.js.map +1 -0
- package/src/lib/components/console/discounts-card.component.d.ts +14 -0
- package/src/lib/components/console/discounts-card.component.js +443 -0
- package/src/lib/components/console/discounts-card.component.js.map +1 -0
- package/src/lib/components/console/entitlement-gate.component.d.ts +76 -0
- package/src/lib/components/console/entitlement-gate.component.js +115 -0
- package/src/lib/components/console/entitlement-gate.component.js.map +1 -0
- package/src/lib/components/console/gift-cards-card.component.d.ts +29 -0
- package/src/lib/components/console/gift-cards-card.component.js +379 -0
- package/src/lib/components/console/gift-cards-card.component.js.map +1 -0
- package/src/lib/components/console/host-coupons-card.component.d.ts +14 -0
- package/src/lib/components/console/host-coupons-card.component.js +252 -0
- package/src/lib/components/console/host-coupons-card.component.js.map +1 -0
- package/src/lib/components/console/host-orders-card.component.d.ts +13 -0
- package/src/lib/components/console/host-orders-card.component.js +861 -0
- package/src/lib/components/console/host-orders-card.component.js.map +1 -0
- package/src/lib/components/console/locations-card.component.d.ts +14 -0
- package/src/lib/components/console/locations-card.component.js +256 -0
- package/src/lib/components/console/locations-card.component.js.map +1 -0
- package/src/lib/components/console/member-posts-card.component.d.ts +8 -0
- package/src/lib/components/console/member-posts-card.component.js +283 -0
- package/src/lib/components/console/member-posts-card.component.js.map +1 -0
- package/src/lib/components/console/newest-site-users-card.component.d.ts +36 -0
- package/src/lib/components/console/newest-site-users-card.component.js +126 -0
- package/src/lib/components/console/newest-site-users-card.component.js.map +1 -0
- package/src/lib/components/console/order-detail-dialog.component.d.ts +28 -0
- package/src/lib/components/console/order-detail-dialog.component.js +976 -0
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -0
- package/src/lib/components/console/paid-media.d.ts +127 -0
- package/src/lib/components/console/paid-media.js +397 -0
- package/src/lib/components/console/paid-media.js.map +1 -0
- package/src/lib/components/console/payments-settings-card.component.d.ts +13 -0
- package/src/lib/components/console/payments-settings-card.component.js +295 -0
- package/src/lib/components/console/payments-settings-card.component.js.map +1 -0
- package/src/lib/components/console/pos-page.component.d.ts +13 -0
- package/src/lib/components/console/pos-page.component.js +916 -0
- package/src/lib/components/console/pos-page.component.js.map +1 -0
- package/src/lib/components/console/product-editor-dialog.component.d.ts +32 -0
- package/src/lib/components/console/product-editor-dialog.component.js +1374 -0
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -0
- package/src/lib/components/console/products-hub-card.component.d.ts +16 -0
- package/src/lib/components/console/products-hub-card.component.js +1280 -0
- package/src/lib/components/console/products-hub-card.component.js.map +1 -0
- package/src/lib/components/console/products-hub-zone.component.d.ts +50 -0
- package/src/lib/components/console/products-hub-zone.component.js +200 -0
- package/src/lib/components/console/products-hub-zone.component.js.map +1 -0
- package/src/lib/components/console/recovery-queue-card.component.d.ts +39 -0
- package/src/lib/components/console/recovery-queue-card.component.js +262 -0
- package/src/lib/components/console/recovery-queue-card.component.js.map +1 -0
- package/src/lib/components/console/registers-card.component.d.ts +21 -0
- package/src/lib/components/console/registers-card.component.js +296 -0
- package/src/lib/components/console/registers-card.component.js.map +1 -0
- package/src/lib/components/console/reservations-card.component.d.ts +14 -0
- package/src/lib/components/console/reservations-card.component.js +650 -0
- package/src/lib/components/console/reservations-card.component.js.map +1 -0
- package/src/lib/components/console/reviews-moderation-card.component.d.ts +8 -0
- package/src/lib/components/console/reviews-moderation-card.component.js +235 -0
- package/src/lib/components/console/reviews-moderation-card.component.js.map +1 -0
- package/src/lib/components/console/shipping-settings-card.component.d.ts +38 -0
- package/src/lib/components/console/shipping-settings-card.component.js +560 -0
- package/src/lib/components/console/shipping-settings-card.component.js.map +1 -0
- package/src/lib/components/console/stock-movements-card.component.d.ts +31 -0
- package/src/lib/components/console/stock-movements-card.component.js +333 -0
- package/src/lib/components/console/stock-movements-card.component.js.map +1 -0
- package/src/lib/components/console/store-settings-card.component.d.ts +14 -0
- package/src/lib/components/console/store-settings-card.component.js +300 -0
- package/src/lib/components/console/store-settings-card.component.js.map +1 -0
- package/src/lib/components/console/storefront-tax-summary-card.component.d.ts +73 -0
- package/src/lib/components/console/storefront-tax-summary-card.component.js +408 -0
- package/src/lib/components/console/storefront-tax-summary-card.component.js.map +1 -0
- package/src/lib/components/console/suppliers-card.component.d.ts +8 -0
- package/src/lib/components/console/suppliers-card.component.js +328 -0
- package/src/lib/components/console/suppliers-card.component.js.map +1 -0
- package/src/lib/components/console/tax-settings-card.component.d.ts +14 -0
- package/src/lib/components/console/tax-settings-card.component.js +473 -0
- package/src/lib/components/console/tax-settings-card.component.js.map +1 -0
- package/src/lib/components/gate.d.ts +39 -0
- package/src/lib/components/gate.js +202 -0
- package/src/lib/components/gate.js.map +1 -0
- package/src/lib/components/gated-video.d.ts +40 -0
- package/src/lib/components/gated-video.js +366 -0
- package/src/lib/components/gated-video.js.map +1 -0
- package/src/lib/components/member-feed.d.ts +32 -0
- package/src/lib/components/member-feed.js +198 -0
- package/src/lib/components/member-feed.js.map +1 -0
- package/src/lib/components/member-recovery.d.ts +35 -0
- package/src/lib/components/member-recovery.js +313 -0
- package/src/lib/components/member-recovery.js.map +1 -0
- package/src/lib/components/member-signin.d.ts +35 -0
- package/src/lib/components/member-signin.js +257 -0
- package/src/lib/components/member-signin.js.map +1 -0
- package/src/lib/components/member-signup.d.ts +34 -0
- package/src/lib/components/member-signup.js +287 -0
- package/src/lib/components/member-signup.js.map +1 -0
- package/src/lib/components/newsletter-signup.d.ts +39 -0
- package/src/lib/components/newsletter-signup.js +229 -0
- package/src/lib/components/newsletter-signup.js.map +1 -0
- package/src/lib/components/product-detail.d.ts +38 -0
- package/src/lib/components/product-detail.js +1022 -0
- package/src/lib/components/product-detail.js.map +1 -0
- package/src/lib/components/product-grid.d.ts +67 -0
- package/src/lib/components/product-grid.js +909 -0
- package/src/lib/components/product-grid.js.map +1 -0
- package/src/lib/components/product-reviews.d.ts +32 -0
- package/src/lib/components/product-reviews.js +349 -0
- package/src/lib/components/product-reviews.js.map +1 -0
- package/src/lib/components/related-products.d.ts +33 -0
- package/src/lib/components/related-products.js +232 -0
- package/src/lib/components/related-products.js.map +1 -0
- package/src/lib/components/reservation-widget.d.ts +33 -0
- package/src/lib/components/reservation-widget.js +418 -0
- package/src/lib/components/reservation-widget.js.map +1 -0
- package/src/lib/components/storefront-payment-element-fallback.d.ts +18 -0
- package/src/lib/components/storefront-payment-element-fallback.js +51 -0
- package/src/lib/components/storefront-payment-element-fallback.js.map +1 -0
- package/src/lib/components/storefront-payment-element.d.ts +78 -0
- package/src/lib/components/storefront-payment-element.js +156 -0
- package/src/lib/components/storefront-payment-element.js.map +1 -0
- package/src/lib/components/wishlist.d.ts +46 -0
- package/src/lib/components/wishlist.js +304 -0
- package/src/lib/components/wishlist.js.map +1 -0
- package/src/lib/constants/bundle-common.d.ts +18 -0
- package/src/lib/constants/bundle-common.js +18 -0
- package/src/lib/constants/bundle-common.js.map +1 -0
- package/src/lib/constants/product-filters.d.ts +35 -0
- package/src/lib/constants/product-filters.js +109 -0
- package/src/lib/constants/product-filters.js.map +1 -0
- package/src/lib/model/commerce-cart.d.ts +52 -0
- package/src/lib/model/commerce-cart.js +102 -0
- package/src/lib/model/commerce-cart.js.map +1 -0
- package/src/lib/model/commerce-connect-transfer.d.ts +243 -0
- package/src/lib/model/commerce-connect-transfer.js +231 -0
- package/src/lib/model/commerce-connect-transfer.js.map +1 -0
- package/src/lib/model/commerce-discounts.d.ts +164 -0
- package/src/lib/model/commerce-discounts.js +231 -0
- package/src/lib/model/commerce-discounts.js.map +1 -0
- package/src/lib/model/commerce-dispute.d.ts +124 -0
- package/src/lib/model/commerce-dispute.js +225 -0
- package/src/lib/model/commerce-dispute.js.map +1 -0
- package/src/lib/model/commerce-gift-cards.d.ts +107 -0
- package/src/lib/model/commerce-gift-cards.js +121 -0
- package/src/lib/model/commerce-gift-cards.js.map +1 -0
- package/src/lib/model/commerce-io.d.ts +36 -0
- package/src/lib/model/commerce-io.js +245 -0
- package/src/lib/model/commerce-io.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +1040 -0
- package/src/lib/model/commerce-orders.js +1022 -0
- package/src/lib/model/commerce-orders.js.map +1 -0
- package/src/lib/model/commerce-promotions.d.ts +188 -0
- package/src/lib/model/commerce-promotions.js +199 -0
- package/src/lib/model/commerce-promotions.js.map +1 -0
- package/src/lib/model/commerce-reservations.d.ts +175 -0
- package/src/lib/model/commerce-reservations.js +157 -0
- package/src/lib/model/commerce-reservations.js.map +1 -0
- package/src/lib/model/commerce-shipping.d.ts +316 -0
- package/src/lib/model/commerce-shipping.js +484 -0
- package/src/lib/model/commerce-shipping.js.map +1 -0
- package/src/lib/model/commerce-stock-holds.d.ts +199 -0
- package/src/lib/model/commerce-stock-holds.js +190 -0
- package/src/lib/model/commerce-stock-holds.js.map +1 -0
- package/src/lib/model/commerce-subscription-status.d.ts +67 -0
- package/src/lib/model/commerce-subscription-status.js +76 -0
- package/src/lib/model/commerce-subscription-status.js.map +1 -0
- package/src/lib/model/commerce-tax-decision.d.ts +222 -0
- package/src/lib/model/commerce-tax-decision.js +165 -0
- package/src/lib/model/commerce-tax-decision.js.map +1 -0
- package/src/lib/model/commerce-tax.d.ts +176 -0
- package/src/lib/model/commerce-tax.js +92 -0
- package/src/lib/model/commerce-tax.js.map +1 -0
- package/src/lib/model/commerce.d.ts +551 -0
- package/src/lib/model/commerce.js +739 -0
- package/src/lib/model/commerce.js.map +1 -0
- package/src/lib/model/index.d.ts +40 -0
- package/src/lib/model/index.js +40 -0
- package/src/lib/model/index.js.map +1 -0
- package/src/lib/model/order-figures.d.ts +48 -0
- package/src/lib/model/order-figures.js +74 -0
- package/src/lib/model/order-figures.js.map +1 -0
- package/src/lib/model/plugin-permissions.d.ts +23 -0
- package/src/lib/model/plugin-permissions.js +34 -0
- package/src/lib/model/plugin-permissions.js.map +1 -0
- package/src/lib/model/purchase-analytics.d.ts +186 -0
- package/src/lib/model/purchase-analytics.js +216 -0
- package/src/lib/model/purchase-analytics.js.map +1 -0
- package/src/lib/plugin-config.d.ts +62 -0
- package/src/lib/plugin-config.js +78 -0
- package/src/lib/plugin-config.js.map +1 -0
- package/src/lib/plugin.d.ts +28 -0
- package/src/lib/plugin.js +147 -0
- package/src/lib/plugin.js.map +1 -0
- package/src/lib/server/billing-webhook.d.ts +26 -0
- package/src/lib/server/billing-webhook.js +4011 -0
- package/src/lib/server/billing-webhook.js.map +1 -0
- package/src/lib/server/cancel-order.d.ts +87 -0
- package/src/lib/server/cancel-order.js +368 -0
- package/src/lib/server/cancel-order.js.map +1 -0
- package/src/lib/server/cart-checkout.d.ts +25 -0
- package/src/lib/server/cart-checkout.js +976 -0
- package/src/lib/server/cart-checkout.js.map +1 -0
- package/src/lib/server/cart-cookie.d.ts +32 -0
- package/src/lib/server/cart-cookie.js +77 -0
- package/src/lib/server/cart-cookie.js.map +1 -0
- package/src/lib/server/cart.d.ts +34 -0
- package/src/lib/server/cart.js +181 -0
- package/src/lib/server/cart.js.map +1 -0
- package/src/lib/server/catalog.d.ts +127 -0
- package/src/lib/server/catalog.js +277 -0
- package/src/lib/server/catalog.js.map +1 -0
- package/src/lib/server/checkout.d.ts +36 -0
- package/src/lib/server/checkout.js +915 -0
- package/src/lib/server/checkout.js.map +1 -0
- package/src/lib/server/connect.d.ts +26 -0
- package/src/lib/server/connect.js +238 -0
- package/src/lib/server/connect.js.map +1 -0
- package/src/lib/server/contact-refund.d.ts +117 -0
- package/src/lib/server/contact-refund.js +196 -0
- package/src/lib/server/contact-refund.js.map +1 -0
- package/src/lib/server/download.d.ts +39 -0
- package/src/lib/server/download.js +206 -0
- package/src/lib/server/download.js.map +1 -0
- package/src/lib/server/draft-order.d.ts +26 -0
- package/src/lib/server/draft-order.js +706 -0
- package/src/lib/server/draft-order.js.map +1 -0
- package/src/lib/server/feed.d.ts +24 -0
- package/src/lib/server/feed.js +59 -0
- package/src/lib/server/feed.js.map +1 -0
- package/src/lib/server/fulfill-order.d.ts +97 -0
- package/src/lib/server/fulfill-order.js +235 -0
- package/src/lib/server/fulfill-order.js.map +1 -0
- package/src/lib/server/gate.d.ts +36 -0
- package/src/lib/server/gate.js +112 -0
- package/src/lib/server/gate.js.map +1 -0
- package/src/lib/server/gift-cards.d.ts +42 -0
- package/src/lib/server/gift-cards.js +219 -0
- package/src/lib/server/gift-cards.js.map +1 -0
- package/src/lib/server/low-stock.d.ts +49 -0
- package/src/lib/server/low-stock.js +60 -0
- package/src/lib/server/low-stock.js.map +1 -0
- package/src/lib/server/manual-tax-rate.d.ts +82 -0
- package/src/lib/server/manual-tax-rate.js +113 -0
- package/src/lib/server/manual-tax-rate.js.map +1 -0
- package/src/lib/server/member-feed.d.ts +24 -0
- package/src/lib/server/member-feed.js +72 -0
- package/src/lib/server/member-feed.js.map +1 -0
- package/src/lib/server/member-name-search.d.ts +37 -0
- package/src/lib/server/member-name-search.js +42 -0
- package/src/lib/server/member-name-search.js.map +1 -0
- package/src/lib/server/member-post.d.ts +23 -0
- package/src/lib/server/member-post.js +166 -0
- package/src/lib/server/member-post.js.map +1 -0
- package/src/lib/server/membership-account.d.ts +37 -0
- package/src/lib/server/membership-account.js +148 -0
- package/src/lib/server/membership-account.js.map +1 -0
- package/src/lib/server/membership-admin-password.d.ts +33 -0
- package/src/lib/server/membership-admin-password.js +202 -0
- package/src/lib/server/membership-admin-password.js.map +1 -0
- package/src/lib/server/membership-content.d.ts +23 -0
- package/src/lib/server/membership-content.js +105 -0
- package/src/lib/server/membership-content.js.map +1 -0
- package/src/lib/server/membership-login.d.ts +19 -0
- package/src/lib/server/membership-login.js +137 -0
- package/src/lib/server/membership-login.js.map +1 -0
- package/src/lib/server/membership-logout.d.ts +19 -0
- package/src/lib/server/membership-logout.js +35 -0
- package/src/lib/server/membership-logout.js.map +1 -0
- package/src/lib/server/membership-recover.d.ts +60 -0
- package/src/lib/server/membership-recover.js +244 -0
- package/src/lib/server/membership-recover.js.map +1 -0
- package/src/lib/server/membership-register.d.ts +22 -0
- package/src/lib/server/membership-register.js +241 -0
- package/src/lib/server/membership-register.js.map +1 -0
- package/src/lib/server/membership-reset.d.ts +26 -0
- package/src/lib/server/membership-reset.js +88 -0
- package/src/lib/server/membership-reset.js.map +1 -0
- package/src/lib/server/membership-wishlist.d.ts +23 -0
- package/src/lib/server/membership-wishlist.js +89 -0
- package/src/lib/server/membership-wishlist.js.map +1 -0
- package/src/lib/server/membership.d.ts +96 -0
- package/src/lib/server/membership.js +222 -0
- package/src/lib/server/membership.js.map +1 -0
- package/src/lib/server/native-checkout.d.ts +170 -0
- package/src/lib/server/native-checkout.js +193 -0
- package/src/lib/server/native-checkout.js.map +1 -0
- package/src/lib/server/newsletter.d.ts +23 -0
- package/src/lib/server/newsletter.js +323 -0
- package/src/lib/server/newsletter.js.map +1 -0
- package/src/lib/server/notify-restock.d.ts +22 -0
- package/src/lib/server/notify-restock.js +84 -0
- package/src/lib/server/notify-restock.js.map +1 -0
- package/src/lib/server/order-analytics.d.ts +134 -0
- package/src/lib/server/order-analytics.js +263 -0
- package/src/lib/server/order-analytics.js.map +1 -0
- package/src/lib/server/order-figures.d.ts +33 -0
- package/src/lib/server/order-figures.js +220 -0
- package/src/lib/server/order-figures.js.map +1 -0
- package/src/lib/server/paid-media-delivery.d.ts +173 -0
- package/src/lib/server/paid-media-delivery.js +212 -0
- package/src/lib/server/paid-media-delivery.js.map +1 -0
- package/src/lib/server/pos-order.d.ts +54 -0
- package/src/lib/server/pos-order.js +1137 -0
- package/src/lib/server/pos-order.js.map +1 -0
- package/src/lib/server/process-abandoned.d.ts +54 -0
- package/src/lib/server/process-abandoned.js +222 -0
- package/src/lib/server/process-abandoned.js.map +1 -0
- package/src/lib/server/process-restock.d.ts +42 -0
- package/src/lib/server/process-restock.js +219 -0
- package/src/lib/server/process-restock.js.map +1 -0
- package/src/lib/server/product-card.d.ts +29 -0
- package/src/lib/server/product-card.js +54 -0
- package/src/lib/server/product-card.js.map +1 -0
- package/src/lib/server/product.d.ts +70 -0
- package/src/lib/server/product.js +105 -0
- package/src/lib/server/product.js.map +1 -0
- package/src/lib/server/promotion-hold.d.ts +144 -0
- package/src/lib/server/promotion-hold.js +174 -0
- package/src/lib/server/promotion-hold.js.map +1 -0
- package/src/lib/server/reconcile-stock.d.ts +106 -0
- package/src/lib/server/reconcile-stock.js +349 -0
- package/src/lib/server/reconcile-stock.js.map +1 -0
- package/src/lib/server/refund.d.ts +45 -0
- package/src/lib/server/refund.js +607 -0
- package/src/lib/server/refund.js.map +1 -0
- package/src/lib/server/related.d.ts +24 -0
- package/src/lib/server/related.js +104 -0
- package/src/lib/server/related.js.map +1 -0
- package/src/lib/server/reservation-availability.d.ts +23 -0
- package/src/lib/server/reservation-availability.js +111 -0
- package/src/lib/server/reservation-availability.js.map +1 -0
- package/src/lib/server/reserve-stock.d.ts +156 -0
- package/src/lib/server/reserve-stock.js +146 -0
- package/src/lib/server/reserve-stock.js.map +1 -0
- package/src/lib/server/reserve.d.ts +25 -0
- package/src/lib/server/reserve.js +426 -0
- package/src/lib/server/reserve.js.map +1 -0
- package/src/lib/server/restock-flag.d.ts +202 -0
- package/src/lib/server/restock-flag.js +449 -0
- package/src/lib/server/restock-flag.js.map +1 -0
- package/src/lib/server/reviews.d.ts +51 -0
- package/src/lib/server/reviews.js +132 -0
- package/src/lib/server/reviews.js.map +1 -0
- package/src/lib/server/site-page-enricher.d.ts +42 -0
- package/src/lib/server/site-page-enricher.js +149 -0
- package/src/lib/server/site-page-enricher.js.map +1 -0
- package/src/lib/server/site-page-resolver.d.ts +26 -0
- package/src/lib/server/site-page-resolver.js +223 -0
- package/src/lib/server/site-page-resolver.js.map +1 -0
- package/src/lib/server/stock-hold.d.ts +127 -0
- package/src/lib/server/stock-hold.js +258 -0
- package/src/lib/server/stock-hold.js.map +1 -0
- package/src/lib/server/storefront-tax-record.d.ts +22 -0
- package/src/lib/server/storefront-tax-record.js +218 -0
- package/src/lib/server/storefront-tax-record.js.map +1 -0
- package/src/lib/server/storefront-tax.d.ts +197 -0
- package/src/lib/server/storefront-tax.js +245 -0
- package/src/lib/server/storefront-tax.js.map +1 -0
- package/src/lib/server/stream.d.ts +25 -0
- package/src/lib/server/stream.js +191 -0
- package/src/lib/server/stream.js.map +1 -0
- package/src/lib/server/subscription-portal.d.ts +24 -0
- package/src/lib/server/subscription-portal.js +91 -0
- package/src/lib/server/subscription-portal.js.map +1 -0
- package/src/lib/server/supplier-outbox.d.ts +190 -0
- package/src/lib/server/supplier-outbox.js +342 -0
- package/src/lib/server/supplier-outbox.js.map +1 -0
- package/src/lib/server/supplier-update.d.ts +23 -0
- package/src/lib/server/supplier-update.js +289 -0
- package/src/lib/server/supplier-update.js.map +1 -0
- package/src/lib/server/tax-profile.d.ts +29 -0
- package/src/lib/server/tax-profile.js +40 -0
- package/src/lib/server/tax-profile.js.map +1 -0
- package/src/lib/server.d.ts +32 -0
- package/src/lib/server.js +311 -0
- package/src/lib/server.js.map +1 -0
- package/src/lib/site.d.ts +27 -0
- package/src/lib/site.js +134 -0
- package/src/lib/site.js.map +1 -0
- package/src/lib/utils/escape-html.d.ts +41 -0
- package/src/lib/utils/escape-html.js +41 -0
- package/src/lib/utils/escape-html.js.map +1 -0
- package/src/lib/utils/generate-preset-id.d.ts +19 -0
- package/src/lib/utils/generate-preset-id.js +25 -0
- package/src/lib/utils/generate-preset-id.js.map +1 -0
- package/src/lib/utils/member-continue.d.ts +45 -0
- package/src/lib/utils/member-continue.js +49 -0
- package/src/lib/utils/member-continue.js.map +1 -0
- package/src/lib/utils/use-storefront-purchase-event.d.ts +60 -0
- package/src/lib/utils/use-storefront-purchase-event.js +142 -0
- package/src/lib/utils/use-storefront-purchase-event.js.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/checkout.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { AttemptClaim, PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { claimAttempt, deriveStripeObjectKey } from '@aglyn/aglyn/server'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { connectLinkageIsReady } from '@aglyn/tenant-data-admin/server/stripe-account-mode'\nimport { readActiveMemberSession } from './membership'\nimport { resolveManualTaxRateId } from './manual-tax-rate'\nimport {\n type PromotionSlotHold,\n holdPromotionSlot,\n promotionHoldKey,\n} from './promotion-hold'\nimport { holdStock, stockHoldKey } from './stock-hold'\nimport {\n applyNativeCheckoutParams,\n nativeCheckoutStripeHeaders,\n readCheckoutSessionPayload,\n resolveNativeCheckoutMode,\n} from './native-checkout'\n\n/**\n * Commerce Starter checkout (AGL-90): a site visitor buys a product. The\n * price ALWAYS comes from the host's product doc (never the request), the\n * money goes to the host owner's Connect account (AGL-46 onboarding) as a\n * DESTINATION charge, and the webhook records the order under the host.\n * 501 without Stripe env.\n *\n * Two claims in this comment were stale and are corrected here (AGL-2295),\n * both in the direction that matters:\n *\n * - The fee is NOT \"2%\". It is `resolveTransactionFeePct(org, productType)`\n * — a per-plan, per-product-type ladder with per-org staff overrides,\n * resolved per request. 2% is one cell of sixteen (Starter, physical).\n * - Selling is gated on `commerce`, not on `marketplaceSelling` (AGL-470).\n * They are different entitlements on different plans: `marketplaceSelling`\n * gates the marketplace, and reading this comment as current would gate a\n * storefront on the wrong one.\n */\nexport const checkoutHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n if (!process.env.STRIPE_SECRET_KEY) {\n return res\n .status(501)\n .json({ error: 'Purchases are not configured on this site.' })\n }\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n const productId = String(body.productId ?? '')\n const variantId = String(body.variantId ?? '')\n const quantity = Math.max(\n 1,\n Math.min(99, Math.round(Number(body.quantity ?? 1))),\n )\n const couponCode = String(body.couponCode ?? '')\n .trim()\n .toUpperCase()\n .slice(0, 40)\n // Buyer-chosen billing (AGL-545): a request, never an instruction —\n // the session mode re-resolves from the product doc below.\n const billingRaw = String(body.billing ?? '')\n const billing: CommerceModel.CheckoutBillingChoice | undefined =\n billingRaw === 'once' || billingRaw === 'subscribe' ? billingRaw : undefined\n // One buy-now attempt, minted by the product page (AGL-1697). Node\n // lowercases incoming headers, but read both spellings — the plugin API\n // request type makes no promise about casing.\n const idempotencyKey = String(\n req.headers['idempotency-key'] ?? req.headers['Idempotency-Key'] ?? '',\n ).trim().slice(0, 200)\n if (!hostId || !productId) {\n return res.status(400).json({ error: 'Missing hostId or productId' })\n }\n\n let claim: AttemptClaim | null = null\n /**\n * The coupon redemption slot this attempt reserved (AGL-2453), hoisted so\n * every refusal below the claim — and the catch — can hand it back. A throw\n * after the hold is placed is exactly the case where a slot would otherwise\n * sit invisibly against the merchant's cap until the TTL lapsed.\n */\n let couponSlot: PromotionSlotHold | null = null\n let couponHoldKey = ''\n const releaseCouponSlot = async (): Promise<void> => {\n const slot = couponSlot\n couponSlot = null\n couponHoldKey = ''\n if (slot) await slot.release()\n }\n /**\n * The units this attempt reserved (AGL-2356), hoisted for the same reason the\n * coupon slot is: every refusal below the claim — and the catch — has to hand\n * them back, or an abandoned request stands on a merchant's last unit until\n * the TTL lapses.\n */\n let stockHold: { holdKey: string; release: () => Promise<void> } | null = null\n let heldStockKey = ''\n const releaseStock = async (): Promise<void> => {\n const held = stockHold\n stockHold = null\n heldStockKey = ''\n if (held) await held.release()\n }\n try {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const [hostSnapshot, productSnapshot] = await Promise.all([\n hostRef.get(),\n hostRef.collection('products').doc(productId).get(),\n ])\n if (!hostSnapshot.exists) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n const product = productSnapshot.data() as any\n if (!product || product.deletedAt) {\n return res.status(404).json({ error: 'Unknown product' })\n }\n // Variant pricing (AGL-292): the buyer's selection maps to a variant;\n // absent variantId keeps the legacy default-variant behavior.\n const lifted = CommerceModel.liftLegacyProduct(product)\n const variant = variantId\n ? lifted.variants.find((item) => item.id === variantId)\n : lifted.variants[0]\n if (!variant) return res.status(404).json({ error: 'Unknown variant' })\n const priceUsd = Number(variant.priceUsd ?? 0)\n if (!(priceUsd > 0) || priceUsd > CommerceModel.COMMERCE_MAX_PRICE_USD) {\n return res.status(400).json({ error: 'Product is not purchasable' })\n }\n // Inventory (AGL-281): variant-aware, honoring the product's oversell\n // policy (backorder products keep selling at zero). Enforced here (the\n // block's display is cosmetic) and decremented by the webhook.\n //\n // DELIBERATELY UNCHANGED for subscription sessions. For a PHYSICAL\n // subscription the gate is honest again since AGL-1750: every paid cycle\n // mints an order and decrements the variant (the webhook's invoice\n // branch), so the number this checks now moves as boxes ship. For a\n // digital or service subscription nothing decrements, and the console\n // withdraws the field there (`stockTrackingApplies`, AGL-1744) rather\n // than skipping this gate — it is the one shared by every purchase path,\n // and skipping it for subscription mode would silently RESUME selling\n // for any merchant who set 0 to stop new subscribers.\n if (!CommerceModel.canPurchase(lifted, variant.id, quantity)) {\n return res.status(409).json({ error: 'Sold out' })\n }\n\n // Seller resolution rides the owning org (AGL-238): plan gate from\n // the org doc, Stripe account from the owner's marketplace profile.\n const ownerOrg = await getOrgForHost(hostId)\n const ownerId = ownerOrg?.org?.ownerUid\n if (!ownerId) {\n return res.status(409).json({ error: 'This site cannot sell yet' })\n }\n const ownerProfile = await firestore\n .collection('profiles')\n .doc(String(ownerId))\n .get()\n // Storefront selling is the `commerce` entitlement (Starter+) — not\n // `marketplaceSelling`, which gates the marketplace marketplace (AGL-470).\n if (!Aglyn.checkEntitlement(ownerOrg.org as any, 'commerce')) {\n return res.status(403).json({ error: 'Selling is not enabled' })\n }\n // Session mode (AGL-545): 'subscribe' only takes effect on\n // subscriptionOptional products, 'once' never applies to\n // subscription-only products, and price/interval always come from\n // the product doc — never the request.\n const isSubscription =\n CommerceModel.resolveCheckoutBillingMode(lifted, billing) ===\n 'subscription'\n // Tiered product types (AGL-470): recurring subscriptions and gift\n // cards are Business+ entitlements, checked per sale — the product doc\n // alone must not unlock them. Gated whenever the session ends up in\n // subscription mode (a subscriptionOptional one-time sale is a plain\n // order, so it rides the base `commerce` entitlement).\n if (\n isSubscription &&\n !Aglyn.checkEntitlement(ownerOrg.org as any, 'storefrontSubscriptions')\n ) {\n return res\n .status(403)\n .json({ error: 'Subscription products require a Business plan' })\n }\n // ALREADY SUBSCRIBED? (AGL-1849)\n //\n // The idempotency key closes the RETRY shape: one attempt, one key, one\n // subscription. What it cannot close is a deliberate second submit under a\n // fresh key — `product-detail.tsx` mints a new UUID on every variant /\n // quantity / billing change — which is two RECURRING charges on one card\n // for one product, billed forever until someone notices.\n //\n // The issue filed this as impossible: the storefront buy-now path is\n // anonymous, so there is no buyer to ask about. That was half right. The\n // path admits anonymous buyers, but it is not ONLY anonymous — a signed-in\n // site member's `aglyn_member_{hostId}` cookie is already on this request\n // (same-origin `fetch` sends it, and `PluginApiRequest` exposes\n // `cookies`), and the handler simply never read it. So the guard covers\n // the case it can actually identify and, deliberately, no more.\n //\n // WHAT THIS DOES NOT COVER, stated rather than implied: a logged-out buyer\n // who types any email at Stripe. There is no identity for them before the\n // hosted page, and inventing one — forcing sign-in to buy — is a UX change\n // that belongs to a product decision, not to a duplicate guard. The\n // honest scope is \"a member cannot silently double-subscribe\", not \"nobody\n // can\".\n //\n // Live-status, NOT existence. A member who cancelled must be able to\n // re-subscribe; refusing on \"a subscription row exists\" would lock a\n // returning customer out of paying us, which is the AGL-1715 lesson\n // restated on the tenant side. `isTenantSubscriptionLive` is the shared\n // tenant predicate, the same one `gate.ts` grants content access with.\n if (isSubscription) {\n const session = await readActiveMemberSession(req, hostId)\n const memberEmail =\n session.status === 'active'\n ? String(session.member.get('email') ?? '')\n : ''\n if (memberEmail) {\n // Scoped by email because that is the join `billing-webhook.ts` writes\n // — `customerEmail` comes off Stripe's `customer_details` — and it is\n // the same key `gate.ts` reads. The 10-doc cap matches gate.ts too: a\n // member with more than ten subscriptions to ONE host is not the\n // shape this refuses, and an unbounded scan on a checkout path is a\n // cost with no matching benefit.\n const existing = await hostRef\n .collection('subscriptions')\n .where('customerEmail', '==', memberEmail)\n .limit(10)\n .get()\n const alreadySubscribed = existing.docs.some(\n (docSnapshot) =>\n docSnapshot.get('productId') === productId &&\n CommerceModel.isTenantSubscriptionLive(docSnapshot.get('status')),\n )\n if (alreadySubscribed) {\n // Above the claim, so the key is never burned: this is a\n // deterministic refusal the buyer resolves by cancelling first, and\n // a retry must get the same answer rather than a stale replay.\n return res.status(409).json({\n error:\n 'You already have an active subscription to this product — ' +\n 'manage it from your account.',\n code: 'already_subscribed',\n })\n }\n }\n }\n if (\n product.giftCard &&\n !Aglyn.checkEntitlement(ownerOrg.org as any, 'giftCards')\n ) {\n return res\n .status(403)\n .json({ error: 'Gift cards require a Business plan' })\n }\n const accountId = ownerProfile.get('stripeAccountId')\n if (\n !connectLinkageIsReady(\n {\n accountId,\n chargesEnabled: ownerProfile.get('stripeChargesEnabled'),\n accountLivemode: ownerProfile.get('stripeAccountLivemode'),\n },\n { subject: `checkout host ${hostId}` },\n )\n ) {\n return res\n .status(409)\n .json({ error: 'This site has not enabled payments yet' })\n }\n\n // The list price per unit, before any coupon (AGL-1711). This is what the\n // order's line-item snapshot records, and it is carried in the session\n // metadata so the webhook never has to re-price from the product doc.\n const listUnitAmountCents = Math.round(priceUsd * 100)\n let amountCents = listUnitAmountCents * quantity\n // THE DISCOUNTS HUB, ON THIS PATH TOO.\n //\n // `hosts/{hostId}/discounts` had exactly one reader — `cart-checkout.ts` —\n // so a code from the Discounts card worked in the cart and was rejected as\n // invalid on Buy now. Same goods, same code, two prices depending on which\n // button the shopper pressed, which is the disagreement AGL-1953 already\n // closed for TAX on these same two paths.\n //\n // Resolved through the SAME `resolveDiscount` the cart uses, so one code\n // cannot mean two amounts. The legacy AGL-96 coupons below stay as the\n // fallback for a code the hub does not know, exactly as the cart orders\n // them.\n //\n // Applied by reducing the AMOUNT rather than by minting a session-level\n // Stripe coupon, which is how this path already applies its legacy coupon.\n // That difference is load-bearing: buy-now carries its manual tax as a\n // real `line_items[1]` product line, and a session coupon spreads across\n // every line — so minting one here would discount the tax line too.\n const buyNowDiscountLines = [\n { productId, amountCents: listUnitAmountCents * quantity },\n ]\n const hubDiscounts = await hostRef.collection('discounts').limit(100).get()\n const resolvedDiscount = CommerceModel.resolveDiscount(\n hubDiscounts.docs.map((docSnapshot) => ({\n ...(docSnapshot.data() as CommerceModel.HostDiscount),\n $id: docSnapshot.id,\n })),\n {\n ...(couponCode ? { code: couponCode } : {}),\n subtotalCents: listUnitAmountCents * quantity,\n productIds: [productId],\n lines: buyNowDiscountLines,\n },\n )\n if (resolvedDiscount?.codeProblem) {\n return res.status(400).json({ error: resolvedDiscount.codeProblem })\n }\n let appliedDiscountId = ''\n if (resolvedDiscount && resolvedDiscount.benefit.kind !== 'none') {\n // Stripe's charge minimum is 50c — never discount below it, the same\n // floor the legacy coupon path applies.\n amountCents = Math.max(50, amountCents - resolvedDiscount.discountCents)\n appliedDiscountId = resolvedDiscount.discountId\n }\n // Coupons (AGL-96): host-defined percent-off codes; invalid codes are\n // a visible 400, never a silent full-price charge. Only consulted when the\n // hub did not already answer for this code.\n let appliedCoupon = ''\n if (couponCode && !resolvedDiscount) {\n const couponSnapshot = await hostRef\n .collection('coupons')\n .doc(couponCode)\n .get()\n const coupon = couponSnapshot.data() as any\n const percentOff = Number(coupon?.percentOff ?? 0)\n const expired =\n coupon?.expiresAtMs != null && Number(coupon.expiresAtMs) < Date.now()\n // A PRE-FILTER, no longer the enforcement (AGL-2453). It answers early so\n // an obviously spent code 400s before a claim is minted, and it counts\n // live holds so it agrees with the transaction below — the authority is\n // `holdPromotionSlot`, which re-asks this same question inside the write.\n const exhausted = CommerceModel.promotionExhausted(coupon, Date.now())\n if (\n !coupon ||\n coupon.enabled === false ||\n expired ||\n !(percentOff > 0 && percentOff <= 100)\n ) {\n return res.status(400).json({ error: 'Invalid or expired coupon' })\n }\n // Named separately from the invalid-code refusal (AGL-2453), and in the\n // same words the transaction below uses. \"Invalid or expired\" is wrong\n // about a code that is perfectly valid and simply spent — and once a live\n // hold can produce this answer, a shopper told their code is invalid\n // would stop trying rather than retry a moment later.\n if (exhausted) {\n return res\n .status(400)\n .json({ error: CommerceModel.PROMOTION_EXHAUSTED_MESSAGE })\n }\n // Stripe's charge minimum is 50¢ — never discount below it. The\n // percentage comes off the ORDER total, matching `cart-checkout.ts`'s\n // `Math.round(itemsCents * percentOff / 100)`, so the same code takes\n // the same money whichever button the shopper pressed. What the SESSION\n // is built from this figure is where the two used to diverge — see the\n // line-item construction below (AGL-2159).\n amountCents = Math.max(\n 50,\n Math.round((amountCents * (100 - percentOff)) / 100),\n )\n appliedCoupon = couponCode\n }\n // The platform fee is NOT computed here (AGL-2152): Stripe's processing\n // cost is charged on the whole card total, so the fee cannot be known\n // until the manual tax and the shipping options are resolved below. The\n // per-plan ladder (AGL-284, AGL-278) is read inside the two resolvers\n // called after the shipping plan — `resolveTransactionFeeCents` for a\n // one-time sale and `resolveSubscriptionFeePercent` for a subscription.\n const referer = String(req.headers.referer ?? '')\n const origin = `https://${req.headers.host}`\n const backUrl = referer.startsWith('http') ? referer : origin\n const separator = backUrl.includes('?') ? '&' : '?'\n\n // Taxes (AGL-285): Stripe Tax when the host opted in; manual mode\n // taxes by store origin here (destination-based arrives with our own\n // checkout, AGL-296). Exempt products skip both.\n const storeSettings = await hostRef\n .collection('settings')\n .doc('store')\n .get()\n const taxSettings = (storeSettings.get('tax') ?? {}) as CommerceModel.TaxSettings\n // AGL-1999: an unset tax mode is NOT a decision, and it must not sell.\n // `mode` was `undefined` for every store whose owner never opened the\n // Taxes card — the default state of every new storefront — and each\n // branch below tested for a specific string, so no branch was taken, no\n // tax was computed, and the shopper was charged an untaxed total. See\n // `commerce-tax-decision.ts` for why refusing beats defaulting to either\n // mode. `mode: 'none'` is the explicit opt-out and passes straight\n // through.\n const taxDecision = CommerceModel.storefrontTaxDecision({\n settings: taxSettings,\n taxExempt: lifted.taxExempt,\n })\n if (taxDecision.kind === 'undecided') {\n // Above the attempt claim, like every other deterministic refusal on\n // this path — there is no claim to release yet.\n return res\n .status(409)\n .json({ error: CommerceModel.STOREFRONT_TAX_UNDECIDED_MESSAGE })\n }\n const taxMisconfigured =\n CommerceModel.storefrontTaxMisconfiguration(taxSettings)\n if (taxMisconfigured) {\n // AGL-2145: the merchant DECIDED to collect and the sale would have\n // charged zero anyway — an unfinished settings card, not a\n // jurisdiction miss. Refused in the same words and the same place as\n // the undecided case above.\n return res.status(409).json({ error: taxMisconfigured })\n }\n const useStripeTax = taxDecision.kind === 'stripe-automatic'\n let taxCents = 0\n let taxLabel = ''\n let taxPct = 0\n if (taxSettings.mode === 'manual' && !lifted.taxExempt) {\n const rate = CommerceModel.resolveTaxRate(taxSettings, taxSettings.origin ?? {})\n if (rate && !taxSettings.pricesIncludeTax) {\n taxCents = CommerceModel.computeTaxCents(amountCents, rate.pct)\n taxLabel = rate.label || `Tax (${rate.pct}%)`\n taxPct = rate.pct\n }\n }\n\n // Recurring manual tax (AGL-1751). The manual tax cannot ride the\n // `line_items[1]` product line on a SUBSCRIPTION session: a non-recurring\n // price in `mode: subscription` is a one-time item, and Stripe bills\n // one-time items on the FIRST invoice only — so the merchant's tax was\n // collected once and then, on every renewal the buyer had been shown a\n // taxed price for, never again. A Stripe Tax Rate attached to the\n // subscription line recurs: Checkout carries `line_items[][tax_rates]`\n // onto the subscription item, every invoice applies it, and the figure\n // lands where the records already look — the session's\n // `total_details.amount_tax` for the sale (AGL-1732) and the invoice's\n // own tax fields for each renewal (AGL-1743). No webhook change.\n //\n // The rate is created on the PLATFORM account — no Stripe-Account header,\n // matching the session call below — because that is where the\n // subscription lives (destination transfer); a connected-account rate id\n // would not resolve. Tax rates are immutable, so one is minted per\n // (percentage, label) and cached under the host; a merchant editing their\n // zone rate misses the cache and mints a fresh object, while the old one\n // stays behind on the subscriptions that already carry it, exactly as it\n // should.\n //\n // A creation failure is a VISIBLE 502, never a fallback: the one-time\n // line is this very bug, and a session with no tax at all would\n // under-collect every invoice silently.\n // Point of no return (AGL-1697): everything past here creates Stripe\n // objects — possibly a Tax Rate, then the session. Server-side a retry\n // looked free, but for `mode: subscription` two completed sessions are\n // two RECURRING subscriptions for the same member and product. The\n // shipping refusals BELOW this line release the claim: they are\n // deterministic asks the shopper answers and retries under the same key,\n // and the released key re-derives the same digests so Stripe replays the\n // tax rate rather than minting a twin (AGL-1714's rule).\n const claimed = await claimAttempt(firestore, {\n kind: 'commerce-checkout',\n scopeId: hostId,\n orgId: String(ownerOrg?.org?.id ?? ''),\n key: idempotencyKey,\n busyMessage: 'This checkout is already being processed',\n })\n if ('replay' in claimed) {\n return res.status(claimed.replay.status).json(claimed.replay.body)\n }\n claim = claimed.claim\n // Derived per OBJECT, never the raw digest (AGL-1714): Stripe\n // parameter-compares a repeated key account-wide, so one digest sent to\n // `/v1/tax_rates` and then `checkout/sessions` would error the second.\n const stripeKeyHeader = (object: string): Record<string, string> => {\n const derived = deriveStripeObjectKey(claimed.claim.stripeKey, object)\n return derived ? { 'Idempotency-Key': derived } : {}\n }\n\n // THE UNITS ARE RESERVED HERE (AGL-2356), not merely read above.\n //\n // The `canPurchase` gate at the top of this handler is a plain `.get()`\n // compared against a number, and the webhook decremented minutes later\n // without ever re-asking. Nothing was written, so there was no document to\n // contend on: every shopper who reached this line while the shelf held one\n // unit passed it, and they all paid. This is the write that makes them\n // serialise — see `model/commerce-stock-holds.ts` for why the shelf count\n // itself deliberately does not move.\n //\n // FIRST among the holds, before the coupon slot below: stock is the\n // likeliest refusal on this path, and reserving a promotion slot for a\n // basket the store cannot fill is a slot held against the merchant's cap\n // for nothing.\n {\n const held = await holdStock({\n firestore,\n hostRef,\n holdKey: stockHoldKey(claimed.claim.stripeKey),\n // Empty when there is nothing to reserve ON — an untracked variant or\n // a `backorder` product, whose merchant chose to sell past zero. See\n // the note in `cart-checkout.ts`: the unlimited case stays off the\n // write path entirely, and `holdStock` opens no transaction for it.\n lines: CommerceModel.stockIsReservable(lifted, variant.id, Date.now())\n ? [{ productId, variantId: variant.id, quantity }]\n : [],\n label: `buy-now ${hostId}/${productId}`,\n })\n if (!held.ok) {\n // Nothing has been minted yet, so the key goes back and the same\n // button works when the units return (AGL-1697). `sold-out` is named\n // separately from an empty shelf: the merchant HAS stock and someone\n // else is in checkout with it, and a shopper told \"sold out\" leaves\n // while a shopper told to wait a few minutes comes back.\n await releaseStock()\n await claim.release()\n return res.status(409).json({\n error:\n held.reason === 'sold-out'\n ? CommerceModel.STOCK_HELD_MESSAGE\n : 'Sold out',\n })\n }\n if (held.holdKey) {\n stockHold = held\n heldStockKey = held.holdKey\n }\n }\n\n // THE COUPON SLOT IS RESERVED HERE (AGL-2453), not merely read above.\n //\n // The check at the top of this handler was a comparison against a plain\n // `.get()`, and the webhook incremented `redemptions` minutes later without\n // re-asking — so a cap of one admitted every shopper who reached checkout\n // before the first of them paid. Placed BELOW the claim because the hold is\n // keyed by the attempt: a retry of one attempt must re-claim its own slot\n // rather than be refused by it.\n //\n // Buy-now runs no automatic-discount path — `resolveDiscount` is the cart's\n // — so the typed coupon is the only counter this handler can burn.\n if (appliedCoupon || appliedDiscountId) {\n const slot = await holdPromotionSlot({\n firestore,\n // The hub discount and the legacy coupon are two counters and only one\n // of them can be in play — the hub answers first and the legacy block\n // is skipped when it did.\n ref: appliedDiscountId\n ? hostRef.collection('discounts').doc(appliedDiscountId)\n : hostRef.collection('coupons').doc(appliedCoupon),\n holdKey: promotionHoldKey(claimed.claim.stripeKey),\n label: appliedDiscountId\n ? `discount ${appliedDiscountId}`\n : `coupon ${appliedCoupon}`,\n })\n if (!slot.ok) {\n // Nothing has been minted yet, so this is retryable and the key goes\n // back (AGL-1697). Exhaustion is named; anything else reads as the\n // same invalid-code refusal the pre-filter above would have given.\n await releaseCouponSlot()\n await releaseStock()\n await claim.release()\n return res.status(400).json({\n error:\n slot.reason === 'exhausted'\n ? CommerceModel.PROMOTION_EXHAUSTED_MESSAGE\n : 'Invalid or expired coupon',\n })\n }\n if (slot.holdKey) {\n couponSlot = slot\n couponHoldKey = slot.holdKey\n }\n }\n\n // Minting and caching now live in `manual-tax-rate.ts` (AGL-1953), which\n // the cart path needs too — two copies of a money-shaped cache is how two\n // paths quietly diverge. Behaviour here is unchanged.\n let recurringTaxRateId = ''\n if (isSubscription && taxCents > 0) {\n recurringTaxRateId =\n (await resolveManualTaxRateId({\n hostRef,\n taxPct,\n taxLabel,\n headers: stripeKeyHeader('tax-rate'),\n })) ?? ''\n if (!recurringTaxRateId) {\n // Nothing was sold — hand the key back so the same button works\n // once Stripe does (AGL-1697).\n await releaseCouponSlot()\n await releaseStock()\n await claim.release()\n return res.status(502).json({ error: 'Checkout failed' })\n }\n }\n\n // Shipping (AGL-1720), from the same settings document tax was just read\n // from. Buy-now declared neither `shipping_address_collection` nor\n // `shipping_options`, so a merchant who configured zones and rates in the\n // console collected them on cart orders (AGL-1707) and nothing at all on\n // buy-now — same merchant, same product, two totals depending on which\n // button the shopper pressed.\n //\n // CONDITIONAL, unlike the cart's, on two things the cart cannot know:\n //\n // - PHYSICAL PRODUCTS ONLY. This is the PDP path for one product, and it\n // sells digital downloads and services too. Asking a shopper buying a\n // PDF for a shipping address, then charging them to ship it, would be\n // a worse bug than the one being fixed. A missing `type` reads as\n // physical, exactly as the fee ladder above already reads it —\n // `liftLegacyProduct` only defaults the field on docs it synthesizes\n // variants for, so a part-migrated doc can reach here without one, and\n // the two reads must not disagree about what that doc is.\n // - ONE-TIME SALES ONLY. Originally because the webhook's\n // `commerce-subscription` branch never touched `total_details`, so\n // shipping charged there would have been real money recorded NOWHERE.\n // Both halves of that are now closed: AGL-1732 decomposed the initial\n // charge onto the subscription document, `amount_shipping` included,\n // and AGL-1743 records every renewal invoice — `shipping_cost` read\n // the same way — so a recurring shipping charge would now land.\n //\n // What remains is the PRODUCT question, and only it: a rate editor\n // written for one-time orders never asks the merchant whether a rate\n // should be re-charged every cycle, and Stripe bills a subscription's\n // one-time line items on the FIRST invoice only, so a shipping rate\n // added as a session line item would be charged once and then silently\n // stop. Answering that is AGL-1720's revisit, not wiring.\n //\n // Weight is per unit TIMES the quantity bought. `weightGrams` alone would\n // quote the lightest tier on a multi-unit order — the same under-collection\n // this issue is about, and adjacent to the hardcoded `quantity: 1` that\n // AGL-1711 just removed from this function.\n //\n // The subtotal fed to the resolver is the PRE-discount list total, matching\n // the cart's, so \"free over $50\" means what the shopper saw it mean.\n //\n // WHICH DESTINATIONS the session may serve is decided with the rates and\n // not after them (AGL-1721): a session that declared every zone's rates\n // while accepting an address in any country let a shopper pick a domestic\n // rate for an international parcel, and Stripe re-checks nothing — a\n // `shipping_rate_data` carries no country. `planCheckoutShipping` pairs\n // the two lists, and refuses rather than guessing.\n const shippingSettings = storeSettings.get('shipping') as\n | CommerceModel.ShippingSettings\n | undefined\n const shipsPhysically =\n (lifted.type ?? 'physical') === 'physical' && !isSubscription\n const shippingPlan = shipsPhysically\n ? CommerceModel.planCheckoutShipping(\n shippingSettings,\n {\n subtotalCents: listUnitAmountCents * quantity,\n totalGrams:\n Math.max(0, Number(variant.weightGrams ?? 0)) * quantity,\n },\n body.shippingCountry,\n )\n : { countries: CommerceModel.CHECKOUT_SHIPPING_COUNTRIES, options: [] }\n if (shippingPlan.refusal === 'destination-required') {\n // A deterministic ask the shopper answers and retries under the same\n // key — released, not burned (AGL-1697). The tax rate possibly minted\n // above replays from its cache (or its derived key) on the retry.\n await releaseCouponSlot()\n await releaseStock()\n await claim.release()\n return res.status(400).json({\n error: 'Choose where you’re shipping to.',\n needsShippingCountry: true,\n shippingCountries: [...CommerceModel.CHECKOUT_SHIPPING_COUNTRIES],\n })\n }\n if (shippingPlan.refusal === 'destination-unserved') {\n const declared = CommerceModel.normalizeCheckoutShippingCountry(\n body.shippingCountry,\n )\n await releaseCouponSlot()\n await releaseStock()\n await claim.release()\n return res.status(409).json({\n error: `This store does not ship to ${\n CommerceModel.CHECKOUT_SHIPPING_COUNTRY_NAMES[declared as string] ??\n 'that country'\n }.`,\n needsShippingCountry: true,\n shippingCountries: [...CommerceModel.CHECKOUT_SHIPPING_COUNTRIES],\n })\n }\n // The merchant priced shipping and no rate of theirs reaches this cart\n // (AGL-2232). Released like every other deterministic refusal on this\n // path — the shopper can change the basket and retry under the same key.\n if (shippingPlan.refusal === 'cart-unpriceable') {\n await releaseCouponSlot()\n await releaseStock()\n await claim.release()\n return res\n .status(409)\n .json({ error: CommerceModel.CART_UNPRICEABLE_SHIPPING_MESSAGE })\n }\n\n // THE PLATFORM FEE (AGL-2152). Formerly `feePct` of the goods and nothing\n // else, which on a DESTINATION charge meant Aglyn paid Stripe 2.9% + 30¢\n // out of its own balance and — on every tier carrying\n // `transactionFeePhysicalPct: 0` — collected nothing back. The advertised\n // platform take is unchanged; the card cost is now passed through at cost\n // so the net per order is the take rather than the take minus Stripe's fee.\n //\n // The charge base is everything Stripe will run the card for: the goods\n // after the coupon, the manual tax line, and the DEAREST shipping option on\n // the session. The dearest, not the cheapest, because the shopper picks\n // after the session is created and `application_fee_amount` is a fixed\n // number Stripe will not let us revise — under-reaching would put the\n // difference back on Aglyn. A store on Stripe Tax is the one residual: its\n // tax is computed by Stripe after this point and cannot be seen from here.\n const shippingCeilingCents = shippingPlan.options.reduce(\n (most, option) => Math.max(most, Math.max(0, Number(option.amountCents ?? 0))),\n 0,\n )\n // A SUBSCRIPTION carries the same pass-through AS A PERCENT (AGL-2655).\n // Stripe offers a Subscription no `application_fee_amount`, only\n // `application_fee_percent`, which cannot carry a fixed 30¢ as cents — so\n // the 30¢ is folded into the rate: `(rate × amount + fixed) ÷ amount`,\n // rounded UP to the two decimals the parameter allows, on top of the\n // plan's own percent. `resolveSubscriptionFeePercent` owns that\n // arithmetic and its rounding. Sized on the recurring goods alone: Stripe\n // applies the percent to the whole invoice, and the paid-invoice\n // correction (`chargeSubscriptionFeeOnItemsOnly`, AGL-2317) scales it\n // back to the items, so the goods are the base the recovery has to come\n // out at cost on.\n //\n // This branch never reaches `payment_intent_data[application_fee_amount]`\n // (the emission below is gated on `!isSubscription`); the percent goes\n // out on `subscription_data` further down, and `feeCents` here is what\n // that percent takes of the opening cycle's goods — the figure\n // `metadata[feeCents]` records as what Aglyn keeps.\n const subscriptionFeePercent = isSubscription\n ? Aglyn.resolveSubscriptionFeePercent(\n ownerOrg.org as any,\n lifted.type ?? 'physical',\n amountCents,\n )\n : 0\n const feeCents = isSubscription\n ? subscriptionFeePercent > 0\n ? Math.max(\n 1,\n Math.round((amountCents * subscriptionFeePercent) / 100),\n )\n : 0\n : Aglyn.resolveTransactionFeeCents(\n ownerOrg.org as any,\n lifted.type ?? 'physical',\n amountCents,\n amountCents + taxCents + shippingCeilingCents,\n )\n\n // The unit price Stripe actually charges — the coupon is priced INTO it\n // rather than sent as a Stripe discount, which is why `amount_discount` is\n // 0 on every buy-now session and the discount has to ride the metadata\n // (AGL-1711). Hoisted so the Stripe param and the metadata cannot drift.\n // THE SESSION CHARGES `amountCents`, EXACTLY (AGL-2159).\n //\n // A Stripe line is `quantity` units at ONE integer `unit_amount`, so it can\n // only express totals divisible by `quantity` — and a percentage coupon\n // does not respect that. This used to be\n // `Math.round(amountCents / quantity)` multiplied back by `quantity`, which\n // missed `amountCents` by up to `quantity / 2` cents in either direction:\n // the shopper was charged something the storefront never quoted, and the\n // fee and the manual tax were computed on a base nothing collected.\n // Rounding UP was worse still — `discountCents` below is\n // `list - charged` under a `Math.max(0, …)`, so a charge that met or passed\n // the list price clamped it to ZERO and the order recorded no discount on a\n // sale the shopper had applied a coupon to. On a 99-unit order of a 50¢\n // item with 1% off that is not a cent of drift: the shopper paid the full\n // $49.50 and the promotion vanished from the merchant's own record.\n //\n // So when the total does not divide evenly, the line becomes ONE unit at\n // the exact total, with the count moved into the display name. The\n // shopper's real quantity was never carried by this field anyway —\n // `metadata[quantity]` is what `computeBuyNowOrder` builds the order's line\n // item from, and it is untouched — so nothing downstream can tell the\n // difference except that the arithmetic now closes.\n //\n // The even case (every sale with no coupon, and most with one) keeps the\n // per-unit shape byte for byte, so the ordinary Stripe checkout page is\n // unchanged.\n const evenUnitAmountCents = Math.floor(amountCents / quantity)\n const remainderCents = amountCents - evenUnitAmountCents * quantity\n const chargedQuantity = remainderCents === 0 ? quantity : 1\n const chargedUnitAmountCents =\n remainderCents === 0 ? evenUnitAmountCents : amountCents\n const chargedLineName = `${String(product.name ?? 'Product')}${\n remainderCents === 0 ? '' : ` × ${quantity}`\n }`.slice(0, 120)\n // What was ACTUALLY discounted, now that the charge and `amountCents` are\n // the same number. `Math.max(0, …)` stays as a guard against a future floor\n // lifting the charge above list; it no longer erases a real discount,\n // because there is no longer a rounding that can push the charge past it.\n const discountCents = Math.max(\n 0,\n listUnitAmountCents * quantity - amountCents,\n )\n\n // Subscriptions (AGL-303): recurring prices bill on the platform with\n // a destination transfer + percent fee; the webhook records the sub.\n // `isSubscription` resolved above (AGL-545).\n const params = new URLSearchParams({\n mode: isSubscription ? 'subscription' : 'payment',\n 'line_items[0][quantity]': String(chargedQuantity),\n 'line_items[0][price_data][currency]': 'usd',\n 'line_items[0][price_data][unit_amount]': String(chargedUnitAmountCents),\n 'line_items[0][price_data][product_data][name]': chargedLineName,\n ...(isSubscription\n ? {\n 'line_items[0][price_data][recurring][interval]':\n lifted.subscription!.interval,\n ...(lifted.subscription!.trialDays\n ? {\n 'subscription_data[trial_period_days]': String(\n lifted.subscription!.trialDays,\n ),\n }\n : {}),\n 'subscription_data[transfer_data][destination]':\n String(accountId),\n // A PERCENT here, and Stripe applies it to the whole invoice —\n // tax and shipping included — because it offers no items-only\n // base for `application_fee_percent` and a Subscription carries\n // no `application_fee_amount` to send instead. The basis is\n // corrected on the paid invoice, where the composition is finally\n // knowable and no event is racing: see\n // `chargeSubscriptionFeeOnItemsOnly` in `billing-webhook.ts`\n // (AGL-2317), which refunds the part taken on tax and shipping\n // back to the merchant. `metadata[feeCents]` below — the\n // items-only figure — is therefore what Aglyn ends up keeping.\n //\n // The percent is the plan's take PLUS the card cost as a rate\n // (AGL-2655), so it is non-zero on every tier, a 0% tier\n // included; the guard only still fires for a recurring amount\n // that resolved to nothing. Stripe rejects a zero here, and\n // omitting the key is how the parameter is left unset. A\n // renewal re-derives the same figure against the merchant's\n // CURRENT plan (AGL-2289), so this is the opening value, not a\n // permanent one.\n ...(subscriptionFeePercent > 0\n ? {\n 'subscription_data[application_fee_percent]':\n String(subscriptionFeePercent),\n }\n : {}),\n 'subscription_data[metadata][type]': 'commerce-subscription',\n 'subscription_data[metadata][hostId]': hostId,\n 'subscription_data[metadata][productId]': productId,\n // WHICH RATE this subscription will bill, on the subscription\n // itself (AGL-2323).\n //\n // The rate above is attached to `line_items[0]` and Checkout\n // carries it onto the subscription item, where it bills correctly\n // and forever. But the id it lands on is then visible to nothing\n // we own: `line_items` is not expanded on a delivered\n // `checkout.session.completed`, and the subscription object the\n // renewal webhook reads does not restate it either.\n //\n // That is what made AGL-2323's second half unanswerable. Tax rates\n // are immutable, so a merchant correcting 8.25% → 6.25% mints a\n // NEW object and every existing subscriber keeps billing the old\n // one — correct behaviour for an immutable object, and invisible\n // drift without this. Naming the rate here makes the live Stripe\n // subscription self-describing, so the set of subscribers a\n // correction would affect can be enumerated before anyone decides\n // whether to touch them.\n //\n // Metadata only. Nothing here changes what is charged.\n ...(recurringTaxRateId\n ? {\n 'subscription_data[metadata][taxRateId]': recurringTaxRateId,\n 'subscription_data[metadata][taxPct]': String(taxPct),\n }\n : {}),\n }\n : {}),\n // One-time sales keep the AGL-1711 construction — an ordinary product\n // line Stripe is never told is tax; subscriptions get the recurring\n // tax rate resolved above instead (AGL-1751). Never both: the rate\n // would tax the opening invoice a second time on top of the line.\n ...(taxCents > 0 && !isSubscription\n ? {\n 'line_items[1][quantity]': '1',\n 'line_items[1][price_data][currency]': 'usd',\n 'line_items[1][price_data][unit_amount]': String(taxCents),\n 'line_items[1][price_data][product_data][name]': taxLabel.slice(\n 0,\n 120,\n ),\n }\n : {}),\n ...(recurringTaxRateId\n ? { 'line_items[0][tax_rates][0]': recurringTaxRateId }\n : {}),\n ...(useStripeTax ? { 'automatic_tax[enabled]': 'true' } : {}),\n // THE CONNECT SPLIT, and on a Stripe Tax sale it is a fixed TRANSFER\n // rather than a fee (AGL-1956). `automatic_tax` computes against Aglyn's\n // registrations and Aglyn remits it (AGL-1904), but\n // `application_fee_amount` makes Stripe transfer `amount_total − fee` —\n // and `amount_total` has that tax inside it, so every taxed sale handed\n // the state's money to the merchant. See\n // `commerce-connect-transfer.ts` for the full mechanism and for why the\n // two knobs are mutually exclusive.\n //\n // Subscriptions are untouched here: a Stripe Subscription has no\n // `transfer_data[amount]` to fix, only `application_fee_percent`, so the\n // recurring path cannot be repaired this way. It is left exactly as it\n // was rather than half-changed — see the AGL-1956 note in\n // `billing-webhook.ts`.\n ...(!isSubscription\n ? CommerceModel.destinationChargeParams({\n accountId: String(accountId),\n feeCents,\n taxOwner: useStripeTax ? 'platform' : 'merchant',\n merchantGoodsCents: amountCents,\n shippingFloorCents:\n CommerceModel.shippingFloorCents(shippingPlan.options),\n })\n : {}),\n // Substituted by Stripe on redirect — see the note in `cart-checkout.ts`\n // (AGL-1641). The single-product path returns the shopper to the product\n // page, so the `purchase` is reported by the PDP rather than the cart.\n success_url: `${backUrl}${separator}order=success&session_id={CHECKOUT_SESSION_ID}`,\n cancel_url: `${backUrl}${separator}order=canceled`,\n 'metadata[type]': isSubscription\n ? 'commerce-subscription'\n : 'commerce-order',\n 'metadata[hostId]': hostId,\n 'metadata[productId]': productId,\n ...(variantId ? { 'metadata[variantId]': variantId } : {}),\n 'metadata[quantity]': String(quantity),\n 'metadata[feeCents]': String(feeCents),\n // WHAT THE MERCHANT WAS ACTUALLY PAID, on the sales where Aglyn fixed it\n // (AGL-1956). Only a Stripe Tax sale carries this: it is the one shape\n // where `transfer.amount` is no longer `charge.amount`, so it is the one\n // shape where the figure cannot be re-derived from the charge. The\n // shipping baseline rides along because the gap between it and the\n // shipping the shopper actually chose is money owed BACK to the\n // merchant, and a number nobody wrote down is a number nobody can repay.\n ...(useStripeTax && !isSubscription\n ? {\n 'metadata[transferCents]': String(\n CommerceModel.platformLiableTransferCents({\n feeCents,\n merchantGoodsCents: amountCents,\n shippingFloorCents: CommerceModel.shippingFloorCents(\n shippingPlan.options,\n ),\n }),\n ),\n 'metadata[transferShippingCents]': String(\n CommerceModel.shippingFloorCents(shippingPlan.options),\n ),\n }\n : {}),\n // The same two facts on the SESSION, because that is the object\n // `billing-webhook.ts` is handed when it writes the subscription record\n // (AGL-2323). Mirrored rather than moved: the subscription copy above is\n // what a Stripe-side enumeration reads, this one is what our own\n // database gets. Absent on every path with no merchant rate to name — a\n // Stripe Tax sale computes against Aglyn's registrations and has no rate\n // object of the merchant's own (AGL-1904), and a one-time sale mints\n // none at all.\n ...(recurringTaxRateId\n ? {\n 'metadata[taxRateId]': recurringTaxRateId,\n 'metadata[taxPct]': String(taxPct),\n }\n : {}),\n // The order's decomposition, snapshotted as we compute it (AGL-1711).\n // Two of these three are invisible to Stripe's own `total_details` BY\n // OUR OWN CONSTRUCTION — the manual tax goes over as an ordinary\n // `line_items[1]` product line, and the coupon is priced into\n // `line_items[0]`'s unit amount — so the webhook cannot recover them\n // from the session. Frozen here rather than re-derived from the product\n // doc at webhook time, which a price edit in between would falsify.\n //\n // EXCEPT the tax on a subscription (AGL-1751): there it is a real tax\n // rate, `amount_tax` carries it, and this snapshot must say 0 —\n // `computeBuyNowOrder` SUMS the metadata figure with Stripe's, on the\n // documented ground that the two are mutually exclusive, so carrying\n // both would double-count the tax on the recorded sale.\n 'metadata[unitAmountCents]': String(listUnitAmountCents),\n 'metadata[taxCents]': String(isSubscription ? 0 : taxCents),\n 'metadata[discountCents]': String(discountCents),\n ...(appliedCoupon ? { 'metadata[couponCode]': appliedCoupon } : {}),\n ...(appliedDiscountId\n ? { 'metadata[discountId]': appliedDiscountId }\n : {}),\n // Set only when a slot was actually reserved (AGL-2453). Its ABSENCE is\n // what tells the webhook to fall back to the unconditional increment,\n // which is right for an uncapped coupon and for a session minted before\n // this deploy — both of which reserved nothing and must still be counted.\n ...(couponHoldKey ? { 'metadata[couponHoldKey]': couponHoldKey } : {}),\n // Set only when units were actually reserved (AGL-2356). Its ABSENCE is\n // what tells the webhook there is nothing to release — right for an\n // untracked or backorder product, and right for a session minted before\n // this deploy, neither of which reserved anything.\n ...(heldStockKey ? { 'metadata[stockHoldKey]': heldStockKey } : {}),\n // A HALF-HOUR SESSION, AND IT IS THE HOLD'S WHOLE SAFETY ARGUMENT\n // (AGL-2356). Stripe's default is 24 hours, and a hold must outlive any\n // session that can still be paid — so the default would mean one\n // abandoned cart standing on a merchant's last unit for a day, which is\n // exactly the unbounded, invisible UNDER-sell this issue was held back\n // for. Shortening the SESSION is what bounds it; 30 minutes is Stripe's\n // own minimum and the same figure the bookings plugin gives a held room.\n // Sent on subscription sessions too, deliberately: a shopper who has been\n // sitting on a payment page for half an hour is starting again either\n // way, and one rule is one thing to reason about.\n expires_at: String(\n Math.floor(\n (Date.now() + CommerceModel.CHECKOUT_SESSION_TTL_MS) / 1000,\n ),\n ),\n })\n // Both keys or neither (AGL-1720). Stripe will not apply a shipping rate\n // without an address to ship to, and a merchant who configured nothing\n // resolves to no options and so gets neither key — a session byte-identical\n // to the one this handler built before shipping existed on it. Buy-now has\n // never collected an address, so declaring one unconditionally would put a\n // shipping form in front of every shopper on a store that charges none.\n if (shippingPlan.options.length > 0) {\n CommerceModel.appendShippingAddressCollectionParams(\n params,\n shippingPlan.countries,\n )\n CommerceModel.appendCheckoutShippingParams(\n params,\n // Free shipping is a zeroed rate, never a coupon — see the cart's note\n // for why a session coupon cannot reach `shipping_options` at all.\n resolvedDiscount?.freeShipping === true\n ? shippingPlan.options.map((option) => ({\n ...option,\n amountCents: 0,\n }))\n : shippingPlan.options,\n )\n }\n // The Payment Element (AGL-1944), and the LAST thing done to the params on\n // purpose. Everything above — price, coupon, tax, shipping, the Connect\n // destination, the fee, every metadata key the webhook reads — is computed\n // identically for both checkout paths, and this rewrite touches none of it:\n // it swaps the redirect's URL pair for `ui_mode` + `return_url` and stops.\n // A native session that priced differently from the hosted one would be the\n // under-collection AGL-1944 warns about, and the only defence that actually\n // holds is that there is nowhere for the two to diverge.\n //\n // Off unless the flag is on for this org AND a publishable key exists, so\n // the default remains the redirect — see `resolveNativeCheckoutMode`.\n const nativeMode = await resolveNativeCheckoutMode(\n String(ownerOrg?.org?.id ?? ''),\n )\n if (nativeMode.native) {\n applyNativeCheckoutParams(\n params,\n `${backUrl}${separator}order=success&session_id={CHECKOUT_SESSION_ID}`,\n )\n }\n const response = await fetch(\n 'https://api.stripe.com/v1/checkout/sessions',\n {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n // The half that costs real money (AGL-1697): Stripe replays the\n // existing session for a repeated key instead of opening a second\n // one — for subscription mode, instead of a second RECURRING\n // subscription — covering the window where the claim is written\n // but the response never arrives.\n ...stripeKeyHeader('session'),\n // Empty on the hosted path, so its request is byte-identical to the\n // one this handler sent before AGL-1944 (AGL-1944).\n ...nativeCheckoutStripeHeaders(nativeMode),\n },\n body: params.toString(),\n },\n )\n const session = (await response.json()) as {\n url?: string\n id?: string\n client_secret?: string\n error?: any\n }\n // `!session.url` was the liveness check, and a `ui_mode` session HAS no\n // url — so the check moves with the mode rather than staying behind and\n // reading every successful native session as a Stripe failure (AGL-1944).\n const payload = response.ok\n ? readCheckoutSessionPayload(session, nativeMode)\n : null\n if (!payload) {\n console.error('Stripe checkout error', session.error)\n // Nothing was sold — hand the key back so one flaky moment does not\n // lock this product out for the shopper.\n await releaseCouponSlot()\n await releaseStock()\n await claim.release()\n return res.status(502).json({ error: 'Checkout failed' })\n }\n // THE SESSION NOW EXISTS, SO THE SLOT BELONGS TO IT (AGL-2453). Past this\n // line the hold must survive: the shopper can still pay, and the webhook\n // settles against the reservation. A catch that released it here would\n // leave a paid order whose redemption is never counted — the same\n // over-redemption this fix closes, arriving through the release path.\n couponSlot = null\n // And so do the units (AGL-2356), for the stronger version of the same\n // reason: releasing here would hand the last unit to the next shopper while\n // this one still holds a payable session, which is the oversell itself.\n stockHold = null\n await claim.record(200, payload)\n return res.status(200).json(payload)\n } catch (error) {\n console.error(error)\n // Release on the way out so a transient failure does not strand the key\n // (AGL-1691's rule).\n await releaseCouponSlot()\n await releaseStock()\n await claim?.release()\n return res.status(500).json({ error: 'Checkout failed' })\n }\n}\n"],"names":["Aglyn","CommerceModel","claimAttempt","deriveStripeObjectKey","firebaseAdmin","getOrgForHost","connectLinkageIsReady","readActiveMemberSession","resolveManualTaxRateId","holdPromotionSlot","promotionHoldKey","holdStock","stockHoldKey","applyNativeCheckoutParams","nativeCheckoutStripeHeaders","readCheckoutSessionPayload","resolveNativeCheckoutMode","checkoutHandler","req","res","body","method","status","json","error","process","env","STRIPE_SECRET_KEY","JSON","parse","hostId","String","productId","variantId","quantity","Math","max","min","round","Number","couponCode","trim","toUpperCase","slice","billingRaw","billing","undefined","idempotencyKey","headers","claim","couponSlot","couponHoldKey","releaseCouponSlot","slot","release","stockHold","heldStockKey","releaseStock","held","variant","storeSettings","lifted","product","ownerOrg","firestore","app","hostRef","collection","doc","hostSnapshot","productSnapshot","Promise","all","get","exists","data","deletedAt","liftLegacyProduct","variants","find","item","id","priceUsd","COMMERCE_MAX_PRICE_USD","canPurchase","ownerId","org","ownerUid","ownerProfile","checkEntitlement","isSubscription","resolveCheckoutBillingMode","session","memberEmail","member","existing","where","limit","alreadySubscribed","docs","some","docSnapshot","isTenantSubscriptionLive","code","giftCard","accountId","chargesEnabled","accountLivemode","subject","listUnitAmountCents","amountCents","buyNowDiscountLines","hubDiscounts","resolvedDiscount","resolveDiscount","map","$id","subtotalCents","productIds","lines","codeProblem","appliedDiscountId","benefit","kind","discountCents","discountId","appliedCoupon","couponSnapshot","coupon","percentOff","expired","expiresAtMs","Date","now","exhausted","promotionExhausted","enabled","PROMOTION_EXHAUSTED_MESSAGE","referer","origin","host","backUrl","startsWith","separator","includes","taxSettings","taxDecision","storefrontTaxDecision","settings","taxExempt","STOREFRONT_TAX_UNDECIDED_MESSAGE","taxMisconfigured","storefrontTaxMisconfiguration","useStripeTax","taxCents","taxLabel","taxPct","mode","rate","resolveTaxRate","pricesIncludeTax","computeTaxCents","pct","label","claimed","scopeId","orgId","key","busyMessage","replay","stripeKeyHeader","object","derived","stripeKey","holdKey","stockIsReservable","ok","reason","STOCK_HELD_MESSAGE","ref","recurringTaxRateId","shippingSettings","shipsPhysically","type","shippingPlan","planCheckoutShipping","totalGrams","weightGrams","shippingCountry","countries","CHECKOUT_SHIPPING_COUNTRIES","options","refusal","needsShippingCountry","shippingCountries","declared","normalizeCheckoutShippingCountry","CHECKOUT_SHIPPING_COUNTRY_NAMES","CART_UNPRICEABLE_SHIPPING_MESSAGE","shippingCeilingCents","reduce","most","option","subscriptionFeePercent","resolveSubscriptionFeePercent","feeCents","resolveTransactionFeeCents","evenUnitAmountCents","floor","remainderCents","chargedQuantity","chargedUnitAmountCents","chargedLineName","name","params","URLSearchParams","subscription","interval","trialDays","destinationChargeParams","taxOwner","merchantGoodsCents","shippingFloorCents","success_url","cancel_url","platformLiableTransferCents","expires_at","CHECKOUT_SESSION_TTL_MS","length","appendShippingAddressCollectionParams","appendCheckoutShippingParams","freeShipping","nativeMode","native","response","fetch","Authorization","toString","payload","console","record"],"mappings":"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"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
import { type PluginApiHandler } from '@aglyn/aglyn/server';
|
|
18
|
+
/**
|
|
19
|
+
* Merchant payments onboarding (AGL-284): Stripe Connect Express for
|
|
20
|
+
* storefront selling. Same account storage the checkout reads
|
|
21
|
+
* (`profiles/{ownerUid}.stripeAccountId`, AGL-46) but without the
|
|
22
|
+
* marketplace-profile prerequisite — commerce merchants aren't necessarily
|
|
23
|
+
* publishers. Only the owning org's owner may onboard (payouts land on
|
|
24
|
+
* their account). 501 without Stripe env.
|
|
25
|
+
*/
|
|
26
|
+
export declare const connectHandler: PluginApiHandler;
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
2
|
+
/**
|
|
3
|
+
* @license
|
|
4
|
+
* Copyright 2026 Aglyn LLC
|
|
5
|
+
*
|
|
6
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
7
|
+
* you may not use this file except in compliance with the License.
|
|
8
|
+
* You may obtain a copy of the License at
|
|
9
|
+
*
|
|
10
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
11
|
+
*
|
|
12
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
13
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
14
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
15
|
+
* See the License for the specific language governing permissions and
|
|
16
|
+
* limitations under the License.
|
|
17
|
+
*/ import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
18
|
+
import { resolvePlatformStripeMode } from "@aglyn/tenant-data-admin/server/stripe-account-mode";
|
|
19
|
+
import { buildRoute, Route } from "@aglyn/aglyn/server";
|
|
20
|
+
async function stripe(path, params) {
|
|
21
|
+
const response = await fetch(`https://api.stripe.com/v1/${path}`, _extends({
|
|
22
|
+
method: params ? 'POST' : 'GET',
|
|
23
|
+
headers: _extends({
|
|
24
|
+
Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`
|
|
25
|
+
}, params && {
|
|
26
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
27
|
+
})
|
|
28
|
+
}, params && {
|
|
29
|
+
body: params.toString()
|
|
30
|
+
}));
|
|
31
|
+
const payload = await response.json();
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
var _ref;
|
|
34
|
+
var _payload_error;
|
|
35
|
+
throw new Error((_ref = payload == null ? void 0 : (_payload_error = payload.error) == null ? void 0 : _payload_error.message) != null ? _ref : `Stripe ${path} failed`);
|
|
36
|
+
}
|
|
37
|
+
return payload;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Merchant payments onboarding (AGL-284): Stripe Connect Express for
|
|
41
|
+
* storefront selling. Same account storage the checkout reads
|
|
42
|
+
* (`profiles/{ownerUid}.stripeAccountId`, AGL-46) but without the
|
|
43
|
+
* marketplace-profile prerequisite — commerce merchants aren't necessarily
|
|
44
|
+
* publishers. Only the owning org's owner may onboard (payouts land on
|
|
45
|
+
* their account). 501 without Stripe env.
|
|
46
|
+
*/ export const connectHandler = async (req, res)=>{
|
|
47
|
+
var _req_headers_authorization, _req_body, _body_hostId;
|
|
48
|
+
if (req.method !== 'POST') {
|
|
49
|
+
return res.status(405).json({
|
|
50
|
+
error: 'Method not allowed'
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
if (!process.env.STRIPE_SECRET_KEY) {
|
|
54
|
+
return res.status(501).json({
|
|
55
|
+
error: 'Payments are not configured (STRIPE_SECRET_KEY).'
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
const authorization = String((_req_headers_authorization = req.headers.authorization) != null ? _req_headers_authorization : '');
|
|
59
|
+
const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined;
|
|
60
|
+
if (!idToken) return res.status(401).json({
|
|
61
|
+
error: 'Unauthenticated'
|
|
62
|
+
});
|
|
63
|
+
const body = typeof req.body === 'string' ? JSON.parse(req.body) : (_req_body = req.body) != null ? _req_body : {};
|
|
64
|
+
const hostId = String((_body_hostId = body.hostId) != null ? _body_hostId : '');
|
|
65
|
+
if (!hostId) return res.status(400).json({
|
|
66
|
+
error: 'Missing hostId'
|
|
67
|
+
});
|
|
68
|
+
try {
|
|
69
|
+
var _req_headers_origin;
|
|
70
|
+
var _ownerOrg_org, _ownerOrg_org1;
|
|
71
|
+
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
72
|
+
const ownerOrg = await getOrgForHost(hostId);
|
|
73
|
+
const ownerUid = ownerOrg == null ? void 0 : (_ownerOrg_org = ownerOrg.org) == null ? void 0 : _ownerOrg_org.ownerUid;
|
|
74
|
+
if (!ownerUid) {
|
|
75
|
+
return res.status(404).json({
|
|
76
|
+
error: 'Unknown site'
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (String(ownerUid) !== decoded.uid) {
|
|
80
|
+
return res.status(403).json({
|
|
81
|
+
error: 'Only the organization owner can set up payments'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
85
|
+
const profileRef = firestore.collection('profiles').doc(decoded.uid);
|
|
86
|
+
const profileSnapshot = await profileRef.get();
|
|
87
|
+
let accountId = profileSnapshot.get('stripeAccountId');
|
|
88
|
+
if (!accountId) {
|
|
89
|
+
const account = await stripe('accounts', new URLSearchParams(_extends({
|
|
90
|
+
type: 'express',
|
|
91
|
+
// Explicit capabilities (AGL-1994, mirroring the marketplace twin
|
|
92
|
+
// hardened by AGL-1547): without them the account's abilities
|
|
93
|
+
// depend on unverified dashboard platform-profile defaults, and
|
|
94
|
+
// the storefront charge is a DESTINATION charge — checkout.ts
|
|
95
|
+
// sends `payment_intent_data[transfer_data][destination]`, which
|
|
96
|
+
// requires the destination to hold `transfers`. A merchant
|
|
97
|
+
// onboarded without it takes money that can never pay out.
|
|
98
|
+
'capabilities[card_payments][requested]': 'true',
|
|
99
|
+
'capabilities[transfers][requested]': 'true',
|
|
100
|
+
'metadata[profileId]': decoded.uid,
|
|
101
|
+
'metadata[purpose]': 'commerce'
|
|
102
|
+
}, decoded.email ? {
|
|
103
|
+
email: decoded.email
|
|
104
|
+
} : {})));
|
|
105
|
+
accountId = account.id;
|
|
106
|
+
await profileRef.set({
|
|
107
|
+
stripeAccountId: accountId,
|
|
108
|
+
stripeChargesEnabled: false
|
|
109
|
+
}, {
|
|
110
|
+
merge: true
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
const account = await stripe(`accounts/${accountId}`);
|
|
114
|
+
const chargesEnabled = Boolean(account == null ? void 0 : account.charges_enabled);
|
|
115
|
+
// WHICH STRIPE WORLD THIS ACCOUNT LIVES IN (AGL-2471).
|
|
116
|
+
//
|
|
117
|
+
// Three production linkages named TEST-mode accounts, one of them with
|
|
118
|
+
// `stripeChargesEnabled: true`, so its storefronts read as payments-ready
|
|
119
|
+
// and could not take a payment. Nothing recorded the mode, and nothing
|
|
120
|
+
// could recover it later: the Account object carries no `livemode` field
|
|
121
|
+
// and an `acct_…` id is the same shape in both worlds.
|
|
122
|
+
//
|
|
123
|
+
// The retrieve above IS the proof. Stripe refuses cross-mode account
|
|
124
|
+
// access outright — "was a test account created with a testmode key" —
|
|
125
|
+
// so an account this key just fetched is in this key's mode, and
|
|
126
|
+
// `resolvePlatformStripeMode` asks Stripe (`/v1/balance.livemode`) what
|
|
127
|
+
// that mode is rather than reading the key string.
|
|
128
|
+
const platformMode = await resolvePlatformStripeMode();
|
|
129
|
+
// Payout readiness rides along (AGL-1994): the sale gate stays on
|
|
130
|
+
// charges_enabled — that is what Stripe checks at charge time — but
|
|
131
|
+
// charges-yes/payouts-no was invisible to the storefront, because
|
|
132
|
+
// commerce wrote only `stripeChargesEnabled`. Recording
|
|
133
|
+
// `payouts_enabled` is what lets a console surface warn a merchant
|
|
134
|
+
// BEFORE the first sale strands funds in a Connect account that
|
|
135
|
+
// cannot pay them out.
|
|
136
|
+
const payoutsEnabled = Boolean(account == null ? void 0 : account.payouts_enabled);
|
|
137
|
+
await profileRef.set({
|
|
138
|
+
stripeChargesEnabled: chargesEnabled,
|
|
139
|
+
stripePayoutsEnabled: payoutsEnabled,
|
|
140
|
+
// ALWAYS written, and `null` when Stripe would not say (AGL-2471).
|
|
141
|
+
// Writing unconditionally is what makes re-onboarding safe: a fresh
|
|
142
|
+
// `stripeAccountId` on a document that still carried the PREVIOUS
|
|
143
|
+
// account's verdict would otherwise inherit it. `null` is not a
|
|
144
|
+
// boolean, so the gate reads it as `mode-unverified` and refuses —
|
|
145
|
+
// never a fabricated `true`.
|
|
146
|
+
stripeAccountLivemode: platformMode ? platformMode === 'live' : null
|
|
147
|
+
}, {
|
|
148
|
+
merge: true
|
|
149
|
+
});
|
|
150
|
+
const origin = (_req_headers_origin = req.headers.origin) != null ? _req_headers_origin : `https://${req.headers.host}`;
|
|
151
|
+
// Stripe bakes these into the onboarding link, so they have to be real
|
|
152
|
+
// console paths. They were `/{hostDocId}/products` — the pre-AGL-621/622
|
|
153
|
+
// shape — so finishing Connect onboarding dropped the seller on a 404
|
|
154
|
+
// (AGL-685). Console products is the plugin route under the org slug and
|
|
155
|
+
// the host SUBDOMAIN, both of which have to be resolved here; the client
|
|
156
|
+
// hook (useConsoleHostRoute) is not available to server code.
|
|
157
|
+
const index = await firebaseAdmin.app().firestore().collection('hostIndex').doc(hostId).get();
|
|
158
|
+
const subdomain = index.get('subdomain');
|
|
159
|
+
const orgSlug = ownerOrg == null ? void 0 : (_ownerOrg_org1 = ownerOrg.org) == null ? void 0 : _ownerOrg_org1.slug;
|
|
160
|
+
// No slug/subdomain means no route to send them to; Stripe requires
|
|
161
|
+
// both URLs, so fall back to the origin root rather than a fabricated
|
|
162
|
+
// path that 404s.
|
|
163
|
+
const productsUrl = orgSlug && subdomain ? `${origin}${buildRoute(Route.HOST_PLUGIN, {
|
|
164
|
+
orgSlug,
|
|
165
|
+
host: subdomain,
|
|
166
|
+
pluginSlug: 'products'
|
|
167
|
+
})}` : origin;
|
|
168
|
+
// A CONNECTED MERCHANT STILL NEEDS A DOOR INTO STRIPE.
|
|
169
|
+
//
|
|
170
|
+
// This route used to return here the moment `charges_enabled` was true,
|
|
171
|
+
// with a status and no link of any kind, and both console cards read the
|
|
172
|
+
// `chargesEnabled` flag before they ever looked for a url. So the one
|
|
173
|
+
// state that most needs Stripe — charges on, payouts NOT released, funds
|
|
174
|
+
// accumulating in a Connect account that cannot pay them out — offered a
|
|
175
|
+
// button labelled "Finish payout setup in Stripe" that raised a toast and
|
|
176
|
+
// went nowhere. An Express account has no password and no direct login, so
|
|
177
|
+
// a link minted here is the merchant's ONLY way in; without one the money
|
|
178
|
+
// is unreachable and no surface says why.
|
|
179
|
+
if (chargesEnabled) {
|
|
180
|
+
if (!payoutsEnabled) {
|
|
181
|
+
// The remediation flow, which is what Stripe points an account with
|
|
182
|
+
// outstanding requirements at. Failures are NOT swallowed: answering
|
|
183
|
+
// 200 with no link is precisely the dead end being closed here, so a
|
|
184
|
+
// mint that fails surfaces as the handler's 502 and the merchant is
|
|
185
|
+
// told to try again rather than left pressing a dead button.
|
|
186
|
+
const fix = await stripe('account_links', new URLSearchParams({
|
|
187
|
+
account: accountId,
|
|
188
|
+
type: 'account_onboarding',
|
|
189
|
+
refresh_url: `${productsUrl}?connect=refresh`,
|
|
190
|
+
return_url: `${productsUrl}?connect=done`
|
|
191
|
+
}));
|
|
192
|
+
return res.status(200).json({
|
|
193
|
+
accountId,
|
|
194
|
+
chargesEnabled: true,
|
|
195
|
+
payoutsEnabled,
|
|
196
|
+
url: fix.url
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// Payouts are flowing, so this is the VIEW: balance, payout schedule,
|
|
200
|
+
// and the reason a payout failed — none of which Aglyn records and none
|
|
201
|
+
// of which the merchant could otherwise reach. Best-effort on purpose:
|
|
202
|
+
// a login link is a convenience, and failing to mint one must never turn
|
|
203
|
+
// a working status check into an error.
|
|
204
|
+
let dashboardUrl;
|
|
205
|
+
try {
|
|
206
|
+
const login = await stripe(`accounts/${accountId}/login_links`, new URLSearchParams());
|
|
207
|
+
dashboardUrl = typeof (login == null ? void 0 : login.url) === 'string' ? login.url : undefined;
|
|
208
|
+
} catch (error) {
|
|
209
|
+
console.error('Stripe Express dashboard link failed', error);
|
|
210
|
+
}
|
|
211
|
+
return res.status(200).json(_extends({
|
|
212
|
+
accountId,
|
|
213
|
+
chargesEnabled: true,
|
|
214
|
+
payoutsEnabled
|
|
215
|
+
}, dashboardUrl ? {
|
|
216
|
+
dashboardUrl
|
|
217
|
+
} : {}));
|
|
218
|
+
}
|
|
219
|
+
const link = await stripe('account_links', new URLSearchParams({
|
|
220
|
+
account: accountId,
|
|
221
|
+
type: 'account_onboarding',
|
|
222
|
+
refresh_url: `${productsUrl}?connect=refresh`,
|
|
223
|
+
return_url: `${productsUrl}?connect=done`
|
|
224
|
+
}));
|
|
225
|
+
return res.status(200).json({
|
|
226
|
+
accountId,
|
|
227
|
+
chargesEnabled: false,
|
|
228
|
+
url: link.url
|
|
229
|
+
});
|
|
230
|
+
} catch (error) {
|
|
231
|
+
console.error(error);
|
|
232
|
+
return res.status(502).json({
|
|
233
|
+
error: 'Payment setup failed'
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/connect.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { resolvePlatformStripeMode } from '@aglyn/tenant-data-admin/server/stripe-account-mode'\nimport { buildRoute, Route, type PluginApiHandler } from '@aglyn/aglyn/server'\n\nasync function stripe(path: string, params?: URLSearchParams) {\n const response = await fetch(`https://api.stripe.com/v1/${path}`, {\n method: params ? 'POST' : 'GET',\n headers: {\n Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`,\n ...(params && {\n 'Content-Type': 'application/x-www-form-urlencoded',\n }),\n },\n ...(params && { body: params.toString() }),\n })\n const payload = await response.json()\n if (!response.ok) {\n throw new Error(payload?.error?.message ?? `Stripe ${path} failed`)\n }\n return payload\n}\n\n/**\n * Merchant payments onboarding (AGL-284): Stripe Connect Express for\n * storefront selling. Same account storage the checkout reads\n * (`profiles/{ownerUid}.stripeAccountId`, AGL-46) but without the\n * marketplace-profile prerequisite — commerce merchants aren't necessarily\n * publishers. Only the owning org's owner may onboard (payouts land on\n * their account). 501 without Stripe env.\n */\nexport const connectHandler: PluginApiHandler = async (req, res) => {\n if (req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n if (!process.env.STRIPE_SECRET_KEY) {\n return res\n .status(501)\n .json({ error: 'Payments are not configured (STRIPE_SECRET_KEY).' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ')\n ? authorization.slice('Bearer '.length)\n : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n const body =\n typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n const hostId = String(body.hostId ?? '')\n if (!hostId) return res.status(400).json({ error: 'Missing hostId' })\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const ownerOrg = await getOrgForHost(hostId)\n const ownerUid = ownerOrg?.org?.ownerUid\n if (!ownerUid) {\n return res.status(404).json({ error: 'Unknown site' })\n }\n if (String(ownerUid) !== decoded.uid) {\n return res\n .status(403)\n .json({ error: 'Only the organization owner can set up payments' })\n }\n const firestore = firebaseAdmin.app().firestore()\n const profileRef = firestore.collection('profiles').doc(decoded.uid)\n const profileSnapshot = await profileRef.get()\n\n let accountId = profileSnapshot.get('stripeAccountId') as\n | string\n | undefined\n if (!accountId) {\n const account = await stripe(\n 'accounts',\n new URLSearchParams({\n type: 'express',\n // Explicit capabilities (AGL-1994, mirroring the marketplace twin\n // hardened by AGL-1547): without them the account's abilities\n // depend on unverified dashboard platform-profile defaults, and\n // the storefront charge is a DESTINATION charge — checkout.ts\n // sends `payment_intent_data[transfer_data][destination]`, which\n // requires the destination to hold `transfers`. A merchant\n // onboarded without it takes money that can never pay out.\n 'capabilities[card_payments][requested]': 'true',\n 'capabilities[transfers][requested]': 'true',\n 'metadata[profileId]': decoded.uid,\n 'metadata[purpose]': 'commerce',\n ...(decoded.email ? { email: decoded.email } : {}),\n }),\n )\n accountId = account.id\n await profileRef.set(\n { stripeAccountId: accountId, stripeChargesEnabled: false },\n { merge: true },\n )\n }\n\n const account = await stripe(`accounts/${accountId}`)\n const chargesEnabled = Boolean(account?.charges_enabled)\n // WHICH STRIPE WORLD THIS ACCOUNT LIVES IN (AGL-2471).\n //\n // Three production linkages named TEST-mode accounts, one of them with\n // `stripeChargesEnabled: true`, so its storefronts read as payments-ready\n // and could not take a payment. Nothing recorded the mode, and nothing\n // could recover it later: the Account object carries no `livemode` field\n // and an `acct_…` id is the same shape in both worlds.\n //\n // The retrieve above IS the proof. Stripe refuses cross-mode account\n // access outright — \"was a test account created with a testmode key\" —\n // so an account this key just fetched is in this key's mode, and\n // `resolvePlatformStripeMode` asks Stripe (`/v1/balance.livemode`) what\n // that mode is rather than reading the key string.\n const platformMode = await resolvePlatformStripeMode()\n // Payout readiness rides along (AGL-1994): the sale gate stays on\n // charges_enabled — that is what Stripe checks at charge time — but\n // charges-yes/payouts-no was invisible to the storefront, because\n // commerce wrote only `stripeChargesEnabled`. Recording\n // `payouts_enabled` is what lets a console surface warn a merchant\n // BEFORE the first sale strands funds in a Connect account that\n // cannot pay them out.\n const payoutsEnabled = Boolean(account?.payouts_enabled)\n await profileRef.set(\n {\n stripeChargesEnabled: chargesEnabled,\n stripePayoutsEnabled: payoutsEnabled,\n // ALWAYS written, and `null` when Stripe would not say (AGL-2471).\n // Writing unconditionally is what makes re-onboarding safe: a fresh\n // `stripeAccountId` on a document that still carried the PREVIOUS\n // account's verdict would otherwise inherit it. `null` is not a\n // boolean, so the gate reads it as `mode-unverified` and refuses —\n // never a fabricated `true`.\n stripeAccountLivemode: platformMode ? platformMode === 'live' : null,\n },\n { merge: true },\n )\n const origin = req.headers.origin ?? `https://${req.headers.host}`\n // Stripe bakes these into the onboarding link, so they have to be real\n // console paths. They were `/{hostDocId}/products` — the pre-AGL-621/622\n // shape — so finishing Connect onboarding dropped the seller on a 404\n // (AGL-685). Console products is the plugin route under the org slug and\n // the host SUBDOMAIN, both of which have to be resolved here; the client\n // hook (useConsoleHostRoute) is not available to server code.\n const index = await firebaseAdmin\n .app()\n .firestore()\n .collection('hostIndex')\n .doc(hostId)\n .get()\n const subdomain = index.get('subdomain') as string | undefined\n const orgSlug = ownerOrg?.org?.slug as string | undefined\n // No slug/subdomain means no route to send them to; Stripe requires\n // both URLs, so fall back to the origin root rather than a fabricated\n // path that 404s.\n const productsUrl =\n orgSlug && subdomain\n ? `${origin}${buildRoute(Route.HOST_PLUGIN, {\n orgSlug,\n host: subdomain,\n pluginSlug: 'products',\n })}`\n : origin\n // A CONNECTED MERCHANT STILL NEEDS A DOOR INTO STRIPE.\n //\n // This route used to return here the moment `charges_enabled` was true,\n // with a status and no link of any kind, and both console cards read the\n // `chargesEnabled` flag before they ever looked for a url. So the one\n // state that most needs Stripe — charges on, payouts NOT released, funds\n // accumulating in a Connect account that cannot pay them out — offered a\n // button labelled \"Finish payout setup in Stripe\" that raised a toast and\n // went nowhere. An Express account has no password and no direct login, so\n // a link minted here is the merchant's ONLY way in; without one the money\n // is unreachable and no surface says why.\n if (chargesEnabled) {\n if (!payoutsEnabled) {\n // The remediation flow, which is what Stripe points an account with\n // outstanding requirements at. Failures are NOT swallowed: answering\n // 200 with no link is precisely the dead end being closed here, so a\n // mint that fails surfaces as the handler's 502 and the merchant is\n // told to try again rather than left pressing a dead button.\n const fix = await stripe(\n 'account_links',\n new URLSearchParams({\n account: accountId as string,\n type: 'account_onboarding',\n refresh_url: `${productsUrl}?connect=refresh`,\n return_url: `${productsUrl}?connect=done`,\n }),\n )\n return res.status(200).json({\n accountId,\n chargesEnabled: true,\n payoutsEnabled,\n url: fix.url,\n })\n }\n // Payouts are flowing, so this is the VIEW: balance, payout schedule,\n // and the reason a payout failed — none of which Aglyn records and none\n // of which the merchant could otherwise reach. Best-effort on purpose:\n // a login link is a convenience, and failing to mint one must never turn\n // a working status check into an error.\n let dashboardUrl: string | undefined\n try {\n const login = await stripe(\n `accounts/${accountId}/login_links`,\n new URLSearchParams(),\n )\n dashboardUrl = typeof login?.url === 'string' ? login.url : undefined\n } catch (error) {\n console.error('Stripe Express dashboard link failed', error)\n }\n return res.status(200).json({\n accountId,\n chargesEnabled: true,\n payoutsEnabled,\n ...(dashboardUrl ? { dashboardUrl } : {}),\n })\n }\n\n const link = await stripe(\n 'account_links',\n new URLSearchParams({\n account: accountId as string,\n type: 'account_onboarding',\n refresh_url: `${productsUrl}?connect=refresh`,\n return_url: `${productsUrl}?connect=done`,\n }),\n )\n return res\n .status(200)\n .json({ accountId, chargesEnabled: false, url: link.url })\n } catch (error) {\n console.error(error)\n return res.status(502).json({ error: 'Payment setup failed' })\n }\n}\n"],"names":["firebaseAdmin","getOrgForHost","resolvePlatformStripeMode","buildRoute","Route","stripe","path","params","response","fetch","method","headers","Authorization","process","env","STRIPE_SECRET_KEY","body","toString","payload","json","ok","Error","error","message","connectHandler","req","res","status","authorization","String","idToken","startsWith","slice","length","undefined","JSON","parse","hostId","ownerOrg","decoded","app","auth","verifyIdToken","ownerUid","org","uid","firestore","profileRef","collection","doc","profileSnapshot","get","accountId","account","URLSearchParams","type","email","id","set","stripeAccountId","stripeChargesEnabled","merge","chargesEnabled","Boolean","charges_enabled","platformMode","payoutsEnabled","payouts_enabled","stripePayoutsEnabled","stripeAccountLivemode","origin","host","index","subdomain","orgSlug","slug","productsUrl","HOST_PLUGIN","pluginSlug","fix","refresh_url","return_url","url","dashboardUrl","login","console","link"],"mappings":"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"}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Money going BACK to a customer, recorded on their contact (AGL-1754).
|
|
19
|
+
*
|
|
20
|
+
* `upsertHostContact` accumulates `ltvCents` and nothing anywhere decremented
|
|
21
|
+
* it, so a customer who bought $500 and returned all of it read identically to
|
|
22
|
+
* one who bought $500 and kept it — and the first surface built on the field
|
|
23
|
+
* (RFM segmentation, a lifetime-value column, a best-customers list) would have
|
|
24
|
+
* ranked a serial returner among the merchant's best customers.
|
|
25
|
+
*
|
|
26
|
+
* WHAT THIS WRITES, AND WHY IT IS NOT A DECREMENT
|
|
27
|
+
*
|
|
28
|
+
* `ltvCents` is left exactly as it is and the reversal is recorded BESIDE it:
|
|
29
|
+
*
|
|
30
|
+
* | field | meaning |
|
|
31
|
+
* | -- | -- |
|
|
32
|
+
* | `ltvCents` | gross money handed over, unchanged (AGL-1748) |
|
|
33
|
+
* | `refundedCents` | gross money handed back |
|
|
34
|
+
* | `ordersCount` | orders placed, unchanged |
|
|
35
|
+
* | `refundedOrdersCount` | of those, how many ended fully reversed |
|
|
36
|
+
* | `lastRefundAtMs` | when money last went back |
|
|
37
|
+
*
|
|
38
|
+
* This is the shape AGL-1747 chose two days earlier for the same question on
|
|
39
|
+
* the orders CSV — `amountUsd` stayed gross under its existing name and
|
|
40
|
+
* `refundedUsd`/`netUsd` were appended — and it is the shape AGL-1748 named
|
|
41
|
+
* when it recorded the gross semantics in `upsert-contact.ts`. Netting in
|
|
42
|
+
* place would make a stored number mean one thing for rows written before this
|
|
43
|
+
* commit and another for rows written after, with nothing on the document to
|
|
44
|
+
* tell the two apart. Two fields cost one number's worth of storage and keep
|
|
45
|
+
* both facts, and a reader that wants the net has `ltvCents - refundedCents`.
|
|
46
|
+
*
|
|
47
|
+
* It also answers "can it go negative?" by moving the question off storage.
|
|
48
|
+
* Both figures are monotonic counters of money that actually moved in one
|
|
49
|
+
* direction, so neither can go negative. The DERIVED net can, for exactly one
|
|
50
|
+
* reason: an order placed before AGL-1748 taught POS, drafts, reservations and
|
|
51
|
+
* bookings to pass `purchaseCents` at all, whose sale was never added and whose
|
|
52
|
+
* refund is subtracted here. That is a true statement about the data, and
|
|
53
|
+
* clamping it away at write time would erase the only evidence that a purchase
|
|
54
|
+
* is missing. AGL-1753 is the backfill that reconciles it, by SETTING both
|
|
55
|
+
* fields from the orders — which already carry `refundedCents`, so whatever a
|
|
56
|
+
* reader decides here costs the rebuild nothing. A merchant-facing reader
|
|
57
|
+
* should show gross and refunded, and clamp only the net it ranks on.
|
|
58
|
+
*
|
|
59
|
+
* `ordersCount` is deliberately NOT decremented. A fully refunded order still
|
|
60
|
+
* happened, and a decrement cannot be undone by a reader that would rather
|
|
61
|
+
* count it — while `refundedOrdersCount` beside it lets RFM's frequency term
|
|
62
|
+
* net the two, or not, at the point of display. Same argument as the amount,
|
|
63
|
+
* one field along.
|
|
64
|
+
*
|
|
65
|
+
* WHEN THE CONTACT IS MISSING, THIS REFUSES — and records the refusal.
|
|
66
|
+
*
|
|
67
|
+
* Refusing is right on AGL-1732's test, "does refusing discard money or work
|
|
68
|
+
* that already happened?", because it does not: the refund is durable on the
|
|
69
|
+
* order (`refundedCents`, the timeline entry, Stripe), and the contact is a
|
|
70
|
+
* derived aggregate that AGL-1753's rebuild reads back out of those same
|
|
71
|
+
* orders. Creating one here would be worse three ways — creation is audience
|
|
72
|
+
* BAND gated, so it hard-bands a Free org (raising the AGL-891 dropped-contacts
|
|
73
|
+
* alert for a record the merchant never had) or meters onto a paid invoice; the
|
|
74
|
+
* contact it created would hold a refund and no purchase, which is the negative
|
|
75
|
+
* lifetime value above with nothing to reconcile it against; and it is the
|
|
76
|
+
* phantom-document shape the `updateExisting` sweep spent yesterday closing.
|
|
77
|
+
*
|
|
78
|
+
* So the write is an `update()`, never `set(…, { merge: true })` — the contact
|
|
79
|
+
* may have been deleted between the query and the write, and a merge-set would
|
|
80
|
+
* resurrect it as a document holding nothing but a refund. Every refusal
|
|
81
|
+
* increments `hosts/{hostId}/counters/contactRefundsUnmatched`, the counter
|
|
82
|
+
* shape `contactsDropped` already uses, so a refund that reached no contact is
|
|
83
|
+
* countable rather than silent.
|
|
84
|
+
*
|
|
85
|
+
* CHARGEBACKS (AGL-1554) are a separate decision and are NOT handled here.
|
|
86
|
+
* This takes `kind` so the handler that eventually answers
|
|
87
|
+
* `charge.dispute.closed` can record a lost dispute through the same writer
|
|
88
|
+
* with the same fields: money reversed is money reversed, whichever door it
|
|
89
|
+
* left by, and only the wording of the timeline entry differs.
|
|
90
|
+
*/
|
|
91
|
+
export declare function recordContactRefund(options: {
|
|
92
|
+
hostId: string;
|
|
93
|
+
/** The order the money went back on — the contact timeline's `refId`. */
|
|
94
|
+
orderId: string;
|
|
95
|
+
/**
|
|
96
|
+
* The buyer as the ORDER recorded them, raw. Normalized here, because that
|
|
97
|
+
* is the key the contacts collection is stored under and orders are not
|
|
98
|
+
* consistent about it (AGL-1753 item 3). No `name` rides along on purpose: a
|
|
99
|
+
* refund is not a capture point and must not rewrite who the contact is.
|
|
100
|
+
*/
|
|
101
|
+
email: unknown;
|
|
102
|
+
/**
|
|
103
|
+
* Cents reversed by THIS attempt, never the order total. `refund.ts` caps
|
|
104
|
+
* each attempt against what is left, so several partials on one order sum to
|
|
105
|
+
* at most the order total; this follows that number rather than recomputing
|
|
106
|
+
* it.
|
|
107
|
+
*/
|
|
108
|
+
amountCents: number;
|
|
109
|
+
/**
|
|
110
|
+
* True only for the write that moved the order INTO `refunded`. Two partials
|
|
111
|
+
* settling concurrently can both observe a fully refunded total, so the
|
|
112
|
+
* caller decides this from the status transition it made, not from the
|
|
113
|
+
* total it read.
|
|
114
|
+
*/
|
|
115
|
+
closedTheOrder: boolean;
|
|
116
|
+
kind?: 'refund' | 'chargeback';
|
|
117
|
+
}): Promise<void>;
|