@aglyn/plugins-commerce 1.0.0-beta.285 → 1.0.0-beta.288
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/README.md +1 -1
- package/package.json +13 -13
- package/src/lib/components/cart.js +25 -12
- package/src/lib/components/cart.js.map +1 -1
- package/src/lib/components/commerce-console-page.js +70 -13
- package/src/lib/components/commerce-console-page.js.map +1 -1
- package/src/lib/components/console/order-detail-dialog.component.js +2 -1
- package/src/lib/components/console/order-detail-dialog.component.js.map +1 -1
- package/src/lib/components/console/order-webhooks-card.component.js +1 -1
- package/src/lib/components/console/order-webhooks-card.component.js.map +1 -1
- package/src/lib/components/console/payment-methods-card.component.js +1 -1
- package/src/lib/components/console/payment-methods-card.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-cart-panel.component.d.ts +5 -0
- package/src/lib/components/console/pos/pos-cart-panel.component.js +18 -12
- package/src/lib/components/console/pos/pos-cart-panel.component.js.map +1 -1
- package/src/lib/components/console/pos/pos-hardware-api.d.ts +76 -5
- package/src/lib/components/console/pos/pos-hardware-api.js +53 -11
- package/src/lib/components/console/pos/pos-hardware-api.js.map +1 -1
- package/src/lib/components/console/pos/pos-hardware-buyer.component.d.ts +18 -0
- package/src/lib/components/console/pos/pos-hardware-buyer.component.js +629 -0
- package/src/lib/components/console/pos/pos-hardware-buyer.component.js.map +1 -0
- package/src/lib/components/console/pos/pos-hardware-sale-api.d.ts +162 -0
- package/src/lib/components/console/pos/pos-hardware-sale-api.js +122 -0
- package/src/lib/components/console/pos/pos-hardware-sale-api.js.map +1 -0
- package/src/lib/components/console/pos/pos-hardware-sale-terms.component.d.ts +39 -0
- package/src/lib/components/console/pos/pos-hardware-sale-terms.component.js +251 -0
- package/src/lib/components/console/pos/pos-hardware-sale-terms.component.js.map +1 -0
- package/src/lib/components/console/pos/use-pos-display.js +50 -0
- package/src/lib/components/console/pos/use-pos-display.js.map +1 -1
- package/src/lib/components/console/pos-cellular-control.component.d.ts +41 -0
- package/src/lib/components/console/pos-cellular-control.component.js +211 -0
- package/src/lib/components/console/pos-cellular-control.component.js.map +1 -0
- package/src/lib/components/console/pos-devices-card.component.d.ts +9 -1
- package/src/lib/components/console/pos-devices-card.component.js +47 -15
- package/src/lib/components/console/pos-devices-card.component.js.map +1 -1
- package/src/lib/components/console/pos-display/pos-display-api.d.ts +6 -0
- package/src/lib/components/console/pos-display/pos-display-api.js +8 -1
- package/src/lib/components/console/pos-display/pos-display-api.js.map +1 -1
- package/src/lib/components/console/pos-display-page.component.d.ts +13 -0
- package/src/lib/components/console/pos-display-page.component.js +44 -2
- package/src/lib/components/console/pos-display-page.component.js.map +1 -1
- package/src/lib/components/console/pos-hardware-card.component.d.ts +12 -4
- package/src/lib/components/console/pos-hardware-card.component.js +157 -36
- package/src/lib/components/console/pos-hardware-card.component.js.map +1 -1
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js +360 -80
- package/src/lib/components/console/pos-hardware-orders-staff-page.component.js.map +1 -1
- package/src/lib/components/console/pos-hardware-terms-page.component.d.ts +34 -0
- package/src/lib/components/console/pos-hardware-terms-page.component.js +257 -0
- package/src/lib/components/console/pos-hardware-terms-page.component.js.map +1 -0
- package/src/lib/components/console/pos-hardware-terms-sign-page.component.d.ts +27 -0
- package/src/lib/components/console/pos-hardware-terms-sign-page.component.js +104 -0
- package/src/lib/components/console/pos-hardware-terms-sign-page.component.js.map +1 -0
- package/src/lib/components/console/pos-hardware-waiting-card.component.d.ts +24 -0
- package/src/lib/components/console/pos-hardware-waiting-card.component.js +205 -0
- package/src/lib/components/console/pos-hardware-waiting-card.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js +297 -201
- package/src/lib/components/console/pos-ops/pos-check-in-dialog.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.d.ts +3 -0
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js +260 -92
- package/src/lib/components/console/pos-ops/pos-check-ins-card.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.d.ts +9 -0
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js +15 -6
- package/src/lib/components/console/pos-ops/pos-operations-bar.component.js.map +1 -1
- package/src/lib/components/console/pos-ops/pos-rain-check-dialog.component.d.ts +25 -0
- package/src/lib/components/console/pos-ops/pos-rain-check-dialog.component.js +136 -0
- package/src/lib/components/console/pos-ops/pos-rain-check-dialog.component.js.map +1 -0
- package/src/lib/components/console/pos-ops/pos-shift-panel.component.d.ts +6 -0
- package/src/lib/components/console/pos-ops/pos-shift-panel.component.js +48 -31
- package/src/lib/components/console/pos-ops/pos-shift-panel.component.js.map +1 -1
- package/src/lib/components/console/pos-page.component.d.ts +1 -1
- package/src/lib/components/console/pos-page.component.js +63 -12
- package/src/lib/components/console/pos-page.component.js.map +1 -1
- package/src/lib/components/console/pos-reader-splash-setting.component.js +1 -1
- package/src/lib/components/console/pos-reader-splash-setting.component.js.map +1 -1
- package/src/lib/components/console/product-check-in-fields.component.d.ts +4 -2
- package/src/lib/components/console/product-check-in-fields.component.js +60 -14
- package/src/lib/components/console/product-check-in-fields.component.js.map +1 -1
- package/src/lib/components/console/product-editor-dialog.component.js +44 -1
- package/src/lib/components/console/product-editor-dialog.component.js.map +1 -1
- package/src/lib/components/console/product-hardware-sku-field.component.d.ts +19 -0
- package/src/lib/components/console/product-hardware-sku-field.component.js +92 -0
- package/src/lib/components/console/product-hardware-sku-field.component.js.map +1 -0
- package/src/lib/components/console/product-kiosk-offer-field.component.d.ts +22 -0
- package/src/lib/components/console/product-kiosk-offer-field.component.js +94 -0
- package/src/lib/components/console/product-kiosk-offer-field.component.js.map +1 -0
- package/src/lib/components/console/product-register-fields.component.d.ts +4 -0
- package/src/lib/components/console/product-register-fields.component.js +31 -2
- package/src/lib/components/console/product-register-fields.component.js.map +1 -1
- package/src/lib/components/console/products-hub-card.component.js +11 -2
- package/src/lib/components/console/products-hub-card.component.js.map +1 -1
- package/src/lib/components/console/register-hardware-card.component.d.ts +11 -0
- package/src/lib/components/console/register-hardware-card.component.js +56 -27
- package/src/lib/components/console/register-hardware-card.component.js.map +1 -1
- package/src/lib/components/console/reservation-guests-dialog.component.d.ts +28 -0
- package/src/lib/components/console/reservation-guests-dialog.component.js +189 -0
- package/src/lib/components/console/reservation-guests-dialog.component.js.map +1 -0
- package/src/lib/components/console/reservations-card.component.d.ts +9 -0
- package/src/lib/components/console/reservations-card.component.js +158 -23
- package/src/lib/components/console/reservations-card.component.js.map +1 -1
- package/src/lib/components/console/shippingeasy-card.component.js +1 -1
- package/src/lib/components/console/shippingeasy-card.component.js.map +1 -1
- package/src/lib/components/console/shipstation-card.component.d.ts +1 -1
- package/src/lib/components/console/shipstation-card.component.js +3 -3
- package/src/lib/components/console/shipstation-card.component.js.map +1 -1
- package/src/lib/components/product-detail.d.ts +4 -0
- package/src/lib/components/product-detail.js +40 -5
- package/src/lib/components/product-detail.js.map +1 -1
- package/src/lib/components/product-reviews.js +28 -11
- package/src/lib/components/product-reviews.js.map +1 -1
- package/src/lib/components/reservation-widget.js +28 -5
- package/src/lib/components/reservation-widget.js.map +1 -1
- package/src/lib/components/use-day-picker.d.ts +25 -0
- package/src/lib/components/use-day-picker.js +57 -0
- package/src/lib/components/use-day-picker.js.map +1 -0
- package/src/lib/constants/operator-alerts.d.ts +55 -0
- package/src/lib/constants/operator-alerts.js +156 -1
- package/src/lib/constants/operator-alerts.js.map +1 -1
- package/src/lib/declarations.console-server.js +54 -1
- package/src/lib/declarations.console-server.js.map +1 -1
- package/src/lib/declarations.d.ts +4 -2
- package/src/lib/declarations.js +15 -4
- package/src/lib/declarations.js.map +1 -1
- package/src/lib/declarations.server.js +9 -0
- package/src/lib/declarations.server.js.map +1 -1
- package/src/lib/model/commerce-cart.d.ts +9 -1
- package/src/lib/model/commerce-cart.js +4 -3
- package/src/lib/model/commerce-cart.js.map +1 -1
- package/src/lib/model/commerce-check-in-product.d.ts +26 -0
- package/src/lib/model/commerce-check-in-product.js +64 -2
- package/src/lib/model/commerce-check-in-product.js.map +1 -1
- package/src/lib/model/commerce-check-in-record.d.ts +127 -0
- package/src/lib/model/commerce-check-in-record.js +36 -0
- package/src/lib/model/commerce-check-in-record.js.map +1 -0
- package/src/lib/model/commerce-check-in.d.ts +91 -87
- package/src/lib/model/commerce-check-in.js +186 -62
- package/src/lib/model/commerce-check-in.js.map +1 -1
- package/src/lib/model/commerce-kiosk-offer.d.ts +31 -0
- package/src/lib/model/commerce-kiosk-offer.js +35 -0
- package/src/lib/model/commerce-kiosk-offer.js.map +1 -0
- package/src/lib/model/commerce-orders.d.ts +76 -0
- package/src/lib/model/commerce-orders.js +6 -0
- package/src/lib/model/commerce-orders.js.map +1 -1
- package/src/lib/model/commerce-pos-day-report.d.ts +54 -1
- package/src/lib/model/commerce-pos-day-report.js +39 -2
- package/src/lib/model/commerce-pos-day-report.js.map +1 -1
- package/src/lib/model/commerce-pos-offline.d.ts +1 -1
- package/src/lib/model/commerce-pos-offline.js +6 -1
- package/src/lib/model/commerce-pos-offline.js.map +1 -1
- package/src/lib/model/commerce-pos-ops.js +3 -2
- package/src/lib/model/commerce-pos-ops.js.map +1 -1
- package/src/lib/model/commerce-pos.d.ts +36 -3
- package/src/lib/model/commerce-pos.js +14 -6
- package/src/lib/model/commerce-pos.js.map +1 -1
- package/src/lib/model/commerce-receipt.js +3 -2
- package/src/lib/model/commerce-receipt.js.map +1 -1
- package/src/lib/model/commerce-reservations.d.ts +69 -4
- package/src/lib/model/commerce-reservations.js +57 -5
- package/src/lib/model/commerce-reservations.js.map +1 -1
- package/src/lib/model/commerce.d.ts +66 -0
- package/src/lib/model/commerce.js +15 -0
- package/src/lib/model/commerce.js.map +1 -1
- package/src/lib/model/order-list-fields.d.ts +11 -0
- package/src/lib/model/order-list-fields.js +17 -1
- package/src/lib/model/order-list-fields.js.map +1 -1
- package/src/lib/model/own-store-tax-summary.d.ts +152 -0
- package/src/lib/model/own-store-tax-summary.js +288 -0
- package/src/lib/model/own-store-tax-summary.js.map +1 -0
- package/src/lib/model/plugin-permissions.d.ts +74 -0
- package/src/lib/model/plugin-permissions.js +224 -7
- package/src/lib/model/plugin-permissions.js.map +1 -1
- package/src/lib/model/pos-cellular.d.ts +223 -0
- package/src/lib/model/pos-cellular.js +293 -0
- package/src/lib/model/pos-cellular.js.map +1 -0
- package/src/lib/model/pos-hardware-in-person.d.ts +98 -0
- package/src/lib/model/pos-hardware-in-person.js +224 -0
- package/src/lib/model/pos-hardware-in-person.js.map +1 -0
- package/src/lib/model/pos-hardware-tax-summary.d.ts +166 -0
- package/src/lib/model/pos-hardware-tax-summary.js +225 -0
- package/src/lib/model/pos-hardware-tax-summary.js.map +1 -0
- package/src/lib/model/pos-hardware-terms.d.ts +74 -0
- package/src/lib/model/pos-hardware-terms.js +115 -0
- package/src/lib/model/pos-hardware-terms.js.map +1 -0
- package/src/lib/model/pos-hardware.d.ts +349 -7
- package/src/lib/model/pos-hardware.js +291 -20
- package/src/lib/model/pos-hardware.js.map +1 -1
- package/src/lib/model/product-write.d.ts +10 -0
- package/src/lib/model/product-write.js +4 -2
- package/src/lib/model/product-write.js.map +1 -1
- package/src/lib/model/workspace-site-permissions.d.ts +26 -0
- package/src/lib/model/workspace-site-permissions.js +81 -0
- package/src/lib/model/workspace-site-permissions.js.map +1 -0
- package/src/lib/plugin.js +16 -0
- package/src/lib/plugin.js.map +1 -1
- package/src/lib/server/billing-webhook.js +25 -1
- package/src/lib/server/billing-webhook.js.map +1 -1
- package/src/lib/server/cancel-order.js +7 -5
- package/src/lib/server/cancel-order.js.map +1 -1
- package/src/lib/server/cart-checkout.js +20 -8
- package/src/lib/server/cart-checkout.js.map +1 -1
- package/src/lib/server/cart.js +29 -0
- package/src/lib/server/cart.js.map +1 -1
- package/src/lib/server/checkout.js +42 -13
- package/src/lib/server/checkout.js.map +1 -1
- package/src/lib/server/collection-membership.d.ts +4 -0
- package/src/lib/server/collection-membership.js +5 -10
- package/src/lib/server/collection-membership.js.map +1 -1
- package/src/lib/server/commerce-site-keys.d.ts +48 -0
- package/src/lib/server/commerce-site-keys.js +61 -0
- package/src/lib/server/commerce-site-keys.js.map +1 -0
- package/src/lib/server/draft-order.js +8 -5
- package/src/lib/server/draft-order.js.map +1 -1
- package/src/lib/server/fulfill-order.js +7 -5
- package/src/lib/server/fulfill-order.js.map +1 -1
- package/src/lib/server/gift-cards.js +8 -5
- package/src/lib/server/gift-cards.js.map +1 -1
- package/src/lib/server/local-fulfillment-status.js +9 -6
- package/src/lib/server/local-fulfillment-status.js.map +1 -1
- package/src/lib/server/member-post.js +6 -5
- package/src/lib/server/member-post.js.map +1 -1
- package/src/lib/server/membership-admin-password.d.ts +4 -2
- package/src/lib/server/membership-admin-password.js +10 -8
- package/src/lib/server/membership-admin-password.js.map +1 -1
- package/src/lib/server/membership-admin-remove.d.ts +7 -5
- package/src/lib/server/membership-admin-remove.js +9 -9
- package/src/lib/server/membership-admin-remove.js.map +1 -1
- package/src/lib/server/order-notifications.js +5 -2
- package/src/lib/server/order-notifications.js.map +1 -1
- package/src/lib/server/order-print.js +7 -5
- package/src/lib/server/order-print.js.map +1 -1
- package/src/lib/server/order-receipt-send.js +8 -5
- package/src/lib/server/order-receipt-send.js.map +1 -1
- package/src/lib/server/order-webhooks.js +8 -8
- package/src/lib/server/order-webhooks.js.map +1 -1
- package/src/lib/server/orders-shipping-prepare.js +9 -6
- package/src/lib/server/orders-shipping-prepare.js.map +1 -1
- package/src/lib/server/own-store-tax-return-source.d.ts +47 -0
- package/src/lib/server/own-store-tax-return-source.js +312 -0
- package/src/lib/server/own-store-tax-return-source.js.map +1 -0
- package/src/lib/server/payment-methods.js +13 -9
- package/src/lib/server/payment-methods.js.map +1 -1
- package/src/lib/server/pos-auth.d.ts +37 -6
- package/src/lib/server/pos-auth.js +40 -15
- package/src/lib/server/pos-auth.js.map +1 -1
- package/src/lib/server/pos-cellular-billing.d.ts +81 -0
- package/src/lib/server/pos-cellular-billing.js +711 -0
- package/src/lib/server/pos-cellular-billing.js.map +1 -0
- package/src/lib/server/pos-cellular-notify.d.ts +47 -0
- package/src/lib/server/pos-cellular-notify.js +150 -0
- package/src/lib/server/pos-cellular-notify.js.map +1 -0
- package/src/lib/server/pos-cellular.d.ts +88 -0
- package/src/lib/server/pos-cellular.js +339 -0
- package/src/lib/server/pos-cellular.js.map +1 -0
- package/src/lib/server/pos-check-in.d.ts +5 -1
- package/src/lib/server/pos-check-in.js +296 -28
- package/src/lib/server/pos-check-in.js.map +1 -1
- package/src/lib/server/pos-day-report.js +29 -3
- package/src/lib/server/pos-day-report.js.map +1 -1
- package/src/lib/server/pos-display.d.ts +16 -0
- package/src/lib/server/pos-display.js +129 -2
- package/src/lib/server/pos-display.js.map +1 -1
- package/src/lib/server/pos-hardware-admin.d.ts +10 -1
- package/src/lib/server/pos-hardware-admin.js +250 -84
- package/src/lib/server/pos-hardware-admin.js.map +1 -1
- package/src/lib/server/pos-hardware-attach.d.ts +105 -0
- package/src/lib/server/pos-hardware-attach.js +443 -0
- package/src/lib/server/pos-hardware-attach.js.map +1 -0
- package/src/lib/server/pos-hardware-buyer-lookup.d.ts +58 -0
- package/src/lib/server/pos-hardware-buyer-lookup.js +180 -0
- package/src/lib/server/pos-hardware-buyer-lookup.js.map +1 -0
- package/src/lib/server/pos-hardware-buyer-record.d.ts +29 -0
- package/src/lib/server/pos-hardware-buyer-record.js +86 -0
- package/src/lib/server/pos-hardware-buyer-record.js.map +1 -0
- package/src/lib/server/pos-hardware-cancel.d.ts +58 -0
- package/src/lib/server/pos-hardware-cancel.js +147 -0
- package/src/lib/server/pos-hardware-cancel.js.map +1 -0
- package/src/lib/server/pos-hardware-closure-notices.d.ts +27 -0
- package/src/lib/server/pos-hardware-closure-notices.js +85 -0
- package/src/lib/server/pos-hardware-closure-notices.js.map +1 -0
- package/src/lib/server/pos-hardware-closure.d.ts +60 -0
- package/src/lib/server/pos-hardware-closure.js +153 -0
- package/src/lib/server/pos-hardware-closure.js.map +1 -0
- package/src/lib/server/pos-hardware-in-person.d.ts +61 -0
- package/src/lib/server/pos-hardware-in-person.js +236 -0
- package/src/lib/server/pos-hardware-in-person.js.map +1 -0
- package/src/lib/server/pos-hardware-notify.d.ts +40 -2
- package/src/lib/server/pos-hardware-notify.js +195 -14
- package/src/lib/server/pos-hardware-notify.js.map +1 -1
- package/src/lib/server/pos-hardware-readers.d.ts +115 -0
- package/src/lib/server/pos-hardware-readers.js +430 -0
- package/src/lib/server/pos-hardware-readers.js.map +1 -0
- package/src/lib/server/pos-hardware-returns.d.ts +116 -0
- package/src/lib/server/pos-hardware-returns.js +502 -0
- package/src/lib/server/pos-hardware-returns.js.map +1 -0
- package/src/lib/server/pos-hardware-sale-fields.d.ts +28 -0
- package/src/lib/server/pos-hardware-sale-fields.js +44 -0
- package/src/lib/server/pos-hardware-sale-fields.js.map +1 -0
- package/src/lib/server/pos-hardware-sale.d.ts +54 -0
- package/src/lib/server/pos-hardware-sale.js +369 -0
- package/src/lib/server/pos-hardware-sale.js.map +1 -0
- package/src/lib/server/pos-hardware-tax-return-source.d.ts +99 -0
- package/src/lib/server/pos-hardware-tax-return-source.js +462 -0
- package/src/lib/server/pos-hardware-tax-return-source.js.map +1 -0
- package/src/lib/server/pos-hardware-terms.d.ts +64 -0
- package/src/lib/server/pos-hardware-terms.js +154 -0
- package/src/lib/server/pos-hardware-terms.js.map +1 -0
- package/src/lib/server/pos-hardware-user-eraser.d.ts +3 -0
- package/src/lib/server/pos-hardware-user-eraser.js +31 -7
- package/src/lib/server/pos-hardware-user-eraser.js.map +1 -1
- package/src/lib/server/pos-hardware-webhook.d.ts +5 -0
- package/src/lib/server/pos-hardware-webhook.js +51 -6
- package/src/lib/server/pos-hardware-webhook.js.map +1 -1
- package/src/lib/server/pos-hardware.d.ts +74 -1
- package/src/lib/server/pos-hardware.js +175 -10
- package/src/lib/server/pos-hardware.js.map +1 -1
- package/src/lib/server/pos-kiosk.d.ts +2 -0
- package/src/lib/server/pos-kiosk.js +17 -12
- package/src/lib/server/pos-kiosk.js.map +1 -1
- package/src/lib/server/pos-offline-card-order.d.ts +64 -0
- package/src/lib/server/pos-offline-card-order.js +102 -0
- package/src/lib/server/pos-offline-card-order.js.map +1 -0
- package/src/lib/server/pos-offline-card-reconcile.d.ts +67 -1
- package/src/lib/server/pos-offline-card-reconcile.js +215 -18
- package/src/lib/server/pos-offline-card-reconcile.js.map +1 -1
- package/src/lib/server/pos-offline-sync.d.ts +39 -0
- package/src/lib/server/pos-offline-sync.js +190 -55
- package/src/lib/server/pos-offline-sync.js.map +1 -1
- package/src/lib/server/pos-ops-gate.d.ts +28 -9
- package/src/lib/server/pos-ops-gate.js +50 -19
- package/src/lib/server/pos-ops-gate.js.map +1 -1
- package/src/lib/server/pos-ops-routes.js +24 -0
- package/src/lib/server/pos-ops-routes.js.map +1 -1
- package/src/lib/server/pos-order-refund-ids.d.ts +28 -0
- package/src/lib/server/pos-order-refund-ids.js +31 -0
- package/src/lib/server/pos-order-refund-ids.js.map +1 -0
- package/src/lib/server/pos-order.js +52 -49
- package/src/lib/server/pos-order.js.map +1 -1
- package/src/lib/server/pos-payment.js +1 -1
- package/src/lib/server/pos-payment.js.map +1 -1
- package/src/lib/server/pos-reader-splash-admin.js +30 -32
- package/src/lib/server/pos-reader-splash-admin.js.map +1 -1
- package/src/lib/server/pos-reader-splash-images-route.d.ts +17 -0
- package/src/lib/server/pos-reader-splash-images-route.js +113 -0
- package/src/lib/server/pos-reader-splash-images-route.js.map +1 -0
- package/src/lib/server/pos-reader-splash-images.d.ts +51 -0
- package/src/lib/server/pos-reader-splash-images.js +123 -0
- package/src/lib/server/pos-reader-splash-images.js.map +1 -0
- package/src/lib/server/pos-reader-splash.d.ts +68 -1
- package/src/lib/server/pos-reader-splash.js +182 -11
- package/src/lib/server/pos-reader-splash.js.map +1 -1
- package/src/lib/server/pos-readers.d.ts +29 -0
- package/src/lib/server/pos-readers.js +134 -35
- package/src/lib/server/pos-readers.js.map +1 -1
- package/src/lib/server/pos-refund-authority.d.ts +3 -1
- package/src/lib/server/pos-refund-authority.js +3 -2
- package/src/lib/server/pos-refund-authority.js.map +1 -1
- package/src/lib/server/pos-return.d.ts +10 -0
- package/src/lib/server/pos-return.js +29 -7
- package/src/lib/server/pos-return.js.map +1 -1
- package/src/lib/server/pos-sale.js +6 -1
- package/src/lib/server/pos-sale.js.map +1 -1
- package/src/lib/server/pos-shift.js +11 -0
- package/src/lib/server/pos-shift.js.map +1 -1
- package/src/lib/server/pos-staff-pin.js +29 -24
- package/src/lib/server/pos-staff-pin.js.map +1 -1
- package/src/lib/server/pos-terminal.d.ts +14 -4
- package/src/lib/server/pos-terminal.js +34 -38
- package/src/lib/server/pos-terminal.js.map +1 -1
- package/src/lib/server/printers.d.ts +7 -3
- package/src/lib/server/printers.js +18 -12
- package/src/lib/server/printers.js.map +1 -1
- package/src/lib/server/product-drafts.d.ts +11 -1
- package/src/lib/server/product-drafts.js +12 -8
- package/src/lib/server/product-drafts.js.map +1 -1
- package/src/lib/server/product.d.ts +15 -1
- package/src/lib/server/product.js +16 -3
- package/src/lib/server/product.js.map +1 -1
- package/src/lib/server/products-write.d.ts +26 -2
- package/src/lib/server/products-write.js +19 -7
- package/src/lib/server/products-write.js.map +1 -1
- package/src/lib/server/refund.js +55 -42
- package/src/lib/server/refund.js.map +1 -1
- package/src/lib/server/reservation-availability-query.d.ts +8 -0
- package/src/lib/server/reservation-availability-query.js +11 -1
- package/src/lib/server/reservation-availability-query.js.map +1 -1
- package/src/lib/server/reservation-availability.d.ts +1 -1
- package/src/lib/server/reservation-availability.js +7 -2
- package/src/lib/server/reservation-availability.js.map +1 -1
- package/src/lib/server/reservation-guests.d.ts +32 -0
- package/src/lib/server/reservation-guests.js +285 -0
- package/src/lib/server/reservation-guests.js.map +1 -0
- package/src/lib/server/reservation-walk-in.d.ts +11 -1
- package/src/lib/server/reservation-walk-in.js +56 -12
- package/src/lib/server/reservation-walk-in.js.map +1 -1
- package/src/lib/server/reserve.js +18 -5
- package/src/lib/server/reserve.js.map +1 -1
- package/src/lib/server/returns.js +9 -6
- package/src/lib/server/returns.js.map +1 -1
- package/src/lib/server/reviews.d.ts +22 -2
- package/src/lib/server/reviews.js +88 -20
- package/src/lib/server/reviews.js.map +1 -1
- package/src/lib/server/shipping-connectors.js +10 -11
- package/src/lib/server/shipping-connectors.js.map +1 -1
- package/src/lib/server/site-page-resolver.js +20 -3
- package/src/lib/server/site-page-resolver.js.map +1 -1
- package/src/lib/server/store-day.d.ts +33 -0
- package/src/lib/server/store-day.js +46 -0
- package/src/lib/server/store-day.js.map +1 -0
- package/src/lib/server/store-setup-ai-writer.d.ts +2 -1
- package/src/lib/server/store-setup-ai-writer.js +5 -6
- package/src/lib/server/store-setup-ai-writer.js.map +1 -1
- package/src/lib/server/storefront-tax.d.ts +13 -0
- package/src/lib/server/storefront-tax.js +8 -1
- package/src/lib/server/storefront-tax.js.map +1 -1
- package/src/lib/server/tax-return-source.d.ts +32 -3
- package/src/lib/server/tax-return-source.js +99 -20
- package/src/lib/server/tax-return-source.js.map +1 -1
- package/src/lib/server.js +100 -2
- package/src/lib/server.js.map +1 -1
- package/src/lib/testing/fake-firestore.js +58 -16
- package/src/lib/testing/fake-firestore.js.map +1 -1
- package/src/lib/testing/pos-ops-memory-firestore.js +18 -4
- package/src/lib/testing/pos-ops-memory-firestore.js.map +1 -1
- package/src/lib/testing/site-register-permissions.d.ts +29 -0
- package/src/lib/testing/site-register-permissions.js +38 -0
- package/src/lib/testing/site-register-permissions.js.map +1 -0
- package/src/lib/transfer/tracking.server.js +8 -5
- package/src/lib/transfer/tracking.server.js.map +1 -1
- package/src/lib/utils/add-to-cart.d.ts +2 -0
- package/src/lib/utils/add-to-cart.js +6 -3
- package/src/lib/utils/add-to-cart.js.map +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/reviews.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 { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { consumeRateLimit, firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { readClientIp } from '@aglyn/aglyn/app-utils/request-ip'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { AggregateField } from 'firebase-admin/firestore'\nimport {\n MEMBER_SUSPENDED_ERROR,\n readActiveMemberSession,\n type MemberSessionCheck,\n} from './membership'\nimport { inspectReviewToken, redeemReviewTokenIn } from './review-tokens'\n\nexport interface PublicProductReview {\n id: string\n rating: number\n body: string\n authorName: string\n verified: boolean\n reply: string | null\n createdAtMs: number\n}\n\nexport interface ProductReviewAggregate {\n count: number\n /** Mean rating, one decimal. */\n average: number\n}\n\n/**\n * Approved reviews for a product, newest first, plus their aggregate.\n *\n * Extracted from the handler (AGL-686) so the site-page resolver can nest\n * `aggregateRating` inside the server-rendered `Product` node. Schema.org\n * wants it as a PROPERTY of the product; the reviews block was emitting a\n * free-standing `AggregateRating` node alongside it, which is orphaned and\n * ignored. One reader means the rating in the structured data and the rating\n * on the page cannot disagree.\n */\nexport async function readProductReviews(\n hostId: string,\n productId: string,\n): Promise<{\n reviews: PublicProductReview[]\n aggregate: ProductReviewAggregate\n}> {\n const hostRef = firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n const reviewsSnapshot = await hostRef\n .collection('reviews')\n .where('productId', '==', productId)\n .where('status', '==', 'approved')\n .limit(100)\n .get()\n const reviews = reviewsSnapshot.docs\n .map((docSnapshot) => ({\n id: docSnapshot.id,\n rating: Number(docSnapshot.get('rating') ?? 0),\n body: String(docSnapshot.get('body') ?? ''),\n authorName: String(docSnapshot.get('authorName') ?? 'Anonymous'),\n verified: Boolean(docSnapshot.get('verified')),\n reply: (docSnapshot.get('reply') as string | undefined) ?? null,\n createdAtMs: Number(docSnapshot.get('createdAtMs') ?? 0),\n }))\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n const count = reviews.length\n const average = count\n ? reviews.reduce((sum, review) => sum + review.rating, 0) / count\n : 0\n return {\n reviews,\n aggregate: { count, average: Math.round(average * 10) / 10 },\n }\n}\n\n/** One approved review as the store-wide list shows it. */\nexport interface PublicStoreReview {\n id: string\n rating: number\n body: string\n /** First name and last initial only (\"Jane D.\"), never the full name. */\n authorName: string\n verified: boolean\n createdAtMs: number\n /** The reviewed product, when it is still on sale. */\n productName?: string\n productSlug?: string\n}\n\n/** The most reviews the store-wide list returns. */\nexport const STORE_REVIEWS_MAX = 12\n/** What the store-wide list returns when the caller names no limit. */\nexport const STORE_REVIEWS_DEFAULT = 6\n\n/** \"Jane Doe\" → \"Jane D.\"; one word stays as it is. */\nexport function reviewerShortName(name: string): string {\n const words = String(name ?? '').trim().split(/\\s+/).filter(Boolean)\n if (!words.length) return 'Anonymous'\n const [first, ...others] = words\n const last = others[others.length - 1]\n return last ? `${first} ${last[0].toUpperCase()}.` : first\n}\n\n/**\n * The store's latest APPROVED reviews across every product, newest first, with\n * the aggregate over all of them — the store-wide reviews band a storefront\n * home carries. Approved only: a pending or rejected review never reaches a\n * page, and nothing here is ever sample text (a published page shows real\n * reviews or none).\n *\n * The list is `status == approved` ordered by `createdAtMs desc`, served by\n * the COLLECTION composite `reviews (status ASC, createdAtMs DESC)` that the\n * moderation queue already declares; the aggregate is a count and an average\n * over the same equality, which the automatic single-field index serves.\n */\nexport async function readStoreReviews(\n hostId: string,\n limit: number = STORE_REVIEWS_DEFAULT,\n): Promise<{ reviews: PublicStoreReview[]; aggregate: ProductReviewAggregate }> {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const approved = hostRef.collection('reviews').where('status', '==', 'approved')\n const max = Math.min(STORE_REVIEWS_MAX, Math.max(1, Math.floor(Number(limit)) || STORE_REVIEWS_DEFAULT))\n const [listSnapshot, aggregateSnapshot] = await Promise.all([\n approved.orderBy('createdAtMs', 'desc').limit(max).get(),\n approved\n .aggregate({ count: AggregateField.count(), average: AggregateField.average('rating') })\n .get(),\n ])\n const rows = listSnapshot.docs.map((docSnapshot) => ({\n id: docSnapshot.id,\n productId: String(docSnapshot.get('productId') ?? ''),\n rating: Number(docSnapshot.get('rating') ?? 0),\n body: String(docSnapshot.get('body') ?? ''),\n authorName: reviewerShortName(String(docSnapshot.get('authorName') ?? '')),\n verified: Boolean(docSnapshot.get('verified')),\n createdAtMs: Number(docSnapshot.get('createdAtMs') ?? 0),\n }))\n // The reviewed products' names, in one batched read of at most `max` docs.\n const productIds = [...new Set(rows.map((row) => row.productId).filter(Boolean))]\n const products = new Map<string, { name: string; slug: string }>()\n if (productIds.length) {\n const snapshots = await firestore.getAll(\n ...productIds.map((id) => hostRef.collection('products').doc(id)),\n )\n for (const snapshot of snapshots) {\n const data = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n // A product sold in person only (AGL-4060) is named and linked nowhere\n // online, as a draft is not.\n if (!CommerceModel.productListedOnline(data)) continue\n products.set(snapshot.id, { name: String(data['name'] ?? ''), slug: String(data['slug'] ?? '') })\n }\n }\n const reviews = rows.map(({ productId, ...row }) => {\n const product = products.get(productId)\n return {\n ...row,\n ...(product?.name ? { productName: product.name } : {}),\n ...(product?.slug ? { productSlug: product.slug } : {}),\n }\n })\n const data = aggregateSnapshot.data() as { count?: number; average?: number | null }\n const count = Number(data.count ?? 0)\n const average = count ? Number(data.average ?? 0) : 0\n return { reviews, aggregate: { count, average: Math.round(average * 10) / 10 } }\n}\n\n/** Reviews one address may submit per site, per hour (AGL-3736). */\nexport const REVIEW_POSTS_PER_IP_PER_HOUR = 10\n/** Reviews one signed-in member may submit per site, per day. */\nexport const REVIEW_POSTS_PER_MEMBER_PER_DAY = 10\nconst HOUR_MS = 60 * 60 * 1000\n\n/**\n * The address a signed-in member has PROVEN they own, or `''`.\n *\n * A site member signs up by typing an email nobody checks, so the address on\n * the member document alone would let anyone register with a buyer's address\n * and collect a \"Verified buyer\" badge on their purchases. Only a member\n * whose inbox has answered — `emailVerifiedAtMs`, stamped when they complete\n * a password reset from the emailed link or redeem a review link while signed\n * in — counts.\n */\nexport function memberProvenEmail(\n member: FirebaseFirestore.DocumentSnapshot | null | undefined,\n): string {\n if (!member?.exists) return ''\n if (!Number(member.get('emailVerifiedAtMs') ?? 0)) return ''\n return String(member.get('email') ?? '').trim().toLowerCase()\n}\n\n/** The first order under `email` that still entitles `productId`, or null. */\nexport async function entitlingOrderId(\n hostRef: FirebaseFirestore.DocumentReference,\n email: string,\n productId: string,\n): Promise<string | null> {\n if (!email) return null\n // By the member's email, as the account page matches order history.\n const orders = await hostRef\n .collection('orders')\n .where('customerEmail', '==', email)\n .limit(25)\n .get()\n for (const docSnapshot of orders.docs) {\n const order = CommerceModel.liftLegacyOrder(docSnapshot.data() as any)\n // The shared entitlement test (AGL-2454): a line refunded by name is\n // goods the buyer no longer has.\n if (CommerceModel.orderEntitlesProduct(order, productId)) return docSnapshot.id\n }\n return null\n}\n\n/** A store's review settings, from `settings/store`. */\nasync function readReviewSettings(\n hostRef: FirebaseFirestore.DocumentReference,\n): Promise<CommerceModel.ReviewSettings> {\n const snapshot = await hostRef\n .collection('settings')\n .doc('store')\n .get()\n .catch(() => null)\n return CommerceModel.reviewSettingsFrom(snapshot?.data?.() ?? null)\n}\n\ntype ReviewsRequest = Parameters<PluginApiHandler>[0]\ntype ReviewsResponse = Parameters<PluginApiHandler>[1]\n\nfunction clientIp(req: ReviewsRequest): string {\n return (\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n 'no-address'\n )\n}\n\n/** What the server established about who is reviewing. */\ninterface ReviewerIdentity {\n signedIn: boolean\n suspended: boolean\n memberId: string | null\n memberEmail: string\n memberName: string\n /** The member's proven email holds an order that entitles the product. */\n memberOrderId: string | null\n}\n\nconst ANONYMOUS_REVIEWER: ReviewerIdentity = {\n signedIn: false,\n suspended: false,\n memberId: null,\n memberEmail: '',\n memberName: '',\n memberOrderId: null,\n}\n\nasync function identifyReviewer(\n req: ReviewsRequest,\n hostRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n productId: string,\n): Promise<ReviewerIdentity> {\n const session: MemberSessionCheck = await readActiveMemberSession(req, hostId).catch(\n () => ({ status: 'anonymous' }) as MemberSessionCheck,\n )\n if (session.status === 'suspended') {\n return { ...ANONYMOUS_REVIEWER, signedIn: true, suspended: true, memberId: session.memberId }\n }\n if (session.status !== 'active') return ANONYMOUS_REVIEWER\n const proven = memberProvenEmail(session.member)\n return {\n signedIn: true,\n suspended: false,\n memberId: session.memberId,\n memberEmail: String(session.member.get('email') ?? '').trim().toLowerCase(),\n memberName: String(session.member.get('displayName') ?? '').trim(),\n memberOrderId: proven ? await entitlingOrderId(hostRef, proven, productId) : null,\n }\n}\n\n/**\n * The storefront form's question (AGL-3736): may this visitor review this\n * product, and would the review be verified? `GET ?mode=eligibility`, never\n * cached — the answer depends on the visitor's cookie and link.\n */\nasync function eligibilityHandler(\n req: ReviewsRequest,\n res: ReviewsResponse,\n hostId: string,\n productId: string,\n) {\n res.setHeader('Cache-Control', 'private, no-store')\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const org = await getOrgForHost(hostId)\n const enabled = Aglyn.checkEntitlement(org?.org as any, 'productReviews')\n const settings = await readReviewSettings(hostRef)\n const reviewer = await identifyReviewer(req, hostRef, hostId, productId)\n const presented = req.query.review\n let tokenVerified = false\n let tokenProblem: CommerceModel.ReviewErrorCode | undefined\n if (presented) {\n const check = await inspectReviewToken(firestore, hostId, presented, productId)\n if (check.ok) tokenVerified = true\n else tokenProblem = check.code ?? CommerceModel.REVIEW_ERROR_CODES.tokenInvalid\n }\n const eligibility = CommerceModel.reviewEligibility({\n policy: settings.policy,\n signedIn: reviewer.signedIn && !reviewer.suspended,\n memberVerified: Boolean(reviewer.memberOrderId),\n tokenVerified,\n tokenProblem,\n })\n return res.status(200).json({\n ...eligibility,\n enabled,\n canReview: enabled && !reviewer.suspended && eligibility.canReview,\n })\n}\n\n/**\n * Product reviews (AGL-324, AGL-3736).\n *\n * GET returns approved reviews + aggregate (`scope=store`: store-wide;\n * `mode=eligibility`: what the form may do). POST submits one review:\n *\n * - VERIFIED when the reviewer presents an unspent, unexpired review link\n * for this product, or is a signed-in member whose proven email has an\n * order that still entitles it. Published at once (`approved`): the FTC's\n * 2024 rule bars suppressing honest negative reviews; the merchant may\n * still hide or reject one for a content reason.\n * - Otherwise, under the store's `verified` policy (the default), 403\n * `verified_purchase_required`; under `anyone`, saved `pending` with the\n * name and email given — the email is never shown and verifies nothing.\n *\n * Rate-limited per address and per member, durably. Plan-gated\n * (`productReviews`).\n */\nexport const reviewsHandler: PluginApiHandler = async (req, res) => {\n const isPost = req.method === 'POST'\n const hostId = String((isPost ? req.body?.hostId : req.query.hostId) ?? '')\n const productId = String(\n (isPost ? req.body?.productId : req.query.productId) ?? '',\n )\n // Store-wide (`scope=store`, no productId): the latest approved reviews\n // across the store, for the storefront's reviews band. Read only, and only\n // where the store's plan has reviews — a store without the feature shows\n // no band rather than reviews it can no longer collect.\n if (!isPost && !productId && req.query.scope === 'store') {\n if (!hostId) return res.status(400).json({ error: 'Missing hostId' })\n try {\n const org = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(org?.org as any, 'productReviews')) {\n return res.status(200).json({ reviews: [], aggregate: { count: 0, average: 0 } })\n }\n const { reviews, aggregate } = await readStoreReviews(hostId, Number(req.query.limit))\n res.setHeader('Cache-Control', 'public, s-maxage=120, stale-while-revalidate=300')\n return res.status(200).json({ reviews, aggregate })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Reviews unavailable' })\n }\n }\n if (!hostId || !productId) {\n return res.status(400).json({ error: 'Missing hostId or productId' })\n }\n try {\n if (isPost) return await submitReview(req, res, hostId, productId)\n if (req.query.mode === 'eligibility') {\n return await eligibilityHandler(req, res, hostId, productId)\n }\n const { reviews, aggregate } = await readProductReviews(hostId, productId)\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=120, stale-while-revalidate=300',\n )\n return res.status(200).json({ reviews, aggregate })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Reviews unavailable' })\n }\n}\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nasync function submitReview(\n req: ReviewsRequest,\n res: ReviewsResponse,\n hostId: string,\n productId: string,\n) {\n const codes = CommerceModel.REVIEW_ERROR_CODES\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const org = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(org?.org as any, 'productReviews')) {\n return res.status(403).json({ error: 'Reviews are not enabled', code: codes.notEnabled })\n }\n const refuseRate = (resetMs: number) => {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((resetMs - Date.now()) / 1000))),\n )\n return res.status(429).json({\n error: 'Too many reviews from here. Try again later.',\n code: codes.rateLimited,\n })\n }\n // Per address, before anything else is read.\n const perIp = await consumeRateLimit(`commerce-review:ip:${hostId}:${clientIp(req)}`, {\n limit: REVIEW_POSTS_PER_IP_PER_HOUR,\n windowMs: HOUR_MS,\n })\n if (!perIp.allowed) return refuseRate(perIp.resetMs)\n\n const rating = Math.min(5, Math.max(1, Math.round(Number(req.body?.rating ?? 0))))\n const body = String(req.body?.body ?? '').trim().slice(0, 2000)\n const typedName = String(req.body?.authorName ?? '').trim().slice(0, 80)\n if (!rating || !body) {\n return res.status(400).json({ error: 'A rating and a review are required' })\n }\n\n const settings = await readReviewSettings(hostRef)\n const reviewer = await identifyReviewer(req, hostRef, hostId, productId)\n if (reviewer.suspended) {\n return res.status(403).json({ error: MEMBER_SUSPENDED_ERROR, code: codes.memberSuspended })\n }\n if (reviewer.memberId) {\n const perMember = await consumeRateLimit(\n `commerce-review:member:${hostId}:${reviewer.memberId}`,\n { limit: REVIEW_POSTS_PER_MEMBER_PER_DAY, windowMs: 24 * HOUR_MS },\n )\n if (!perMember.allowed) return refuseRate(perMember.resetMs)\n }\n\n // 1. A review link from the email. A link that does not work is an error,\n // never a silent fall back to an unverified review.\n const presented = req.body?.reviewToken\n if (presented) {\n const check = await inspectReviewToken(firestore, hostId, presented, productId)\n if (!check.ok || !check.record || !check.ref) {\n const code = check.code ?? codes.tokenInvalid\n return res.status(403).json({ error: tokenProblemMessage(code), code })\n }\n const reviewRef = hostRef.collection('reviews').doc(createResourceUid())\n const nowMs = Date.now()\n const problem = await firestore.runTransaction(async (transaction) => {\n const spent = await redeemReviewTokenIn(transaction, check.ref, productId, reviewRef.id, nowMs)\n if (spent) return spent\n transaction.create(reviewRef, {\n productId,\n rating,\n body,\n authorName: typedName || reviewer.memberName || 'Anonymous',\n authorEmail: check.record.email,\n ...(reviewer.memberId ? { memberId: reviewer.memberId } : {}),\n verified: true,\n verifiedVia: 'token',\n orderId: check.record.orderId,\n status: CommerceModel.initialReviewStatus(true),\n createdAtMs: nowMs,\n })\n return null\n })\n if (problem) {\n return res.status(403).json({ error: tokenProblemMessage(problem), code: problem })\n }\n // The link came through the inbox, so a member signed in with the same\n // address has now proven it.\n if (reviewer.memberId && reviewer.memberEmail === check.record.email) {\n await hostRef\n .collection('siteMembers')\n .doc(reviewer.memberId)\n .set({ emailVerifiedAtMs: nowMs }, { merge: true })\n .catch(() => undefined)\n }\n return res.status(200).json({ ok: true, pending: false, verified: true })\n }\n\n // 2. A signed-in member whose proven email bought it. One verified review\n // per member per product.\n if (reviewer.memberOrderId) {\n const already = await hostRef\n .collection('reviews')\n .where('productId', '==', productId)\n .where('memberId', '==', reviewer.memberId)\n .limit(1)\n .get()\n if (!already.empty) {\n return res.status(409).json({\n error: 'You have already reviewed this product.',\n code: codes.alreadyReviewed,\n })\n }\n await hostRef.collection('reviews').doc(createResourceUid()).set({\n productId,\n rating,\n body,\n authorName: typedName || reviewer.memberName || 'Anonymous',\n authorEmail: reviewer.memberEmail,\n memberId: reviewer.memberId,\n verified: true,\n verifiedVia: 'member',\n orderId: reviewer.memberOrderId,\n status: CommerceModel.initialReviewStatus(true),\n createdAtMs: Date.now(),\n })\n return res.status(200).json({ ok: true, pending: false, verified: true })\n }\n\n // 3. Nobody the store can verify.\n if (settings.policy !== 'anyone') {\n return res.status(403).json({\n error: 'Only customers who bought this can review it.',\n code: codes.verifiedPurchaseRequired,\n })\n }\n const authorEmail = reviewer.signedIn\n ? reviewer.memberEmail\n : String(req.body?.authorEmail ?? '').trim().toLowerCase().slice(0, 120)\n const authorName = typedName || reviewer.memberName\n if (!authorName || !EMAIL_PATTERN.test(authorEmail)) {\n return res.status(400).json({ error: 'Your name and email are required' })\n }\n await hostRef.collection('reviews').doc(createResourceUid()).set({\n productId,\n rating,\n body,\n authorName,\n // Kept for the merchant; never shown, and it verifies nothing.\n authorEmail,\n ...(reviewer.memberId ? { memberId: reviewer.memberId } : {}),\n verified: false,\n status: CommerceModel.initialReviewStatus(false),\n createdAtMs: Date.now(),\n })\n return res.status(200).json({ ok: true, pending: true, verified: false })\n}\n\n/** The words for a review link that does not work. */\nexport function tokenProblemMessage(code: CommerceModel.ReviewErrorCode): string {\n switch (code) {\n case CommerceModel.REVIEW_ERROR_CODES.tokenUsed:\n return 'This review link has already been used.'\n case CommerceModel.REVIEW_ERROR_CODES.tokenExpired:\n return 'This review link has expired.'\n default:\n return 'This review link is not valid for this product.'\n }\n}\n"],"names":["Aglyn","CommerceModel","consumeRateLimit","firebaseAdmin","getOrgForHost","readClientIp","createResourceUid","AggregateField","MEMBER_SUSPENDED_ERROR","readActiveMemberSession","inspectReviewToken","redeemReviewTokenIn","readProductReviews","hostId","productId","hostRef","app","firestore","collection","doc","reviewsSnapshot","where","limit","get","reviews","docs","map","docSnapshot","id","rating","Number","body","String","authorName","verified","Boolean","reply","createdAtMs","sort","a","b","count","length","average","reduce","sum","review","aggregate","Math","round","STORE_REVIEWS_MAX","STORE_REVIEWS_DEFAULT","reviewerShortName","name","words","trim","split","filter","first","others","last","toUpperCase","readStoreReviews","data","approved","max","min","floor","listSnapshot","aggregateSnapshot","Promise","all","orderBy","rows","productIds","Set","row","products","Map","snapshots","getAll","snapshot","exists","productListedOnline","set","slug","product","productName","productSlug","REVIEW_POSTS_PER_IP_PER_HOUR","REVIEW_POSTS_PER_MEMBER_PER_DAY","HOUR_MS","memberProvenEmail","member","toLowerCase","entitlingOrderId","email","orders","order","liftLegacyOrder","orderEntitlesProduct","readReviewSettings","catch","reviewSettingsFrom","clientIp","req","headers","remoteAddress","socket","ANONYMOUS_REVIEWER","signedIn","suspended","memberId","memberEmail","memberName","memberOrderId","identifyReviewer","session","status","proven","eligibilityHandler","res","setHeader","org","enabled","checkEntitlement","settings","reviewer","presented","query","tokenVerified","tokenProblem","check","ok","code","REVIEW_ERROR_CODES","tokenInvalid","eligibility","reviewEligibility","policy","memberVerified","json","canReview","reviewsHandler","isPost","method","scope","error","console","submitReview","mode","EMAIL_PATTERN","codes","notEnabled","refuseRate","resetMs","ceil","Date","now","rateLimited","perIp","windowMs","allowed","slice","typedName","memberSuspended","perMember","reviewToken","record","ref","tokenProblemMessage","reviewRef","nowMs","problem","runTransaction","transaction","spent","create","authorEmail","verifiedVia","orderId","initialReviewStatus","emailVerifiedAtMs","merge","undefined","pending","already","empty","alreadyReviewed","verifiedPurchaseRequired","test","tokenUsed","tokenExpired"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/reviews.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 { PluginApiHandler } from '@aglyn/aglyn/server'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport * as CommerceModel from '../model'\nimport { consumeRateLimit, firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport { readClientIp } from '@aglyn/aglyn/app-utils/request-ip'\nimport { createResourceUid } from '@aglyn/aglyn/app-utils/create-resource-uid'\nimport { AggregateField } from 'firebase-admin/firestore'\nimport {\n MEMBER_SUSPENDED_ERROR,\n readActiveMemberSession,\n type MemberSessionCheck,\n} from './membership'\nimport { inspectReviewToken, redeemReviewTokenIn } from './review-tokens'\n\nexport interface PublicProductReview {\n id: string\n rating: number\n body: string\n authorName: string\n verified: boolean\n reply: string | null\n createdAtMs: number\n}\n\nexport interface ProductReviewAggregate {\n count: number\n /** Mean rating, one decimal. */\n average: number\n}\n\n/**\n * Approved reviews for a product, newest first, plus their aggregate.\n *\n * Extracted from the handler (AGL-686) so the site-page resolver can nest\n * `aggregateRating` inside the server-rendered `Product` node. Schema.org\n * wants it as a PROPERTY of the product; the reviews block was emitting a\n * free-standing `AggregateRating` node alongside it, which is orphaned and\n * ignored. One reader means the rating in the structured data and the rating\n * on the page cannot disagree.\n */\nexport async function readProductReviews(\n hostId: string,\n productId: string,\n): Promise<{\n reviews: PublicProductReview[]\n aggregate: ProductReviewAggregate\n}> {\n const hostRef = firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n const reviewsSnapshot = await hostRef\n .collection('reviews')\n .where('productId', '==', productId)\n .where('status', '==', 'approved')\n .limit(100)\n .get()\n const reviews = reviewsSnapshot.docs\n .map((docSnapshot) => ({\n id: docSnapshot.id,\n rating: Number(docSnapshot.get('rating') ?? 0),\n body: String(docSnapshot.get('body') ?? ''),\n authorName: String(docSnapshot.get('authorName') ?? 'Anonymous'),\n verified: Boolean(docSnapshot.get('verified')),\n reply: (docSnapshot.get('reply') as string | undefined) ?? null,\n createdAtMs: Number(docSnapshot.get('createdAtMs') ?? 0),\n }))\n .sort((a, b) => b.createdAtMs - a.createdAtMs)\n const count = reviews.length\n const average = count\n ? reviews.reduce((sum, review) => sum + review.rating, 0) / count\n : 0\n return {\n reviews,\n aggregate: { count, average: Math.round(average * 10) / 10 },\n }\n}\n\n/** One approved review as the store-wide list shows it. */\nexport interface PublicStoreReview {\n id: string\n rating: number\n body: string\n /** First name and last initial only (\"Jane D.\"), never the full name. */\n authorName: string\n verified: boolean\n createdAtMs: number\n /** The reviewed product, when it is still on sale. */\n productName?: string\n /** Its page, when it is sold online; a product sold in person only has none. */\n productSlug?: string\n /**\n * The product is sold in person only and the store shows its reviews\n * (AGL-4060): the band marks it \"Sold in store\" and links nowhere.\n */\n soldInStore?: true\n}\n\n/** The most reviews the store-wide list returns. */\nexport const STORE_REVIEWS_MAX = 12\n/** What the store-wide list returns when the caller names no limit. */\nexport const STORE_REVIEWS_DEFAULT = 6\n\n/** \"Jane Doe\" → \"Jane D.\"; one word stays as it is. */\nexport function reviewerShortName(name: string): string {\n const words = String(name ?? '').trim().split(/\\s+/).filter(Boolean)\n if (!words.length) return 'Anonymous'\n const [first, ...others] = words\n const last = others[others.length - 1]\n return last ? `${first} ${last[0].toUpperCase()}.` : first\n}\n\n/** The most products a store may keep its in-store reviews off the site for, read at once. */\nexport const HIDDEN_REVIEW_PRODUCTS_MAX = 500\n/** How many pages of the newest reviews the band reads to fill itself past hidden ones. */\nconst STORE_REVIEWS_PAGES = 4\n/** Firestore's ceiling on the values of one `in` filter. */\nconst IN_FILTER_MAX = 30\n\n/**\n * The products sold in person only whose reviews the store keeps off the site\n * (AGL-4060, `inStoreReviews: 'hide'`). Two equalities, which Firestore\n * serves from its automatic single-field indexes.\n */\nexport async function readHiddenReviewProductIds(\n hostRef: FirebaseFirestore.DocumentReference,\n): Promise<Set<string>> {\n const snapshot = await hostRef\n .collection('products')\n .where('sellOnline', '==', false)\n .where('inStoreReviews', '==', 'hide')\n .limit(HIDDEN_REVIEW_PRODUCTS_MAX)\n .get()\n return new Set(snapshot.docs.map((doc) => doc.id))\n}\n\n/**\n * The store's latest APPROVED reviews across every product, newest first, with\n * the aggregate over all of them — the store-wide reviews band a storefront\n * home carries. Approved only: a pending or rejected review never reaches a\n * page, and nothing here is ever sample text (a published page shows real\n * reviews or none).\n *\n * A product sold in person only (AGL-4060) shows its reviews here by default,\n * named, marked `soldInStore` and never linked, since it has no page; one\n * whose store chose to hide them (`inStoreReviews: 'hide'`) is in neither the\n * list nor the aggregate.\n *\n * The list is `status == approved` ordered by `createdAtMs desc`, served by\n * the COLLECTION composite `reviews (status ASC, createdAtMs DESC)` that the\n * moderation queue already declares, read a page at a time past hidden ones;\n * the aggregate is a count and a sum over the same equality, less the hidden\n * products' (`productId in …`, at most 30 at a time).\n */\nexport async function readStoreReviews(\n hostId: string,\n limit: number = STORE_REVIEWS_DEFAULT,\n): Promise<{ reviews: PublicStoreReview[]; aggregate: ProductReviewAggregate }> {\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const approved = hostRef.collection('reviews').where('status', '==', 'approved')\n const max = Math.min(STORE_REVIEWS_MAX, Math.max(1, Math.floor(Number(limit)) || STORE_REVIEWS_DEFAULT))\n const hidden = await readHiddenReviewProductIds(hostRef)\n const hiddenIds = [...hidden]\n const tally = { count: AggregateField.count(), sum: AggregateField.sum('rating') }\n const aggregates = Promise.all([\n approved.aggregate(tally).get(),\n ...Array.from({ length: Math.ceil(hiddenIds.length / IN_FILTER_MAX) }, (_, chunk) =>\n approved\n .where('productId', 'in', hiddenIds.slice(chunk * IN_FILTER_MAX, (chunk + 1) * IN_FILTER_MAX))\n .aggregate(tally)\n .get(),\n ),\n ])\n\n // The newest approved reviews, a page at a time, past any the store hides.\n const docs: FirebaseFirestore.QueryDocumentSnapshot[] = []\n let after: FirebaseFirestore.QueryDocumentSnapshot | null = null\n for (let page = 0; page < STORE_REVIEWS_PAGES && docs.length < max; page += 1) {\n let query = approved.orderBy('createdAtMs', 'desc')\n if (after) query = query.startAfter(after)\n const snapshot = await query.limit(max).get()\n docs.push(...snapshot.docs.filter((doc) => !hidden.has(String(doc.get('productId') ?? ''))))\n if (snapshot.docs.length < max || !hidden.size) break\n after = snapshot.docs[snapshot.docs.length - 1]\n }\n const rows = docs.slice(0, max).map((docSnapshot) => ({\n id: docSnapshot.id,\n productId: String(docSnapshot.get('productId') ?? ''),\n rating: Number(docSnapshot.get('rating') ?? 0),\n body: String(docSnapshot.get('body') ?? ''),\n authorName: reviewerShortName(String(docSnapshot.get('authorName') ?? '')),\n verified: Boolean(docSnapshot.get('verified')),\n createdAtMs: Number(docSnapshot.get('createdAtMs') ?? 0),\n }))\n // The reviewed products, in one batched read of at most `max` docs.\n const productIds = [...new Set(rows.map((row) => row.productId).filter(Boolean))]\n const products = new Map<string, { name: string; slug: string | null; online: CommerceModel.ProductReviewsOnline }>()\n if (productIds.length) {\n const snapshots = await firestore.getAll(\n ...productIds.map((id) => hostRef.collection('products').doc(id)),\n )\n for (const snapshot of snapshots) {\n const data = snapshot.exists ? (snapshot.data() as Record<string, unknown>) : null\n const online = CommerceModel.productReviewsOnline(data)\n // A draft or a deleted product is named nowhere, as before.\n if (!online) continue\n products.set(snapshot.id, {\n name: String(data['name'] ?? ''),\n // Sold in person only: no page to link to.\n slug: online === 'listed' ? String(data['slug'] ?? '') : null,\n online,\n })\n }\n }\n const reviews = rows\n // A product hidden since the list was read is still never shown.\n .filter((row) => products.get(row.productId)?.online !== 'hidden')\n .map(({ productId, ...row }) => {\n const product = products.get(productId)\n return {\n ...row,\n ...(product?.name ? { productName: product.name } : {}),\n ...(product?.slug ? { productSlug: product.slug } : {}),\n ...(product?.online === 'sold-in-store' ? { soldInStore: true as const } : {}),\n }\n })\n const [all, ...hiddenTallies] = await aggregates\n const counted = (snapshot: { data: () => unknown }) => {\n const data = snapshot.data() as { count?: number; sum?: number | null }\n return { count: Number(data.count ?? 0), sum: Number(data.sum ?? 0) }\n }\n const total = counted(all)\n for (const tallied of hiddenTallies.map(counted)) {\n total.count -= tallied.count\n total.sum -= tallied.sum\n }\n const count = Math.max(0, total.count)\n const average = count ? total.sum / count : 0\n return { reviews, aggregate: { count, average: Math.round(average * 10) / 10 } }\n}\n\n/** Reviews one address may submit per site, per hour (AGL-3736). */\nexport const REVIEW_POSTS_PER_IP_PER_HOUR = 10\n/** Reviews one signed-in member may submit per site, per day. */\nexport const REVIEW_POSTS_PER_MEMBER_PER_DAY = 10\nconst HOUR_MS = 60 * 60 * 1000\n\n/**\n * The address a signed-in member has PROVEN they own, or `''`.\n *\n * A site member signs up by typing an email nobody checks, so the address on\n * the member document alone would let anyone register with a buyer's address\n * and collect a \"Verified buyer\" badge on their purchases. Only a member\n * whose inbox has answered — `emailVerifiedAtMs`, stamped when they complete\n * a password reset from the emailed link or redeem a review link while signed\n * in — counts.\n */\nexport function memberProvenEmail(\n member: FirebaseFirestore.DocumentSnapshot | null | undefined,\n): string {\n if (!member?.exists) return ''\n if (!Number(member.get('emailVerifiedAtMs') ?? 0)) return ''\n return String(member.get('email') ?? '').trim().toLowerCase()\n}\n\n/** The first order under `email` that still entitles `productId`, or null. */\nexport async function entitlingOrderId(\n hostRef: FirebaseFirestore.DocumentReference,\n email: string,\n productId: string,\n): Promise<string | null> {\n if (!email) return null\n // By the member's email, as the account page matches order history.\n const orders = await hostRef\n .collection('orders')\n .where('customerEmail', '==', email)\n .limit(25)\n .get()\n for (const docSnapshot of orders.docs) {\n const order = CommerceModel.liftLegacyOrder(docSnapshot.data() as any)\n // The shared entitlement test (AGL-2454): a line refunded by name is\n // goods the buyer no longer has.\n if (CommerceModel.orderEntitlesProduct(order, productId)) return docSnapshot.id\n }\n return null\n}\n\n/** A store's review settings, from `settings/store`. */\nasync function readReviewSettings(\n hostRef: FirebaseFirestore.DocumentReference,\n): Promise<CommerceModel.ReviewSettings> {\n const snapshot = await hostRef\n .collection('settings')\n .doc('store')\n .get()\n .catch(() => null)\n return CommerceModel.reviewSettingsFrom(snapshot?.data?.() ?? null)\n}\n\ntype ReviewsRequest = Parameters<PluginApiHandler>[0]\ntype ReviewsResponse = Parameters<PluginApiHandler>[1]\n\nfunction clientIp(req: ReviewsRequest): string {\n return (\n readClientIp(req.headers, { remoteAddress: req.socket?.remoteAddress }) ??\n 'no-address'\n )\n}\n\n/** What the server established about who is reviewing. */\ninterface ReviewerIdentity {\n signedIn: boolean\n suspended: boolean\n memberId: string | null\n memberEmail: string\n memberName: string\n /** The member's proven email holds an order that entitles the product. */\n memberOrderId: string | null\n}\n\nconst ANONYMOUS_REVIEWER: ReviewerIdentity = {\n signedIn: false,\n suspended: false,\n memberId: null,\n memberEmail: '',\n memberName: '',\n memberOrderId: null,\n}\n\nasync function identifyReviewer(\n req: ReviewsRequest,\n hostRef: FirebaseFirestore.DocumentReference,\n hostId: string,\n productId: string,\n): Promise<ReviewerIdentity> {\n const session: MemberSessionCheck = await readActiveMemberSession(req, hostId).catch(\n () => ({ status: 'anonymous' }) as MemberSessionCheck,\n )\n if (session.status === 'suspended') {\n return { ...ANONYMOUS_REVIEWER, signedIn: true, suspended: true, memberId: session.memberId }\n }\n if (session.status !== 'active') return ANONYMOUS_REVIEWER\n const proven = memberProvenEmail(session.member)\n return {\n signedIn: true,\n suspended: false,\n memberId: session.memberId,\n memberEmail: String(session.member.get('email') ?? '').trim().toLowerCase(),\n memberName: String(session.member.get('displayName') ?? '').trim(),\n memberOrderId: proven ? await entitlingOrderId(hostRef, proven, productId) : null,\n }\n}\n\n/**\n * The storefront form's question (AGL-3736): may this visitor review this\n * product, and would the review be verified? `GET ?mode=eligibility`, never\n * cached — the answer depends on the visitor's cookie and link.\n */\nasync function eligibilityHandler(\n req: ReviewsRequest,\n res: ReviewsResponse,\n hostId: string,\n productId: string,\n) {\n res.setHeader('Cache-Control', 'private, no-store')\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const org = await getOrgForHost(hostId)\n const enabled = Aglyn.checkEntitlement(org?.org as any, 'productReviews')\n const settings = await readReviewSettings(hostRef)\n const reviewer = await identifyReviewer(req, hostRef, hostId, productId)\n const presented = req.query.review\n let tokenVerified = false\n let tokenProblem: CommerceModel.ReviewErrorCode | undefined\n if (presented) {\n const check = await inspectReviewToken(firestore, hostId, presented, productId)\n if (check.ok) tokenVerified = true\n else tokenProblem = check.code ?? CommerceModel.REVIEW_ERROR_CODES.tokenInvalid\n }\n const eligibility = CommerceModel.reviewEligibility({\n policy: settings.policy,\n signedIn: reviewer.signedIn && !reviewer.suspended,\n memberVerified: Boolean(reviewer.memberOrderId),\n tokenVerified,\n tokenProblem,\n })\n return res.status(200).json({\n ...eligibility,\n enabled,\n canReview: enabled && !reviewer.suspended && eligibility.canReview,\n })\n}\n\n/**\n * Product reviews (AGL-324, AGL-3736).\n *\n * GET returns approved reviews + aggregate (`scope=store`: store-wide;\n * `mode=eligibility`: what the form may do). POST submits one review:\n *\n * - VERIFIED when the reviewer presents an unspent, unexpired review link\n * for this product, or is a signed-in member whose proven email has an\n * order that still entitles it. Published at once (`approved`): the FTC's\n * 2024 rule bars suppressing honest negative reviews; the merchant may\n * still hide or reject one for a content reason.\n * - Otherwise, under the store's `verified` policy (the default), 403\n * `verified_purchase_required`; under `anyone`, saved `pending` with the\n * name and email given — the email is never shown and verifies nothing.\n *\n * Rate-limited per address and per member, durably. Plan-gated\n * (`productReviews`).\n */\nexport const reviewsHandler: PluginApiHandler = async (req, res) => {\n const isPost = req.method === 'POST'\n const hostId = String((isPost ? req.body?.hostId : req.query.hostId) ?? '')\n const productId = String(\n (isPost ? req.body?.productId : req.query.productId) ?? '',\n )\n // Store-wide (`scope=store`, no productId): the latest approved reviews\n // across the store, for the storefront's reviews band. Read only, and only\n // where the store's plan has reviews — a store without the feature shows\n // no band rather than reviews it can no longer collect.\n if (!isPost && !productId && req.query.scope === 'store') {\n if (!hostId) return res.status(400).json({ error: 'Missing hostId' })\n try {\n const org = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(org?.org as any, 'productReviews')) {\n return res.status(200).json({ reviews: [], aggregate: { count: 0, average: 0 } })\n }\n const { reviews, aggregate } = await readStoreReviews(hostId, Number(req.query.limit))\n res.setHeader('Cache-Control', 'public, s-maxage=120, stale-while-revalidate=300')\n return res.status(200).json({ reviews, aggregate })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Reviews unavailable' })\n }\n }\n if (!hostId || !productId) {\n return res.status(400).json({ error: 'Missing hostId or productId' })\n }\n try {\n if (isPost) return await submitReview(req, res, hostId, productId)\n if (req.query.mode === 'eligibility') {\n return await eligibilityHandler(req, res, hostId, productId)\n }\n // A product sold in person only whose store hides its reviews from the\n // site (AGL-4060) answers none, here as on every other online surface.\n const product = await firebaseAdmin\n .app()\n .firestore()\n .collection('hosts')\n .doc(hostId)\n .collection('products')\n .doc(productId)\n .get()\n const hiddenOnline =\n CommerceModel.productReviewsOnline(product?.exists ? (product.data() as Record<string, unknown>) : null) ===\n 'hidden'\n const { reviews, aggregate } = hiddenOnline\n ? { reviews: [], aggregate: { count: 0, average: 0 } }\n : await readProductReviews(hostId, productId)\n res.setHeader(\n 'Cache-Control',\n 'public, s-maxage=120, stale-while-revalidate=300',\n )\n return res.status(200).json({ reviews, aggregate })\n } catch (error) {\n console.error(error)\n return res.status(500).json({ error: 'Reviews unavailable' })\n }\n}\n\nconst EMAIL_PATTERN = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/\n\nasync function submitReview(\n req: ReviewsRequest,\n res: ReviewsResponse,\n hostId: string,\n productId: string,\n) {\n const codes = CommerceModel.REVIEW_ERROR_CODES\n const firestore = firebaseAdmin.app().firestore()\n const hostRef = firestore.collection('hosts').doc(hostId)\n const org = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(org?.org as any, 'productReviews')) {\n return res.status(403).json({ error: 'Reviews are not enabled', code: codes.notEnabled })\n }\n const refuseRate = (resetMs: number) => {\n res.setHeader(\n 'Retry-After',\n String(Math.max(1, Math.ceil((resetMs - Date.now()) / 1000))),\n )\n return res.status(429).json({\n error: 'Too many reviews from here. Try again later.',\n code: codes.rateLimited,\n })\n }\n // Per address, before anything else is read.\n const perIp = await consumeRateLimit(`commerce-review:ip:${hostId}:${clientIp(req)}`, {\n limit: REVIEW_POSTS_PER_IP_PER_HOUR,\n windowMs: HOUR_MS,\n })\n if (!perIp.allowed) return refuseRate(perIp.resetMs)\n\n const rating = Math.min(5, Math.max(1, Math.round(Number(req.body?.rating ?? 0))))\n const body = String(req.body?.body ?? '').trim().slice(0, 2000)\n const typedName = String(req.body?.authorName ?? '').trim().slice(0, 80)\n if (!rating || !body) {\n return res.status(400).json({ error: 'A rating and a review are required' })\n }\n\n const settings = await readReviewSettings(hostRef)\n const reviewer = await identifyReviewer(req, hostRef, hostId, productId)\n if (reviewer.suspended) {\n return res.status(403).json({ error: MEMBER_SUSPENDED_ERROR, code: codes.memberSuspended })\n }\n if (reviewer.memberId) {\n const perMember = await consumeRateLimit(\n `commerce-review:member:${hostId}:${reviewer.memberId}`,\n { limit: REVIEW_POSTS_PER_MEMBER_PER_DAY, windowMs: 24 * HOUR_MS },\n )\n if (!perMember.allowed) return refuseRate(perMember.resetMs)\n }\n\n // 1. A review link from the email. A link that does not work is an error,\n // never a silent fall back to an unverified review.\n const presented = req.body?.reviewToken\n if (presented) {\n const check = await inspectReviewToken(firestore, hostId, presented, productId)\n if (!check.ok || !check.record || !check.ref) {\n const code = check.code ?? codes.tokenInvalid\n return res.status(403).json({ error: tokenProblemMessage(code), code })\n }\n const reviewRef = hostRef.collection('reviews').doc(createResourceUid())\n const nowMs = Date.now()\n const problem = await firestore.runTransaction(async (transaction) => {\n const spent = await redeemReviewTokenIn(transaction, check.ref, productId, reviewRef.id, nowMs)\n if (spent) return spent\n transaction.create(reviewRef, {\n productId,\n rating,\n body,\n authorName: typedName || reviewer.memberName || 'Anonymous',\n authorEmail: check.record.email,\n ...(reviewer.memberId ? { memberId: reviewer.memberId } : {}),\n verified: true,\n verifiedVia: 'token',\n orderId: check.record.orderId,\n status: CommerceModel.initialReviewStatus(true),\n createdAtMs: nowMs,\n })\n return null\n })\n if (problem) {\n return res.status(403).json({ error: tokenProblemMessage(problem), code: problem })\n }\n // The link came through the inbox, so a member signed in with the same\n // address has now proven it.\n if (reviewer.memberId && reviewer.memberEmail === check.record.email) {\n await hostRef\n .collection('siteMembers')\n .doc(reviewer.memberId)\n .set({ emailVerifiedAtMs: nowMs }, { merge: true })\n .catch(() => undefined)\n }\n return res.status(200).json({ ok: true, pending: false, verified: true })\n }\n\n // 2. A signed-in member whose proven email bought it. One verified review\n // per member per product.\n if (reviewer.memberOrderId) {\n const already = await hostRef\n .collection('reviews')\n .where('productId', '==', productId)\n .where('memberId', '==', reviewer.memberId)\n .limit(1)\n .get()\n if (!already.empty) {\n return res.status(409).json({\n error: 'You have already reviewed this product.',\n code: codes.alreadyReviewed,\n })\n }\n await hostRef.collection('reviews').doc(createResourceUid()).set({\n productId,\n rating,\n body,\n authorName: typedName || reviewer.memberName || 'Anonymous',\n authorEmail: reviewer.memberEmail,\n memberId: reviewer.memberId,\n verified: true,\n verifiedVia: 'member',\n orderId: reviewer.memberOrderId,\n status: CommerceModel.initialReviewStatus(true),\n createdAtMs: Date.now(),\n })\n return res.status(200).json({ ok: true, pending: false, verified: true })\n }\n\n // 3. Nobody the store can verify.\n if (settings.policy !== 'anyone') {\n return res.status(403).json({\n error: 'Only customers who bought this can review it.',\n code: codes.verifiedPurchaseRequired,\n })\n }\n const authorEmail = reviewer.signedIn\n ? reviewer.memberEmail\n : String(req.body?.authorEmail ?? '').trim().toLowerCase().slice(0, 120)\n const authorName = typedName || reviewer.memberName\n if (!authorName || !EMAIL_PATTERN.test(authorEmail)) {\n return res.status(400).json({ error: 'Your name and email are required' })\n }\n await hostRef.collection('reviews').doc(createResourceUid()).set({\n productId,\n rating,\n body,\n authorName,\n // Kept for the merchant; never shown, and it verifies nothing.\n authorEmail,\n ...(reviewer.memberId ? { memberId: reviewer.memberId } : {}),\n verified: false,\n status: CommerceModel.initialReviewStatus(false),\n createdAtMs: Date.now(),\n })\n return res.status(200).json({ ok: true, pending: true, verified: false })\n}\n\n/** The words for a review link that does not work. */\nexport function tokenProblemMessage(code: CommerceModel.ReviewErrorCode): string {\n switch (code) {\n case CommerceModel.REVIEW_ERROR_CODES.tokenUsed:\n return 'This review link has already been used.'\n case CommerceModel.REVIEW_ERROR_CODES.tokenExpired:\n return 'This review link has expired.'\n default:\n return 'This review link is not valid for this product.'\n }\n}\n"],"names":["Aglyn","CommerceModel","consumeRateLimit","firebaseAdmin","getOrgForHost","readClientIp","createResourceUid","AggregateField","MEMBER_SUSPENDED_ERROR","readActiveMemberSession","inspectReviewToken","redeemReviewTokenIn","readProductReviews","hostId","productId","hostRef","app","firestore","collection","doc","reviewsSnapshot","where","limit","get","reviews","docs","map","docSnapshot","id","rating","Number","body","String","authorName","verified","Boolean","reply","createdAtMs","sort","a","b","count","length","average","reduce","sum","review","aggregate","Math","round","STORE_REVIEWS_MAX","STORE_REVIEWS_DEFAULT","reviewerShortName","name","words","trim","split","filter","first","others","last","toUpperCase","HIDDEN_REVIEW_PRODUCTS_MAX","STORE_REVIEWS_PAGES","IN_FILTER_MAX","readHiddenReviewProductIds","snapshot","Set","readStoreReviews","approved","max","min","floor","hidden","hiddenIds","tally","aggregates","Promise","all","Array","from","ceil","_","chunk","slice","after","page","query","orderBy","startAfter","push","has","size","rows","productIds","row","products","Map","snapshots","getAll","data","exists","online","productReviewsOnline","set","slug","product","productName","productSlug","soldInStore","hiddenTallies","counted","total","tallied","REVIEW_POSTS_PER_IP_PER_HOUR","REVIEW_POSTS_PER_MEMBER_PER_DAY","HOUR_MS","memberProvenEmail","member","toLowerCase","entitlingOrderId","email","orders","order","liftLegacyOrder","orderEntitlesProduct","readReviewSettings","catch","reviewSettingsFrom","clientIp","req","headers","remoteAddress","socket","ANONYMOUS_REVIEWER","signedIn","suspended","memberId","memberEmail","memberName","memberOrderId","identifyReviewer","session","status","proven","eligibilityHandler","res","setHeader","org","enabled","checkEntitlement","settings","reviewer","presented","tokenVerified","tokenProblem","check","ok","code","REVIEW_ERROR_CODES","tokenInvalid","eligibility","reviewEligibility","policy","memberVerified","json","canReview","reviewsHandler","isPost","method","scope","error","console","submitReview","mode","hiddenOnline","EMAIL_PATTERN","codes","notEnabled","refuseRate","resetMs","Date","now","rateLimited","perIp","windowMs","allowed","typedName","memberSuspended","perMember","reviewToken","record","ref","tokenProblemMessage","reviewRef","nowMs","problem","runTransaction","transaction","spent","create","authorEmail","verifiedVia","orderId","initialReviewStatus","emailVerifiedAtMs","merge","undefined","pending","already","empty","alreadyReviewed","verifiedPurchaseRequired","test","tokenUsed","tokenExpired"],"mappings":"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"}
|
|
@@ -16,6 +16,8 @@ import { _ as _extends } from "@swc/helpers/_/_extends";
|
|
|
16
16
|
* limitations under the License.
|
|
17
17
|
*/ import * as Aglyn from "@aglyn/aglyn/server";
|
|
18
18
|
import { firebaseAdmin, getOrgForHost, logHostActivity } from "@aglyn/tenant-data-admin";
|
|
19
|
+
import { commerceSitePermissionRefusal } from "../model/plugin-permissions.js";
|
|
20
|
+
import { holdsCommerceSiteKey } from "./commerce-site-keys.js";
|
|
19
21
|
import { readCommerceSecretKeyring } from "./order-webhooks.js";
|
|
20
22
|
import { prepareOpenOrders } from "./orders-shipping-prepare.js";
|
|
21
23
|
import { SHIPPINGEASY_ACTIONS, shippingEasyConnectorAction } from "./shippingeasy.js";
|
|
@@ -54,10 +56,6 @@ const ACTIONS = [
|
|
|
54
56
|
'shipstation',
|
|
55
57
|
'shippingeasy'
|
|
56
58
|
];
|
|
57
|
-
const CONNECTOR_NAMES = {
|
|
58
|
-
shipstation: 'ShipStation',
|
|
59
|
-
shippingeasy: 'ShippingEasy'
|
|
60
|
-
};
|
|
61
59
|
export const shippingConnectorsHandler = async (req, res)=>{
|
|
62
60
|
var _req_headers_authorization, _ref, _ref1, _body_action;
|
|
63
61
|
if (req.method !== 'GET' && req.method !== 'POST') {
|
|
@@ -97,20 +95,21 @@ export const shippingConnectorsHandler = async (req, res)=>{
|
|
|
97
95
|
});
|
|
98
96
|
}
|
|
99
97
|
try {
|
|
100
|
-
var
|
|
98
|
+
var _decoded_email;
|
|
101
99
|
const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken);
|
|
102
100
|
const firestore = firebaseAdmin.app().firestore();
|
|
103
101
|
const hostSnapshot = await firestore.collection('hosts').doc(hostId).get();
|
|
104
102
|
if (!hostSnapshot.exists) return res.status(404).json({
|
|
105
103
|
error: 'Unknown site'
|
|
106
104
|
});
|
|
107
|
-
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
const
|
|
111
|
-
|
|
105
|
+
// Reading the status is the order book's ("Manage orders"); anything that
|
|
106
|
+
// makes, shows or ends a credential that reads it is "Connect shipping
|
|
107
|
+
// apps" (AGL-4047), whose preset is the site Admin who alone did it before.
|
|
108
|
+
const mayManage = holdsCommerceSiteKey(hostSnapshot, decoded.uid, 'commerce.shippingConnections');
|
|
109
|
+
const changes = action !== 'status';
|
|
110
|
+
if (changes ? !mayManage : !mayManage && !holdsCommerceSiteKey(hostSnapshot, decoded.uid, 'commerce.orders')) {
|
|
112
111
|
return res.status(403).json({
|
|
113
|
-
error:
|
|
112
|
+
error: commerceSitePermissionRefusal(changes ? 'commerce.shippingConnections' : 'commerce.orders')
|
|
114
113
|
});
|
|
115
114
|
}
|
|
116
115
|
const owner = await getOrgForHost(hostId);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/shipping-connectors.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 { firebaseAdmin, getOrgForHost, logHostActivity } from '@aglyn/tenant-data-admin'\nimport { readCommerceSecretKeyring } from './order-webhooks'\nimport { prepareOpenOrders } from './orders-shipping-prepare'\nimport { SHIPPINGEASY_ACTIONS, shippingEasyConnectorAction, type ShippingEasyAction } from './shippingeasy'\nimport {\n SHIPSTATION_CONNECTIONS,\n mintShipStationCredentials,\n openShipStationPassword,\n sealShipStationPassword,\n type ShipStationConnection,\n} from './shipstation'\n\n/*\n * THE CONSOLE'S SIDE OF THE SHIPPING CONNECTORS (AGL-3613, AGL-3633):\n * `commerce/shipping-connectors`. One door for every connector card: it\n * proves the caller and the site once, then answers for the connector the\n * request names (`connector`, `shipstation` when absent). ShippingEasy's\n * actions are `shippingEasyConnectorAction` in `./shippingeasy`; what follows\n * is ShipStation's.\n *\n * - GET `?hostId=` — whether ShipStation can be connected on this deployment\n * (`available`: commerce's secret keyring exists), whether it is, its\n * username and when it last called. Never the password.\n * - POST `{ hostId, action: 'connect' | 'rotate' | 'reveal' | 'disconnect' }`\n * — a site's ADMINS only, because the credentials read every shippable\n * order's name and address. `connect`, `rotate` and `reveal` answer the\n * password; `rotate` ends the old one at once. `connect` also stamps the\n * open orders written before the feed's fields existed\n * (`prepareOpenOrders`), so ShipStation's first import finds them.\n */\n\n/** What the card shows about a connection. */\nexport interface ShipStationConnectionStatus {\n /** Whether this deployment can seal a password at all. */\n available: boolean\n connected: boolean\n username?: string\n createdAtMs?: number\n rotatedAtMs?: number\n lastExportAtMs?: number\n lastShipNoticeAtMs?: number\n}\n\nfunction statusOf(connection: ShipStationConnection | null, available: boolean): ShipStationConnectionStatus {\n if (!connection) return { available, connected: false }\n return {\n available,\n connected: true,\n username: connection.username,\n createdAtMs: connection.createdAtMs,\n ...(connection.rotatedAtMs ? { rotatedAtMs: connection.rotatedAtMs } : {}),\n ...(connection.lastExportAtMs ? { lastExportAtMs: connection.lastExportAtMs } : {}),\n ...(connection.lastShipNoticeAtMs ? { lastShipNoticeAtMs: connection.lastShipNoticeAtMs } : {}),\n }\n}\n\nconst ACTIVITY = { type: 'commerce:shipStation', id: 'shipstation', name: 'ShipStation' } as const\n\nconst ACTIONS = ['status', 'connect', 'rotate', 'reveal', 'disconnect'] as const\ntype Action = (typeof ACTIONS)[number] | ShippingEasyAction\n\n/** The shipping apps the card route manages. `shipstation` when a request names none. */\nconst CONNECTORS = ['shipstation', 'shippingeasy'] as const\ntype Connector = (typeof CONNECTORS)[number]\nconst CONNECTOR_NAMES: Readonly<Record<Connector, string>> = { shipstation: 'ShipStation', shippingeasy: 'ShippingEasy' }\n\nexport const shippingConnectorsHandler: Aglyn.PluginApiHandler = async (req, res) => {\n if (req.method !== 'GET' && req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n let body: Record<string, unknown> = {}\n if (req.method === 'POST') {\n try {\n body = typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n } catch {\n return res.status(400).json({ error: 'The request could not be read' })\n }\n }\n const hostId = String((req.method === 'GET' ? req.query['hostId'] : body['hostId']) ?? '')\n if (!/^[A-Za-z0-9_-]{1,128}$/.test(hostId)) return res.status(400).json({ error: 'Missing hostId' })\n const connector = String((req.method === 'GET' ? req.query['connector'] : body['connector']) ?? 'shipstation') as Connector\n if (!CONNECTORS.includes(connector)) return res.status(400).json({ error: 'Unknown connector' })\n const action = (req.method === 'GET' ? 'status' : String(body['action'] ?? '')) as Action\n const known: readonly string[] = connector === 'shippingeasy' ? SHIPPINGEASY_ACTIONS : ACTIONS\n if (!known.includes(action) || (req.method === 'POST' && action === 'status')) {\n return res.status(400).json({ error: 'Unknown action' })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostSnapshot = await firestore.collection('hosts').doc(hostId).get()\n if (!hostSnapshot.exists) return res.status(404).json({ error: 'Unknown site' })\n const role = (hostSnapshot.get('memberRoles') ?? {})[decoded.uid]\n // Reading the status is the order book's role; anything that makes,\n // shows or ends a credential that reads it is the admin's.\n const adminOnly = action !== 'status'\n if (adminOnly ? role !== 'admin' : role !== 'admin' && role !== 'editor') {\n return res.status(403).json({\n error: adminOnly ? `Only a site admin can manage the ${CONNECTOR_NAMES[connector]} connection` : 'Not permitted',\n })\n }\n const owner = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(owner?.org as never, 'commerce')) {\n return res.status(403).json({ error: 'Selling is not enabled' })\n }\n\n const actor = { uid: decoded.uid, email: decoded.email ?? null }\n if (connector === 'shippingeasy') {\n const answer = await shippingEasyConnectorAction({\n hostId,\n action: action as ShippingEasyAction,\n body,\n actor,\n log: (what, target) => logHostActivity(hostId, actor, what, target),\n prepareOpenOrders,\n })\n return res.status(answer.status).json(answer.body)\n }\n\n const keyring = readCommerceSecretKeyring()\n const available = keyring !== null\n const ref = firestore.collection(SHIPSTATION_CONNECTIONS).doc(hostId)\n\n if (action === 'status') {\n const doc = await ref.get()\n return res.status(200).json(statusOf(doc.exists ? (doc.data() as ShipStationConnection) : null, available))\n }\n if (action === 'disconnect') {\n const doc = await ref.get()\n if (!doc.exists) return res.status(200).json({ available, connected: false })\n await ref.delete()\n await logHostActivity(hostId, actor, 'Disconnected ShipStation', ACTIVITY)\n return res.status(200).json({ available, connected: false })\n }\n if (!keyring) {\n return res.status(503).json({ error: 'ShipStation cannot be connected on this deployment yet' })\n }\n if (action === 'reveal') {\n const doc = await ref.get()\n if (!doc.exists) return res.status(409).json({ error: 'ShipStation is not connected.' })\n const connection = doc.data() as ShipStationConnection\n const opened = openShipStationPassword(connection, keyring)\n if (!opened) {\n return res.status(409).json({ error: 'The password cannot be shown. Make a new password instead.' })\n }\n await logHostActivity(hostId, actor, 'Showed the ShipStation password', ACTIVITY)\n return res.status(200).json({ ...statusOf(connection, available), password: opened.password })\n }\n\n const { username, password } = mintShipStationCredentials()\n const sealed = sealShipStationPassword(hostId, password, keyring)\n const nowMs = Date.now()\n const outcome = await firestore.runTransaction(async (transaction) => {\n const doc = await transaction.get(ref)\n if (action === 'connect') {\n if (doc.exists) return { conflict: 'ShipStation is already connected. Make a new password instead.' }\n const connection: ShipStationConnection = {\n hostId,\n username,\n ...sealed,\n createdAtMs: nowMs,\n createdBy: decoded.uid,\n }\n transaction.create(ref, connection)\n return { connection }\n }\n if (!doc.exists) return { conflict: 'ShipStation is not connected.' }\n const current = doc.data() as ShipStationConnection\n // A new password ends the old one at once: the merchant gives\n // ShipStation the new one, and anything holding the old one is refused.\n const connection: ShipStationConnection = { ...current, ...sealed, rotatedAtMs: nowMs }\n delete connection.exportCursor\n // Documents sealed before the secret box carried a digest; drop it.\n delete (connection as unknown as Record<string, unknown>)['passwordHash']\n transaction.set(ref, connection)\n return { connection }\n })\n if ('conflict' in outcome) return res.status(409).json({ error: outcome.conflict })\n if (action === 'connect') {\n await prepareOpenOrders(hostId).catch((error: unknown) =>\n console.warn('shipping-connectors: open orders not stamped', hostId, error),\n )\n }\n await logHostActivity(\n hostId,\n actor,\n action === 'connect' ? 'Connected ShipStation' : 'Made a new ShipStation password',\n ACTIVITY,\n )\n const connection = outcome.connection as ShipStationConnection\n return res.status(200).json({ ...statusOf(connection, available), password })\n } catch (error) {\n console.error('shipping-connectors failed', action, error)\n return res.status(500).json({ error: 'That did not work. Try again.' })\n }\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","logHostActivity","readCommerceSecretKeyring","prepareOpenOrders","SHIPPINGEASY_ACTIONS","shippingEasyConnectorAction","SHIPSTATION_CONNECTIONS","mintShipStationCredentials","openShipStationPassword","sealShipStationPassword","statusOf","connection","available","connected","username","createdAtMs","rotatedAtMs","lastExportAtMs","lastShipNoticeAtMs","ACTIVITY","type","id","name","ACTIONS","CONNECTORS","CONNECTOR_NAMES","shipstation","shippingeasy","shippingConnectorsHandler","req","res","body","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","undefined","JSON","parse","hostId","query","test","connector","includes","action","known","hostSnapshot","decoded","app","auth","verifyIdToken","firestore","collection","doc","get","exists","role","uid","adminOnly","owner","checkEntitlement","org","actor","email","answer","log","what","target","keyring","ref","data","delete","opened","password","sealed","nowMs","Date","now","outcome","runTransaction","transaction","conflict","createdBy","create","current","exportCursor","set","catch","console","warn"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/shipping-connectors.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 { firebaseAdmin, getOrgForHost, logHostActivity } from '@aglyn/tenant-data-admin'\nimport { commerceSitePermissionRefusal } from '../model/plugin-permissions'\nimport { holdsCommerceSiteKey } from './commerce-site-keys'\nimport { readCommerceSecretKeyring } from './order-webhooks'\nimport { prepareOpenOrders } from './orders-shipping-prepare'\nimport { SHIPPINGEASY_ACTIONS, shippingEasyConnectorAction, type ShippingEasyAction } from './shippingeasy'\nimport {\n SHIPSTATION_CONNECTIONS,\n mintShipStationCredentials,\n openShipStationPassword,\n sealShipStationPassword,\n type ShipStationConnection,\n} from './shipstation'\n\n/*\n * THE CONSOLE'S SIDE OF THE SHIPPING CONNECTORS (AGL-3613, AGL-3633):\n * `commerce/shipping-connectors`. One door for every connector card: it\n * proves the caller and the site once, then answers for the connector the\n * request names (`connector`, `shipstation` when absent). ShippingEasy's\n * actions are `shippingEasyConnectorAction` in `./shippingeasy`; what follows\n * is ShipStation's.\n *\n * - GET `?hostId=` — whether ShipStation can be connected on this deployment\n * (`available`: commerce's secret keyring exists), whether it is, its\n * username and when it last called. Never the password.\n * - POST `{ hostId, action: 'connect' | 'rotate' | 'reveal' | 'disconnect' }`\n * — a site's ADMINS only, because the credentials read every shippable\n * order's name and address. `connect`, `rotate` and `reveal` answer the\n * password; `rotate` ends the old one at once. `connect` also stamps the\n * open orders written before the feed's fields existed\n * (`prepareOpenOrders`), so ShipStation's first import finds them.\n */\n\n/** What the card shows about a connection. */\nexport interface ShipStationConnectionStatus {\n /** Whether this deployment can seal a password at all. */\n available: boolean\n connected: boolean\n username?: string\n createdAtMs?: number\n rotatedAtMs?: number\n lastExportAtMs?: number\n lastShipNoticeAtMs?: number\n}\n\nfunction statusOf(connection: ShipStationConnection | null, available: boolean): ShipStationConnectionStatus {\n if (!connection) return { available, connected: false }\n return {\n available,\n connected: true,\n username: connection.username,\n createdAtMs: connection.createdAtMs,\n ...(connection.rotatedAtMs ? { rotatedAtMs: connection.rotatedAtMs } : {}),\n ...(connection.lastExportAtMs ? { lastExportAtMs: connection.lastExportAtMs } : {}),\n ...(connection.lastShipNoticeAtMs ? { lastShipNoticeAtMs: connection.lastShipNoticeAtMs } : {}),\n }\n}\n\nconst ACTIVITY = { type: 'commerce:shipStation', id: 'shipstation', name: 'ShipStation' } as const\n\nconst ACTIONS = ['status', 'connect', 'rotate', 'reveal', 'disconnect'] as const\ntype Action = (typeof ACTIONS)[number] | ShippingEasyAction\n\n/** The shipping apps the card route manages. `shipstation` when a request names none. */\nconst CONNECTORS = ['shipstation', 'shippingeasy'] as const\ntype Connector = (typeof CONNECTORS)[number]\n\nexport const shippingConnectorsHandler: Aglyn.PluginApiHandler = async (req, res) => {\n if (req.method !== 'GET' && req.method !== 'POST') {\n return res.status(405).json({ error: 'Method not allowed' })\n }\n const authorization = String(req.headers.authorization ?? '')\n const idToken = authorization.startsWith('Bearer ') ? authorization.slice('Bearer '.length) : undefined\n if (!idToken) return res.status(401).json({ error: 'Unauthenticated' })\n let body: Record<string, unknown> = {}\n if (req.method === 'POST') {\n try {\n body = typeof req.body === 'string' ? JSON.parse(req.body) : (req.body ?? {})\n } catch {\n return res.status(400).json({ error: 'The request could not be read' })\n }\n }\n const hostId = String((req.method === 'GET' ? req.query['hostId'] : body['hostId']) ?? '')\n if (!/^[A-Za-z0-9_-]{1,128}$/.test(hostId)) return res.status(400).json({ error: 'Missing hostId' })\n const connector = String((req.method === 'GET' ? req.query['connector'] : body['connector']) ?? 'shipstation') as Connector\n if (!CONNECTORS.includes(connector)) return res.status(400).json({ error: 'Unknown connector' })\n const action = (req.method === 'GET' ? 'status' : String(body['action'] ?? '')) as Action\n const known: readonly string[] = connector === 'shippingeasy' ? SHIPPINGEASY_ACTIONS : ACTIONS\n if (!known.includes(action) || (req.method === 'POST' && action === 'status')) {\n return res.status(400).json({ error: 'Unknown action' })\n }\n\n try {\n const decoded = await firebaseAdmin.app().auth().verifyIdToken(idToken)\n const firestore = firebaseAdmin.app().firestore()\n const hostSnapshot = await firestore.collection('hosts').doc(hostId).get()\n if (!hostSnapshot.exists) return res.status(404).json({ error: 'Unknown site' })\n // Reading the status is the order book's (\"Manage orders\"); anything that\n // makes, shows or ends a credential that reads it is \"Connect shipping\n // apps\" (AGL-4047), whose preset is the site Admin who alone did it before.\n const mayManage = holdsCommerceSiteKey(hostSnapshot, decoded.uid, 'commerce.shippingConnections')\n const changes = action !== 'status'\n if (changes ? !mayManage : !mayManage && !holdsCommerceSiteKey(hostSnapshot, decoded.uid, 'commerce.orders')) {\n return res.status(403).json({\n error: commerceSitePermissionRefusal(changes ? 'commerce.shippingConnections' : 'commerce.orders'),\n })\n }\n const owner = await getOrgForHost(hostId)\n if (!Aglyn.checkEntitlement(owner?.org as never, 'commerce')) {\n return res.status(403).json({ error: 'Selling is not enabled' })\n }\n\n const actor = { uid: decoded.uid, email: decoded.email ?? null }\n if (connector === 'shippingeasy') {\n const answer = await shippingEasyConnectorAction({\n hostId,\n action: action as ShippingEasyAction,\n body,\n actor,\n log: (what, target) => logHostActivity(hostId, actor, what, target),\n prepareOpenOrders,\n })\n return res.status(answer.status).json(answer.body)\n }\n\n const keyring = readCommerceSecretKeyring()\n const available = keyring !== null\n const ref = firestore.collection(SHIPSTATION_CONNECTIONS).doc(hostId)\n\n if (action === 'status') {\n const doc = await ref.get()\n return res.status(200).json(statusOf(doc.exists ? (doc.data() as ShipStationConnection) : null, available))\n }\n if (action === 'disconnect') {\n const doc = await ref.get()\n if (!doc.exists) return res.status(200).json({ available, connected: false })\n await ref.delete()\n await logHostActivity(hostId, actor, 'Disconnected ShipStation', ACTIVITY)\n return res.status(200).json({ available, connected: false })\n }\n if (!keyring) {\n return res.status(503).json({ error: 'ShipStation cannot be connected on this deployment yet' })\n }\n if (action === 'reveal') {\n const doc = await ref.get()\n if (!doc.exists) return res.status(409).json({ error: 'ShipStation is not connected.' })\n const connection = doc.data() as ShipStationConnection\n const opened = openShipStationPassword(connection, keyring)\n if (!opened) {\n return res.status(409).json({ error: 'The password cannot be shown. Make a new password instead.' })\n }\n await logHostActivity(hostId, actor, 'Showed the ShipStation password', ACTIVITY)\n return res.status(200).json({ ...statusOf(connection, available), password: opened.password })\n }\n\n const { username, password } = mintShipStationCredentials()\n const sealed = sealShipStationPassword(hostId, password, keyring)\n const nowMs = Date.now()\n const outcome = await firestore.runTransaction(async (transaction) => {\n const doc = await transaction.get(ref)\n if (action === 'connect') {\n if (doc.exists) return { conflict: 'ShipStation is already connected. Make a new password instead.' }\n const connection: ShipStationConnection = {\n hostId,\n username,\n ...sealed,\n createdAtMs: nowMs,\n createdBy: decoded.uid,\n }\n transaction.create(ref, connection)\n return { connection }\n }\n if (!doc.exists) return { conflict: 'ShipStation is not connected.' }\n const current = doc.data() as ShipStationConnection\n // A new password ends the old one at once: the merchant gives\n // ShipStation the new one, and anything holding the old one is refused.\n const connection: ShipStationConnection = { ...current, ...sealed, rotatedAtMs: nowMs }\n delete connection.exportCursor\n // Documents sealed before the secret box carried a digest; drop it.\n delete (connection as unknown as Record<string, unknown>)['passwordHash']\n transaction.set(ref, connection)\n return { connection }\n })\n if ('conflict' in outcome) return res.status(409).json({ error: outcome.conflict })\n if (action === 'connect') {\n await prepareOpenOrders(hostId).catch((error: unknown) =>\n console.warn('shipping-connectors: open orders not stamped', hostId, error),\n )\n }\n await logHostActivity(\n hostId,\n actor,\n action === 'connect' ? 'Connected ShipStation' : 'Made a new ShipStation password',\n ACTIVITY,\n )\n const connection = outcome.connection as ShipStationConnection\n return res.status(200).json({ ...statusOf(connection, available), password })\n } catch (error) {\n console.error('shipping-connectors failed', action, error)\n return res.status(500).json({ error: 'That did not work. Try again.' })\n }\n}\n"],"names":["Aglyn","firebaseAdmin","getOrgForHost","logHostActivity","commerceSitePermissionRefusal","holdsCommerceSiteKey","readCommerceSecretKeyring","prepareOpenOrders","SHIPPINGEASY_ACTIONS","shippingEasyConnectorAction","SHIPSTATION_CONNECTIONS","mintShipStationCredentials","openShipStationPassword","sealShipStationPassword","statusOf","connection","available","connected","username","createdAtMs","rotatedAtMs","lastExportAtMs","lastShipNoticeAtMs","ACTIVITY","type","id","name","ACTIONS","CONNECTORS","shippingConnectorsHandler","req","res","body","method","status","json","error","authorization","String","headers","idToken","startsWith","slice","length","undefined","JSON","parse","hostId","query","test","connector","includes","action","known","decoded","app","auth","verifyIdToken","firestore","hostSnapshot","collection","doc","get","exists","mayManage","uid","changes","owner","checkEntitlement","org","actor","email","answer","log","what","target","keyring","ref","data","delete","opened","password","sealed","nowMs","Date","now","outcome","runTransaction","transaction","conflict","createdBy","create","current","exportCursor","set","catch","console","warn"],"mappings":"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"}
|
|
@@ -25,7 +25,8 @@ import getScreen from "@aglyn/tenant-runtime/get-screen";
|
|
|
25
25
|
import { collectSocialImageFacts } from "@aglyn/tenant-runtime/social-image-facts";
|
|
26
26
|
import { firebaseAdmin, getOrgForHost } from "@aglyn/tenant-data-admin";
|
|
27
27
|
import * as CommerceModel from "../model/index.js";
|
|
28
|
-
import { toPublicProductDetail } from "./product.js";
|
|
28
|
+
import { productAsksUseDateAnywhere, toPublicProductDetail } from "./product.js";
|
|
29
|
+
import { readStoreDay } from "./store-day.js";
|
|
29
30
|
import { readProductReviews } from "./reviews.js";
|
|
30
31
|
/**
|
|
31
32
|
* Commerce page resolver (AGL-292/298/418), relocated verbatim from the
|
|
@@ -141,7 +142,10 @@ import { readProductReviews } from "./reviews.js";
|
|
|
141
142
|
// loaded here — hand it down so it renders server-side.
|
|
142
143
|
pageData: {
|
|
143
144
|
commerce: _extends({
|
|
144
|
-
product: toPublicProductDetail(productSnapshot.docs[0].id, product
|
|
145
|
+
product: toPublicProductDetail(productSnapshot.docs[0].id, product, // The store's calendar, for a pass that asks for a day.
|
|
146
|
+
{
|
|
147
|
+
timeZone: await passDayTimeZone(hostId, hostRes.host, product)
|
|
148
|
+
})
|
|
145
149
|
}, productReviews.aggregate.count ? {
|
|
146
150
|
reviews: productReviews
|
|
147
151
|
} : {})
|
|
@@ -425,7 +429,9 @@ async function composeBuiltInProductPage(hostId, host, productId, product, rawRe
|
|
|
425
429
|
// its Product structured data, as on a template page (AGL-659).
|
|
426
430
|
pageData: {
|
|
427
431
|
commerce: _extends({
|
|
428
|
-
product: toPublicProductDetail(productId, product
|
|
432
|
+
product: toPublicProductDetail(productId, product, {
|
|
433
|
+
timeZone: await passDayTimeZone(hostId, host, product)
|
|
434
|
+
})
|
|
429
435
|
}, productReviews.aggregate.count ? {
|
|
430
436
|
reviews: productReviews
|
|
431
437
|
} : {}, returns ? {
|
|
@@ -589,5 +595,16 @@ async function composeReturnRequestPage(hostId, host) {
|
|
|
589
595
|
return undefined;
|
|
590
596
|
}
|
|
591
597
|
}
|
|
598
|
+
/**
|
|
599
|
+
* The store's time zone for a product page whose product asks for a use day
|
|
600
|
+
* (AGL-4043), so the seeded page offers the same days the API's does; null
|
|
601
|
+
* otherwise, without a read.
|
|
602
|
+
*/ async function passDayTimeZone(hostId, host, product) {
|
|
603
|
+
if (!productAsksUseDateAnywhere(product)) return null;
|
|
604
|
+
const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId);
|
|
605
|
+
return (await readStoreDay(hostRef, {
|
|
606
|
+
host: host != null ? host : null
|
|
607
|
+
})).timeZone;
|
|
608
|
+
}
|
|
592
609
|
|
|
593
610
|
//# sourceMappingURL=site-page-resolver.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/site-page-resolver.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { SitePageResolver } from '@aglyn/aglyn/server'\nimport { hostCollectionKind } from '@aglyn/aglyn/server'\nimport composeScreenNodes, {\n composeNodesWithChrome,\n} from '@aglyn/tenant-runtime/compose-screen-nodes'\nimport { resolveBuiltInPageLayoutId } from '@aglyn/tenant-runtime/built-in-page-layout'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { ORDER_STATUS_PATH } from './order-status-token'\nimport { ORDER_STATUS_COMPONENT_ID } from '../constants/order-status'\nimport { RETURN_REQUEST_COMPONENT_ID, RETURN_REQUEST_PATH } from '../constants/return-request'\nimport getScreen from '@aglyn/tenant-runtime/get-screen'\nimport { collectSocialImageFacts } from '@aglyn/tenant-runtime/social-image-facts'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { toPublicProductDetail } from './product'\nimport { readProductReviews } from './reviews'\n\n/**\n * Commerce page resolver (AGL-292/298/418), relocated verbatim from the\n * tenant loader: /products/{slug} renders the host's designated PDP\n * template with product tokens, /collections/{slug} the collection\n * template — both compose server-side and return complete page props.\n * Runs only for paths that didn't match a published screen; returns\n * undefined for everything else so the loader falls through.\n */\nexport const commerceSitePageResolver: SitePageResolver = async ({\n hostId,\n host,\n path,\n}) => {\n const hostRes = { host }\n // Product detail routes (AGL-292): /products/{slug} renders the\n // host's designated PDP template screen (settings/store\n // `pdpScreenId`, AGL-295) with product tokens; the product-detail\n // block hydrates variants client-side from the same slug. Same\n // mechanism as entry-template screens below.\n const pdpSegments = path.split('/').filter(Boolean)\n // The guest order-status page (AGL-3610), at /order-status — reached only\n // when the site has no page of its own there. The site's header and footer\n // around the order-status block, which reads the signed `?o=&t=` link after\n // hydration, so this page holds no order and is safe to cache. UNLISTED, so\n // the head says noindex: it is a private page, not a landing page.\n if (pdpSegments.length === 1 && pdpSegments[0] === ORDER_STATUS_PATH) {\n return composeOrderStatusPage(hostId, host)\n }\n // The buyer's return form (AGL-3611), at /order-return, under the same\n // terms: no page of the site's own there, the order read after hydration\n // from the link's `?o=&t=` (or the member's session), UNLISTED.\n if (pdpSegments.length === 1 && pdpSegments[0] === RETURN_REQUEST_PATH) {\n return composeReturnRequestPage(hostId, host)\n }\n if (pdpSegments.length === 2 && pdpSegments[0] === 'products') {\n const adminFirestore = firebaseAdmin.app().firestore()\n const hostDocRef = adminFirestore.collection('hosts').doc(hostId)\n const [storeSettings, productSnapshot] = await Promise.all([\n hostDocRef.collection('settings').doc('store').get(),\n hostDocRef\n .collection('products')\n .where('slug', '==', pdpSegments[1])\n .limit(1)\n .get(),\n ])\n const pdpScreenId = storeSettings.get('pdpScreenId')\n const productRaw = productSnapshot.docs[0]?.data() as any\n // Live, active and sold online (AGL-4060): a product sold in person only\n // has no page here, designed or built in, and falls through to the 404.\n const listedOnline = CommerceModel.productListedOnline(productRaw)\n if (pdpScreenId && listedOnline) {\n const product = CommerceModel.liftLegacyProduct(productRaw)\n const templateRes = await getScreen({\n hostId,\n screenId: pdpScreenId,\n // A PDP template renders against a routed PRODUCT, never at an address\n // of its own, so it is served here and 404s everywhere else (AGL-1400).\n allowTemplate: true,\n })\n if (templateRes.screen) {\n const [minPrice, maxPrice] = CommerceModel.productPriceRange(product)\n // Best-effort: a reviews read that fails costs the page its rating\n // snippet, not the page.\n const productReviews = await readProductReviews(\n hostId,\n productSnapshot.docs[0].id,\n ).catch((error) => {\n console.error('product review aggregate failed', error)\n return { reviews: [], aggregate: { count: 0, average: 0 } }\n })\n // The head shares this page as the template's image, then the site\n // default, so their documents are read in the template's own batch\n // (AGL-2850).\n const card = collectSocialImageFacts([\n (templateRes.screen as any).seo?.image,\n hostRes.host?.seo?.image,\n ])\n const templateNodes = await composeScreenNodes({\n hostId,\n screenId: pdpScreenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // The template's host variables, and its layout's, fill in from\n // this site as they do on every other page (AGL-2883).\n host: hostRes.host,\n tokens: {\n 'product.name': product.name,\n 'product.description': product.description ?? '',\n 'product.price': productPriceText(product, minPrice, maxPrice),\n 'product.image':\n product.mediaUrls?.[0] ?? product.imageUrl ?? '',\n 'product.slug': product.slug,\n },\n // Named as the product template it is if the page review holds it\n // (AGL-3374); without it the route has no page.\n page: {\n template: {\n role: 'entry',\n route: '/products/:slug',\n collectionName: 'Products',\n entryPath: `/products/${product.slug}`,\n fallback: 'not-found',\n },\n },\n })\n if (templateNodes) {\n return {\n props: JSON.parse(\n JSON.stringify({\n // The PDP block used to fetch this in an effect, so the\n // server rendered a skeleton and crawlers saw a product page\n // with no product in it (AGL-659). The product is already\n // loaded here — hand it down so it renders server-side.\n pageData: {\n commerce: {\n product: toPublicProductDetail(\n productSnapshot.docs[0].id,\n product,\n ),\n // Rating aggregate (AGL-686): needed HERE because\n // `aggregateRating` belongs nested inside the Product\n // structured data, and that is emitted server-side.\n // The reviews block used to publish a free-standing\n // AggregateRating node, which schema.org ignores.\n ...(productReviews.aggregate.count\n ? { reviews: productReviews }\n : {}),\n },\n },\n data: {\n host: hostRes.host,\n screen: {\n data: {\n ...templateRes.screen,\n // The product NAMES this page; the template's own name\n // (\"Product detail\") describes none of the products it\n // renders. Kept distinct from the SEO title because the\n // head treats an authored title as verbatim and a name\n // as the side the site title joins (AGL-1341) — so a\n // product with its own SEO title gets exactly that, and\n // one without gets \"Blue Widget – Acme Store\".\n displayName: product.name,\n seo: {\n ...((templateRes.screen as any).seo ?? {}),\n // Only when the PRODUCT authored one: inheriting the\n // template screen's title would give every product\n // page in the store the same `<title>`.\n title: product.seo?.title ?? undefined,\n description:\n product.seo?.description ??\n product.description ??\n undefined,\n },\n },\n },\n },\n nodes: templateNodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n }\n }\n }\n // No product template designated, or none that renders: the store's\n // built-in product page (AGL-3676), in the site's own header and footer,\n // rather than a 404 behind every card the product grid links. A store a\n // guided start built has no template of its own, and its grid links each\n // product here.\n if (listedOnline) {\n return composeBuiltInProductPage(\n hostId,\n host,\n productSnapshot.docs[0].id,\n CommerceModel.liftLegacyProduct(productRaw),\n storeSettings.get('returns'),\n )\n }\n }\n // Commerce collection routes (AGL-298): /collections/{slug} renders\n // the designated collection template with collection tokens; the\n // product-grid block derives the same slug from the URL.\n if (pdpSegments.length === 2 && pdpSegments[0] === 'collections') {\n const adminFirestore = firebaseAdmin.app().firestore()\n const hostDocRef = adminFirestore.collection('hosts').doc(hostId)\n const [storeSettings, collectionSnapshot] = await Promise.all([\n hostDocRef.collection('settings').doc('store').get(),\n hostDocRef\n .collection('collections')\n .where('slug', '==', pdpSegments[1])\n .limit(5)\n .get(),\n ])\n const collectionScreenId = storeSettings.get('collectionScreenId')\n // Content collections share this path and a slug is only unique within a\n // kind (AGL-954), so take the first CATALOG match rather than the first\n // doc — otherwise a blog with the same slug renders as a product page.\n const shopCollection = collectionSnapshot.docs\n .find((docSnapshot) => hostCollectionKind(docSnapshot.data()) === 'catalog')\n ?.data() as CommerceModel.HostCollection | undefined\n if (collectionScreenId && shopCollection) {\n const templateRes = await getScreen({\n hostId,\n screenId: collectionScreenId,\n allowTemplate: true,\n })\n if (templateRes.screen) {\n // As on the PDP above: the template's image, then the site default\n // (AGL-2850).\n const card = collectSocialImageFacts([\n (templateRes.screen as any).seo?.image,\n hostRes.host?.seo?.image,\n ])\n const templateNodes = await composeScreenNodes({\n hostId,\n screenId: collectionScreenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // As on the PDP above (AGL-2883).\n host: hostRes.host,\n tokens: {\n 'collection.name': shopCollection.name,\n 'collection.description': shopCollection.description ?? '',\n 'collection.image': shopCollection.imageUrl ?? '',\n 'collection.slug': shopCollection.slug,\n },\n // As on the PDP above (AGL-3374).\n page: {\n template: {\n role: 'entry',\n route: '/collections/:slug',\n collectionName: 'Store collections',\n entryPath: `/collections/${shopCollection.slug}`,\n fallback: 'not-found',\n },\n },\n })\n if (templateNodes) {\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host: hostRes.host,\n screen: {\n data: {\n ...templateRes.screen,\n // As on the PDP above: the catalog collection names the\n // page, and a name takes the site title after it while\n // an authored SEO title would not (AGL-1341). A shop\n // collection has no SEO title field of its own, so the\n // template's must not stand in for one.\n displayName: shopCollection.name,\n seo: {\n ...((templateRes.screen as any).seo ?? {}),\n title: undefined,\n description:\n shopCollection.description ?? undefined,\n },\n },\n },\n },\n nodes: templateNodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n }\n }\n }\n }\n return undefined\n}\n\n/**\n * What `{{product.price}}` says: the price, \"From\" the lowest where variants\n * differ, or \"Price coming soon\" for a product listed before any variant has\n * one (AGL-3676) — never \"$0\".\n */\nexport function productPriceText(\n product: Pick<CommerceModel.HostProduct, 'variants'>,\n minPrice: number,\n maxPrice: number,\n): string {\n if (!(product.variants ?? []).some((variant) => CommerceModel.variantHasPrice(variant))) {\n return 'Price coming soon'\n }\n return minPrice === maxPrice ? `$${minPrice}` : `From $${minPrice}`\n}\n\nconst PRODUCT_PAGE_NODE = 'pdp__detail'\nconst PRODUCT_PAGE_REVIEWS_NODE = 'pdp__reviews'\nconst PRODUCT_PAGE_RELATED_NODE = 'pdp__related'\n\n/**\n * The store's built-in product page (AGL-3676), root first, for the layout's\n * slot: the product block for the routed slug — with its Details and\n * Shipping & returns folds under the description, as a designer theme's\n * product page has them — then the product's reviews where the store's plan\n * collects them, then the related products as a grid of photo cards.\n */\nexport function buildProductPageNodes(\n slug: string,\n options: { reviews?: boolean } = {},\n): Record<string, Aglyn.AglynNodeSchema> {\n const reviews = Boolean(options.reviews)\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['pdp__container'],\n } as Aglyn.AglynNodeSchema,\n pdp__container: {\n $id: 'pdp__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'lg', sx: { paddingTop: 6, paddingBottom: 10 } },\n nodes: [\n PRODUCT_PAGE_NODE,\n ...(reviews ? [PRODUCT_PAGE_REVIEWS_NODE] : []),\n PRODUCT_PAGE_RELATED_NODE,\n ],\n } as Aglyn.AglynNodeSchema,\n [PRODUCT_PAGE_NODE]: {\n $id: PRODUCT_PAGE_NODE,\n parentId: 'pdp__container',\n componentId: 'product-detail',\n pluginId: 'commerce',\n props: { slug, showDetails: true, showShipping: true },\n } as Aglyn.AglynNodeSchema,\n ...(reviews\n ? {\n [PRODUCT_PAGE_REVIEWS_NODE]: {\n $id: PRODUCT_PAGE_REVIEWS_NODE,\n parentId: 'pdp__container',\n componentId: 'product-reviews',\n pluginId: 'commerce',\n props: { heading: 'Reviews', sx: { marginTop: 8 } },\n } as Aglyn.AglynNodeSchema,\n }\n : {}),\n [PRODUCT_PAGE_RELATED_NODE]: {\n $id: PRODUCT_PAGE_RELATED_NODE,\n parentId: 'pdp__container',\n componentId: 'related-products',\n pluginId: 'commerce',\n props: {\n heading: 'You may also like',\n maxItems: 4,\n layout: 'grid',\n sx: { marginTop: 8 },\n },\n } as Aglyn.AglynNodeSchema,\n }\n}\n\n/**\n * The store's return window for this product, when the store takes returns of\n * its kind: the settings the return form enforces (`readReturnSettings`), so\n * the page never states a policy the store does not keep.\n */\nexport function productReturnWindow(\n product: Pick<CommerceModel.HostProduct, 'type'>,\n rawReturns: unknown,\n): { windowDays: number } | undefined {\n const settings = CommerceModel.readReturnSettings(rawReturns)\n if (!settings.enabled || settings.windowDays <= 0) return undefined\n if (!settings.eligibleTypes.includes(product.type ?? 'physical')) return undefined\n return { windowDays: settings.windowDays }\n}\n\n/** Whether the store's plan collects reviews; a failed read leaves them off. */\nasync function storeTakesReviews(hostId: string): Promise<boolean> {\n try {\n const org = await getOrgForHost(hostId)\n return Aglyn.checkEntitlement(org?.org as never, 'productReviews')\n } catch (error) {\n console.error('reviews entitlement read failed', error)\n return false\n }\n}\n\nasync function composeBuiltInProductPage(\n hostId: string,\n host: unknown,\n productId: string,\n product: ReturnType<typeof CommerceModel.liftLegacyProduct>,\n rawReturns?: unknown,\n) {\n try {\n const [layoutId, productReviews, reviewsOn] = await Promise.all([\n resolveBuiltInPageLayoutId({ hostId, host: host as never }),\n readProductReviews(hostId, productId).catch((error) => {\n console.error('product review aggregate failed', error)\n return { reviews: [], aggregate: { count: 0, average: 0 } }\n }),\n storeTakesReviews(hostId),\n ])\n const returns = productReturnWindow(product, rawReturns)\n const card = collectSocialImageFacts([\n product.mediaUrls?.[0] ?? product.imageUrl,\n (host as { seo?: { image?: string } } | null)?.seo?.image,\n ])\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildProductPageNodes(product.slug, { reviews: reviewsOn }),\n socialImages: card.socialImages,\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n // Seeded, so the product renders server-side and the head writes\n // its Product structured data, as on a template page (AGL-659).\n pageData: {\n commerce: {\n product: toPublicProductDetail(productId, product),\n ...(productReviews.aggregate.count ? { reviews: productReviews } : {}),\n // The Shipping & returns fold's return line, only when the\n // store takes returns of this product's kind.\n ...(returns ? { returns } : {}),\n },\n },\n data: {\n host,\n screen: {\n data: {\n displayName: product.name,\n seo: {\n title: product.seo?.title ?? undefined,\n description: product.seo?.description ?? product.description ?? undefined,\n },\n },\n },\n },\n nodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n } catch (error) {\n console.error('built-in product page composition failed', error)\n return undefined\n }\n}\n\nconst ORDER_STATUS_NODE = 'order-status__block'\n\n/** The order-status page's node tree, root first, for the layout's slot. */\nexport function buildOrderStatusNodes(): Record<string, Aglyn.AglynNodeSchema> {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['order-status__container'],\n } as Aglyn.AglynNodeSchema,\n 'order-status__container': {\n $id: 'order-status__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'md', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [ORDER_STATUS_NODE],\n } as Aglyn.AglynNodeSchema,\n [ORDER_STATUS_NODE]: {\n $id: ORDER_STATUS_NODE,\n parentId: 'order-status__container',\n componentId: ORDER_STATUS_COMPONENT_ID,\n pluginId: 'commerce',\n props: {},\n } as Aglyn.AglynNodeSchema,\n }\n}\n\nasync function composeOrderStatusPage(hostId: string, host: unknown) {\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host: host as never })\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildOrderStatusNodes(),\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host,\n screen: {\n data: {\n displayName: 'Order status',\n visibility: Aglyn.HostScreenVisibility.UNLISTED,\n },\n },\n },\n nodes,\n }),\n ),\n revalidate: 3600,\n }\n } catch (error) {\n console.error('order status page composition failed', error)\n return undefined\n }\n}\n\nconst RETURN_REQUEST_NODE = 'ret__block'\n\n/** The return form's node tree, root first, for the layout's slot. */\nexport function buildReturnRequestNodes(): Record<string, Aglyn.AglynNodeSchema> {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['ret__container'],\n } as Aglyn.AglynNodeSchema,\n ret__container: {\n $id: 'ret__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'md', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [RETURN_REQUEST_NODE],\n } as Aglyn.AglynNodeSchema,\n [RETURN_REQUEST_NODE]: {\n $id: RETURN_REQUEST_NODE,\n parentId: 'ret__container',\n componentId: RETURN_REQUEST_COMPONENT_ID,\n pluginId: 'commerce',\n props: {},\n } as Aglyn.AglynNodeSchema,\n }\n}\n\nasync function composeReturnRequestPage(hostId: string, host: unknown) {\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host: host as never })\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildReturnRequestNodes(),\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host,\n screen: {\n data: {\n displayName: 'Request a return',\n visibility: Aglyn.HostScreenVisibility.UNLISTED,\n },\n },\n },\n nodes,\n }),\n ),\n revalidate: 3600,\n }\n } catch (error) {\n console.error('return request page composition failed', error)\n return undefined\n }\n}\n"],"names":["hostCollectionKind","composeScreenNodes","composeNodesWithChrome","resolveBuiltInPageLayoutId","Aglyn","ORDER_STATUS_PATH","ORDER_STATUS_COMPONENT_ID","RETURN_REQUEST_COMPONENT_ID","RETURN_REQUEST_PATH","getScreen","collectSocialImageFacts","firebaseAdmin","getOrgForHost","CommerceModel","toPublicProductDetail","readProductReviews","commerceSitePageResolver","hostId","host","path","hostRes","pdpSegments","split","filter","Boolean","length","composeOrderStatusPage","composeReturnRequestPage","productSnapshot","adminFirestore","app","firestore","hostDocRef","collection","doc","storeSettings","Promise","all","get","where","limit","pdpScreenId","productRaw","docs","data","listedOnline","productListedOnline","product","liftLegacyProduct","templateRes","screenId","allowTemplate","screen","minPrice","maxPrice","productPriceRange","productReviews","id","catch","error","console","reviews","aggregate","count","average","card","seo","image","templateNodes","socialImages","tokens","name","description","productPriceText","mediaUrls","imageUrl","slug","page","template","role","route","collectionName","entryPath","fallback","props","JSON","parse","stringify","pageData","commerce","displayName","title","undefined","nodes","collected","revalidate","composeBuiltInProductPage","collectionSnapshot","collectionScreenId","shopCollection","find","docSnapshot","variants","some","variant","variantHasPrice","PRODUCT_PAGE_NODE","PRODUCT_PAGE_REVIEWS_NODE","PRODUCT_PAGE_RELATED_NODE","buildProductPageNodes","options","NODE_ROOT_ID","$id","componentId","pdp__container","parentId","pluginId","maxWidth","sx","paddingTop","paddingBottom","showDetails","showShipping","heading","marginTop","maxItems","layout","productReturnWindow","rawReturns","settings","readReturnSettings","enabled","windowDays","eligibleTypes","includes","type","storeTakesReviews","org","checkEntitlement","productId","layoutId","reviewsOn","returns","screenNodes","ORDER_STATUS_NODE","buildOrderStatusNodes","visibility","HostScreenVisibility","UNLISTED","RETURN_REQUEST_NODE","buildReturnRequestNodes","ret__container"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["../../../../../../../libs/plugins/commerce/src/lib/server/site-page-resolver.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport type { SitePageResolver } from '@aglyn/aglyn/server'\nimport { hostCollectionKind } from '@aglyn/aglyn/server'\nimport composeScreenNodes, {\n composeNodesWithChrome,\n} from '@aglyn/tenant-runtime/compose-screen-nodes'\nimport { resolveBuiltInPageLayoutId } from '@aglyn/tenant-runtime/built-in-page-layout'\nimport * as Aglyn from '@aglyn/aglyn/server'\nimport { ORDER_STATUS_PATH } from './order-status-token'\nimport { ORDER_STATUS_COMPONENT_ID } from '../constants/order-status'\nimport { RETURN_REQUEST_COMPONENT_ID, RETURN_REQUEST_PATH } from '../constants/return-request'\nimport getScreen from '@aglyn/tenant-runtime/get-screen'\nimport { collectSocialImageFacts } from '@aglyn/tenant-runtime/social-image-facts'\nimport { firebaseAdmin, getOrgForHost } from '@aglyn/tenant-data-admin'\nimport * as CommerceModel from '../model'\nimport { productAsksUseDateAnywhere, toPublicProductDetail } from './product'\nimport { readStoreDay } from './store-day'\nimport { readProductReviews } from './reviews'\n\n/**\n * Commerce page resolver (AGL-292/298/418), relocated verbatim from the\n * tenant loader: /products/{slug} renders the host's designated PDP\n * template with product tokens, /collections/{slug} the collection\n * template — both compose server-side and return complete page props.\n * Runs only for paths that didn't match a published screen; returns\n * undefined for everything else so the loader falls through.\n */\nexport const commerceSitePageResolver: SitePageResolver = async ({\n hostId,\n host,\n path,\n}) => {\n const hostRes = { host }\n // Product detail routes (AGL-292): /products/{slug} renders the\n // host's designated PDP template screen (settings/store\n // `pdpScreenId`, AGL-295) with product tokens; the product-detail\n // block hydrates variants client-side from the same slug. Same\n // mechanism as entry-template screens below.\n const pdpSegments = path.split('/').filter(Boolean)\n // The guest order-status page (AGL-3610), at /order-status — reached only\n // when the site has no page of its own there. The site's header and footer\n // around the order-status block, which reads the signed `?o=&t=` link after\n // hydration, so this page holds no order and is safe to cache. UNLISTED, so\n // the head says noindex: it is a private page, not a landing page.\n if (pdpSegments.length === 1 && pdpSegments[0] === ORDER_STATUS_PATH) {\n return composeOrderStatusPage(hostId, host)\n }\n // The buyer's return form (AGL-3611), at /order-return, under the same\n // terms: no page of the site's own there, the order read after hydration\n // from the link's `?o=&t=` (or the member's session), UNLISTED.\n if (pdpSegments.length === 1 && pdpSegments[0] === RETURN_REQUEST_PATH) {\n return composeReturnRequestPage(hostId, host)\n }\n if (pdpSegments.length === 2 && pdpSegments[0] === 'products') {\n const adminFirestore = firebaseAdmin.app().firestore()\n const hostDocRef = adminFirestore.collection('hosts').doc(hostId)\n const [storeSettings, productSnapshot] = await Promise.all([\n hostDocRef.collection('settings').doc('store').get(),\n hostDocRef\n .collection('products')\n .where('slug', '==', pdpSegments[1])\n .limit(1)\n .get(),\n ])\n const pdpScreenId = storeSettings.get('pdpScreenId')\n const productRaw = productSnapshot.docs[0]?.data() as any\n // Live, active and sold online (AGL-4060): a product sold in person only\n // has no page here, designed or built in, and falls through to the 404.\n const listedOnline = CommerceModel.productListedOnline(productRaw)\n if (pdpScreenId && listedOnline) {\n const product = CommerceModel.liftLegacyProduct(productRaw)\n const templateRes = await getScreen({\n hostId,\n screenId: pdpScreenId,\n // A PDP template renders against a routed PRODUCT, never at an address\n // of its own, so it is served here and 404s everywhere else (AGL-1400).\n allowTemplate: true,\n })\n if (templateRes.screen) {\n const [minPrice, maxPrice] = CommerceModel.productPriceRange(product)\n // Best-effort: a reviews read that fails costs the page its rating\n // snippet, not the page.\n const productReviews = await readProductReviews(\n hostId,\n productSnapshot.docs[0].id,\n ).catch((error) => {\n console.error('product review aggregate failed', error)\n return { reviews: [], aggregate: { count: 0, average: 0 } }\n })\n // The head shares this page as the template's image, then the site\n // default, so their documents are read in the template's own batch\n // (AGL-2850).\n const card = collectSocialImageFacts([\n (templateRes.screen as any).seo?.image,\n hostRes.host?.seo?.image,\n ])\n const templateNodes = await composeScreenNodes({\n hostId,\n screenId: pdpScreenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // The template's host variables, and its layout's, fill in from\n // this site as they do on every other page (AGL-2883).\n host: hostRes.host,\n tokens: {\n 'product.name': product.name,\n 'product.description': product.description ?? '',\n 'product.price': productPriceText(product, minPrice, maxPrice),\n 'product.image':\n product.mediaUrls?.[0] ?? product.imageUrl ?? '',\n 'product.slug': product.slug,\n },\n // Named as the product template it is if the page review holds it\n // (AGL-3374); without it the route has no page.\n page: {\n template: {\n role: 'entry',\n route: '/products/:slug',\n collectionName: 'Products',\n entryPath: `/products/${product.slug}`,\n fallback: 'not-found',\n },\n },\n })\n if (templateNodes) {\n return {\n props: JSON.parse(\n JSON.stringify({\n // The PDP block used to fetch this in an effect, so the\n // server rendered a skeleton and crawlers saw a product page\n // with no product in it (AGL-659). The product is already\n // loaded here — hand it down so it renders server-side.\n pageData: {\n commerce: {\n product: toPublicProductDetail(\n productSnapshot.docs[0].id,\n product,\n // The store's calendar, for a pass that asks for a day.\n { timeZone: await passDayTimeZone(hostId, hostRes.host, product) },\n ),\n // Rating aggregate (AGL-686): needed HERE because\n // `aggregateRating` belongs nested inside the Product\n // structured data, and that is emitted server-side.\n // The reviews block used to publish a free-standing\n // AggregateRating node, which schema.org ignores.\n ...(productReviews.aggregate.count\n ? { reviews: productReviews }\n : {}),\n },\n },\n data: {\n host: hostRes.host,\n screen: {\n data: {\n ...templateRes.screen,\n // The product NAMES this page; the template's own name\n // (\"Product detail\") describes none of the products it\n // renders. Kept distinct from the SEO title because the\n // head treats an authored title as verbatim and a name\n // as the side the site title joins (AGL-1341) — so a\n // product with its own SEO title gets exactly that, and\n // one without gets \"Blue Widget – Acme Store\".\n displayName: product.name,\n seo: {\n ...((templateRes.screen as any).seo ?? {}),\n // Only when the PRODUCT authored one: inheriting the\n // template screen's title would give every product\n // page in the store the same `<title>`.\n title: product.seo?.title ?? undefined,\n description:\n product.seo?.description ??\n product.description ??\n undefined,\n },\n },\n },\n },\n nodes: templateNodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n }\n }\n }\n // No product template designated, or none that renders: the store's\n // built-in product page (AGL-3676), in the site's own header and footer,\n // rather than a 404 behind every card the product grid links. A store a\n // guided start built has no template of its own, and its grid links each\n // product here.\n if (listedOnline) {\n return composeBuiltInProductPage(\n hostId,\n host,\n productSnapshot.docs[0].id,\n CommerceModel.liftLegacyProduct(productRaw),\n storeSettings.get('returns'),\n )\n }\n }\n // Commerce collection routes (AGL-298): /collections/{slug} renders\n // the designated collection template with collection tokens; the\n // product-grid block derives the same slug from the URL.\n if (pdpSegments.length === 2 && pdpSegments[0] === 'collections') {\n const adminFirestore = firebaseAdmin.app().firestore()\n const hostDocRef = adminFirestore.collection('hosts').doc(hostId)\n const [storeSettings, collectionSnapshot] = await Promise.all([\n hostDocRef.collection('settings').doc('store').get(),\n hostDocRef\n .collection('collections')\n .where('slug', '==', pdpSegments[1])\n .limit(5)\n .get(),\n ])\n const collectionScreenId = storeSettings.get('collectionScreenId')\n // Content collections share this path and a slug is only unique within a\n // kind (AGL-954), so take the first CATALOG match rather than the first\n // doc — otherwise a blog with the same slug renders as a product page.\n const shopCollection = collectionSnapshot.docs\n .find((docSnapshot) => hostCollectionKind(docSnapshot.data()) === 'catalog')\n ?.data() as CommerceModel.HostCollection | undefined\n if (collectionScreenId && shopCollection) {\n const templateRes = await getScreen({\n hostId,\n screenId: collectionScreenId,\n allowTemplate: true,\n })\n if (templateRes.screen) {\n // As on the PDP above: the template's image, then the site default\n // (AGL-2850).\n const card = collectSocialImageFacts([\n (templateRes.screen as any).seo?.image,\n hostRes.host?.seo?.image,\n ])\n const templateNodes = await composeScreenNodes({\n hostId,\n screenId: collectionScreenId,\n screen: templateRes.screen,\n socialImages: card.socialImages,\n // As on the PDP above (AGL-2883).\n host: hostRes.host,\n tokens: {\n 'collection.name': shopCollection.name,\n 'collection.description': shopCollection.description ?? '',\n 'collection.image': shopCollection.imageUrl ?? '',\n 'collection.slug': shopCollection.slug,\n },\n // As on the PDP above (AGL-3374).\n page: {\n template: {\n role: 'entry',\n route: '/collections/:slug',\n collectionName: 'Store collections',\n entryPath: `/collections/${shopCollection.slug}`,\n fallback: 'not-found',\n },\n },\n })\n if (templateNodes) {\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host: hostRes.host,\n screen: {\n data: {\n ...templateRes.screen,\n // As on the PDP above: the catalog collection names the\n // page, and a name takes the site title after it while\n // an authored SEO title would not (AGL-1341). A shop\n // collection has no SEO title field of its own, so the\n // template's must not stand in for one.\n displayName: shopCollection.name,\n seo: {\n ...((templateRes.screen as any).seo ?? {}),\n title: undefined,\n description:\n shopCollection.description ?? undefined,\n },\n },\n },\n },\n nodes: templateNodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n }\n }\n }\n }\n return undefined\n}\n\n/**\n * What `{{product.price}}` says: the price, \"From\" the lowest where variants\n * differ, or \"Price coming soon\" for a product listed before any variant has\n * one (AGL-3676) — never \"$0\".\n */\nexport function productPriceText(\n product: Pick<CommerceModel.HostProduct, 'variants'>,\n minPrice: number,\n maxPrice: number,\n): string {\n if (!(product.variants ?? []).some((variant) => CommerceModel.variantHasPrice(variant))) {\n return 'Price coming soon'\n }\n return minPrice === maxPrice ? `$${minPrice}` : `From $${minPrice}`\n}\n\nconst PRODUCT_PAGE_NODE = 'pdp__detail'\nconst PRODUCT_PAGE_REVIEWS_NODE = 'pdp__reviews'\nconst PRODUCT_PAGE_RELATED_NODE = 'pdp__related'\n\n/**\n * The store's built-in product page (AGL-3676), root first, for the layout's\n * slot: the product block for the routed slug — with its Details and\n * Shipping & returns folds under the description, as a designer theme's\n * product page has them — then the product's reviews where the store's plan\n * collects them, then the related products as a grid of photo cards.\n */\nexport function buildProductPageNodes(\n slug: string,\n options: { reviews?: boolean } = {},\n): Record<string, Aglyn.AglynNodeSchema> {\n const reviews = Boolean(options.reviews)\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['pdp__container'],\n } as Aglyn.AglynNodeSchema,\n pdp__container: {\n $id: 'pdp__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'lg', sx: { paddingTop: 6, paddingBottom: 10 } },\n nodes: [\n PRODUCT_PAGE_NODE,\n ...(reviews ? [PRODUCT_PAGE_REVIEWS_NODE] : []),\n PRODUCT_PAGE_RELATED_NODE,\n ],\n } as Aglyn.AglynNodeSchema,\n [PRODUCT_PAGE_NODE]: {\n $id: PRODUCT_PAGE_NODE,\n parentId: 'pdp__container',\n componentId: 'product-detail',\n pluginId: 'commerce',\n props: { slug, showDetails: true, showShipping: true },\n } as Aglyn.AglynNodeSchema,\n ...(reviews\n ? {\n [PRODUCT_PAGE_REVIEWS_NODE]: {\n $id: PRODUCT_PAGE_REVIEWS_NODE,\n parentId: 'pdp__container',\n componentId: 'product-reviews',\n pluginId: 'commerce',\n props: { heading: 'Reviews', sx: { marginTop: 8 } },\n } as Aglyn.AglynNodeSchema,\n }\n : {}),\n [PRODUCT_PAGE_RELATED_NODE]: {\n $id: PRODUCT_PAGE_RELATED_NODE,\n parentId: 'pdp__container',\n componentId: 'related-products',\n pluginId: 'commerce',\n props: {\n heading: 'You may also like',\n maxItems: 4,\n layout: 'grid',\n sx: { marginTop: 8 },\n },\n } as Aglyn.AglynNodeSchema,\n }\n}\n\n/**\n * The store's return window for this product, when the store takes returns of\n * its kind: the settings the return form enforces (`readReturnSettings`), so\n * the page never states a policy the store does not keep.\n */\nexport function productReturnWindow(\n product: Pick<CommerceModel.HostProduct, 'type'>,\n rawReturns: unknown,\n): { windowDays: number } | undefined {\n const settings = CommerceModel.readReturnSettings(rawReturns)\n if (!settings.enabled || settings.windowDays <= 0) return undefined\n if (!settings.eligibleTypes.includes(product.type ?? 'physical')) return undefined\n return { windowDays: settings.windowDays }\n}\n\n/** Whether the store's plan collects reviews; a failed read leaves them off. */\nasync function storeTakesReviews(hostId: string): Promise<boolean> {\n try {\n const org = await getOrgForHost(hostId)\n return Aglyn.checkEntitlement(org?.org as never, 'productReviews')\n } catch (error) {\n console.error('reviews entitlement read failed', error)\n return false\n }\n}\n\nasync function composeBuiltInProductPage(\n hostId: string,\n host: unknown,\n productId: string,\n product: ReturnType<typeof CommerceModel.liftLegacyProduct>,\n rawReturns?: unknown,\n) {\n try {\n const [layoutId, productReviews, reviewsOn] = await Promise.all([\n resolveBuiltInPageLayoutId({ hostId, host: host as never }),\n readProductReviews(hostId, productId).catch((error) => {\n console.error('product review aggregate failed', error)\n return { reviews: [], aggregate: { count: 0, average: 0 } }\n }),\n storeTakesReviews(hostId),\n ])\n const returns = productReturnWindow(product, rawReturns)\n const card = collectSocialImageFacts([\n product.mediaUrls?.[0] ?? product.imageUrl,\n (host as { seo?: { image?: string } } | null)?.seo?.image,\n ])\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildProductPageNodes(product.slug, { reviews: reviewsOn }),\n socialImages: card.socialImages,\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n // Seeded, so the product renders server-side and the head writes\n // its Product structured data, as on a template page (AGL-659).\n pageData: {\n commerce: {\n product: toPublicProductDetail(productId, product, {\n timeZone: await passDayTimeZone(hostId, host, product),\n }),\n ...(productReviews.aggregate.count ? { reviews: productReviews } : {}),\n // The Shipping & returns fold's return line, only when the\n // store takes returns of this product's kind.\n ...(returns ? { returns } : {}),\n },\n },\n data: {\n host,\n screen: {\n data: {\n displayName: product.name,\n seo: {\n title: product.seo?.title ?? undefined,\n description: product.seo?.description ?? product.description ?? undefined,\n },\n },\n },\n },\n nodes,\n ...card.collected(),\n }),\n ),\n revalidate: 60,\n }\n } catch (error) {\n console.error('built-in product page composition failed', error)\n return undefined\n }\n}\n\nconst ORDER_STATUS_NODE = 'order-status__block'\n\n/** The order-status page's node tree, root first, for the layout's slot. */\nexport function buildOrderStatusNodes(): Record<string, Aglyn.AglynNodeSchema> {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['order-status__container'],\n } as Aglyn.AglynNodeSchema,\n 'order-status__container': {\n $id: 'order-status__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'md', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [ORDER_STATUS_NODE],\n } as Aglyn.AglynNodeSchema,\n [ORDER_STATUS_NODE]: {\n $id: ORDER_STATUS_NODE,\n parentId: 'order-status__container',\n componentId: ORDER_STATUS_COMPONENT_ID,\n pluginId: 'commerce',\n props: {},\n } as Aglyn.AglynNodeSchema,\n }\n}\n\nasync function composeOrderStatusPage(hostId: string, host: unknown) {\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host: host as never })\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildOrderStatusNodes(),\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host,\n screen: {\n data: {\n displayName: 'Order status',\n visibility: Aglyn.HostScreenVisibility.UNLISTED,\n },\n },\n },\n nodes,\n }),\n ),\n revalidate: 3600,\n }\n } catch (error) {\n console.error('order status page composition failed', error)\n return undefined\n }\n}\n\nconst RETURN_REQUEST_NODE = 'ret__block'\n\n/** The return form's node tree, root first, for the layout's slot. */\nexport function buildReturnRequestNodes(): Record<string, Aglyn.AglynNodeSchema> {\n return {\n [Aglyn.NODE_ROOT_ID]: {\n $id: Aglyn.NODE_ROOT_ID,\n componentId: 'div',\n nodes: ['ret__container'],\n } as Aglyn.AglynNodeSchema,\n ret__container: {\n $id: 'ret__container',\n parentId: Aglyn.NODE_ROOT_ID,\n componentId: 'muiContainer',\n pluginId: 'mui',\n props: { maxWidth: 'md', sx: { paddingTop: 6, paddingBottom: 8 } },\n nodes: [RETURN_REQUEST_NODE],\n } as Aglyn.AglynNodeSchema,\n [RETURN_REQUEST_NODE]: {\n $id: RETURN_REQUEST_NODE,\n parentId: 'ret__container',\n componentId: RETURN_REQUEST_COMPONENT_ID,\n pluginId: 'commerce',\n props: {},\n } as Aglyn.AglynNodeSchema,\n }\n}\n\nasync function composeReturnRequestPage(hostId: string, host: unknown) {\n try {\n const layoutId = await resolveBuiltInPageLayoutId({ hostId, host: host as never })\n const nodes = await composeNodesWithChrome({\n hostId,\n layoutId,\n screenNodes: buildReturnRequestNodes(),\n host: host as Aglyn.HostTokenSource,\n })\n if (!nodes) return undefined\n return {\n props: JSON.parse(\n JSON.stringify({\n data: {\n host,\n screen: {\n data: {\n displayName: 'Request a return',\n visibility: Aglyn.HostScreenVisibility.UNLISTED,\n },\n },\n },\n nodes,\n }),\n ),\n revalidate: 3600,\n }\n } catch (error) {\n console.error('return request page composition failed', error)\n return undefined\n }\n}\n\n/**\n * The store's time zone for a product page whose product asks for a use day\n * (AGL-4043), so the seeded page offers the same days the API's does; null\n * otherwise, without a read.\n */\nasync function passDayTimeZone(\n hostId: string,\n host: unknown,\n product: ReturnType<typeof CommerceModel.liftLegacyProduct>,\n): Promise<string | null> {\n if (!productAsksUseDateAnywhere(product)) return null\n const hostRef = firebaseAdmin.app().firestore().collection('hosts').doc(hostId)\n return (await readStoreDay(hostRef, { host: (host ?? null) as Record<string, unknown> | null })).timeZone\n}\n"],"names":["hostCollectionKind","composeScreenNodes","composeNodesWithChrome","resolveBuiltInPageLayoutId","Aglyn","ORDER_STATUS_PATH","ORDER_STATUS_COMPONENT_ID","RETURN_REQUEST_COMPONENT_ID","RETURN_REQUEST_PATH","getScreen","collectSocialImageFacts","firebaseAdmin","getOrgForHost","CommerceModel","productAsksUseDateAnywhere","toPublicProductDetail","readStoreDay","readProductReviews","commerceSitePageResolver","hostId","host","path","hostRes","pdpSegments","split","filter","Boolean","length","composeOrderStatusPage","composeReturnRequestPage","productSnapshot","adminFirestore","app","firestore","hostDocRef","collection","doc","storeSettings","Promise","all","get","where","limit","pdpScreenId","productRaw","docs","data","listedOnline","productListedOnline","product","liftLegacyProduct","templateRes","screenId","allowTemplate","screen","minPrice","maxPrice","productPriceRange","productReviews","id","catch","error","console","reviews","aggregate","count","average","card","seo","image","templateNodes","socialImages","tokens","name","description","productPriceText","mediaUrls","imageUrl","slug","page","template","role","route","collectionName","entryPath","fallback","props","JSON","parse","stringify","pageData","commerce","timeZone","passDayTimeZone","displayName","title","undefined","nodes","collected","revalidate","composeBuiltInProductPage","collectionSnapshot","collectionScreenId","shopCollection","find","docSnapshot","variants","some","variant","variantHasPrice","PRODUCT_PAGE_NODE","PRODUCT_PAGE_REVIEWS_NODE","PRODUCT_PAGE_RELATED_NODE","buildProductPageNodes","options","NODE_ROOT_ID","$id","componentId","pdp__container","parentId","pluginId","maxWidth","sx","paddingTop","paddingBottom","showDetails","showShipping","heading","marginTop","maxItems","layout","productReturnWindow","rawReturns","settings","readReturnSettings","enabled","windowDays","eligibleTypes","includes","type","storeTakesReviews","org","checkEntitlement","productId","layoutId","reviewsOn","returns","screenNodes","ORDER_STATUS_NODE","buildOrderStatusNodes","visibility","HostScreenVisibility","UNLISTED","RETURN_REQUEST_NODE","buildReturnRequestNodes","ret__container","hostRef"],"mappings":"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"}
|