@mohasinac/appkit 4.62.0 → 4.63.0
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/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +129 -9
- package/dist/_internal/server/features/checkout/actions.js +850 -79
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
- package/dist/_internal/server/features/invoicing/allocate.js +162 -0
- package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
- package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
- package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
- package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
- package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
- package/dist/_internal/server/features/invoicing/issue.js +202 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/actions.d.ts +8 -1
- package/dist/_internal/server/features/orders/actions.js +81 -8
- package/dist/_internal/server/features/orders/adapters.js +54 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
- package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
- package/dist/_internal/server/features/points/award.js +46 -4
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/refunds/actions.js +26 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
- package/dist/_internal/server/features/shipping/courier-block.js +264 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +2 -2
- package/dist/_internal/server/features/site-settings/adapters.js +29 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +65 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +95 -2
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +11 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
- package/dist/_internal/server/jobs/core/index.d.ts +2 -1
- package/dist/_internal/server/jobs/core/index.js +2 -1
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
- package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +65 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
- package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +25 -2
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +296 -0
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +174 -0
- package/dist/_internal/shared/checkout/order-math.js +159 -0
- package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
- package/dist/_internal/shared/checkout/refundable-base.js +13 -1
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +75 -0
- package/dist/_internal/shared/checkout/split.js +120 -0
- package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
- package/dist/_internal/shared/content/disclaimers.js +217 -0
- package/dist/_internal/shared/content/published-values.d.ts +93 -0
- package/dist/_internal/shared/content/published-values.js +206 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +139 -0
- package/dist/_internal/shared/features/admin/integrations.js +263 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +29 -0
- package/dist/_internal/shared/features/points/accepts.js +49 -0
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
- package/dist/_internal/shared/fees/calculator.d.ts +33 -1
- package/dist/_internal/shared/fees/calculator.js +32 -2
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +21 -0
- package/dist/client.js +43 -0
- package/dist/constants/api-endpoints.d.ts +114 -0
- package/dist/constants/api-endpoints.js +55 -0
- package/dist/constants/field-names.d.ts +42 -0
- package/dist/constants/field-names.js +52 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOrdersView.js +47 -35
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +33 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +81 -3
- package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
- package/dist/features/admin/components/AdminOrdersView.js +86 -5
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +62 -0
- package/dist/features/admin/schemas/firestore.js +5 -0
- package/dist/features/ads/schemas/booking-write.d.ts +120 -0
- package/dist/features/ads/schemas/booking-write.js +75 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +140 -0
- package/dist/features/audit-log/schemas/firestore.js +89 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/permissions/constants.js +13 -0
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +25 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +29 -6
- package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
- package/dist/features/cart/components/PointsTenderPicker.js +15 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.d.ts +33 -0
- package/dist/features/cart/repository/cart.repository.js +47 -0
- package/dist/features/cart/schemas/firestore.d.ts +116 -0
- package/dist/features/contact/email.d.ts +27 -10
- package/dist/features/contact/email.js +20 -3
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +62 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +108 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +62 -0
- package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
- package/dist/features/faq/components/FAQAccordion.js +29 -4
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
- package/dist/features/invoicing/schemas/firestore.js +45 -0
- package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
- package/dist/features/invoicing/utils/financial-year.js +53 -0
- package/dist/features/jobs/actions/enqueue-job.js +18 -1
- package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
- package/dist/features/jobs/repository/jobs.repository.js +84 -1
- package/dist/features/jobs/schemas/firestore.d.ts +80 -0
- package/dist/features/jobs/schemas/firestore.js +37 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/lottery/schemas/zod.d.ts +6 -6
- package/dist/features/media/MediaImage.js +22 -1
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/obligations/constants/policies.js +24 -1
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
- package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
- package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
- package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +119 -0
- package/dist/features/orders/components/RefundRequestView.js +19 -1
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +85 -1
- package/dist/features/orders/repository/orders.repository.js +205 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +398 -1
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +56 -8
- package/dist/features/orders/schemas/index.js +12 -0
- package/dist/features/orders/types/index.d.ts +47 -0
- package/dist/features/orders/utils/courier-block.d.ts +25 -0
- package/dist/features/orders/utils/courier-block.js +23 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
- package/dist/features/payment-cases/schemas/firestore.js +1 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
- package/dist/features/promotions/actions/coupon-stacking.js +21 -2
- package/dist/features/promotions/components/CouponCard.js +13 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
- package/dist/features/promotions/constants/coupon-help.js +9 -1
- package/dist/features/promotions/schemas/firestore.d.ts +24 -0
- package/dist/features/promotions/schemas/firestore.js +4 -0
- package/dist/features/promotions/types/index.d.ts +19 -1
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/search/action-index/tab-entries.js +1 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +37 -3
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +221 -27
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipments/schemas/validation.d.ts +4 -4
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +82 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +145 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +80 -0
- package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
- package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +112 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/stores/types/index.d.ts +19 -0
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +8 -8
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +33 -1
- package/dist/next/routing/route-map.d.ts +112 -0
- package/dist/next/routing/route-map.js +63 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
- package/dist/providers/shipping-shiprocket/index.js +84 -1
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +69 -34
- package/dist/schemas/sieve.d.ts +2 -2
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +36 -1
- package/dist/server.js +183 -1
- package/dist/styles.css +412 -80
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -0
- package/dist/tokens/themes/default-light.js +1 -0
- package/dist/tokens/themes/required.d.ts +1 -1
- package/dist/tokens/themes/required.js +1 -0
- package/dist/tokens/tokens.css +15 -0
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +83 -0
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +8 -0
- package/dist/ui/index.js +9 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +83 -79
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1834 -1822
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one read a dashboard hub or landing page needs.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Read on the SERVER, not fetched from the client
|
|
5
|
+
*
|
|
6
|
+
* The plan named an endpoint — `/api/dashboard/attention` — and the shape it
|
|
7
|
+
* asked for is "one read covering every section", which this is. It is read
|
|
8
|
+
* here instead because every one of these pages is **already per-request**:
|
|
9
|
+
* `admin/layout.tsx` and `store/layout.tsx` `await getServerSessionUser()`, so
|
|
10
|
+
* the whole subtree is dynamic with or without an endpoint (§ "…a dynamic API
|
|
11
|
+
* in a DASHBOARD layout is the correct lever").
|
|
12
|
+
*
|
|
13
|
+
* An endpoint would therefore have added a SECOND billed invocation per hub
|
|
14
|
+
* visit to fetch data the page's own render could already have, which is
|
|
15
|
+
* exactly the shape of Root Cause #94 — a client fetch that reads as free in
|
|
16
|
+
* review and bills per mount. There is no endpoint, and one should not be added
|
|
17
|
+
* unless something outside a dashboard page needs these numbers.
|
|
18
|
+
*
|
|
19
|
+
* ## Two reads, and both fail OPEN
|
|
20
|
+
*
|
|
21
|
+
* `navConfig` decides which nav entries an admin has switched off, and the hub
|
|
22
|
+
* must agree with the sidebar about that — so it is read rather than ignored.
|
|
23
|
+
* On failure it resolves to `undefined`, which means "nothing is disabled" and
|
|
24
|
+
* shows MORE cards than it should. That is the right direction: a disabled nav
|
|
25
|
+
* entry is a configuration choice rather than a permission, and the page behind
|
|
26
|
+
* every card still enforces its own route guard. Hiding sections because a
|
|
27
|
+
* settings read blipped would be the damaging failure.
|
|
28
|
+
*
|
|
29
|
+
* The counts fail to `null` per card — see `attention.ts`, which exists to
|
|
30
|
+
* explain why `0` is the one answer a badge must never invent.
|
|
31
|
+
*/
|
|
32
|
+
import { type DashboardHubSection } from "../../../shared/features/dashboard/hub-sections";
|
|
33
|
+
import type { SidebarNavGroup } from "../../../shared/features/layout/types";
|
|
34
|
+
import { type AttentionCounts } from "./attention";
|
|
35
|
+
export interface DashboardHubData {
|
|
36
|
+
/** Permission- and toggle-filtered, in nav order. Never empty-group padded. */
|
|
37
|
+
sections: DashboardHubSection[];
|
|
38
|
+
counts: AttentionCounts;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the sections a viewer may see, plus their attention counts.
|
|
42
|
+
*
|
|
43
|
+
* @param groups the portal's nav groups, unfiltered — this applies the filter
|
|
44
|
+
* @param permissions `null` means admin (the convention `filterNavGroups` owns)
|
|
45
|
+
* @param storeId the viewer's store SLUG, for seller-scoped counts
|
|
46
|
+
*/
|
|
47
|
+
export declare function getDashboardHubData(groups: readonly SidebarNavGroup[], permissions: string[] | null | undefined, storeId?: string): Promise<DashboardHubData>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one read a dashboard hub or landing page needs.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Read on the SERVER, not fetched from the client
|
|
5
|
+
*
|
|
6
|
+
* The plan named an endpoint — `/api/dashboard/attention` — and the shape it
|
|
7
|
+
* asked for is "one read covering every section", which this is. It is read
|
|
8
|
+
* here instead because every one of these pages is **already per-request**:
|
|
9
|
+
* `admin/layout.tsx` and `store/layout.tsx` `await getServerSessionUser()`, so
|
|
10
|
+
* the whole subtree is dynamic with or without an endpoint (§ "…a dynamic API
|
|
11
|
+
* in a DASHBOARD layout is the correct lever").
|
|
12
|
+
*
|
|
13
|
+
* An endpoint would therefore have added a SECOND billed invocation per hub
|
|
14
|
+
* visit to fetch data the page's own render could already have, which is
|
|
15
|
+
* exactly the shape of Root Cause #94 — a client fetch that reads as free in
|
|
16
|
+
* review and bills per mount. There is no endpoint, and one should not be added
|
|
17
|
+
* unless something outside a dashboard page needs these numbers.
|
|
18
|
+
*
|
|
19
|
+
* ## Two reads, and both fail OPEN
|
|
20
|
+
*
|
|
21
|
+
* `navConfig` decides which nav entries an admin has switched off, and the hub
|
|
22
|
+
* must agree with the sidebar about that — so it is read rather than ignored.
|
|
23
|
+
* On failure it resolves to `undefined`, which means "nothing is disabled" and
|
|
24
|
+
* shows MORE cards than it should. That is the right direction: a disabled nav
|
|
25
|
+
* entry is a configuration choice rather than a permission, and the page behind
|
|
26
|
+
* every card still enforces its own route guard. Hiding sections because a
|
|
27
|
+
* settings read blipped would be the damaging failure.
|
|
28
|
+
*
|
|
29
|
+
* The counts fail to `null` per card — see `attention.ts`, which exists to
|
|
30
|
+
* explain why `0` is the one answer a badge must never invent.
|
|
31
|
+
*/
|
|
32
|
+
import { cache } from "react";
|
|
33
|
+
import { safeRead } from "../../../../errors/safe-read.js";
|
|
34
|
+
import { siteSettingsRepository } from "../../../../features/admin/repository/site-settings.repository.js";
|
|
35
|
+
import { filterNavGroups } from "../../../client/features/layout/filterNavItems.js";
|
|
36
|
+
import { hubSections, } from "../../../shared/features/dashboard/hub-sections.js";
|
|
37
|
+
import { collectAttentionCounts } from "./attention.js";
|
|
38
|
+
/**
|
|
39
|
+
* `navConfig` alone, cached per request.
|
|
40
|
+
*
|
|
41
|
+
* `React.cache` because a landing page renders the hub once but a hub page may
|
|
42
|
+
* also want the section list for its own heading, and the narrowest shared call
|
|
43
|
+
* is the one to cache (§ "`React.cache` discipline" — cache the repository
|
|
44
|
+
* read, not the viewer-specific wrapper around it, or two call sites with
|
|
45
|
+
* different arguments dedupe nothing).
|
|
46
|
+
*/
|
|
47
|
+
const getNavConfig = cache(async () => safeRead(async () => {
|
|
48
|
+
const settings = (await siteSettingsRepository.getSingleton());
|
|
49
|
+
return settings?.navConfig;
|
|
50
|
+
}, {
|
|
51
|
+
route: "dashboard-hub",
|
|
52
|
+
key: "navConfig",
|
|
53
|
+
fallback: undefined,
|
|
54
|
+
}));
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the sections a viewer may see, plus their attention counts.
|
|
57
|
+
*
|
|
58
|
+
* @param groups the portal's nav groups, unfiltered — this applies the filter
|
|
59
|
+
* @param permissions `null` means admin (the convention `filterNavGroups` owns)
|
|
60
|
+
* @param storeId the viewer's store SLUG, for seller-scoped counts
|
|
61
|
+
*/
|
|
62
|
+
export async function getDashboardHubData(groups, permissions, storeId) {
|
|
63
|
+
const navConfig = await getNavConfig();
|
|
64
|
+
/*
|
|
65
|
+
* Filter FIRST, then count — the visible ids are the scope (see
|
|
66
|
+
* `collectAttentionCounts`). Doing it the other way round costs an employee
|
|
67
|
+
* five queries to render one card, and computes figures from collections
|
|
68
|
+
* their permission set does not cover.
|
|
69
|
+
*
|
|
70
|
+
* Sequential rather than parallel with the settings read for the same
|
|
71
|
+
* reason: the second call's input is the first call's output. Two
|
|
72
|
+
* round trips, inside Rule #6's ~3.
|
|
73
|
+
*/
|
|
74
|
+
const sections = hubSections(filterNavGroups([...groups], navConfig, permissions));
|
|
75
|
+
const visibleItemIds = sections.flatMap((s) => s.items.map((i) => i.id).filter(Boolean));
|
|
76
|
+
const counts = await safeRead(() => collectAttentionCounts(visibleItemIds, storeId), {
|
|
77
|
+
route: "dashboard-hub",
|
|
78
|
+
key: "attention",
|
|
79
|
+
fallback: {},
|
|
80
|
+
});
|
|
81
|
+
return { sections, counts };
|
|
82
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hold stock for something that is not an order yet.
|
|
3
|
+
*
|
|
4
|
+
* ## What this is for
|
|
5
|
+
*
|
|
6
|
+
* The plan's Part 6 (shared purchase) names this "the largest hidden cost",
|
|
7
|
+
* and it is the only one of its three prerequisites that had NOTHING behind
|
|
8
|
+
* it. See the schema file for why the existing order-keyed mechanism cannot
|
|
9
|
+
* serve a multi-day arrangement.
|
|
10
|
+
*
|
|
11
|
+
* ## 🛑 Every function here moves the row and the counter TOGETHER
|
|
12
|
+
*
|
|
13
|
+
* `availableQuantity` is what a buyer can take; the gap between it and
|
|
14
|
+
* `stockQuantity` is the outstanding hold (`resolveStockResync`'s own words).
|
|
15
|
+
* A reservation row written without the decrement holds nothing at all, and a
|
|
16
|
+
* decrement written without a row is a hold nothing can expire — so both
|
|
17
|
+
* happen inside ONE `runTransaction`, with the product read inside it so two
|
|
18
|
+
* concurrent reserves cannot both see the same `availableQuantity`.
|
|
19
|
+
*
|
|
20
|
+
* ## 🛑 Firestore wants every READ before every WRITE
|
|
21
|
+
*
|
|
22
|
+
* So `endReservation` reads the reservation AND the product up front, even
|
|
23
|
+
* on the consume path where the product read is thrown away. The natural
|
|
24
|
+
* shape — read the reservation, decide, then read the product — throws
|
|
25
|
+
* `Firestore transactions require all reads to be executed before all
|
|
26
|
+
* writes`, and only at runtime, which is exactly the class of fault
|
|
27
|
+
* `restoreStockForOrder`'s header already warns about one directory over.
|
|
28
|
+
*
|
|
29
|
+
* ## Why it writes `availableQuantity` EXPLICITLY
|
|
30
|
+
*
|
|
31
|
+
* `ProductRepository.update`'s `resolveStockResync` returns `{}` the moment a
|
|
32
|
+
* write names `availableQuantity` — deliberately, so the checkout decrement,
|
|
33
|
+
* the stock-restore job and `updateAvailableQuantity` stay authoritative over
|
|
34
|
+
* their own number. This is a fourth member of that set and takes the same
|
|
35
|
+
* deal: it is the one deciding the value, so the resync must keep its hands
|
|
36
|
+
* off it. (These writes go through the raw collection rather than the
|
|
37
|
+
* repository for the same reason `deriveTaxonomy` reads categories through
|
|
38
|
+
* `this.db` — a transaction needs the ref, not a repository method.)
|
|
39
|
+
*/
|
|
40
|
+
import { type ReservationHolderKind, type StockReservationDocument } from "../../../../features/inventory/schemas/stock-reservation";
|
|
41
|
+
export interface ReserveStockInput {
|
|
42
|
+
productId: string;
|
|
43
|
+
quantity: number;
|
|
44
|
+
holderKind: ReservationHolderKind;
|
|
45
|
+
holderId: string;
|
|
46
|
+
createdBy: string;
|
|
47
|
+
/** Defaults to `RESERVATION_DEFAULT_TTL_HOURS`; capped at the 30-day ceiling. */
|
|
48
|
+
ttlHours?: number;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Take a hold. Returns the reservation id.
|
|
52
|
+
*
|
|
53
|
+
* 🛑 Refuses rather than over-reserving. A seller's stock is a real number of
|
|
54
|
+
* real objects, so "reserve 5, only 3 left" is a **409** — the request is
|
|
55
|
+
* well-formed and the world moved, which is the same distinction the lottery
|
|
56
|
+
* config write makes for a slot somebody has already pulled.
|
|
57
|
+
*/
|
|
58
|
+
export declare function reserveStock(input: ReserveStockInput): Promise<string>;
|
|
59
|
+
/**
|
|
60
|
+
* End a hold and PUT THE UNITS BACK — the holder changed their mind, an admin
|
|
61
|
+
* took it back, or the sweep found it expired.
|
|
62
|
+
*/
|
|
63
|
+
export declare function releaseReservation(reservationId: string, opts?: {
|
|
64
|
+
status?: "released" | "expired";
|
|
65
|
+
reason?: string;
|
|
66
|
+
}): Promise<void>;
|
|
67
|
+
/**
|
|
68
|
+
* End a hold and KEEP the units off the shelf — the arrangement completed and
|
|
69
|
+
* an order now owns them.
|
|
70
|
+
*
|
|
71
|
+
* 🛑 This is the one path that must NOT restore. The order's own decrement is
|
|
72
|
+
* what holds the units from here, so restoring first would hand the same
|
|
73
|
+
* stock to the next buyer for the moment between the two writes — and if the
|
|
74
|
+
* order write never lands, restoring would have been right. That race is why
|
|
75
|
+
* the caller should place the order and consume in one flow it can reason
|
|
76
|
+
* about; this function exists so it can do that rather than calling release
|
|
77
|
+
* and hoping.
|
|
78
|
+
*/
|
|
79
|
+
export declare function consumeReservation(reservationId: string): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Release every hold a holder owns. Used when a shared purchase is cancelled
|
|
82
|
+
* or abandoned — the arrangement knows its own id, and should not have to
|
|
83
|
+
* remember which reservations it took.
|
|
84
|
+
*/
|
|
85
|
+
export declare function releaseReservationsForHolder(holderKind: ReservationHolderKind, holderId: string, reason?: string): Promise<number>;
|
|
86
|
+
/**
|
|
87
|
+
* The sweep. Expire every hold past its `expiresAt` and restore its units.
|
|
88
|
+
*
|
|
89
|
+
* 🛑 Bounded, and the bound is REPORTED. A silent cap reads as "nothing left
|
|
90
|
+
* to expire" on the one run where it mattered — the same lie a truncated
|
|
91
|
+
* recount tells (Root Cause #102).
|
|
92
|
+
*/
|
|
93
|
+
export declare function expireDueReservations(limit?: number): Promise<{
|
|
94
|
+
scanned: number;
|
|
95
|
+
expired: number;
|
|
96
|
+
truncated: boolean;
|
|
97
|
+
}>;
|
|
98
|
+
/** Units currently held on a product, for an admin explaining an unbuyable listing. */
|
|
99
|
+
export declare function activeReservationsForProduct(productId: string): Promise<StockReservationDocument[]>;
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hold stock for something that is not an order yet.
|
|
3
|
+
*
|
|
4
|
+
* ## What this is for
|
|
5
|
+
*
|
|
6
|
+
* The plan's Part 6 (shared purchase) names this "the largest hidden cost",
|
|
7
|
+
* and it is the only one of its three prerequisites that had NOTHING behind
|
|
8
|
+
* it. See the schema file for why the existing order-keyed mechanism cannot
|
|
9
|
+
* serve a multi-day arrangement.
|
|
10
|
+
*
|
|
11
|
+
* ## 🛑 Every function here moves the row and the counter TOGETHER
|
|
12
|
+
*
|
|
13
|
+
* `availableQuantity` is what a buyer can take; the gap between it and
|
|
14
|
+
* `stockQuantity` is the outstanding hold (`resolveStockResync`'s own words).
|
|
15
|
+
* A reservation row written without the decrement holds nothing at all, and a
|
|
16
|
+
* decrement written without a row is a hold nothing can expire — so both
|
|
17
|
+
* happen inside ONE `runTransaction`, with the product read inside it so two
|
|
18
|
+
* concurrent reserves cannot both see the same `availableQuantity`.
|
|
19
|
+
*
|
|
20
|
+
* ## 🛑 Firestore wants every READ before every WRITE
|
|
21
|
+
*
|
|
22
|
+
* So `endReservation` reads the reservation AND the product up front, even
|
|
23
|
+
* on the consume path where the product read is thrown away. The natural
|
|
24
|
+
* shape — read the reservation, decide, then read the product — throws
|
|
25
|
+
* `Firestore transactions require all reads to be executed before all
|
|
26
|
+
* writes`, and only at runtime, which is exactly the class of fault
|
|
27
|
+
* `restoreStockForOrder`'s header already warns about one directory over.
|
|
28
|
+
*
|
|
29
|
+
* ## Why it writes `availableQuantity` EXPLICITLY
|
|
30
|
+
*
|
|
31
|
+
* `ProductRepository.update`'s `resolveStockResync` returns `{}` the moment a
|
|
32
|
+
* write names `availableQuantity` — deliberately, so the checkout decrement,
|
|
33
|
+
* the stock-restore job and `updateAvailableQuantity` stay authoritative over
|
|
34
|
+
* their own number. This is a fourth member of that set and takes the same
|
|
35
|
+
* deal: it is the one deciding the value, so the resync must keep its hands
|
|
36
|
+
* off it. (These writes go through the raw collection rather than the
|
|
37
|
+
* repository for the same reason `deriveTaxonomy` reads categories through
|
|
38
|
+
* `this.db` — a transaction needs the ref, not a repository method.)
|
|
39
|
+
*/
|
|
40
|
+
import { getAdminDb } from "../../../../providers/db-firebase/index.js";
|
|
41
|
+
import { ValidationError } from "../../../../errors/validation-error.js";
|
|
42
|
+
import { NotFoundError } from "../../../../errors/not-found-error.js";
|
|
43
|
+
import { ConflictError } from "../../../../errors/conflict-error.js";
|
|
44
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
45
|
+
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
46
|
+
import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firestore.js";
|
|
47
|
+
import { STOCK_RESERVATION_FIELDS } from "../../../../constants/field-names.js";
|
|
48
|
+
import { RESERVATION_DEFAULT_TTL_HOURS, RESERVATION_MAX_TTL_HOURS, STOCK_RESERVATION_COLLECTION, } from "../../../../features/inventory/schemas/stock-reservation.js";
|
|
49
|
+
/**
|
|
50
|
+
* Take a hold. Returns the reservation id.
|
|
51
|
+
*
|
|
52
|
+
* 🛑 Refuses rather than over-reserving. A seller's stock is a real number of
|
|
53
|
+
* real objects, so "reserve 5, only 3 left" is a **409** — the request is
|
|
54
|
+
* well-formed and the world moved, which is the same distinction the lottery
|
|
55
|
+
* config write makes for a slot somebody has already pulled.
|
|
56
|
+
*/
|
|
57
|
+
export async function reserveStock(input) {
|
|
58
|
+
const quantity = Math.floor(input.quantity);
|
|
59
|
+
if (!Number.isFinite(quantity) || quantity < 1) {
|
|
60
|
+
throw new ValidationError("Reserve at least one unit.");
|
|
61
|
+
}
|
|
62
|
+
const ttlHours = clampTtl(input.ttlHours);
|
|
63
|
+
const db = getAdminDb();
|
|
64
|
+
const productRef = db.collection(PRODUCT_COLLECTION).doc(input.productId);
|
|
65
|
+
const reservationRef = db.collection(STOCK_RESERVATION_COLLECTION).doc();
|
|
66
|
+
await db.runTransaction(async (tx) => {
|
|
67
|
+
const snap = await tx.get(productRef);
|
|
68
|
+
if (!snap.exists)
|
|
69
|
+
throw new NotFoundError("Product not found.");
|
|
70
|
+
const available = readAvailable(snap.data());
|
|
71
|
+
if (available < quantity) {
|
|
72
|
+
throw new ConflictError(`Only ${Math.max(0, available)} left — cannot hold ${quantity}.`);
|
|
73
|
+
}
|
|
74
|
+
const now = new Date();
|
|
75
|
+
const doc = {
|
|
76
|
+
productId: input.productId,
|
|
77
|
+
quantity,
|
|
78
|
+
holderKind: input.holderKind,
|
|
79
|
+
holderId: input.holderId,
|
|
80
|
+
createdBy: input.createdBy,
|
|
81
|
+
status: "held",
|
|
82
|
+
expiresAt: new Date(now.getTime() + ttlHours * 60 * 60 * 1000),
|
|
83
|
+
createdAt: now,
|
|
84
|
+
updatedAt: now,
|
|
85
|
+
};
|
|
86
|
+
tx.set(reservationRef, doc);
|
|
87
|
+
tx.update(productRef, { availableQuantity: available - quantity, updatedAt: now });
|
|
88
|
+
});
|
|
89
|
+
return reservationRef.id;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* End a hold and PUT THE UNITS BACK — the holder changed their mind, an admin
|
|
93
|
+
* took it back, or the sweep found it expired.
|
|
94
|
+
*/
|
|
95
|
+
export async function releaseReservation(reservationId, opts = {}) {
|
|
96
|
+
await endReservation(reservationId, opts.status ?? "released", true, opts.reason);
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* End a hold and KEEP the units off the shelf — the arrangement completed and
|
|
100
|
+
* an order now owns them.
|
|
101
|
+
*
|
|
102
|
+
* 🛑 This is the one path that must NOT restore. The order's own decrement is
|
|
103
|
+
* what holds the units from here, so restoring first would hand the same
|
|
104
|
+
* stock to the next buyer for the moment between the two writes — and if the
|
|
105
|
+
* order write never lands, restoring would have been right. That race is why
|
|
106
|
+
* the caller should place the order and consume in one flow it can reason
|
|
107
|
+
* about; this function exists so it can do that rather than calling release
|
|
108
|
+
* and hoping.
|
|
109
|
+
*/
|
|
110
|
+
export async function consumeReservation(reservationId) {
|
|
111
|
+
await endReservation(reservationId, "consumed", false);
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Release every hold a holder owns. Used when a shared purchase is cancelled
|
|
115
|
+
* or abandoned — the arrangement knows its own id, and should not have to
|
|
116
|
+
* remember which reservations it took.
|
|
117
|
+
*/
|
|
118
|
+
export async function releaseReservationsForHolder(holderKind, holderId, reason) {
|
|
119
|
+
const db = getAdminDb();
|
|
120
|
+
const snap = await db
|
|
121
|
+
.collection(STOCK_RESERVATION_COLLECTION)
|
|
122
|
+
.where(STOCK_RESERVATION_FIELDS.HOLDER_KIND, "==", holderKind)
|
|
123
|
+
.where(STOCK_RESERVATION_FIELDS.HOLDER_ID, "==", holderId)
|
|
124
|
+
.where(STOCK_RESERVATION_FIELDS.STATUS, "==", "held")
|
|
125
|
+
.get();
|
|
126
|
+
let released = 0;
|
|
127
|
+
for (const doc of snap.docs) {
|
|
128
|
+
try {
|
|
129
|
+
await endReservation(doc.id, "released", true, reason);
|
|
130
|
+
released += 1;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
const normalized = normalizeError(error);
|
|
134
|
+
/*
|
|
135
|
+
* One bad row must not strand the rest. A holder with eight holds and
|
|
136
|
+
* one deleted product should still get the other seven back.
|
|
137
|
+
*/
|
|
138
|
+
serverLogger.warn("releaseReservationsForHolder: skipped one", {
|
|
139
|
+
reservationId: doc.id,
|
|
140
|
+
message: normalized.message,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return released;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The sweep. Expire every hold past its `expiresAt` and restore its units.
|
|
148
|
+
*
|
|
149
|
+
* 🛑 Bounded, and the bound is REPORTED. A silent cap reads as "nothing left
|
|
150
|
+
* to expire" on the one run where it mattered — the same lie a truncated
|
|
151
|
+
* recount tells (Root Cause #102).
|
|
152
|
+
*/
|
|
153
|
+
export async function expireDueReservations(limit = 200) {
|
|
154
|
+
const db = getAdminDb();
|
|
155
|
+
const snap = await db
|
|
156
|
+
.collection(STOCK_RESERVATION_COLLECTION)
|
|
157
|
+
.where(STOCK_RESERVATION_FIELDS.STATUS, "==", "held")
|
|
158
|
+
.where(STOCK_RESERVATION_FIELDS.EXPIRES_AT, "<=", new Date())
|
|
159
|
+
.orderBy(STOCK_RESERVATION_FIELDS.EXPIRES_AT, "asc")
|
|
160
|
+
.limit(limit)
|
|
161
|
+
.get();
|
|
162
|
+
let expired = 0;
|
|
163
|
+
for (const doc of snap.docs) {
|
|
164
|
+
try {
|
|
165
|
+
await endReservation(doc.id, "expired", true, "TTL elapsed");
|
|
166
|
+
expired += 1;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
const normalized = normalizeError(error);
|
|
170
|
+
serverLogger.warn("expireDueReservations: skipped one", {
|
|
171
|
+
reservationId: doc.id,
|
|
172
|
+
message: normalized.message,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return { scanned: snap.size, expired, truncated: snap.size === limit };
|
|
177
|
+
}
|
|
178
|
+
/** Units currently held on a product, for an admin explaining an unbuyable listing. */
|
|
179
|
+
export async function activeReservationsForProduct(productId) {
|
|
180
|
+
const db = getAdminDb();
|
|
181
|
+
const snap = await db
|
|
182
|
+
.collection(STOCK_RESERVATION_COLLECTION)
|
|
183
|
+
.where(STOCK_RESERVATION_FIELDS.PRODUCT_ID, "==", productId)
|
|
184
|
+
.where(STOCK_RESERVATION_FIELDS.STATUS, "==", "held")
|
|
185
|
+
.get();
|
|
186
|
+
return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
|
|
187
|
+
}
|
|
188
|
+
function clampTtl(hours) {
|
|
189
|
+
if (hours === undefined)
|
|
190
|
+
return RESERVATION_DEFAULT_TTL_HOURS;
|
|
191
|
+
if (!Number.isFinite(hours) || hours <= 0) {
|
|
192
|
+
throw new ValidationError("A hold must last longer than zero hours.");
|
|
193
|
+
}
|
|
194
|
+
return Math.min(hours, RESERVATION_MAX_TTL_HOURS);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* 🛑 `?? stockQuantity` and NOT `?? 0`.
|
|
198
|
+
*
|
|
199
|
+
* `availableQuantity` is optional on rows written before it existed, and
|
|
200
|
+
* treating an absent value as zero would refuse every reservation on a
|
|
201
|
+
* fully-stocked legacy listing — a sold-out answer for a product that has
|
|
202
|
+
* stock, which reads as a stock bug rather than a schema gap.
|
|
203
|
+
*/
|
|
204
|
+
function readAvailable(data) {
|
|
205
|
+
const product = (data ?? {});
|
|
206
|
+
return product.availableQuantity ?? product.stockQuantity ?? 0;
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The one write path. Every transition goes through here so the
|
|
210
|
+
* row-and-counter pairing cannot be forgotten by a new caller — the same
|
|
211
|
+
* argument `deriveLinePrice` makes inside the cart repository.
|
|
212
|
+
*/
|
|
213
|
+
async function endReservation(reservationId, status, restore, reason) {
|
|
214
|
+
const db = getAdminDb();
|
|
215
|
+
const reservationRef = db.collection(STOCK_RESERVATION_COLLECTION).doc(reservationId);
|
|
216
|
+
await db.runTransaction(async (tx) => {
|
|
217
|
+
const snap = await tx.get(reservationRef);
|
|
218
|
+
if (!snap.exists)
|
|
219
|
+
throw new NotFoundError("Reservation not found.");
|
|
220
|
+
const reservation = snap.data();
|
|
221
|
+
/*
|
|
222
|
+
* 🛑 Idempotent by STATUS, not by a flag. The sweep and a manual release
|
|
223
|
+
* can reach the same row in the same second; a second restore would
|
|
224
|
+
* invent units that do not exist, which is worse than a hold that lingers
|
|
225
|
+
* one sweep longer.
|
|
226
|
+
*/
|
|
227
|
+
if (reservation.status !== "held")
|
|
228
|
+
return;
|
|
229
|
+
/*
|
|
230
|
+
* Read the product BEFORE the first write, even when `restore` is false
|
|
231
|
+
* and the value is discarded — see this file's header. One wasted read on
|
|
232
|
+
* the consume path buys a function whose transaction shape cannot be
|
|
233
|
+
* broken by a later edit reordering the branches.
|
|
234
|
+
*/
|
|
235
|
+
const productRef = db.collection(PRODUCT_COLLECTION).doc(reservation.productId);
|
|
236
|
+
const productSnap = restore ? await tx.get(productRef) : null;
|
|
237
|
+
const now = new Date();
|
|
238
|
+
tx.update(reservationRef, {
|
|
239
|
+
status,
|
|
240
|
+
endedAt: now,
|
|
241
|
+
updatedAt: now,
|
|
242
|
+
...(reason ? { endedReason: reason } : {}),
|
|
243
|
+
});
|
|
244
|
+
if (!restore)
|
|
245
|
+
return;
|
|
246
|
+
if (!productSnap?.exists) {
|
|
247
|
+
/*
|
|
248
|
+
* The product is gone, so there is no counter to restore — but the row
|
|
249
|
+
* still has to close, or the sweep retries it forever. Recorded rather
|
|
250
|
+
* than thrown: an orphaned hold is a data problem, not a reason to fail
|
|
251
|
+
* the release of the others beside it.
|
|
252
|
+
*/
|
|
253
|
+
serverLogger.warn("endReservation: product missing, counter not restored", {
|
|
254
|
+
reservationId,
|
|
255
|
+
productId: reservation.productId,
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const product = productSnap.data();
|
|
260
|
+
const available = readAvailable(product);
|
|
261
|
+
const stock = product.stockQuantity ?? available;
|
|
262
|
+
/*
|
|
263
|
+
* 🛑 Clamped at `stockQuantity`. A seller who lowered stock while units
|
|
264
|
+
* were held would otherwise get an `availableQuantity` ABOVE their own
|
|
265
|
+
* stock on release — a listing offering more than exists, which is the
|
|
266
|
+
* one direction a stock bug must never go.
|
|
267
|
+
*/
|
|
268
|
+
tx.update(productRef, {
|
|
269
|
+
availableQuantity: Math.min(stock, available + reservation.quantity),
|
|
270
|
+
updatedAt: now,
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allocating a number from a gapless series (Phase 7, D12).
|
|
3
|
+
*
|
|
4
|
+
* ## The whole design in one paragraph
|
|
5
|
+
*
|
|
6
|
+
* One transaction does three things: read the (series, FY) counter, `.create()`
|
|
7
|
+
* an allocation claim keyed on the subject, and write `lastNumber + 1`. Any one
|
|
8
|
+
* of the three failing rolls back all of them, so there is no state where a
|
|
9
|
+
* number was consumed without being recorded, and no state where a subject
|
|
10
|
+
* holds two numbers.
|
|
11
|
+
*
|
|
12
|
+
* ## 🛑 Why the claim is INSIDE the transaction
|
|
13
|
+
*
|
|
14
|
+
* The obvious shape is "check whether this order already has a number, then
|
|
15
|
+
* allocate". That is a read-then-write race: two concurrent triggers both read
|
|
16
|
+
* "no number", both allocate, and the order ends up with two — one of which is
|
|
17
|
+
* a permanent gap in a series an auditor reads.
|
|
18
|
+
*
|
|
19
|
+
* `.create()` inside the transaction makes it impossible rather than unlikely.
|
|
20
|
+
* A duplicate attempt fails the transaction, and the caller treats that as
|
|
21
|
+
* "already allocated" and reads the existing number back. This is the fourth
|
|
22
|
+
* use of that primitive in this codebase (refund claims, the gateway-event
|
|
23
|
+
* inbox, the gateway-order claim), and the one with the least recoverable
|
|
24
|
+
* failure mode.
|
|
25
|
+
*
|
|
26
|
+
* ## 🛑 Allocate on `paymentStatus → paid`, never at order creation
|
|
27
|
+
*
|
|
28
|
+
* COD and manual orders are created `pending`. Numbering at creation burns a
|
|
29
|
+
* number on every abandoned order, and *gapless* means those gaps are
|
|
30
|
+
* unexplainable — "that one was abandoned" is not an answer an auditor accepts
|
|
31
|
+
* for a missing invoice number.
|
|
32
|
+
*/
|
|
33
|
+
import { type InvoiceSeriesKind } from "../../../../features/invoicing/schemas/firestore";
|
|
34
|
+
export interface AllocateSeriesNumberInput {
|
|
35
|
+
kind: InvoiceSeriesKind;
|
|
36
|
+
/**
|
|
37
|
+
* What the number is FOR, and the idempotency key.
|
|
38
|
+
*
|
|
39
|
+
* An invoice passes the order id. A credit note passes
|
|
40
|
+
* `${orderId}_${refundId}`, because an order may have several.
|
|
41
|
+
*/
|
|
42
|
+
subjectId: string;
|
|
43
|
+
orderId: string;
|
|
44
|
+
/** Defaults to now. Injectable so a backfill can issue into a past FY. */
|
|
45
|
+
at?: Date;
|
|
46
|
+
}
|
|
47
|
+
export interface AllocateSeriesNumberResult {
|
|
48
|
+
number: string;
|
|
49
|
+
fy: string;
|
|
50
|
+
sequence: number;
|
|
51
|
+
/** True when this call did the allocating; false when it found an existing one. */
|
|
52
|
+
allocated: boolean;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Allocate (or return the already-allocated) number for one subject.
|
|
56
|
+
*
|
|
57
|
+
* 🛑 Idempotent and NOT best-effort. It throws rather than returning a
|
|
58
|
+
* sentinel, because every caller's correct response to "we could not number
|
|
59
|
+
* this" is to leave the order un-invoiced and retry — a caller that treated
|
|
60
|
+
* failure as "no number needed" would produce a paid order with no invoice,
|
|
61
|
+
* silently.
|
|
62
|
+
*/
|
|
63
|
+
export declare function allocateSeriesNumber(input: AllocateSeriesNumberInput): Promise<AllocateSeriesNumberResult>;
|