@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/supplier-outbox.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 PluginJobHostGate } from '@aglyn/aglyn/server'\nimport {\n firebaseAdmin,\n notifyHostManagers,\n} from '@aglyn/tenant-data-admin'\nimport { isDocumentId } from '@aglyn/tenant-data-admin/server/document-id'\nimport { createHmac } from 'crypto'\n\n/**\n * The durable outbox for the dropship supplier notification (AGL-2473).\n *\n * ## What was wrong\n *\n * `billing-webhook.ts` routed a paid dropship order inside\n * `void (async () => { … })()`, and the last thing that block did was\n * `await fetch(supplier.webhookUrl, …)` — an endpoint the MERCHANT typed into\n * a console field, on a host Aglyn neither runs nor monitors. Vercel freezes\n * the container the instant the response is written, so a supplier that is\n * slow, rate-limiting, or simply down is a supplier that is never told. The\n * order is paid, the buyer is thanked, the merchant sees \"Sent to supplier X\"\n * on the timeline, and NOTHING ANYWHERE records that the POST is still owed —\n * so no later pass can find it and no report can list it. It surfaces when the\n * customer asks where their parcel is.\n *\n * ## Why not just `await` it\n *\n * AGL-2161 fixed the other four fire-and-forget sites in that file and\n * deliberately left this one, because awaiting it is worse rather than better:\n * a supplier taking 30s pushes the handler past Stripe's delivery window,\n * Stripe redelivers, and the redelivery re-enters a fan-out whose only\n * idempotency is one `created` flag upstream. A silently dropped notification\n * would have been traded for a DUPLICATED order. `refund.ts` awaits its own\n * sibling and says so in a comment — correctly, because that sibling only\n * touches our own Firestore.\n *\n * So the line this module draws is not \"fast vs slow\" but **ours vs theirs**.\n * Everything the routing block does to Aglyn's own storage — the callback\n * token, the timeline entry, the supplier email through our own provider — is\n * now AWAITED on the response path, exactly like `refund.ts`. The one call to a\n * stranger's server becomes a row here, and the response no longer waits on it.\n *\n * ## The mechanism\n *\n * A Firestore-backed outbox drained by the plugin-job beat that already runs\n * the two commerce recovery passes (`server.ts`). No new infrastructure: the\n * beat is a Cloud Scheduler function POSTing the tenant runner route, and it\n * has carried bounded scans since AGL-2227.\n *\n * * **Idempotent enqueue.** The id is `{hostId}__{orderId}__{supplierId}` and\n * the write is `create()`, which REFUSES an existing document, so a\n * redelivery cannot reset a retry part-way through its backoff. The AGL-498\n * existence guard upstream already returns before this fan-out, but\n * `reconcile-stock.ts` documents at length why one flag over a dozen effects\n * is not something to lean on.\n * * **Backoff.** 1m, 5m, 15m, 1h, 6h — about seven and a half hours of cover,\n * which is a supplier's overnight deploy but not their week off.\n * * **Dead letter.** After {@link SUPPLIER_DELIVERY_MAX_ATTEMPTS} the row is\n * left as `failed`, the ORDER's own timeline is stamped, and host managers\n * get a notification. The merchant reading the order they were paid for can\n * see that it never reached the supplier — the thing the old code could not\n * tell anyone, because it wrote nothing down.\n *\n * ## Two placement decisions, both deliberate\n *\n * **TOP-LEVEL, not `hosts/{hostId}/supplierDeliveries`.** The host catch-all in\n * the rules file is PERMISSIVE by default — a subcollection nobody names in its\n * three exclusion lists is one every site editor can write from the browser —\n * and the row carries the exact JSON body we sign and POST, so an editor-\n * writable row is an editor-chosen payload delivered under the supplier's own\n * shared secret. A top-level collection matches no rule in\n * `cloud/firebase-firestore.rules` at all and is therefore denied to every\n * client by default, the same footing as `platform/pluginJobs`. It also keeps\n * the drain on an ordinary collection query: a collection-GROUP `where` would\n * need a `COLLECTION_GROUP_ASC` fieldOverride, and AGL-1793 is the record of\n * both commerce crons being silently dead in production for want of exactly\n * that. Neither a rules deploy nor an index deploy is owed for this change.\n *\n * **A delivered row is DELETED, not stamped `delivered`.** The body holds the\n * buyer's email and name, and a top-level document is outside the\n * `hosts/{hostId}` recursive delete an erasure request runs. Retaining the\n * happy path would accumulate third-party PII in a place erasure does not\n * reach, to record something the order's own `routed` timeline entry already\n * says. Dead-lettered rows survive, because those are the ones a human has to\n * act on and the merchant has been told about them.\n */\n\n/** Top-level collection. Matches no rule, so no client may read or write it. */\nexport const SUPPLIER_DELIVERY_COLLECTION = 'supplierDeliveries'\n\n/** Attempts before a delivery is dead-lettered. */\nexport const SUPPLIER_DELIVERY_MAX_ATTEMPTS = 6\n\n/**\n * Delay before the next retry, indexed by attempts ALREADY made.\n *\n * The first entry is 60s and the job beat is also 60s, so a supplier that\n * blipped is retried on the next tick rather than a quarter of an hour later —\n * the difference between a parcel that ships today and one that does not.\n */\nexport const SUPPLIER_DELIVERY_BACKOFF_MS: readonly number[] = [\n 60_000,\n 300_000,\n 900_000,\n 3_600_000,\n 21_600_000,\n]\n\n/**\n * Per-attempt timeout. Nothing user-facing waits on this — it exists so one\n * hung supplier cannot hold the shared job beat open for the whole scan.\n */\nexport const SUPPLIER_DELIVERY_TIMEOUT_MS = 10_000\n\n/** Rows examined per drain. Bounded like every other scan on the beat. */\nexport const SUPPLIER_DELIVERY_SCAN_LIMIT = 100\n\nexport type SupplierDeliveryStatus = 'pending' | 'failed'\n\nexport interface SupplierDeliveryRecord {\n status: SupplierDeliveryStatus\n hostId: string\n orderId: string\n supplierId: string\n supplierName: string\n /** The endpoint as configured when the order was routed, for the record. */\n url: string\n /** The exact JSON the supplier is sent; signed at delivery time. */\n body: string\n attempts: number\n nextAttemptAtMs: number\n createdAtMs: number\n lastError?: string\n lastStatus?: number\n failedAtMs?: number\n}\n\nexport interface SupplierDeliveryScanResult {\n scanned: number\n delivered: number\n retried: number\n deadLettered: number\n /** Retired without delivery because the merchant removed the endpoint. */\n cancelled: number\n /** Left pending, with the retry budget intact, because the site is locked. */\n skippedLocked: number\n}\n\n/** gRPC `Status.ALREADY_EXISTS` — what `create()` on a live path carries. */\nconst GRPC_ALREADY_EXISTS = 6\n\n/**\n * The delay owed after `attempts` failures, clamped to the last step.\n *\n * Clamped rather than extrapolated: the tail entry is already six hours, and a\n * dead letter is what comes after it, not a longer and longer silence.\n */\nexport function supplierDeliveryBackoffMs(attempts: number): number {\n const last = SUPPLIER_DELIVERY_BACKOFF_MS.length - 1\n return SUPPLIER_DELIVERY_BACKOFF_MS[\n Math.max(0, Math.min(attempts - 1, last))\n ]\n}\n\n/**\n * The outbox document id for one host/order/supplier triple.\n *\n * Deterministic, so `create()` is the idempotency. `__` rather than `:` or `/`:\n * a slash would make `.doc()` build a PATH instead of an id (AGL-1771), and the\n * separator has to survive a Stripe session id, which already contains `_`.\n * Firestore reserves ids fully matching `__.*__`; this never is, because all\n * three parts are non-empty and the id neither starts nor ends with `__`.\n */\nexport function supplierDeliveryId(\n hostId: string,\n orderId: string,\n supplierId: string,\n): string {\n return `${hostId}__${orderId}__${supplierId}`\n}\n\n/**\n * Records that a supplier POST is owed. One Firestore write, awaited.\n *\n * Returns `'queued'`, `'exists'` when a row for this triple is already in\n * flight, or `'skipped'`. NEVER THROWS: the caller is a paid order's fulfilment\n * path, and a queue that cannot be written must not take the receipt down with\n * it. The refusal is logged, which is strictly more than the old code managed\n * on the same failure.\n */\nexport async function enqueueSupplierDelivery(options: {\n firestore: FirebaseFirestore.Firestore\n hostId: string\n orderId: string\n supplierId: string\n supplierName: string\n url: string\n body: string\n now?: number\n}): Promise<'queued' | 'exists' | 'skipped'> {\n const { firestore, hostId, orderId, supplierId, supplierName, url, body } =\n options\n const now = options.now ?? Date.now()\n const id = supplierDeliveryId(hostId, orderId, supplierId)\n if (!isDocumentId(id)) {\n console.error('Supplier delivery not queued: unusable id', id)\n return 'skipped'\n }\n const record: SupplierDeliveryRecord = {\n status: 'pending',\n hostId,\n orderId,\n supplierId,\n supplierName,\n url,\n body,\n attempts: 0,\n // Due immediately: the next beat picks it up, at most a minute out.\n nextAttemptAtMs: now,\n createdAtMs: now,\n }\n try {\n await firestore\n .collection(SUPPLIER_DELIVERY_COLLECTION)\n .doc(id)\n .create(record as unknown as FirebaseFirestore.DocumentData)\n return 'queued'\n } catch (error) {\n if ((error as { code?: number })?.code === GRPC_ALREADY_EXISTS) {\n return 'exists'\n }\n console.error('Supplier delivery not queued', hostId, orderId, error)\n return 'skipped'\n }\n}\n\n/**\n * Stamps a routing failure on the order the merchant actually reads.\n *\n * `update()`, not a merge-set, for `recordRedemptionOrphan`'s reason: the order\n * exists — the delivery was queued from its own fulfilment — and a merge would\n * mint an order stub on the one path where it does not.\n */\nasync function stampOrderRoutingFailure(\n firestore: FirebaseFirestore.Firestore,\n hostId: string,\n orderId: string,\n detail: string,\n): Promise<void> {\n if (!isDocumentId(hostId) || !isDocumentId(orderId)) return\n await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('orders')\n .doc(orderId)\n .update({\n timeline: firebaseAdmin.firestore.FieldValue.arrayUnion({\n atMs: Date.now(),\n event: 'routing-failed',\n detail,\n }),\n })\n .catch(() => undefined)\n}\n\ntype DeliveryOutcome = 'delivered' | 'retried' | 'dead-lettered' | 'cancelled'\n\n/**\n * One delivery attempt.\n *\n * Never throws: the drain is a shared beat, and one merchant's supplier must\n * not abort every other merchant's queue — the `process-restock.ts` lesson,\n * where a single poisoned row killed back-in-stock email platform-wide.\n */\nasync function attemptSupplierDelivery(\n firestore: FirebaseFirestore.Firestore,\n snapshot: FirebaseFirestore.QueryDocumentSnapshot,\n now: number,\n): Promise<DeliveryOutcome> {\n const data = snapshot.data() as SupplierDeliveryRecord\n const hostId = String(data.hostId ?? '')\n const attempts = Number(data.attempts ?? 0) + 1\n\n const retire = async (): Promise<'cancelled'> => {\n await snapshot.ref.delete().catch(() => undefined)\n return 'cancelled'\n }\n if (!isDocumentId(hostId) || !isDocumentId(String(data.supplierId ?? ''))) {\n return retire()\n }\n\n // The endpoint and the secret are read FRESH rather than frozen into the row:\n // a merchant correcting a typo'd URL corrects what is already queued, and the\n // supplier's shared secret is never copied into a second document.\n const supplierSnapshot = await firestore\n .collection('hosts')\n .doc(hostId)\n .collection('suppliers')\n .doc(String(data.supplierId))\n .get()\n .catch(() => null)\n // A read that FAILED is not a supplier that is gone. Retrying beats retiring\n // a real delivery on a transient Firestore error — the `reconcile-stock.ts`\n // rule that a failed read is not an absent marker.\n if (!supplierSnapshot) {\n return recordFailure(\n firestore,\n snapshot,\n data,\n attempts,\n now,\n 'could not read the supplier record',\n undefined,\n )\n }\n const url = String(supplierSnapshot.get('webhookUrl') ?? '')\n if (!url) {\n // Deliberately NOT a dead letter: the merchant removed the endpoint (or the\n // supplier), which is a decision rather than an outage. Alarming them about\n // a delivery they cancelled is how an alert stops being read.\n return retire()\n }\n const secret = String(supplierSnapshot.get('webhookSecret') ?? '')\n const body = String(data.body ?? '')\n\n let status: number\n try {\n const response = await fetch(url, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n // NO EMPTY-KEY HMAC (AGL-2455). A signature computed under `''` is one\n // any party can compute, and a supplier who also left their secret\n // unset would verify it and believe the delivery was authenticated. An\n // absent header fails their check closed; a forgeable one passes it.\n ...(secret\n ? {\n 'x-aglyn-signature': createHmac('sha256', secret)\n .update(body)\n .digest('hex'),\n }\n : {}),\n },\n body,\n signal: AbortSignal.timeout(SUPPLIER_DELIVERY_TIMEOUT_MS),\n })\n status = Number(response?.status ?? 0)\n if (!response?.ok) {\n return recordFailure(\n firestore,\n snapshot,\n data,\n attempts,\n now,\n `supplier answered HTTP ${status}`,\n status,\n )\n }\n } catch (error) {\n return recordFailure(\n firestore,\n snapshot,\n data,\n attempts,\n now,\n String((error as Error)?.message ?? error),\n undefined,\n )\n }\n\n // Delivered rows are DELETED rather than stamped — see the module note: the\n // body holds the buyer's email and name, and a top-level document is outside\n // the recursive delete an erasure request runs over `hosts/{hostId}`.\n await snapshot.ref.delete().catch(() => undefined)\n return 'delivered'\n}\n\n/** Books one failure: another retry, or the dead letter and its two alarms. */\nasync function recordFailure(\n firestore: FirebaseFirestore.Firestore,\n snapshot: FirebaseFirestore.QueryDocumentSnapshot,\n data: SupplierDeliveryRecord,\n attempts: number,\n now: number,\n reason: string,\n status: number | undefined,\n): Promise<'retried' | 'dead-lettered'> {\n const exhausted = attempts >= SUPPLIER_DELIVERY_MAX_ATTEMPTS\n await snapshot.ref\n .update({\n status: exhausted ? 'failed' : 'pending',\n attempts,\n lastError: reason,\n ...(status ? { lastStatus: status } : {}),\n ...(exhausted\n ? { failedAtMs: now }\n : { nextAttemptAtMs: now + supplierDeliveryBackoffMs(attempts) }),\n })\n .catch(() => undefined)\n if (!exhausted) return 'retried'\n\n const hostId = String(data.hostId ?? '')\n const orderId = String(data.orderId ?? '')\n const supplierName = String(data.supplierName ?? '') || 'the supplier'\n await stampOrderRoutingFailure(\n firestore,\n hostId,\n orderId,\n `Could not reach ${supplierName} after ${attempts} attempts ` +\n `(${reason}). This order was never routed.`,\n )\n // The bell as well as the timeline: nobody opens an order they have no\n // reason to suspect.\n await notifyHostManagers(hostId, {\n type: 'content.order',\n title: `Order not routed to ${supplierName}`,\n body: `Order ${orderId} could not be sent after ${attempts} attempts.`,\n link: `/${hostId}/products`,\n }).catch(() => undefined)\n return 'dead-lettered'\n}\n\n/**\n * Drains the supplier outbox. Bounded, and safe to run twice.\n *\n * `nextAttemptAtMs` is filtered IN MEMORY rather than added to the query, which\n * would need a composite index for one field's worth of selectivity. The cost\n * is that a row still inside its backoff occupies one of the\n * {@link SUPPLIER_DELIVERY_SCAN_LIMIT} slots; with at most\n * {@link SUPPLIER_DELIVERY_MAX_ATTEMPTS} attempts per row the queue always\n * drains rather than growing a permanent head.\n */\nexport async function scanSupplierDeliveries(\n /** The lockdown gate, injected by the caller (AGL-2495). Not optional. */\n gate: PluginJobHostGate,\n now: number = Date.now(),\n): Promise<SupplierDeliveryScanResult> {\n const firestore = firebaseAdmin.app().firestore()\n const pending = await firestore\n .collection(SUPPLIER_DELIVERY_COLLECTION)\n .where('status', '==', 'pending')\n .limit(SUPPLIER_DELIVERY_SCAN_LIMIT)\n .get()\n const result: SupplierDeliveryScanResult = {\n scanned: pending.size,\n delivered: 0,\n retried: 0,\n deadLettered: 0,\n cancelled: 0,\n skippedLocked: 0,\n }\n for (const snapshot of pending.docs) {\n const data = snapshot.data() as SupplierDeliveryRecord\n // LOCKDOWN (AGL-2495). The row is a TOP-LEVEL document carrying its own\n // `hostId` rather than living under `hosts/{hostId}`, so the host is read\n // from the record instead of the parent chain — the one job of the six\n // whose subject is not in its path.\n //\n // An outbound POST to the merchant's supplier is this platform acting in\n // a suspended merchant's name against a third party, which is precisely\n // what a takedown is for. The attempt also mutates the row (`attempts`,\n // `nextAttemptAtMs`, or the dead-letter) and can notify the host's\n // managers.\n //\n // SKIPPED, NOT DROPPED, and the strongest case of the six: the row stays\n // `pending` with its `attempts` count untouched, so a long lock cannot\n // burn a paid order's retry budget and dead-letter it for being locked.\n // The queue drains from where it stood the minute the lock lifts.\n if (await gate.isLocked(String(data?.hostId ?? ''))) {\n result.skippedLocked += 1\n continue\n }\n if (Number(data?.nextAttemptAtMs ?? 0) > now) continue\n const outcome = await attemptSupplierDelivery(\n firestore,\n snapshot,\n now,\n ).catch((error) => {\n console.error(\n 'Supplier delivery attempt failed',\n snapshot.ref.path,\n error,\n )\n return null\n })\n if (outcome === 'delivered') result.delivered += 1\n else if (outcome === 'retried') result.retried += 1\n else if (outcome === 'dead-lettered') result.deadLettered += 1\n else if (outcome === 'cancelled') result.cancelled += 1\n }\n return result\n}\n"],"names":["firebaseAdmin","notifyHostManagers","isDocumentId","createHmac","SUPPLIER_DELIVERY_COLLECTION","SUPPLIER_DELIVERY_MAX_ATTEMPTS","SUPPLIER_DELIVERY_BACKOFF_MS","SUPPLIER_DELIVERY_TIMEOUT_MS","SUPPLIER_DELIVERY_SCAN_LIMIT","GRPC_ALREADY_EXISTS","supplierDeliveryBackoffMs","attempts","last","length","Math","max","min","supplierDeliveryId","hostId","orderId","supplierId","enqueueSupplierDelivery","options","firestore","supplierName","url","body","now","Date","id","console","error","record","status","nextAttemptAtMs","createdAtMs","collection","doc","create","code","stampOrderRoutingFailure","detail","update","timeline","FieldValue","arrayUnion","atMs","event","catch","undefined","attemptSupplierDelivery","snapshot","data","supplierSnapshot","String","Number","retire","ref","delete","get","recordFailure","secret","response","fetch","method","headers","digest","signal","AbortSignal","timeout","ok","message","reason","exhausted","lastError","lastStatus","failedAtMs","type","title","link","scanSupplierDeliveries","gate","app","pending","where","limit","result","scanned","size","delivered","retried","deadLettered","cancelled","skippedLocked","docs","isLocked","outcome","path"],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
* Supplier tracking callback (AGL-289): the routed order carried a
|
|
20
|
+
* per-order token; posting (or GET-ing, for email links) tracking with
|
|
21
|
+
* it fulfills the order. Token-gated — suppliers have no Aglyn account.
|
|
22
|
+
*/
|
|
23
|
+
export declare const supplierUpdateHandler: PluginApiHandler;
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
17
|
+
import * as CommerceModel from "../model/index.js";
|
|
18
|
+
import { firebaseAdmin, notifyHostManagers } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { escapeHtml } from "../utils/escape-html.js";
|
|
20
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
21
|
+
import { tokenSigningSecret } from "./download.js";
|
|
22
|
+
/**
|
|
23
|
+
* Constant-time compare for the per-order supplier token (AGL-2268).
|
|
24
|
+
*
|
|
25
|
+
* `!==` on a secret leaks its prefix through response timing, and this token is
|
|
26
|
+
* a bearer credential for a route with no account behind it — the only thing
|
|
27
|
+
* standing between the open internet and marking a merchant's order shipped.
|
|
28
|
+
* `download.ts` has compared its own token this way since it shipped; this is
|
|
29
|
+
* the sibling that did not.
|
|
30
|
+
*
|
|
31
|
+
* Length is compared first and OUTSIDE the constant-time call, because
|
|
32
|
+
* `timingSafeEqual` throws on a length mismatch. That leaks the token's LENGTH
|
|
33
|
+
* and nothing else, which is the same concession `download.ts` makes.
|
|
34
|
+
*/ function tokenMatches(stored, presented) {
|
|
35
|
+
const expected = typeof stored === 'string' ? stored : '';
|
|
36
|
+
if (!expected || !presented) return false;
|
|
37
|
+
const a = Buffer.from(presented);
|
|
38
|
+
const b = Buffer.from(expected);
|
|
39
|
+
if (a.length !== b.length) return false;
|
|
40
|
+
return timingSafeEqual(new Uint8Array(a), new Uint8Array(b));
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* WHICH supplier is posting (AGL-2455), and the observation that makes the
|
|
44
|
+
* issue's "structural blocker" not structural after all.
|
|
45
|
+
*
|
|
46
|
+
* The order document holds ONE `supplierToken` field, so the issue's reading
|
|
47
|
+
* was that scoping lines per supplier would strand every other supplier's lines
|
|
48
|
+
* permanently: only the last token written could authenticate, and the others
|
|
49
|
+
* would have no valid credential at all. That reading is right about the stored
|
|
50
|
+
* field and wrong about the token.
|
|
51
|
+
*
|
|
52
|
+
* The token is not a random secret that has to be stored to be known. It is
|
|
53
|
+
* `HMAC(hostId:orderId:supplierId)` under `TOKEN_SIGNING_SECRET`
|
|
54
|
+
* (`billing-webhook.ts`), a pure function of three identifiers the order
|
|
55
|
+
* already carries — `supplierId` is stamped onto every line at purchase time.
|
|
56
|
+
* So the expected token for EVERY supplier on the order can be re-derived here
|
|
57
|
+
* and compared, which both identifies the poster and lets every supplier
|
|
58
|
+
* authenticate. No schema change, no migration, no map.
|
|
59
|
+
*
|
|
60
|
+
* Every candidate is compared even after a match, so the work done is a
|
|
61
|
+
* function of the order's supplier count and not of which supplier posted.
|
|
62
|
+
*
|
|
63
|
+
* Returns `null` when nothing matches; the stored-scalar fallback is the
|
|
64
|
+
* caller's, because only it knows whether that fallback is ambiguous.
|
|
65
|
+
*/ function resolvePostingSupplier(hostId, orderId, order, presented) {
|
|
66
|
+
let secret;
|
|
67
|
+
try {
|
|
68
|
+
secret = tokenSigningSecret();
|
|
69
|
+
} catch (unused) {
|
|
70
|
+
// Unset secret: no token could have been MINTED either, so there is nothing
|
|
71
|
+
// to derive. Fall through to the stored scalar rather than throwing a 500
|
|
72
|
+
// at a supplier who cannot do anything about it.
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
let matched = null;
|
|
76
|
+
for (const supplierId of CommerceModel.orderSupplierIds(order)){
|
|
77
|
+
const expected = createHmac('sha256', secret).update(`${hostId}:${orderId}:${supplierId}`).digest('hex').slice(0, 32);
|
|
78
|
+
if (tokenMatches(expected, presented)) matched = supplierId;
|
|
79
|
+
}
|
|
80
|
+
return matched;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Supplier tracking callback (AGL-289): the routed order carried a
|
|
84
|
+
* per-order token; posting (or GET-ing, for email links) tracking with
|
|
85
|
+
* it fulfills the order. Token-gated — suppliers have no Aglyn account.
|
|
86
|
+
*/ export const supplierUpdateHandler = async (req, res)=>{
|
|
87
|
+
var _req_body, _source_hostId, _source_orderId, _source_token, _source_trackingNumber, _source_carrier;
|
|
88
|
+
const source = req.method === 'POST' ? _extends({}, req.query, typeof req.body === 'string' ? JSON.parse(req.body || '{}') : (_req_body = req.body) != null ? _req_body : {}) : req.query;
|
|
89
|
+
const hostId = String((_source_hostId = source.hostId) != null ? _source_hostId : '');
|
|
90
|
+
const orderId = String((_source_orderId = source.orderId) != null ? _source_orderId : '');
|
|
91
|
+
const token = String((_source_token = source.token) != null ? _source_token : '');
|
|
92
|
+
const trackingNumber = String((_source_trackingNumber = source.trackingNumber) != null ? _source_trackingNumber : '').slice(0, 60);
|
|
93
|
+
const carrier = String((_source_carrier = source.carrier) != null ? _source_carrier : '').slice(0, 40);
|
|
94
|
+
if (!hostId || !orderId || !token) {
|
|
95
|
+
return res.status(400).json({
|
|
96
|
+
error: 'Missing hostId, orderId, or token'
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
// Firestore reserves `__…__` ids and `.doc()` throws SYNCHRONOUSLY on one,
|
|
100
|
+
// which fell into the catch below and answered 500 to what is a 400
|
|
101
|
+
// (AGL-2455). `fulfill-order.ts` has had this guard since it shipped; this is
|
|
102
|
+
// the sibling route that did not.
|
|
103
|
+
if (/^__.*__$/.test(hostId) || /^__.*__$/.test(orderId)) {
|
|
104
|
+
return res.status(400).json({
|
|
105
|
+
error: 'Missing hostId, orderId, or token'
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
var _outcome_remaining;
|
|
110
|
+
const firestore = firebaseAdmin.app().firestore();
|
|
111
|
+
const orderRef = firestore.collection('hosts').doc(hostId).collection('orders').doc(orderId);
|
|
112
|
+
// A GET NEVER MOVES THE ORDER (AGL-2268). The supplier email carries this
|
|
113
|
+
// URL, and a GET that fulfils means every link scanner, spam filter,
|
|
114
|
+
// preview generator and browser prefetch on the path between us and the
|
|
115
|
+
// supplier's inbox marks the order shipped — before anything shipped, and
|
|
116
|
+
// with no tracking number on it. The link still works and the email is
|
|
117
|
+
// unchanged: a GET renders a one-button page that POSTs the same
|
|
118
|
+
// parameters, so the supplier's experience is one extra click and the
|
|
119
|
+
// machines that open mail for a living take no action at all.
|
|
120
|
+
if (req.method === 'GET') {
|
|
121
|
+
res.setHeader('Content-Type', 'text/html');
|
|
122
|
+
const field = (name, value)=>`<input type="hidden" name="${name}" value="${escapeHtml(value)}">`;
|
|
123
|
+
return res.status(200).send('<h3>Confirm this shipment</h3>' + '<p>Press the button to record it against the order.</p>' + '<form method="POST">' + field('hostId', hostId) + field('orderId', orderId) + field('token', token) + field('carrier', carrier) + field('trackingNumber', trackingNumber) + '<button type="submit">Mark as shipped</button>' + '</form>');
|
|
124
|
+
}
|
|
125
|
+
// THE READ, THE TRANSITION CHECK AND THE WRITE ARE ONE ACT (AGL-2268).
|
|
126
|
+
// They used to be a plain `get()`, a check, and a `set(…, {merge:true})`,
|
|
127
|
+
// which is the exact stale-read hole AGL-1808/1818/1819 closed on the
|
|
128
|
+
// console's own fulfil, cancel and refund routes — reopened here, on the
|
|
129
|
+
// one door with no account behind it. A supplier POST racing a refund read
|
|
130
|
+
// `paid`, the refund landed, and the POST then wrote `fulfilled` over
|
|
131
|
+
// `refunded`. `ORDER_TRANSITIONS` says `refunded: []`, and the cost is not
|
|
132
|
+
// cosmetic: `gate.ts`, `download.ts`, `membership-account.ts` and
|
|
133
|
+
// `reviews.ts` all withdraw a shopper's entitlement by matching that
|
|
134
|
+
// literal, so the refunded buyer got their downloads back.
|
|
135
|
+
const outcome = await firebaseAdmin.app().firestore().runTransaction(async (transaction)=>{
|
|
136
|
+
var _order_lineItems, _order_fulfillments, _order_lineItems1, _order_lineItems2;
|
|
137
|
+
const orderSnapshot = await transaction.get(orderRef);
|
|
138
|
+
if (!orderSnapshot.exists) {
|
|
139
|
+
return {
|
|
140
|
+
status: 404,
|
|
141
|
+
body: {
|
|
142
|
+
error: 'Unknown order'
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const order = CommerceModel.liftLegacyOrder(orderSnapshot.data());
|
|
147
|
+
// ONE SUPPLIER SHIPS ONE SUPPLIER'S LINES (AGL-2455).
|
|
148
|
+
//
|
|
149
|
+
// `lineItemIds` used to be every index on the order and the status was
|
|
150
|
+
// written as the literal `'fulfilled'`, so the first supplier to post
|
|
151
|
+
// marked the whole order shipped — lines they had never seen included —
|
|
152
|
+
// and the second supplier's POST met a 409. The buyer was told
|
|
153
|
+
// everything shipped with one carrier and one tracking number.
|
|
154
|
+
const suppliers = CommerceModel.orderSupplierIds(order);
|
|
155
|
+
const postingSupplier = resolvePostingSupplier(hostId, orderId, order, token);
|
|
156
|
+
let myLines;
|
|
157
|
+
if (postingSupplier) {
|
|
158
|
+
myLines = CommerceModel.supplierLineItemIds(order, postingSupplier);
|
|
159
|
+
} else if (tokenMatches(orderSnapshot.get('supplierToken'), token)) {
|
|
160
|
+
var _order_lineItems3;
|
|
161
|
+
// The STORED scalar matched but no supplier on the order derives to
|
|
162
|
+
// it — an order whose lines carry no `supplierId`, which is every
|
|
163
|
+
// order routed before that field was stamped.
|
|
164
|
+
//
|
|
165
|
+
// With at most one supplier there is only one door, so claiming every
|
|
166
|
+
// line is the same answer this route has always given and is right.
|
|
167
|
+
// With TWO OR MORE, the scalar cannot say whose token it is, and
|
|
168
|
+
// guessing would either close lines a supplier never shipped (the
|
|
169
|
+
// defect) or strand them (the naive fix). REFUSED LOUDLY instead:
|
|
170
|
+
// the merchant fulfils from the console, where they can see the
|
|
171
|
+
// lines, and the message says so rather than leaving them to work it
|
|
172
|
+
// out from an order that silently went quiet.
|
|
173
|
+
if (suppliers.length > 1) {
|
|
174
|
+
return {
|
|
175
|
+
status: 409,
|
|
176
|
+
body: {
|
|
177
|
+
error: 'This order has more than one supplier and the link cannot ' + 'say which of them you are. Nothing was changed. Please ask ' + 'the merchant to record this shipment from their console.'
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
myLines = ((_order_lineItems3 = order.lineItems) != null ? _order_lineItems3 : []).map((_line, index)=>index);
|
|
182
|
+
} else {
|
|
183
|
+
return {
|
|
184
|
+
status: 403,
|
|
185
|
+
body: {
|
|
186
|
+
error: 'Invalid token'
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (myLines.length === 0) {
|
|
191
|
+
return {
|
|
192
|
+
status: 409,
|
|
193
|
+
body: {
|
|
194
|
+
error: 'No lines on this order are yours'
|
|
195
|
+
}
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
const coveredBefore = CommerceModel.coveredLineItemIds(order);
|
|
199
|
+
if (myLines.every((index)=>coveredBefore.has(index))) {
|
|
200
|
+
// Every line of theirs is already shipped: a redelivered POST, or the
|
|
201
|
+
// supplier pressing the emailed button twice. Refused rather than
|
|
202
|
+
// appending a second copy of the same fulfillment — the same rule
|
|
203
|
+
// `fulfill-order.ts` applies to a retried click.
|
|
204
|
+
return {
|
|
205
|
+
status: 409,
|
|
206
|
+
body: {
|
|
207
|
+
error: suppliers.length > 1 ? 'Your lines on this order are already marked shipped' : `Order is already ${order.status}`
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const coveredAfter = new Set([
|
|
212
|
+
...coveredBefore,
|
|
213
|
+
...myLines
|
|
214
|
+
]);
|
|
215
|
+
// COMPUTED, not written as a literal. `partially_fulfilled` has been in
|
|
216
|
+
// `ORDER_TRANSITIONS` since orders shipped and nothing ever wrote it.
|
|
217
|
+
const nextStatus = CommerceModel.statusAfterFulfilling(order, coveredAfter);
|
|
218
|
+
// Re-asked inside the transaction that writes, as AGL-2268 established:
|
|
219
|
+
// a supplier POST racing a refund must not write over `refunded`. Only
|
|
220
|
+
// when the status actually MOVES — a second supplier posting onto an
|
|
221
|
+
// already `partially_fulfilled` order changes nothing about the status
|
|
222
|
+
// and must not be refused by a table that has no self-edge.
|
|
223
|
+
if (nextStatus !== order.status && !CommerceModel.canTransitionOrder(order.status, nextStatus)) {
|
|
224
|
+
return {
|
|
225
|
+
status: 409,
|
|
226
|
+
body: {
|
|
227
|
+
error: `Order is already ${order.status}`
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const fulfillment = _extends({
|
|
232
|
+
id: `supplier-${Date.now().toString(36)}`,
|
|
233
|
+
lineItemIds: myLines
|
|
234
|
+
}, carrier ? {
|
|
235
|
+
carrier
|
|
236
|
+
} : {}, trackingNumber ? {
|
|
237
|
+
trackingNumber
|
|
238
|
+
} : {}, {
|
|
239
|
+
atMs: Date.now()
|
|
240
|
+
});
|
|
241
|
+
const remaining = ((_order_lineItems = order.lineItems) != null ? _order_lineItems : []).filter((_line, index)=>!coveredAfter.has(index)).length;
|
|
242
|
+
transaction.set(orderRef, {
|
|
243
|
+
status: nextStatus,
|
|
244
|
+
fulfillments: [
|
|
245
|
+
...(_order_fulfillments = order.fulfillments) != null ? _order_fulfillments : [],
|
|
246
|
+
fulfillment
|
|
247
|
+
],
|
|
248
|
+
timeline: CommerceModel.appendOrderEvent(order, nextStatus, `Supplier shipped ${myLines.length} of ${((_order_lineItems1 = order.lineItems) != null ? _order_lineItems1 : []).length} line${((_order_lineItems2 = order.lineItems) != null ? _order_lineItems2 : []).length === 1 ? '' : 's'}` + (trackingNumber ? ` — ${carrier || 'tracking'} ${trackingNumber}` : '') + (remaining > 0 ? `. ${remaining} line${remaining === 1 ? '' : 's'} still to ship.` : ''))
|
|
249
|
+
}, {
|
|
250
|
+
merge: true
|
|
251
|
+
});
|
|
252
|
+
return {
|
|
253
|
+
status: 200,
|
|
254
|
+
body: {
|
|
255
|
+
ok: true,
|
|
256
|
+
lineItemIds: myLines,
|
|
257
|
+
orderStatus: nextStatus
|
|
258
|
+
},
|
|
259
|
+
order,
|
|
260
|
+
nextStatus,
|
|
261
|
+
remaining
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
if (outcome.status !== 200) {
|
|
265
|
+
return res.status(outcome.status).json(outcome.body);
|
|
266
|
+
}
|
|
267
|
+
// The merchant is told what is STILL OUTSTANDING (AGL-2455). "Supplier
|
|
268
|
+
// shipped #1042" on an order where one of three suppliers has posted reads
|
|
269
|
+
// as done, which is the same misreport the status literal made — the
|
|
270
|
+
// merchant would stop watching an order that is two thirds unshipped.
|
|
271
|
+
const outstanding = Number((_outcome_remaining = outcome.remaining) != null ? _outcome_remaining : 0);
|
|
272
|
+
void notifyHostManagers(hostId, _extends({
|
|
273
|
+
type: 'content.order',
|
|
274
|
+
title: `Supplier shipped ${CommerceModel.formatOrderNumber(outcome.order, orderId)}${outstanding > 0 ? ` — ${outstanding} line${outstanding === 1 ? '' : 's'} still to ship` : ''}`
|
|
275
|
+
}, trackingNumber ? {
|
|
276
|
+
body: `${carrier} ${trackingNumber}`
|
|
277
|
+
} : {}, {
|
|
278
|
+
link: `/${hostId}/products`
|
|
279
|
+
}));
|
|
280
|
+
return res.status(200).json(outcome.body);
|
|
281
|
+
} catch (error) {
|
|
282
|
+
console.error(error);
|
|
283
|
+
return res.status(500).json({
|
|
284
|
+
error: 'Update failed'
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
};
|
|
288
|
+
|
|
289
|
+
//# sourceMappingURL=supplier-update.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/supplier-update.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { firebaseAdmin, notifyHostManagers } from '@aglyn/tenant-data-admin'\nimport { escapeHtml } from '../utils/escape-html'\nimport { createHmac, timingSafeEqual } from 'crypto'\nimport { tokenSigningSecret } from './download'\nimport { type PluginApiHandler } from '@aglyn/aglyn/server'\n\n/**\n * Constant-time compare for the per-order supplier token (AGL-2268).\n *\n * `!==` on a secret leaks its prefix through response timing, and this token is\n * a bearer credential for a route with no account behind it — the only thing\n * standing between the open internet and marking a merchant's order shipped.\n * `download.ts` has compared its own token this way since it shipped; this is\n * the sibling that did not.\n *\n * Length is compared first and OUTSIDE the constant-time call, because\n * `timingSafeEqual` throws on a length mismatch. That leaks the token's LENGTH\n * and nothing else, which is the same concession `download.ts` makes.\n */\nfunction tokenMatches(stored: unknown, presented: string): boolean {\n const expected = typeof stored === 'string' ? stored : ''\n if (!expected || !presented) return false\n const a = Buffer.from(presented)\n const b = Buffer.from(expected)\n if (a.length !== b.length) return false\n return timingSafeEqual(new Uint8Array(a), new Uint8Array(b))\n}\n\n/**\n * WHICH supplier is posting (AGL-2455), and the observation that makes the\n * issue's \"structural blocker\" not structural after all.\n *\n * The order document holds ONE `supplierToken` field, so the issue's reading\n * was that scoping lines per supplier would strand every other supplier's lines\n * permanently: only the last token written could authenticate, and the others\n * would have no valid credential at all. That reading is right about the stored\n * field and wrong about the token.\n *\n * The token is not a random secret that has to be stored to be known. It is\n * `HMAC(hostId:orderId:supplierId)` under `TOKEN_SIGNING_SECRET`\n * (`billing-webhook.ts`), a pure function of three identifiers the order\n * already carries — `supplierId` is stamped onto every line at purchase time.\n * So the expected token for EVERY supplier on the order can be re-derived here\n * and compared, which both identifies the poster and lets every supplier\n * authenticate. No schema change, no migration, no map.\n *\n * Every candidate is compared even after a match, so the work done is a\n * function of the order's supplier count and not of which supplier posted.\n *\n * Returns `null` when nothing matches; the stored-scalar fallback is the\n * caller's, because only it knows whether that fallback is ambiguous.\n */\nfunction resolvePostingSupplier(\n hostId: string,\n orderId: string,\n order: CommerceModel.HostOrder,\n presented: string,\n): string | null {\n let secret: string\n try {\n secret = tokenSigningSecret()\n } catch {\n // Unset secret: no token could have been MINTED either, so there is nothing\n // to derive. Fall through to the stored scalar rather than throwing a 500\n // at a supplier who cannot do anything about it.\n return null\n }\n let matched: string | null = null\n for (const supplierId of CommerceModel.orderSupplierIds(order)) {\n const expected = createHmac('sha256', secret)\n .update(`${hostId}:${orderId}:${supplierId}`)\n .digest('hex')\n .slice(0, 32)\n if (tokenMatches(expected, presented)) matched = supplierId\n }\n return matched\n}\n\n\n/**\n * Supplier tracking callback (AGL-289): the routed order carried a\n * per-order token; posting (or GET-ing, for email links) tracking with\n * it fulfills the order. Token-gated — suppliers have no Aglyn account.\n */\nexport const supplierUpdateHandler: PluginApiHandler = async (req, res) => {\n const source = req.method === 'POST' ? { ...req.query, ...(typeof req.body === 'string' ? JSON.parse(req.body || '{}') : (req.body ?? {})) } : req.query\n const hostId = String(source.hostId ?? '')\n const orderId = String(source.orderId ?? '')\n const token = String(source.token ?? '')\n const trackingNumber = String(source.trackingNumber ?? '').slice(0, 60)\n const carrier = String(source.carrier ?? '').slice(0, 40)\n if (!hostId || !orderId || !token) {\n return res.status(400).json({ error: 'Missing hostId, orderId, or token' })\n }\n // Firestore reserves `__…__` ids and `.doc()` throws SYNCHRONOUSLY on one,\n // which fell into the catch below and answered 500 to what is a 400\n // (AGL-2455). `fulfill-order.ts` has had this guard since it shipped; this is\n // the sibling route that did not.\n if (/^__.*__$/.test(hostId) || /^__.*__$/.test(orderId)) {\n return res.status(400).json({ error: 'Missing hostId, orderId, or token' })\n }\n\n try {\n const firestore = firebaseAdmin.app().firestore()\n const orderRef = firestore\n .collection('hosts')\n .doc(hostId)\n .collection('orders')\n .doc(orderId)\n // A GET NEVER MOVES THE ORDER (AGL-2268). The supplier email carries this\n // URL, and a GET that fulfils means every link scanner, spam filter,\n // preview generator and browser prefetch on the path between us and the\n // supplier's inbox marks the order shipped — before anything shipped, and\n // with no tracking number on it. The link still works and the email is\n // unchanged: a GET renders a one-button page that POSTs the same\n // parameters, so the supplier's experience is one extra click and the\n // machines that open mail for a living take no action at all.\n if (req.method === 'GET') {\n res.setHeader('Content-Type', 'text/html')\n const field = (name: string, value: string) =>\n `<input type=\"hidden\" name=\"${name}\" value=\"${escapeHtml(value)}\">`\n return res.status(200).send(\n '<h3>Confirm this shipment</h3>' +\n '<p>Press the button to record it against the order.</p>' +\n '<form method=\"POST\">' +\n field('hostId', hostId) +\n field('orderId', orderId) +\n field('token', token) +\n field('carrier', carrier) +\n field('trackingNumber', trackingNumber) +\n '<button type=\"submit\">Mark as shipped</button>' +\n '</form>',\n )\n }\n\n // THE READ, THE TRANSITION CHECK AND THE WRITE ARE ONE ACT (AGL-2268).\n // They used to be a plain `get()`, a check, and a `set(…, {merge:true})`,\n // which is the exact stale-read hole AGL-1808/1818/1819 closed on the\n // console's own fulfil, cancel and refund routes — reopened here, on the\n // one door with no account behind it. A supplier POST racing a refund read\n // `paid`, the refund landed, and the POST then wrote `fulfilled` over\n // `refunded`. `ORDER_TRANSITIONS` says `refunded: []`, and the cost is not\n // cosmetic: `gate.ts`, `download.ts`, `membership-account.ts` and\n // `reviews.ts` all withdraw a shopper's entitlement by matching that\n // literal, so the refunded buyer got their downloads back.\n const outcome = await firebaseAdmin\n .app()\n .firestore()\n .runTransaction(async (transaction) => {\n const orderSnapshot = await transaction.get(orderRef)\n if (!orderSnapshot.exists) {\n return { status: 404, body: { error: 'Unknown order' } as any }\n }\n const order = CommerceModel.liftLegacyOrder(orderSnapshot.data() as any)\n // ONE SUPPLIER SHIPS ONE SUPPLIER'S LINES (AGL-2455).\n //\n // `lineItemIds` used to be every index on the order and the status was\n // written as the literal `'fulfilled'`, so the first supplier to post\n // marked the whole order shipped — lines they had never seen included —\n // and the second supplier's POST met a 409. The buyer was told\n // everything shipped with one carrier and one tracking number.\n const suppliers = CommerceModel.orderSupplierIds(order)\n const postingSupplier = resolvePostingSupplier(\n hostId,\n orderId,\n order,\n token,\n )\n let myLines: number[]\n if (postingSupplier) {\n myLines = CommerceModel.supplierLineItemIds(order, postingSupplier)\n } else if (tokenMatches(orderSnapshot.get('supplierToken'), token)) {\n // The STORED scalar matched but no supplier on the order derives to\n // it — an order whose lines carry no `supplierId`, which is every\n // order routed before that field was stamped.\n //\n // With at most one supplier there is only one door, so claiming every\n // line is the same answer this route has always given and is right.\n // With TWO OR MORE, the scalar cannot say whose token it is, and\n // guessing would either close lines a supplier never shipped (the\n // defect) or strand them (the naive fix). REFUSED LOUDLY instead:\n // the merchant fulfils from the console, where they can see the\n // lines, and the message says so rather than leaving them to work it\n // out from an order that silently went quiet.\n if (suppliers.length > 1) {\n return {\n status: 409,\n body: {\n error:\n 'This order has more than one supplier and the link cannot ' +\n 'say which of them you are. Nothing was changed. Please ask ' +\n 'the merchant to record this shipment from their console.',\n } as any,\n }\n }\n myLines = (order.lineItems ?? []).map((_line, index) => index)\n } else {\n return { status: 403, body: { error: 'Invalid token' } as any }\n }\n if (myLines.length === 0) {\n return {\n status: 409,\n body: { error: 'No lines on this order are yours' } as any,\n }\n }\n const coveredBefore = CommerceModel.coveredLineItemIds(order)\n if (myLines.every((index) => coveredBefore.has(index))) {\n // Every line of theirs is already shipped: a redelivered POST, or the\n // supplier pressing the emailed button twice. Refused rather than\n // appending a second copy of the same fulfillment — the same rule\n // `fulfill-order.ts` applies to a retried click.\n return {\n status: 409,\n body: {\n error:\n suppliers.length > 1\n ? 'Your lines on this order are already marked shipped'\n : `Order is already ${order.status}`,\n } as any,\n }\n }\n const coveredAfter = new Set([...coveredBefore, ...myLines])\n // COMPUTED, not written as a literal. `partially_fulfilled` has been in\n // `ORDER_TRANSITIONS` since orders shipped and nothing ever wrote it.\n const nextStatus = CommerceModel.statusAfterFulfilling(\n order,\n coveredAfter,\n )\n // Re-asked inside the transaction that writes, as AGL-2268 established:\n // a supplier POST racing a refund must not write over `refunded`. Only\n // when the status actually MOVES — a second supplier posting onto an\n // already `partially_fulfilled` order changes nothing about the status\n // and must not be refused by a table that has no self-edge.\n if (\n nextStatus !== order.status &&\n !CommerceModel.canTransitionOrder(order.status, nextStatus)\n ) {\n return {\n status: 409,\n body: { error: `Order is already ${order.status}` } as any,\n }\n }\n const fulfillment: CommerceModel.OrderFulfillment = {\n id: `supplier-${Date.now().toString(36)}`,\n lineItemIds: myLines,\n ...(carrier ? { carrier } : {}),\n ...(trackingNumber ? { trackingNumber } : {}),\n atMs: Date.now(),\n }\n const remaining = (order.lineItems ?? []).filter(\n (_line, index) => !coveredAfter.has(index),\n ).length\n transaction.set(\n orderRef,\n {\n status: nextStatus,\n fulfillments: [...(order.fulfillments ?? []), fulfillment],\n timeline: CommerceModel.appendOrderEvent(\n order,\n nextStatus,\n `Supplier shipped ${myLines.length} of ${\n (order.lineItems ?? []).length\n } line${(order.lineItems ?? []).length === 1 ? '' : 's'}` +\n (trackingNumber\n ? ` — ${carrier || 'tracking'} ${trackingNumber}`\n : '') +\n (remaining > 0\n ? `. ${remaining} line${remaining === 1 ? '' : 's'} still to ship.`\n : ''),\n ),\n },\n { merge: true },\n )\n return {\n status: 200,\n body: { ok: true, lineItemIds: myLines, orderStatus: nextStatus } as any,\n order,\n nextStatus,\n remaining,\n }\n })\n if (outcome.status !== 200) {\n return res.status(outcome.status).json(outcome.body)\n }\n // The merchant is told what is STILL OUTSTANDING (AGL-2455). \"Supplier\n // shipped #1042\" on an order where one of three suppliers has posted reads\n // as done, which is the same misreport the status literal made — the\n // merchant would stop watching an order that is two thirds unshipped.\n const outstanding = Number((outcome as any).remaining ?? 0)\n void notifyHostManagers(hostId, {\n type: 'content.order',\n title: `Supplier shipped ${CommerceModel.formatOrderNumber(\n outcome.order as CommerceModel.HostOrder,\n orderId,\n )}${outstanding > 0 ? ` — ${outstanding} line${outstanding === 1 ? '' : 's'} still to ship` : ''}`,\n ...(trackingNumber ? { body: `${carrier} ${trackingNumber}` } : {}),\n link: `/${hostId}/products`,\n })\n return res.status(200).json(outcome.body)\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Update failed' })\n }\n}\n"],"names":["CommerceModel","firebaseAdmin","notifyHostManagers","escapeHtml","createHmac","timingSafeEqual","tokenSigningSecret","tokenMatches","stored","presented","expected","a","Buffer","from","b","length","Uint8Array","resolvePostingSupplier","hostId","orderId","order","secret","matched","supplierId","orderSupplierIds","update","digest","slice","supplierUpdateHandler","req","res","source","method","query","body","JSON","parse","String","token","trackingNumber","carrier","status","json","error","test","firestore","app","orderRef","collection","doc","setHeader","field","name","value","send","outcome","runTransaction","transaction","orderSnapshot","get","exists","liftLegacyOrder","data","suppliers","postingSupplier","myLines","supplierLineItemIds","lineItems","map","_line","index","coveredBefore","coveredLineItemIds","every","has","coveredAfter","Set","nextStatus","statusAfterFulfilling","canTransitionOrder","fulfillment","id","Date","now","toString","lineItemIds","atMs","remaining","filter","set","fulfillments","timeline","appendOrderEvent","merge","ok","orderStatus","outstanding","Number","type","title","formatOrderNumber","link","console"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC;AAGD,YAAYA,mBAAmB,oBAAU;AACzC,SAASC,aAAa,EAAEC,kBAAkB,QAAQ,2BAA0B;AAC5E,SAASC,UAAU,QAAQ,0BAAsB;AACjD,SAASC,UAAU,EAAEC,eAAe,QAAQ,SAAQ;AACpD,SAASC,kBAAkB,QAAQ,gBAAY;AAG/C;;;;;;;;;;;;CAYC,GACD,SAASC,aAAaC,MAAe,EAAEC,SAAiB;IACtD,MAAMC,WAAW,OAAOF,WAAW,WAAWA,SAAS;IACvD,IAAI,CAACE,YAAY,CAACD,WAAW,OAAO;IACpC,MAAME,IAAIC,OAAOC,IAAI,CAACJ;IACtB,MAAMK,IAAIF,OAAOC,IAAI,CAACH;IACtB,IAAIC,EAAEI,MAAM,KAAKD,EAAEC,MAAM,EAAE,OAAO;IAClC,OAAOV,gBAAgB,IAAIW,WAAWL,IAAI,IAAIK,WAAWF;AAC3D;AAEA;;;;;;;;;;;;;;;;;;;;;;;CAuBC,GACD,SAASG,uBACPC,MAAc,EACdC,OAAe,EACfC,KAA8B,EAC9BX,SAAiB;IAEjB,IAAIY;IACJ,IAAI;QACFA,SAASf;IACX,EAAE,eAAM;QACN,4EAA4E;QAC5E,0EAA0E;QAC1E,iDAAiD;QACjD,OAAO;IACT;IACA,IAAIgB,UAAyB;IAC7B,KAAK,MAAMC,cAAcvB,cAAcwB,gBAAgB,CAACJ,OAAQ;QAC9D,MAAMV,WAAWN,WAAW,UAAUiB,QACnCI,MAAM,CAAC,GAAGP,OAAO,CAAC,EAAEC,QAAQ,CAAC,EAAEI,YAAY,EAC3CG,MAAM,CAAC,OACPC,KAAK,CAAC,GAAG;QACZ,IAAIpB,aAAaG,UAAUD,YAAYa,UAAUC;IACnD;IACA,OAAOD;AACT;AAGA;;;;CAIC,GACD,OAAO,MAAMM,wBAA0C,OAAOC,KAAKC;QACyDD,WACpGE,gBACCA,iBACFA,eACSA,wBACPA;IALvB,MAAMA,SAASF,IAAIG,MAAM,KAAK,SAAS,aAAKH,IAAII,KAAK,EAAM,OAAOJ,IAAIK,IAAI,KAAK,WAAWC,KAAKC,KAAK,CAACP,IAAIK,IAAI,IAAI,SAASL,YAAAA,IAAIK,IAAI,YAARL,YAAY,CAAC,KAAQA,IAAII,KAAK;IACxJ,MAAMf,SAASmB,QAAON,iBAAAA,OAAOb,MAAM,YAAba,iBAAiB;IACvC,MAAMZ,UAAUkB,QAAON,kBAAAA,OAAOZ,OAAO,YAAdY,kBAAkB;IACzC,MAAMO,QAAQD,QAAON,gBAAAA,OAAOO,KAAK,YAAZP,gBAAgB;IACrC,MAAMQ,iBAAiBF,QAAON,yBAAAA,OAAOQ,cAAc,YAArBR,yBAAyB,IAAIJ,KAAK,CAAC,GAAG;IACpE,MAAMa,UAAUH,QAAON,kBAAAA,OAAOS,OAAO,YAAdT,kBAAkB,IAAIJ,KAAK,CAAC,GAAG;IACtD,IAAI,CAACT,UAAU,CAACC,WAAW,CAACmB,OAAO;QACjC,OAAOR,IAAIW,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAoC;IAC3E;IACA,2EAA2E;IAC3E,oEAAoE;IACpE,8EAA8E;IAC9E,kCAAkC;IAClC,IAAI,WAAWC,IAAI,CAAC1B,WAAW,WAAW0B,IAAI,CAACzB,UAAU;QACvD,OAAOW,IAAIW,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAoC;IAC3E;IAEA,IAAI;YA0LyB;QAzL3B,MAAME,YAAY5C,cAAc6C,GAAG,GAAGD,SAAS;QAC/C,MAAME,WAAWF,UACdG,UAAU,CAAC,SACXC,GAAG,CAAC/B,QACJ8B,UAAU,CAAC,UACXC,GAAG,CAAC9B;QACP,0EAA0E;QAC1E,qEAAqE;QACrE,wEAAwE;QACxE,0EAA0E;QAC1E,uEAAuE;QACvE,iEAAiE;QACjE,sEAAsE;QACtE,8DAA8D;QAC9D,IAAIU,IAAIG,MAAM,KAAK,OAAO;YACxBF,IAAIoB,SAAS,CAAC,gBAAgB;YAC9B,MAAMC,QAAQ,CAACC,MAAcC,QAC3B,CAAC,2BAA2B,EAAED,KAAK,SAAS,EAAEjD,WAAWkD,OAAO,EAAE,CAAC;YACrE,OAAOvB,IAAIW,MAAM,CAAC,KAAKa,IAAI,CACzB,mCACE,4DACA,yBACAH,MAAM,UAAUjC,UAChBiC,MAAM,WAAWhC,WACjBgC,MAAM,SAASb,SACfa,MAAM,WAAWX,WACjBW,MAAM,kBAAkBZ,kBACxB,mDACA;QAEN;QAEA,uEAAuE;QACvE,0EAA0E;QAC1E,sEAAsE;QACtE,yEAAyE;QACzE,2EAA2E;QAC3E,sEAAsE;QACtE,2EAA2E;QAC3E,kEAAkE;QAClE,qEAAqE;QACrE,2DAA2D;QAC3D,MAAMgB,UAAU,MAAMtD,cACnB6C,GAAG,GACHD,SAAS,GACTW,cAAc,CAAC,OAAOC;gBAqGFrC,kBAOIA,qBAKdA,mBACMA;YAjHf,MAAMsC,gBAAgB,MAAMD,YAAYE,GAAG,CAACZ;YAC5C,IAAI,CAACW,cAAcE,MAAM,EAAE;gBACzB,OAAO;oBAAEnB,QAAQ;oBAAKP,MAAM;wBAAES,OAAO;oBAAgB;gBAAS;YAChE;YACA,MAAMvB,QAAQpB,cAAc6D,eAAe,CAACH,cAAcI,IAAI;YAC9D,sDAAsD;YACtD,EAAE;YACF,uEAAuE;YACvE,sEAAsE;YACtE,wEAAwE;YACxE,+DAA+D;YAC/D,+DAA+D;YAC/D,MAAMC,YAAY/D,cAAcwB,gBAAgB,CAACJ;YACjD,MAAM4C,kBAAkB/C,uBACtBC,QACAC,SACAC,OACAkB;YAEF,IAAI2B;YACJ,IAAID,iBAAiB;gBACnBC,UAAUjE,cAAckE,mBAAmB,CAAC9C,OAAO4C;YACrD,OAAO,IAAIzD,aAAamD,cAAcC,GAAG,CAAC,kBAAkBrB,QAAQ;oBAwBvDlB;gBAvBX,oEAAoE;gBACpE,kEAAkE;gBAClE,8CAA8C;gBAC9C,EAAE;gBACF,sEAAsE;gBACtE,oEAAoE;gBACpE,iEAAiE;gBACjE,kEAAkE;gBAClE,kEAAkE;gBAClE,gEAAgE;gBAChE,qEAAqE;gBACrE,8CAA8C;gBAC9C,IAAI2C,UAAUhD,MAAM,GAAG,GAAG;oBACxB,OAAO;wBACL0B,QAAQ;wBACRP,MAAM;4BACJS,OACE,+DACA,gEACA;wBACJ;oBACF;gBACF;gBACAsB,UAAU,EAAC7C,oBAAAA,MAAM+C,SAAS,YAAf/C,oBAAmB,EAAE,EAAEgD,GAAG,CAAC,CAACC,OAAOC,QAAUA;YAC1D,OAAO;gBACL,OAAO;oBAAE7B,QAAQ;oBAAKP,MAAM;wBAAES,OAAO;oBAAgB;gBAAS;YAChE;YACA,IAAIsB,QAAQlD,MAAM,KAAK,GAAG;gBACxB,OAAO;oBACL0B,QAAQ;oBACRP,MAAM;wBAAES,OAAO;oBAAmC;gBACpD;YACF;YACA,MAAM4B,gBAAgBvE,cAAcwE,kBAAkB,CAACpD;YACvD,IAAI6C,QAAQQ,KAAK,CAAC,CAACH,QAAUC,cAAcG,GAAG,CAACJ,SAAS;gBACtD,sEAAsE;gBACtE,kEAAkE;gBAClE,kEAAkE;gBAClE,iDAAiD;gBACjD,OAAO;oBACL7B,QAAQ;oBACRP,MAAM;wBACJS,OACEoB,UAAUhD,MAAM,GAAG,IACf,wDACA,CAAC,iBAAiB,EAAEK,MAAMqB,MAAM,EAAE;oBAC1C;gBACF;YACF;YACA,MAAMkC,eAAe,IAAIC,IAAI;mBAAIL;mBAAkBN;aAAQ;YAC3D,wEAAwE;YACxE,sEAAsE;YACtE,MAAMY,aAAa7E,cAAc8E,qBAAqB,CACpD1D,OACAuD;YAEF,wEAAwE;YACxE,uEAAuE;YACvE,qEAAqE;YACrE,uEAAuE;YACvE,4DAA4D;YAC5D,IACEE,eAAezD,MAAMqB,MAAM,IAC3B,CAACzC,cAAc+E,kBAAkB,CAAC3D,MAAMqB,MAAM,EAAEoC,aAChD;gBACA,OAAO;oBACLpC,QAAQ;oBACRP,MAAM;wBAAES,OAAO,CAAC,iBAAiB,EAAEvB,MAAMqB,MAAM,EAAE;oBAAC;gBACpD;YACF;YACA,MAAMuC,cAA8C;gBAClDC,IAAI,CAAC,SAAS,EAAEC,KAAKC,GAAG,GAAGC,QAAQ,CAAC,KAAK;gBACzCC,aAAapB;eACTzB,UAAU;gBAAEA;YAAQ,IAAI,CAAC,GACzBD,iBAAiB;gBAAEA;YAAe,IAAI,CAAC;gBAC3C+C,MAAMJ,KAAKC,GAAG;;YAEhB,MAAMI,YAAY,EAACnE,mBAAAA,MAAM+C,SAAS,YAAf/C,mBAAmB,EAAE,EAAEoE,MAAM,CAC9C,CAACnB,OAAOC,QAAU,CAACK,aAAaD,GAAG,CAACJ,QACpCvD,MAAM;YACR0C,YAAYgC,GAAG,CACb1C,UACA;gBACEN,QAAQoC;gBACRa,cAAc;wBAAKtE,sBAAAA,MAAMsE,YAAY,YAAlBtE,sBAAsB,EAAE;oBAAG4D;iBAAY;gBAC1DW,UAAU3F,cAAc4F,gBAAgB,CACtCxE,OACAyD,YACA,CAAC,iBAAiB,EAAEZ,QAAQlD,MAAM,CAAC,IAAI,EACrC,EAACK,oBAAAA,MAAM+C,SAAS,YAAf/C,oBAAmB,EAAE,EAAEL,MAAM,CAC/B,KAAK,EAAE,EAACK,oBAAAA,MAAM+C,SAAS,YAAf/C,oBAAmB,EAAE,EAAEL,MAAM,KAAK,IAAI,KAAK,KAAK,GACtDwB,CAAAA,iBACG,CAAC,GAAG,EAAEC,WAAW,WAAW,CAAC,EAAED,gBAAgB,GAC/C,EAAC,IACJgD,CAAAA,YAAY,IACT,CAAC,EAAE,EAAEA,UAAU,KAAK,EAAEA,cAAc,IAAI,KAAK,IAAI,eAAe,CAAC,GACjE,EAAC;YAEX,GACA;gBAAEM,OAAO;YAAK;YAEhB,OAAO;gBACLpD,QAAQ;gBACRP,MAAM;oBAAE4D,IAAI;oBAAMT,aAAapB;oBAAS8B,aAAalB;gBAAW;gBAChEzD;gBACAyD;gBACAU;YACF;QACF;QACF,IAAIhC,QAAQd,MAAM,KAAK,KAAK;YAC1B,OAAOX,IAAIW,MAAM,CAACc,QAAQd,MAAM,EAAEC,IAAI,CAACa,QAAQrB,IAAI;QACrD;QACA,uEAAuE;QACvE,2EAA2E;QAC3E,qEAAqE;QACrE,sEAAsE;QACtE,MAAM8D,cAAcC,QAAO,qBAAA,AAAC1C,QAAgBgC,SAAS,YAA1B,qBAA8B;QACzD,KAAKrF,mBAAmBgB,QAAQ;YAC9BgF,MAAM;YACNC,OAAO,CAAC,iBAAiB,EAAEnG,cAAcoG,iBAAiB,CACxD7C,QAAQnC,KAAK,EACbD,WACE6E,cAAc,IAAI,CAAC,GAAG,EAAEA,YAAY,KAAK,EAAEA,gBAAgB,IAAI,KAAK,IAAI,cAAc,CAAC,GAAG,IAAI;WAC9FzD,iBAAiB;YAAEL,MAAM,GAAGM,QAAQ,CAAC,EAAED,gBAAgB;QAAC,IAAI,CAAC;YACjE8D,MAAM,CAAC,CAAC,EAAEnF,OAAO,SAAS,CAAC;;QAE7B,OAAOY,IAAIW,MAAM,CAAC,KAAKC,IAAI,CAACa,QAAQrB,IAAI;IAC1C,EAAE,OAAOS,OAAO;QACd2D,QAAQ3D,KAAK,CAACA;QACd,OAAOb,IAAIW,MAAM,CAAC,KAAKC,IAAI,CAAC;YAAEC,OAAO;QAAgB;IACvD;AACF,EAAC"}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
* The merchant's tax rule, published for every plugin that charges.
|
|
19
|
+
*
|
|
20
|
+
* A merchant sets tax once, in the store settings this plugin keeps. A plugin
|
|
21
|
+
* that sells something else — an appointment — prices its own charge, and used
|
|
22
|
+
* to import `resolveFlatTaxCents` and `storefrontTaxModeOf` from here to do it.
|
|
23
|
+
* It asks the tax-profile contract now, and gets these same two functions: one
|
|
24
|
+
* rounding rule, one reading of a settled payment, wherever money is taken.
|
|
25
|
+
*
|
|
26
|
+
* Registered from BOTH server registrars, because a booking is priced in the
|
|
27
|
+
* tenant app and confirmed by the console's billing webhook.
|
|
28
|
+
*/
|
|
29
|
+
export declare function registerCommerceTaxProfile(): void;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ import { registerPluginTaxProfile } from "@aglyn/aglyn/plugin-manager/plugin-tax-profile";
|
|
17
|
+
import { BUNDLE_ID } from "../constants/bundle-common.js";
|
|
18
|
+
import { resolveFlatTaxCents } from "../model/index.js";
|
|
19
|
+
import { storefrontTaxModeOf } from "./storefront-tax.js";
|
|
20
|
+
/**
|
|
21
|
+
* The merchant's tax rule, published for every plugin that charges.
|
|
22
|
+
*
|
|
23
|
+
* A merchant sets tax once, in the store settings this plugin keeps. A plugin
|
|
24
|
+
* that sells something else — an appointment — prices its own charge, and used
|
|
25
|
+
* to import `resolveFlatTaxCents` and `storefrontTaxModeOf` from here to do it.
|
|
26
|
+
* It asks the tax-profile contract now, and gets these same two functions: one
|
|
27
|
+
* rounding rule, one reading of a settled payment, wherever money is taken.
|
|
28
|
+
*
|
|
29
|
+
* Registered from BOTH server registrars, because a booking is priced in the
|
|
30
|
+
* tenant app and confirmed by the console's billing webhook.
|
|
31
|
+
*/ export function registerCommerceTaxProfile() {
|
|
32
|
+
registerPluginTaxProfile({
|
|
33
|
+
flatTax: (rate, chargeCents, fallbackLabel)=>resolveFlatTaxCents(rate, chargeCents, fallbackLabel),
|
|
34
|
+
taxModeOf: (settledPayment, manualTaxCents)=>storefrontTaxModeOf(settledPayment, manualTaxCents)
|
|
35
|
+
}, {
|
|
36
|
+
pluginId: BUNDLE_ID
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
//# sourceMappingURL=tax-profile.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/tax-profile.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 { registerPluginTaxProfile } from '@aglyn/aglyn/plugin-manager/plugin-tax-profile'\nimport { BUNDLE_ID } from '../constants/bundle-common'\nimport { type FlatTaxRate, resolveFlatTaxCents } from '../model'\nimport { storefrontTaxModeOf } from './storefront-tax'\n\n/**\n * The merchant's tax rule, published for every plugin that charges.\n *\n * A merchant sets tax once, in the store settings this plugin keeps. A plugin\n * that sells something else — an appointment — prices its own charge, and used\n * to import `resolveFlatTaxCents` and `storefrontTaxModeOf` from here to do it.\n * It asks the tax-profile contract now, and gets these same two functions: one\n * rounding rule, one reading of a settled payment, wherever money is taken.\n *\n * Registered from BOTH server registrars, because a booking is priced in the\n * tenant app and confirmed by the console's billing webhook.\n */\nexport function registerCommerceTaxProfile(): void {\n registerPluginTaxProfile(\n {\n flatTax: (rate, chargeCents, fallbackLabel) =>\n resolveFlatTaxCents(\n rate as FlatTaxRate | undefined | null,\n chargeCents,\n fallbackLabel,\n ),\n taxModeOf: (settledPayment, manualTaxCents) =>\n storefrontTaxModeOf(settledPayment, manualTaxCents),\n },\n { pluginId: BUNDLE_ID },\n )\n}\n"],"names":["registerPluginTaxProfile","BUNDLE_ID","resolveFlatTaxCents","storefrontTaxModeOf","registerCommerceTaxProfile","flatTax","rate","chargeCents","fallbackLabel","taxModeOf","settledPayment","manualTaxCents","pluginId"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED,SAASA,wBAAwB,QAAQ,iDAAgD;AACzF,SAASC,SAAS,QAAQ,gCAA4B;AACtD,SAA2BC,mBAAmB,QAAQ,oBAAU;AAChE,SAASC,mBAAmB,QAAQ,sBAAkB;AAEtD;;;;;;;;;;;CAWC,GACD,OAAO,SAASC;IACdJ,yBACE;QACEK,SAAS,CAACC,MAAMC,aAAaC,gBAC3BN,oBACEI,MACAC,aACAC;QAEJC,WAAW,CAACC,gBAAgBC,iBAC1BR,oBAAoBO,gBAAgBC;IACxC,GACA;QAAEC,UAAUX;IAAU;AAE1B"}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
/** Registers the commerce plugin's storefront API routes. */
|
|
18
|
+
export declare function registerCommerceApi(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Registers the commerce plugin's console-side API routes (AGL-396):
|
|
21
|
+
* merchant/staff operations (Connect onboarding, refunds, draft & POS
|
|
22
|
+
* orders, member posts), the supplier tracking callback, and the HTTP doors
|
|
23
|
+
* for the abandoned-cart / restock passes.
|
|
24
|
+
*
|
|
25
|
+
* Those last two are NOT what schedules them — the `registerPluginJob` calls
|
|
26
|
+
* above are (AGL-2227). This comment used to call them "the scheduler-driven
|
|
27
|
+
* jobs", which is how they stayed dark for months: it asserted the wiring
|
|
28
|
+
* instead of having it.
|
|
29
|
+
*/
|
|
30
|
+
export declare function registerCommerceConsoleApi(): void;
|
|
31
|
+
export { mintDownloadToken } from './server/download';
|
|
32
|
+
export * from './server/membership';
|