@mohasinac/appkit 4.10.0 → 4.11.1
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/_internal/client/features/layout/DashboardLayoutClient.d.ts +6 -1
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +4 -1
- package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +1 -1
- package/dist/_internal/client/features/lottery/LotteryPullForm.js +2 -2
- package/dist/_internal/server/features/admin/daily-digest-actions.d.ts +18 -0
- package/dist/_internal/server/features/admin/daily-digest-actions.js +37 -0
- package/dist/_internal/server/features/audit-log/actions.d.ts +25 -0
- package/dist/_internal/server/features/audit-log/actions.js +38 -0
- package/dist/_internal/server/features/brands/actions.js +0 -2
- package/dist/_internal/server/features/brands/og.d.ts +4 -1
- package/dist/_internal/server/features/brands/og.js +1 -1
- package/dist/_internal/server/features/grouped/data.d.ts +4 -0
- package/dist/_internal/server/features/grouped/data.js +24 -0
- package/dist/_internal/server/features/grouped/index.d.ts +1 -1
- package/dist/_internal/server/features/grouped/index.js +1 -1
- package/dist/_internal/server/features/orders/actions.js +9 -0
- package/dist/_internal/server/features/orders/adapters.js +25 -0
- package/dist/_internal/server/features/products/data.d.ts +2 -2
- package/dist/_internal/server/features/products/data.js +5 -23
- package/dist/_internal/server/functions/scheduled.d.ts +3 -1
- package/dist/_internal/server/functions/scheduled.js +17 -1
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +40 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +164 -0
- package/dist/_internal/server/jobs/core/hardBanCascade.js +11 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.d.ts +1 -0
- package/dist/_internal/server/jobs/core/testerSandboxCleanup.js +4 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.d.ts +12 -0
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +85 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +16 -0
- package/dist/_internal/server/jobs/handlers/dailyStatusDigest.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/dailyStatusDigest.js +4 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/index.js +3 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testerSandboxRefresh.js +2 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +27 -0
- package/dist/_internal/server/notifications/channel-health.js +101 -0
- package/dist/_internal/shared/features/brands/schema.d.ts +0 -6
- package/dist/_internal/shared/features/brands/schema.js +0 -1
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +7 -0
- package/dist/_internal/shared/features/categories/bundle-schemas.js +3 -0
- package/dist/_internal/shared/features/orders/schema.d.ts +8 -8
- package/dist/_internal/shared/features/products/to-product-item.d.ts +25 -0
- package/dist/_internal/shared/features/products/to-product-item.js +45 -0
- package/dist/client.d.ts +21 -6
- package/dist/client.js +13 -3
- package/dist/constants/api-endpoints.d.ts +6 -9
- package/dist/constants/api-endpoints.js +2 -3
- package/dist/contracts/client-auth.d.ts +3 -7
- package/dist/contracts/email.d.ts +1 -0
- package/dist/features/account/components/AddressForm.js +24 -2
- package/dist/features/account/components/BecomeSellerView.d.ts +1 -2
- package/dist/features/account/components/BecomeSellerView.js +1 -1
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/components/UserBidsView.d.ts +1 -0
- package/dist/features/account/components/UserBidsView.js +47 -0
- package/dist/features/account/components/UserOrdersView.d.ts +4 -11
- package/dist/features/account/components/UserOrdersView.js +54 -4
- package/dist/features/account/components/UserReturnsView.d.ts +4 -9
- package/dist/features/account/components/UserReturnsView.js +37 -3
- package/dist/features/account/components/index.d.ts +3 -2
- package/dist/features/account/components/index.js +1 -0
- package/dist/features/admin/actions/admin-actions.js +22 -0
- package/dist/features/admin/actions/admin-coupon-actions.js +10 -0
- package/dist/features/admin/actions/notification-actions.js +4 -3
- package/dist/features/admin/components/AdminAdsView.d.ts +2 -3
- package/dist/features/admin/components/AdminAdsView.js +86 -89
- package/dist/features/admin/components/AdminAllEventEntriesView.js +18 -3
- package/dist/features/admin/components/AdminAuditLogView.d.ts +11 -0
- package/dist/features/admin/components/AdminAuditLogView.js +95 -0
- package/dist/features/admin/components/AdminBlogEditorView.js +37 -3
- package/dist/features/admin/components/AdminBrandEditorView.js +10 -6
- package/dist/features/admin/components/AdminBundleEditorView.js +18 -2
- package/dist/features/admin/components/AdminCarouselEditorView.js +2 -2
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +2 -2
- package/dist/features/admin/components/AdminCartsView.js +34 -4
- package/dist/features/admin/components/AdminCategoryEditorView.js +5 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +2 -2
- package/dist/features/admin/components/AdminFaqEditorView.js +2 -2
- package/dist/features/admin/components/AdminHistoryView.js +11 -1
- package/dist/features/admin/components/AdminNotificationsView.js +27 -1
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +10 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +4 -3
- package/dist/features/admin/components/AdminOrdersView.js +92 -24
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.d.ts +7 -0
- package/dist/features/admin/components/AdminPayoutMarkPaidModal.js +47 -0
- package/dist/features/admin/components/AdminPayoutsView.js +15 -31
- package/dist/features/admin/components/AdminProductEditorView.js +26 -3
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminStoreAddressesView.js +22 -2
- package/dist/features/admin/components/AdminStoresView.js +7 -0
- package/dist/features/admin/components/AdminSublistingCategoryEditorView.js +2 -2
- package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +2 -2
- package/dist/features/admin/components/AdminUserEditorView.d.ts +2 -2
- package/dist/features/admin/components/AdminUserEditorView.js +4 -2
- package/dist/features/admin/components/AdminUsersView.js +1 -1
- package/dist/features/admin/components/AdminWishlistsView.js +14 -1
- package/dist/features/admin/components/DataListingView.d.ts +2 -2
- package/dist/features/admin/components/ViewAuditLogEntryModal.d.ts +19 -0
- package/dist/features/admin/components/ViewAuditLogEntryModal.js +22 -0
- package/dist/features/admin/components/ViewNotificationModal.d.ts +21 -0
- package/dist/features/admin/components/ViewNotificationModal.js +15 -0
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.d.ts +4 -7
- package/dist/features/admin/components/analytics/AdminPageViewsReportView.js +56 -36
- package/dist/features/admin/components/index.d.ts +8 -1
- package/dist/features/admin/components/index.js +4 -0
- package/dist/features/admin/constants/filter-tabs.d.ts +49 -0
- package/dist/features/admin/constants/filter-tabs.js +27 -0
- package/dist/features/admin/hooks/useAdminListing.d.ts +6 -0
- package/dist/features/admin/hooks/useAdminListing.js +3 -1
- package/dist/features/admin/hooks/useAdminListingData.d.ts +9 -1
- package/dist/features/admin/hooks/useAdminListingData.js +9 -2
- package/dist/features/admin/repository/site-settings.repository.d.ts +5 -0
- package/dist/features/admin/schemas/firestore.d.ts +12 -0
- package/dist/features/analytics/types.d.ts +1 -1
- package/dist/features/analytics/types.js +8 -0
- package/dist/features/auctions/schemas/index.d.ts +5 -0
- package/dist/features/audit-log/repository/audit-log.repository.d.ts +45 -0
- package/dist/features/audit-log/repository/audit-log.repository.js +30 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +59 -0
- package/dist/features/audit-log/schemas/firestore.js +35 -0
- package/dist/features/auth/actions/index.d.ts +0 -1
- package/dist/features/auth/actions/index.js +0 -1
- package/dist/features/auth/hooks/useAuth.d.ts +8 -35
- package/dist/features/auth/hooks/useAuth.js +21 -37
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +2 -0
- package/dist/features/blog/schemas/index.d.ts +32 -0
- package/dist/features/catalogue/components/AdminCatalogueApprovalsView.js +31 -3
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +2 -1
- package/dist/features/categories/components/BrandDetailPageView.js +14 -12
- package/dist/features/categories/components/CategoryDetailPageView.js +7 -3
- package/dist/features/categories/components/CategoryHighlightsAndFaqSection.d.ts +14 -0
- package/dist/features/categories/components/CategoryHighlightsAndFaqSection.js +22 -0
- package/dist/features/categories/components/CategoryProductsView.d.ts +1 -5
- package/dist/features/categories/components/CategoryProductsView.js +1 -1
- package/dist/features/categories/components/CategorySelectorCreate.js +16 -2
- package/dist/features/categories/schemas/firestore.d.ts +19 -2
- package/dist/features/categories/schemas/index.d.ts +24 -3
- package/dist/features/categories/schemas/index.js +3 -1
- package/dist/features/categories/types/index.d.ts +7 -0
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +17 -3
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +101 -22
- package/dist/features/contact/components/ContactForm.js +17 -4
- package/dist/features/contact/email.d.ts +1 -0
- package/dist/features/contact/email.js +3 -0
- package/dist/features/events/components/AdminEventEditorView.js +54 -2
- package/dist/features/events/components/AdminEventEntriesView.d.ts +2 -16
- package/dist/features/events/components/AdminEventEntriesView.js +56 -89
- package/dist/features/events/components/EventFormDrawer.js +5 -1
- package/dist/features/events/components/EventRaffleEntryForm.d.ts +1 -3
- package/dist/features/events/components/EventRaffleEntryForm.js +1 -1
- package/dist/features/events/schemas/index.d.ts +22 -0
- package/dist/features/grouped/components/GroupedListingsCarousel.js +4 -1
- package/dist/features/grouped/repository/grouped-listings.repository.d.ts +4 -0
- package/dist/features/grouped/repository/grouped-listings.repository.js +18 -0
- package/dist/features/homepage/components/CharacterHotspotForm.js +202 -187
- package/dist/features/homepage/components/SectionCarousel.d.ts +1 -7
- package/dist/features/homepage/components/SectionCarousel.js +1 -1
- package/dist/features/layout/TitleBarLayout.d.ts +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/media/MediaImage.js +39 -3
- package/dist/features/media/types/index.d.ts +10 -0
- package/dist/features/media/types/index.js +1 -0
- package/dist/features/media/upload/MediaUploadField.d.ts +7 -1
- package/dist/features/media/upload/MediaUploadField.js +11 -7
- package/dist/features/media/upload/video-poster.d.ts +2 -0
- package/dist/features/media/upload/video-poster.js +16 -1
- package/dist/features/media/upload/video-poster.test.js +1 -1
- package/dist/features/orders/components/OrderStatusTimeline.d.ts +12 -0
- package/dist/features/orders/components/OrderStatusTimeline.js +55 -0
- package/dist/features/orders/components/index.d.ts +2 -0
- package/dist/features/orders/components/index.js +1 -0
- package/dist/features/orders/constants/payment-window.d.ts +25 -0
- package/dist/features/orders/constants/payment-window.js +25 -0
- package/dist/features/orders/repository/orders.repository.d.ts +32 -0
- package/dist/features/orders/repository/orders.repository.js +56 -0
- package/dist/features/orders/schemas/index.d.ts +24 -24
- package/dist/features/orders/types/index.d.ts +33 -0
- package/dist/features/pre-orders/schemas/index.d.ts +5 -0
- package/dist/features/products/components/BidHistory.js +9 -1
- package/dist/features/products/components/GroupSettingsPanel.js +25 -3
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +1 -4
- package/dist/features/products/components/PrizeDrawEntryActions.js +1 -1
- package/dist/features/products/components/ProductDetailActions.d.ts +1 -2
- package/dist/features/products/components/ProductDetailActions.js +1 -1
- package/dist/features/products/components/ProductGrid.js +15 -1
- package/dist/features/products/constants/action-defs.d.ts +1 -0
- package/dist/features/products/constants/action-defs.js +1 -0
- package/dist/features/products/schemas/index.d.ts +5 -0
- package/dist/features/promotions/components/CouponsIndexListing.d.ts +1 -3
- package/dist/features/promotions/components/CouponsIndexListing.js +1 -1
- package/dist/features/search/components/SearchView.d.ts +1 -3
- package/dist/features/search/components/SearchView.js +1 -1
- package/dist/features/seller/components/QuickProductForm.js +2 -1
- package/dist/features/seller/components/SellerBidsView.js +66 -107
- package/dist/features/seller/components/SellerGoogleReviewsView.js +17 -1
- package/dist/features/seller/components/SellerOrdersView.d.ts +15 -3
- package/dist/features/seller/components/SellerOrdersView.js +134 -163
- package/dist/features/seller/components/SellerPayoutMethodsView.js +60 -48
- package/dist/features/seller/components/SellerPayoutSettingsView.js +69 -4
- package/dist/features/seller/components/SellerPayoutsView.d.ts +12 -0
- package/dist/features/seller/components/SellerPayoutsView.js +25 -5
- package/dist/features/seller/components/SellerProductShell.d.ts +10 -3
- package/dist/features/seller/components/SellerProductShell.js +89 -36
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +2 -7
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +4 -4
- package/dist/features/seller/components/SellerProductsView.d.ts +2 -4
- package/dist/features/seller/components/SellerProductsView.js +131 -189
- package/dist/features/seller/components/SellerReviewsView.js +95 -133
- package/dist/features/seller/components/SellerShippingConfigsView.js +47 -67
- package/dist/features/seller/components/SellerShippingView.js +41 -4
- package/dist/features/seller/components/SellerStoreCategoriesView.js +51 -59
- package/dist/features/seller/components/SellerStorefrontView.d.ts +6 -1
- package/dist/features/seller/components/SellerStorefrontView.js +42 -5
- package/dist/features/seller/components/index.d.ts +2 -2
- package/dist/features/seller/components/index.js +2 -2
- package/dist/features/shell/FormShell.d.ts +24 -4
- package/dist/features/shell/FormShell.js +12 -6
- package/dist/features/shell/QuickFormDrawer.js +67 -12
- package/dist/features/shell/StepForm.d.ts +30 -1
- package/dist/features/shell/StepForm.js +47 -4
- package/dist/features/shell/index.d.ts +1 -1
- package/dist/features/shell/index.js +1 -1
- package/dist/features/shipments/components/AdminShipmentEditorView.js +2 -2
- package/dist/features/shipments/schemas/validation.d.ts +14 -4
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +23 -2
- package/dist/features/tester/components/TesterChecklistStepRow.js +1 -1
- package/dist/features/tester/components/TesterHubView.js +3 -2
- package/dist/features/tester/seed-data/orders-tester-seed-data.js +9 -1
- package/dist/features/tester/seed-data/products-tester-seed-data.js +43 -32
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +404 -15
- package/dist/index.d.ts +15 -3
- package/dist/index.js +22 -5
- package/dist/next/routing/route-map.d.ts +11 -0
- package/dist/next/routing/route-map.js +5 -0
- package/dist/providers/email-resend/provider.js +1 -0
- package/dist/providers/firebase-client/auth.d.ts +1 -0
- package/dist/providers/firebase-client/auth.js +7 -1
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +61 -25
- package/dist/security/pii-mask.js +1 -1
- package/dist/seed/categories-seed-data.js +69 -0
- package/dist/seed/products-auctions-seed-data.js +11 -0
- package/dist/seed/products-standard-seed-data.js +93 -4
- package/dist/seed/site-settings-seed-data.js +13 -1
- package/dist/seed/store-extensions-seed-data.js +1 -1
- package/dist/server-entry.d.ts +2 -1
- package/dist/server-entry.js +20 -2
- package/dist/server.d.ts +7 -2
- package/dist/server.js +8 -3
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Form.d.ts +11 -2
- package/dist/ui/components/Form.js +4 -3
- package/dist/ui/components/FormField.d.ts +3 -1
- package/dist/ui/components/FormField.js +22 -4
- package/dist/ui/components/ListingLayout.d.ts +1 -2
- package/dist/ui/components/ListingLayout.js +1 -1
- package/dist/ui/components/RecordDetailModal.d.ts +68 -0
- package/dist/ui/components/RecordDetailModal.js +8 -0
- package/dist/ui/components/SlottedListingView.js +9 -1
- package/dist/ui/forms/FormErrorSummary.d.ts +25 -0
- package/dist/ui/forms/FormErrorSummary.js +37 -0
- package/dist/ui/forms/FormShell.d.ts +37 -2
- package/dist/ui/forms/FormShell.js +37 -7
- package/dist/ui/forms/FormShell.style.css +17 -0
- package/dist/ui/forms/index.d.ts +2 -0
- package/dist/ui/forms/index.js +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +5 -0
- package/dist/utils/media-url.d.ts +3 -1
- package/dist/utils/media-url.js +20 -1
- package/dist/validation/schemas.d.ts +4 -4
- package/package.json +1 -1
- package/dist/_internal/client/features/classified/ClassifiedDetailView.d.ts +0 -9
- package/dist/_internal/client/features/classified/ClassifiedDetailView.js +0 -55
- package/dist/_internal/client/features/digital-code/DigitalCodeDetailView.d.ts +0 -18
- package/dist/_internal/client/features/digital-code/DigitalCodeDetailView.js +0 -21
- package/dist/_internal/client/features/live/LiveItemDetailView.d.ts +0 -9
- package/dist/_internal/client/features/live/LiveItemDetailView.js +0 -25
- package/dist/_internal/server/jobs/core/prizeRevealClose.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealClose.js +0 -28
- package/dist/_internal/server/jobs/core/prizeRevealExpiry.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealExpiry.js +0 -61
- package/dist/_internal/server/jobs/core/prizeRevealOpen.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealOpen.js +0 -68
- package/dist/_internal/server/jobs/core/prizeRevealReminder.d.ts +0 -2
- package/dist/_internal/server/jobs/core/prizeRevealReminder.js +0 -49
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealClose.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealExpiry.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealOpen.js +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/prizeRevealReminder.js +0 -2
- package/dist/features/auth/consent-otp.d.ts +0 -57
- package/dist/features/auth/consent-otp.js +0 -127
- package/dist/features/cart/components/CheckoutOtpModal.d.ts +0 -15
- package/dist/features/cart/components/CheckoutOtpModal.js +0 -12
- package/dist/features/checkout/actions/checkout-actions.d.ts +0 -31
- package/dist/features/checkout/actions/checkout-actions.js +0 -124
- package/dist/features/classified/components/ClassifiedIndexListing.d.ts +0 -5
- package/dist/features/classified/components/ClassifiedIndexListing.js +0 -123
- package/dist/features/classified/components/ClassifiedListView.d.ts +0 -6
- package/dist/features/classified/components/ClassifiedListView.js +0 -44
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.d.ts +0 -5
- package/dist/features/digital-codes/components/DigitalCodesIndexListing.js +0 -114
- package/dist/features/digital-codes/components/DigitalCodesListView.d.ts +0 -6
- package/dist/features/digital-codes/components/DigitalCodesListView.js +0 -38
- package/dist/features/live/components/LiveItemsIndexListing.d.ts +0 -5
- package/dist/features/live/components/LiveItemsIndexListing.js +0 -115
- package/dist/features/live/components/LiveItemsListView.d.ts +0 -6
- package/dist/features/live/components/LiveItemsListView.js +0 -44
- package/dist/features/seller/components/SellerTemplatesView.d.ts +0 -12
- package/dist/features/seller/components/SellerTemplatesView.js +0 -276
- package/dist/react/hooks/useVisibleItems.d.ts +0 -37
- package/dist/react/hooks/useVisibleItems.js +0 -59
- package/dist/ui/components/TabStrip.d.ts +0 -34
- package/dist/ui/components/TabStrip.js +0 -56
- package/dist/ui/components/TabStrip.style.css +0 -97
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Core: daily status digest email.
|
|
4
|
+
*
|
|
5
|
+
* Emails the previous 24h of order activity to the team every morning. The
|
|
6
|
+
* digest's arrival is itself the platform health signal — if it stops
|
|
7
|
+
* landing, something in the Firestore → Functions → Resend chain needs a
|
|
8
|
+
* look. Deliberately no separate error/exception tracking: there is no
|
|
9
|
+
* structured error-log collection to query, and building one just to fill
|
|
10
|
+
* this email would be scope no one asked for.
|
|
11
|
+
*
|
|
12
|
+
* Runs from a scheduled Firebase Function (not a Vercel route), so the
|
|
13
|
+
* unbounded-ish order scan below is safe against CLAUDE.md Rule #6's 10s
|
|
14
|
+
* sync ceiling — it has the Function's much larger budget.
|
|
15
|
+
*/
|
|
16
|
+
import { renderToStaticMarkup } from "react-dom/server";
|
|
17
|
+
import { getAdminDb } from "../../../../providers/db-firebase";
|
|
18
|
+
import { sendEmail } from "../../../../features/contact/email";
|
|
19
|
+
import { siteSettingsRepository } from "../../../../features/admin/repository/site-settings.repository";
|
|
20
|
+
import { ORDER_COLLECTION, OrderStatusValues } from "../../../../features/orders/schemas/firestore";
|
|
21
|
+
import { PRODUCT_COLLECTION, ProductStatusValues } from "../../../../features/products/schemas/firestore";
|
|
22
|
+
import { ORDER_FIELDS, PRODUCT_FIELDS } from "../../../../constants/field-names";
|
|
23
|
+
import { EmailBold, EmailContainer, EmailDivider, EmailDoc, EmailFooter, EmailHeader, EmailRow, } from "../../../../features/email/primitives";
|
|
24
|
+
import { normalizeError } from "../../../../errors/normalize";
|
|
25
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
26
|
+
/** Statuses that shouldn't count toward revenue for the day. */
|
|
27
|
+
const NON_REVENUE_STATUSES = new Set([
|
|
28
|
+
OrderStatusValues.CANCELLED,
|
|
29
|
+
OrderStatusValues.REFUNDED,
|
|
30
|
+
]);
|
|
31
|
+
function currency(amount) {
|
|
32
|
+
return `₹${amount.toLocaleString("en-IN", { maximumFractionDigits: 2 })}`;
|
|
33
|
+
}
|
|
34
|
+
/** One "Label: value" line. Uses only email primitives — email clients need
|
|
35
|
+
* inline-styled, table-based markup, which the Email* components provide. */
|
|
36
|
+
function StatLine({ label, value }) {
|
|
37
|
+
return (_jsxs(_Fragment, { children: [label, ": ", _jsx(EmailBold, { children: String(value) }), _jsx("br", {})] }));
|
|
38
|
+
}
|
|
39
|
+
/** Gathers the last-24h numbers. Exported so the admin manual-trigger route can reuse it. */
|
|
40
|
+
export async function collectDailyStatusSummary() {
|
|
41
|
+
const db = getAdminDb();
|
|
42
|
+
const since = new Date(Date.now() - DAY_MS);
|
|
43
|
+
const recentSnap = await db
|
|
44
|
+
.collection(ORDER_COLLECTION)
|
|
45
|
+
.where(ORDER_FIELDS.CREATED_AT, ">=", since)
|
|
46
|
+
.get();
|
|
47
|
+
let revenue = 0;
|
|
48
|
+
const statusBreakdown = {};
|
|
49
|
+
for (const doc of recentSnap.docs) {
|
|
50
|
+
const data = doc.data();
|
|
51
|
+
const status = data.status ?? "unknown";
|
|
52
|
+
statusBreakdown[status] = (statusBreakdown[status] ?? 0) + 1;
|
|
53
|
+
if (!NON_REVENUE_STATUSES.has(status)) {
|
|
54
|
+
revenue += Number(data.totalPrice ?? 0) || 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Orders still sitting in PENDING beyond the 24h window — the one number
|
|
58
|
+
// here that signals something needs attention rather than just reporting
|
|
59
|
+
// volume.
|
|
60
|
+
const stuckSnap = await db
|
|
61
|
+
.collection(ORDER_COLLECTION)
|
|
62
|
+
.where(ORDER_FIELDS.STATUS, "==", OrderStatusValues.PENDING)
|
|
63
|
+
.where(ORDER_FIELDS.CREATED_AT, "<", since)
|
|
64
|
+
.count()
|
|
65
|
+
.get();
|
|
66
|
+
const activeSnap = await db
|
|
67
|
+
.collection(PRODUCT_COLLECTION)
|
|
68
|
+
.where(PRODUCT_FIELDS.STATUS, "==", ProductStatusValues.PUBLISHED)
|
|
69
|
+
.count()
|
|
70
|
+
.get();
|
|
71
|
+
return {
|
|
72
|
+
orderCount: recentSnap.size,
|
|
73
|
+
revenue,
|
|
74
|
+
statusBreakdown,
|
|
75
|
+
stuckPendingCount: stuckSnap.data().count,
|
|
76
|
+
activeListingCount: activeSnap.data().count,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function renderDigestHtml(summary, siteName, version) {
|
|
80
|
+
const statusRows = Object.entries(summary.statusBreakdown).sort(([a], [b]) => a.localeCompare(b));
|
|
81
|
+
const title = siteName ? `${siteName} — Daily Status` : "Daily Status";
|
|
82
|
+
return `<!DOCTYPE html>${renderToStaticMarkup(_jsx(EmailDoc, { title: title, children: _jsxs(EmailContainer, { children: [_jsx(EmailHeader, { brandName: siteName }), _jsxs(EmailRow, { children: [version
|
|
83
|
+
? `Deployment ${version} is live. Activity for the last 24 hours.`
|
|
84
|
+
: "Activity for the last 24 hours.", _jsx("br", {}), _jsx("br", {}), _jsx(StatLine, { label: "Orders placed", value: summary.orderCount }), _jsx(StatLine, { label: "Revenue (excl. cancelled/refunded)", value: currency(summary.revenue) }), _jsx(StatLine, { label: "Active listings", value: summary.activeListingCount }), _jsx(StatLine, { label: "Pending > 24h (needs attention)", value: summary.stuckPendingCount })] }), _jsx(EmailDivider, {}), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Orders by status" }), _jsx("br", {}), _jsx("br", {}), statusRows.length === 0 ? ("No orders in this window.") : (statusRows.map(([status, count]) => (_jsx(StatLine, { label: status, value: count }, status))))] }), _jsx(EmailFooter, { copyright: `© ${new Date().getFullYear()}${siteName ? ` ${siteName}` : ""}. All rights reserved.` })] }) }))}`;
|
|
85
|
+
}
|
|
86
|
+
const DEPLOY_MARKER_COLLECTION = "system";
|
|
87
|
+
const DEPLOY_MARKER_DOC = "deployDigest";
|
|
88
|
+
/**
|
|
89
|
+
* Sends the digest once per deployment version.
|
|
90
|
+
*
|
|
91
|
+
* Called from server startup. On Vercel that means EVERY lambda cold start,
|
|
92
|
+
* not once per deploy — so the version marker below is what makes this a
|
|
93
|
+
* per-deployment event rather than a per-cold-start email storm. The claim
|
|
94
|
+
* runs in a transaction because several cold starts can race on the first
|
|
95
|
+
* request after a deploy; exactly one wins and sends.
|
|
96
|
+
*/
|
|
97
|
+
export async function runDeploymentDigest(ctx, version) {
|
|
98
|
+
if (!version)
|
|
99
|
+
return { sent: false, reason: "no_version" };
|
|
100
|
+
const markerRef = getAdminDb().collection(DEPLOY_MARKER_COLLECTION).doc(DEPLOY_MARKER_DOC);
|
|
101
|
+
let claimed = false;
|
|
102
|
+
try {
|
|
103
|
+
await getAdminDb().runTransaction(async (tx) => {
|
|
104
|
+
const snap = await tx.get(markerRef);
|
|
105
|
+
const lastVersion = snap.data()?.lastVersion;
|
|
106
|
+
if (lastVersion === version)
|
|
107
|
+
return;
|
|
108
|
+
tx.set(markerRef, { lastVersion: version, sentAt: new Date() }, { merge: true });
|
|
109
|
+
claimed = true;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
catch (err) {
|
|
113
|
+
void normalizeError(err);
|
|
114
|
+
ctx.logger.warn("deploymentDigest: could not claim version marker — skipping", { version });
|
|
115
|
+
return { sent: false, reason: "claim_failed" };
|
|
116
|
+
}
|
|
117
|
+
if (!claimed)
|
|
118
|
+
return { sent: false, reason: "already_sent_for_version" };
|
|
119
|
+
ctx.logger.info("deploymentDigest: claimed version, sending", { version });
|
|
120
|
+
return runDailyStatusDigest(ctx, version);
|
|
121
|
+
}
|
|
122
|
+
export async function runDailyStatusDigest(ctx, version) {
|
|
123
|
+
const settings = await siteSettingsRepository.getSingleton().catch((err) => {
|
|
124
|
+
void normalizeError(err);
|
|
125
|
+
return null;
|
|
126
|
+
});
|
|
127
|
+
const digestConfig = settings?.emailSettings?.dailyDigest;
|
|
128
|
+
if (digestConfig?.enabled !== true) {
|
|
129
|
+
ctx.logger.info("dailyStatusDigest: disabled in site settings — skipping");
|
|
130
|
+
return { sent: false, reason: "disabled" };
|
|
131
|
+
}
|
|
132
|
+
const recipients = (digestConfig.recipients ?? []).filter((r) => r.trim().length > 0);
|
|
133
|
+
if (recipients.length === 0) {
|
|
134
|
+
ctx.logger.warn("dailyStatusDigest: enabled but no recipients configured — skipping");
|
|
135
|
+
return { sent: false, reason: "no_recipients" };
|
|
136
|
+
}
|
|
137
|
+
const ccRecipients = (digestConfig.ccRecipients ?? []).filter((r) => r.trim().length > 0);
|
|
138
|
+
const summary = await collectDailyStatusSummary();
|
|
139
|
+
// No hardcoded brand fallback — appkit is consumer-agnostic; the site name
|
|
140
|
+
// comes from settings or the consumer's own env var.
|
|
141
|
+
const siteName = settings?.siteName?.trim() || process.env.NEXT_PUBLIC_SITE_NAME?.trim() || "";
|
|
142
|
+
const { error } = await sendEmail({
|
|
143
|
+
to: recipients,
|
|
144
|
+
// Spread rather than `cc: undefined` — SendEmailOptions has a
|
|
145
|
+
// Record<string, JsonValue> index signature that rejects undefined.
|
|
146
|
+
...(ccRecipients.length > 0 ? { cc: ccRecipients } : {}),
|
|
147
|
+
subject: "Daily Status",
|
|
148
|
+
html: renderDigestHtml(summary, siteName, version),
|
|
149
|
+
text: (version ? `Deployment ${version} is live.\n\n` : "") +
|
|
150
|
+
`Orders placed: ${summary.orderCount}\n` +
|
|
151
|
+
`Revenue: ${currency(summary.revenue)}\n` +
|
|
152
|
+
`Active listings: ${summary.activeListingCount}\n` +
|
|
153
|
+
`Pending > 24h: ${summary.stuckPendingCount}`,
|
|
154
|
+
});
|
|
155
|
+
if (error) {
|
|
156
|
+
ctx.logger.error("dailyStatusDigest: send failed", { error: String(error) });
|
|
157
|
+
throw new Error(`Daily status digest send failed: ${String(error)}`);
|
|
158
|
+
}
|
|
159
|
+
ctx.logger.info("dailyStatusDigest: sent", {
|
|
160
|
+
recipients: recipients.length,
|
|
161
|
+
orderCount: summary.orderCount,
|
|
162
|
+
});
|
|
163
|
+
return { sent: true, summary };
|
|
164
|
+
}
|
|
@@ -21,6 +21,8 @@ import { userRepository, sessionRepository, storeRepository, productRepository,
|
|
|
21
21
|
import { sendNotification } from "../../../../features/admin/actions/notification-actions";
|
|
22
22
|
import { isSellerUser } from "../../../../features/auth/role-predicates";
|
|
23
23
|
import { getAdminAuth } from "../../../../providers/db-firebase";
|
|
24
|
+
import { recordAdminAction } from "../../features/audit-log/actions";
|
|
25
|
+
import { AdminAuditActionValues } from "../../../../features/audit-log/schemas/firestore";
|
|
24
26
|
async function cascadeAddressClusterBan(uid, banData) {
|
|
25
27
|
const userAddresses = await addressesRepository.listByOwner("user", uid);
|
|
26
28
|
const uniqueHashes = [...new Set(userAddresses.map((a) => a.addressHash).filter(Boolean))];
|
|
@@ -84,6 +86,15 @@ export async function runHardBanCascade(input, ctx) {
|
|
|
84
86
|
hardBanExpiresAt: expiresAt ?? null,
|
|
85
87
|
...(fraudOrderId ? { hardBanFraudOrderId: fraudOrderId } : {}),
|
|
86
88
|
});
|
|
89
|
+
void recordAdminAction({
|
|
90
|
+
actorUid: bannedBy,
|
|
91
|
+
action: AdminAuditActionValues.USER_HARD_BAN,
|
|
92
|
+
targetType: "user",
|
|
93
|
+
targetId: uid,
|
|
94
|
+
targetLabel: target.displayName ?? uid,
|
|
95
|
+
reason,
|
|
96
|
+
metadata: { expiresAt: expiresAt?.toISOString() ?? null, fraudOrderId: fraudOrderId ?? null },
|
|
97
|
+
});
|
|
87
98
|
// 3. Delete active sessions
|
|
88
99
|
try {
|
|
89
100
|
const sessions = await sessionRepository.findActiveByUser(uid);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { JobContext } from "../runtime/types";
|
|
2
|
+
export declare const SANDBOX_COLLECTIONS: readonly ["categories", "stores", "products", "blogPosts", "events"];
|
|
2
3
|
export interface TesterSandboxCleanupOptions {
|
|
3
4
|
/** true = wipe every isTestData:true doc regardless of testDataExpiresAt (manual force-purge). */
|
|
4
5
|
force?: boolean;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { batchDelete, getTestDataRefs } from "../handlers/_helpers";
|
|
2
2
|
import { BID_FIELDS } from "../../../../constants/field-names";
|
|
3
|
-
|
|
3
|
+
// Exported so testerSandboxRefresh.ts (the every-4h revert+prune job) reuses
|
|
4
|
+
// the exact same collection scope instead of redefining it — one source of
|
|
5
|
+
// truth for "what counts as sandbox scope."
|
|
6
|
+
export const SANDBOX_COLLECTIONS = ["categories", "stores", "products", "blogPosts", "events"];
|
|
4
7
|
const BID_CHUNK_SIZE = 30; // Firestore `in` query cap
|
|
5
8
|
export async function runTesterSandboxCleanup(ctx, opts = {}) {
|
|
6
9
|
const cutoff = opts.force ? null : new Date();
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { JobContext } from "../runtime/types";
|
|
2
|
+
/**
|
|
3
|
+
* Reverts every live tester-sandbox fixture back to its canonical seeded
|
|
4
|
+
* shape (undoing edits made through admin/seller forms) and prunes any
|
|
5
|
+
* isTestData doc that isn't one of the known fixtures (something a tester
|
|
6
|
+
* created during QA, e.g. a cloned product). Runs every 4 hours — distinct
|
|
7
|
+
* from `runTesterSandboxCleanup`, which only deletes docs whose 7-day
|
|
8
|
+
* `testDataExpiresAt` TTL has actually lapsed and never touches still-live
|
|
9
|
+
* docs' fields. Scoped to the tester sandbox only (`isTestData: true`) —
|
|
10
|
+
* the permanent Beyblade catalog real visitors browse is never touched.
|
|
11
|
+
*/
|
|
12
|
+
export declare function runTesterSandboxRefresh(ctx: JobContext): Promise<void>;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { batchDelete, getTestDataRefs } from "../handlers/_helpers";
|
|
2
|
+
import { BID_FIELDS } from "../../../../constants/field-names";
|
|
3
|
+
import { SANDBOX_COLLECTIONS } from "./testerSandboxCleanup";
|
|
4
|
+
// Direct imports from each defining module (not the seed-data barrel) —
|
|
5
|
+
// this is internal appkit code, not part of the package's public API surface.
|
|
6
|
+
import { categoriesTesterSeedData } from "../../../../features/tester/seed-data/categories-tester-seed-data";
|
|
7
|
+
import { storesTesterSeedData } from "../../../../features/tester/seed-data/stores-tester-seed-data";
|
|
8
|
+
import { productsTesterSeedData } from "../../../../features/tester/seed-data/products-tester-seed-data";
|
|
9
|
+
import { blogTesterSeedData } from "../../../../features/tester/seed-data/blog-tester-seed-data";
|
|
10
|
+
import { eventsTesterSeedData } from "../../../../features/tester/seed-data/events-tester-seed-data";
|
|
11
|
+
import { bidsTesterSeedData } from "../../../../features/tester/seed-data/bids-tester-seed-data";
|
|
12
|
+
const SEED_BY_COLLECTION = {
|
|
13
|
+
categories: categoriesTesterSeedData,
|
|
14
|
+
stores: storesTesterSeedData,
|
|
15
|
+
products: productsTesterSeedData,
|
|
16
|
+
blogPosts: blogTesterSeedData,
|
|
17
|
+
events: eventsTesterSeedData,
|
|
18
|
+
};
|
|
19
|
+
const BID_CHUNK_SIZE = 30; // Firestore `in` query cap
|
|
20
|
+
/**
|
|
21
|
+
* Reverts every live tester-sandbox fixture back to its canonical seeded
|
|
22
|
+
* shape (undoing edits made through admin/seller forms) and prunes any
|
|
23
|
+
* isTestData doc that isn't one of the known fixtures (something a tester
|
|
24
|
+
* created during QA, e.g. a cloned product). Runs every 4 hours — distinct
|
|
25
|
+
* from `runTesterSandboxCleanup`, which only deletes docs whose 7-day
|
|
26
|
+
* `testDataExpiresAt` TTL has actually lapsed and never touches still-live
|
|
27
|
+
* docs' fields. Scoped to the tester sandbox only (`isTestData: true`) —
|
|
28
|
+
* the permanent Beyblade catalog real visitors browse is never touched.
|
|
29
|
+
*/
|
|
30
|
+
export async function runTesterSandboxRefresh(ctx) {
|
|
31
|
+
let reverted = 0;
|
|
32
|
+
let deletedExtras = 0;
|
|
33
|
+
for (const collection of SANDBOX_COLLECTIONS) {
|
|
34
|
+
const seedDocs = SEED_BY_COLLECTION[collection] ?? [];
|
|
35
|
+
const validIds = new Set(seedDocs.map((d) => d.id).filter((id) => Boolean(id)));
|
|
36
|
+
if (seedDocs.length > 0) {
|
|
37
|
+
const batch = ctx.db.batch();
|
|
38
|
+
for (const doc of seedDocs) {
|
|
39
|
+
const { id, ...data } = doc;
|
|
40
|
+
if (!id)
|
|
41
|
+
continue;
|
|
42
|
+
// merge:true mirrors seed-cli.mjs's loadGeneric — the seed payload
|
|
43
|
+
// carries every canonical field, so this fully overwrites anything
|
|
44
|
+
// a tester edited through the UI.
|
|
45
|
+
batch.set(ctx.db.collection(collection).doc(id), data, { merge: true });
|
|
46
|
+
reverted++;
|
|
47
|
+
}
|
|
48
|
+
await batch.commit();
|
|
49
|
+
}
|
|
50
|
+
const testDataRefs = await getTestDataRefs(ctx.db, collection, null);
|
|
51
|
+
const extraRefs = testDataRefs.filter((ref) => !validIds.has(ref.id));
|
|
52
|
+
deletedExtras += await batchDelete(ctx, extraRefs);
|
|
53
|
+
}
|
|
54
|
+
// Bids aren't isTestData-flagged themselves — scoped by productId
|
|
55
|
+
// referencing a sandbox product instead, mirroring runTesterSandboxCleanup's
|
|
56
|
+
// cascade convention.
|
|
57
|
+
const sandboxProductIds = (SEED_BY_COLLECTION.products ?? [])
|
|
58
|
+
.map((d) => d.id)
|
|
59
|
+
.filter((id) => Boolean(id));
|
|
60
|
+
const validBidIds = new Set(bidsTesterSeedData.map((d) => d.id).filter((id) => Boolean(id)));
|
|
61
|
+
if (bidsTesterSeedData.length > 0) {
|
|
62
|
+
const bidBatch = ctx.db.batch();
|
|
63
|
+
for (const bid of bidsTesterSeedData) {
|
|
64
|
+
const { id, ...data } = bid;
|
|
65
|
+
if (!id)
|
|
66
|
+
continue;
|
|
67
|
+
bidBatch.set(ctx.db.collection("bids").doc(id), data, { merge: true });
|
|
68
|
+
reverted++;
|
|
69
|
+
}
|
|
70
|
+
await bidBatch.commit();
|
|
71
|
+
}
|
|
72
|
+
const extraBidRefs = [];
|
|
73
|
+
for (let i = 0; i < sandboxProductIds.length; i += BID_CHUNK_SIZE) {
|
|
74
|
+
const chunk = sandboxProductIds.slice(i, i + BID_CHUNK_SIZE);
|
|
75
|
+
if (chunk.length === 0)
|
|
76
|
+
continue;
|
|
77
|
+
const snap = await ctx.db.collection("bids").where(BID_FIELDS.PRODUCT_ID, "in", chunk).get();
|
|
78
|
+
for (const doc of snap.docs) {
|
|
79
|
+
if (!validBidIds.has(doc.id))
|
|
80
|
+
extraBidRefs.push(doc.ref);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
deletedExtras += await batchDelete(ctx, extraBidRefs);
|
|
84
|
+
ctx.logger.info("Tester sandbox refresh complete", { reverted, deletedExtras });
|
|
85
|
+
}
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { normalizeError } from "../../../../errors/normalize";
|
|
21
21
|
import { notificationRepository } from "../../../../features/admin/repository/notification.repository";
|
|
22
22
|
import { sendWhatsAppTemplateMessage, sendWhatsAppBusinessMessage, } from "../../../../features/whatsapp-bot/helpers/whatsapp";
|
|
23
|
+
import { isChannelHealthy, recordChannelOutcome } from "../../notifications/channel-health";
|
|
23
24
|
const MAX_ATTEMPTS = 3;
|
|
24
25
|
const RETRY_DELAY_MS = 1500;
|
|
25
26
|
function delay(ms) {
|
|
@@ -27,6 +28,20 @@ function delay(ms) {
|
|
|
27
28
|
}
|
|
28
29
|
export async function runWhatsAppNotify(payload, ctx) {
|
|
29
30
|
const { toPhone, title, message, templateName, templateLanguage, notificationId, phoneNumberId, accessToken } = payload;
|
|
31
|
+
if (!(await isChannelHealthy("whatsapp"))) {
|
|
32
|
+
ctx.logger.warn("whatsappNotify: WhatsApp channel circuit is open — skipping send", { notificationId });
|
|
33
|
+
await notificationRepository
|
|
34
|
+
.update(notificationId, { whatsappStatus: "skipped" })
|
|
35
|
+
.catch((err) => {
|
|
36
|
+
void normalizeError(err);
|
|
37
|
+
});
|
|
38
|
+
return {
|
|
39
|
+
summary: { total: 1, succeeded: 0, skipped: 1, failed: 0 },
|
|
40
|
+
succeeded: [],
|
|
41
|
+
skipped: [notificationId],
|
|
42
|
+
failed: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
30
45
|
let sent = false;
|
|
31
46
|
let lastError;
|
|
32
47
|
for (let attempt = 1; attempt <= MAX_ATTEMPTS && !sent; attempt++) {
|
|
@@ -61,6 +76,7 @@ export async function runWhatsAppNotify(payload, ctx) {
|
|
|
61
76
|
if (!templateName) {
|
|
62
77
|
ctx.logger.warn("whatsappNotify: no approved template configured for this notification type — sent as free-form text, which Meta rejects outside the 24h customer-service window", { notificationId });
|
|
63
78
|
}
|
|
79
|
+
await recordChannelOutcome("whatsapp", sent);
|
|
64
80
|
await notificationRepository
|
|
65
81
|
.update(notificationId, { whatsappStatus: sent ? "sent" : "failed" })
|
|
66
82
|
.catch((err) => {
|
|
@@ -29,6 +29,7 @@ export { hardBanReinstatementHandler } from "./hardBanReinstatement";
|
|
|
29
29
|
export { paymentReviewAutoApproveHandler } from "./paymentReviewAutoApprove";
|
|
30
30
|
export { productStatsSyncHandler } from "./productStatsSync";
|
|
31
31
|
export { revenueRollupHandler } from "./revenueRollup";
|
|
32
|
+
export { dailyStatusDigestHandler } from "./dailyStatusDigest";
|
|
32
33
|
export { positionsReconcileHandler } from "./positionsReconcile";
|
|
33
34
|
export { payoutBatchHandler } from "./payoutBatch";
|
|
34
35
|
export { weeklyPayoutEligibilityHandler } from "./weeklyPayoutEligibility";
|
|
@@ -52,6 +53,7 @@ export { onScamReportCreateHandler } from "./onScamReportCreate";
|
|
|
52
53
|
export { onScamReportUpdateHandler } from "./onScamReportUpdate";
|
|
53
54
|
export { draftPruneHandler } from "./draftPrune";
|
|
54
55
|
export { testerSandboxCleanupHandler } from "./testerSandboxCleanup";
|
|
56
|
+
export { testerSandboxRefreshHandler } from "./testerSandboxRefresh";
|
|
55
57
|
export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
|
|
56
58
|
export { onShipmentItemWriteHandler } from "./onShipmentItemWrite";
|
|
57
59
|
export { onShipmentLotWriteHandler, onShipmentHeaderWriteHandler } from "./onShipmentAllocationSync";
|
|
@@ -29,6 +29,7 @@ export { hardBanReinstatementHandler } from "./hardBanReinstatement";
|
|
|
29
29
|
export { paymentReviewAutoApproveHandler } from "./paymentReviewAutoApprove";
|
|
30
30
|
export { productStatsSyncHandler } from "./productStatsSync";
|
|
31
31
|
export { revenueRollupHandler } from "./revenueRollup";
|
|
32
|
+
export { dailyStatusDigestHandler } from "./dailyStatusDigest";
|
|
32
33
|
export { positionsReconcileHandler } from "./positionsReconcile";
|
|
33
34
|
export { payoutBatchHandler } from "./payoutBatch";
|
|
34
35
|
export { weeklyPayoutEligibilityHandler } from "./weeklyPayoutEligibility";
|
|
@@ -64,6 +65,8 @@ export { onScamReportUpdateHandler } from "./onScamReportUpdate";
|
|
|
64
65
|
export { draftPruneHandler } from "./draftPrune";
|
|
65
66
|
// Tester program — 7-day auto-expiry sweep for the shared admin-seeded test sandbox
|
|
66
67
|
export { testerSandboxCleanupHandler } from "./testerSandboxCleanup";
|
|
68
|
+
// Tester program — every-4h revert-live-edits + prune-extras refresh (faster than the 7-day sweep above)
|
|
69
|
+
export { testerSandboxRefreshHandler } from "./testerSandboxRefresh";
|
|
67
70
|
// EMI — installment reminder + overdue-flagging sweep
|
|
68
71
|
export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
|
|
69
72
|
// Procurement Shipments — 3-function recompute cascade (item→lot, lot/header→shipment allocation, cascade-delete)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit breaker for outbound notification channels (email, WhatsApp).
|
|
3
|
+
*
|
|
4
|
+
* Persisted as a singleton Firestore doc `system/channelHealth` so the
|
|
5
|
+
* "is this channel currently known-bad" state survives across requests and
|
|
6
|
+
* Firebase Function invocations — a channel that just failed 3 times in a
|
|
7
|
+
* row is treated as down for a 15-minute cooldown instead of being retried
|
|
8
|
+
* on every subsequent send attempt (wasted latency + hammering a dead
|
|
9
|
+
* provider). After the cooldown, the next attempt is a half-open probe:
|
|
10
|
+
* its own outcome decides whether the circuit re-closes or re-opens.
|
|
11
|
+
*/
|
|
12
|
+
export type NotificationChannel = "email" | "whatsapp";
|
|
13
|
+
/**
|
|
14
|
+
* Returns false only when the circuit is open and still within its cooldown
|
|
15
|
+
* window. Any other state (closed, or open-but-cooldown-elapsed) is treated
|
|
16
|
+
* as healthy — the latter lets the next call through as a half-open probe.
|
|
17
|
+
*/
|
|
18
|
+
export declare function isChannelHealthy(channel: NotificationChannel): Promise<boolean>;
|
|
19
|
+
export declare function recordChannelOutcome(channel: NotificationChannel, success: boolean): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Runs `fn` up to 3 times (500ms then 1.5s between attempts) and records the
|
|
22
|
+
* final outcome against the channel's health state. `fn` must throw on
|
|
23
|
+
* failure — for a helper that returns a boolean instead (e.g. the WhatsApp
|
|
24
|
+
* Cloud API helpers), convert `false` into a thrown error at the call site
|
|
25
|
+
* before passing it in.
|
|
26
|
+
*/
|
|
27
|
+
export declare function withChannelRetry<T>(channel: NotificationChannel, fn: () => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Circuit breaker for outbound notification channels (email, WhatsApp).
|
|
3
|
+
*
|
|
4
|
+
* Persisted as a singleton Firestore doc `system/channelHealth` so the
|
|
5
|
+
* "is this channel currently known-bad" state survives across requests and
|
|
6
|
+
* Firebase Function invocations — a channel that just failed 3 times in a
|
|
7
|
+
* row is treated as down for a 15-minute cooldown instead of being retried
|
|
8
|
+
* on every subsequent send attempt (wasted latency + hammering a dead
|
|
9
|
+
* provider). After the cooldown, the next attempt is a half-open probe:
|
|
10
|
+
* its own outcome decides whether the circuit re-closes or re-opens.
|
|
11
|
+
*/
|
|
12
|
+
import { getAdminDb } from "../../../providers/db-firebase";
|
|
13
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
14
|
+
import { serverLogger } from "../../../monitoring";
|
|
15
|
+
const HEALTH_COLLECTION = "system";
|
|
16
|
+
const HEALTH_DOC = "channelHealth";
|
|
17
|
+
const FAILURE_THRESHOLD = 3;
|
|
18
|
+
const COOLDOWN_MS = 15 * 60 * 1000;
|
|
19
|
+
const RETRY_DELAYS_MS = [500, 1500];
|
|
20
|
+
function defaultState() {
|
|
21
|
+
return { consecutiveFailures: 0, state: "closed" };
|
|
22
|
+
}
|
|
23
|
+
async function readHealthDoc() {
|
|
24
|
+
try {
|
|
25
|
+
const snap = await getAdminDb().collection(HEALTH_COLLECTION).doc(HEALTH_DOC).get();
|
|
26
|
+
return snap.data() ?? {};
|
|
27
|
+
}
|
|
28
|
+
catch (err) {
|
|
29
|
+
void normalizeError(err);
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Returns false only when the circuit is open and still within its cooldown
|
|
35
|
+
* window. Any other state (closed, or open-but-cooldown-elapsed) is treated
|
|
36
|
+
* as healthy — the latter lets the next call through as a half-open probe.
|
|
37
|
+
*/
|
|
38
|
+
export async function isChannelHealthy(channel) {
|
|
39
|
+
const doc = await readHealthDoc();
|
|
40
|
+
const state = doc[channel];
|
|
41
|
+
if (!state || state.state !== "open")
|
|
42
|
+
return true;
|
|
43
|
+
if (state.openedAt && Date.now() - state.openedAt >= COOLDOWN_MS)
|
|
44
|
+
return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
export async function recordChannelOutcome(channel, success) {
|
|
48
|
+
try {
|
|
49
|
+
const doc = await readHealthDoc();
|
|
50
|
+
const prev = doc[channel] ?? defaultState();
|
|
51
|
+
const next = success
|
|
52
|
+
? defaultState()
|
|
53
|
+
: (() => {
|
|
54
|
+
const consecutiveFailures = prev.consecutiveFailures + 1;
|
|
55
|
+
return consecutiveFailures >= FAILURE_THRESHOLD
|
|
56
|
+
? { consecutiveFailures, state: "open", openedAt: Date.now() }
|
|
57
|
+
: { consecutiveFailures, state: "closed" };
|
|
58
|
+
})();
|
|
59
|
+
await getAdminDb()
|
|
60
|
+
.collection(HEALTH_COLLECTION)
|
|
61
|
+
.doc(HEALTH_DOC)
|
|
62
|
+
.set({ [channel]: next }, { merge: true });
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
void normalizeError(err);
|
|
66
|
+
serverLogger.warn("channel-health: failed to record outcome", { channel, success });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function delay(ms) {
|
|
70
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Runs `fn` up to 3 times (500ms then 1.5s between attempts) and records the
|
|
74
|
+
* final outcome against the channel's health state. `fn` must throw on
|
|
75
|
+
* failure — for a helper that returns a boolean instead (e.g. the WhatsApp
|
|
76
|
+
* Cloud API helpers), convert `false` into a thrown error at the call site
|
|
77
|
+
* before passing it in.
|
|
78
|
+
*/
|
|
79
|
+
export async function withChannelRetry(channel, fn) {
|
|
80
|
+
const maxAttempts = RETRY_DELAYS_MS.length + 1;
|
|
81
|
+
// Narrowed to Error at catch time rather than held as `unknown` — same
|
|
82
|
+
// conversion the throw below used to do, just done once at the source so
|
|
83
|
+
// `unknown` never escapes the catch clause (CLAUDE.md Root Cause #31).
|
|
84
|
+
let lastError;
|
|
85
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
86
|
+
try {
|
|
87
|
+
const result = await fn();
|
|
88
|
+
await recordChannelOutcome(channel, true);
|
|
89
|
+
return result;
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
void normalizeError(err);
|
|
93
|
+
lastError = err instanceof Error ? err : new Error(String(err));
|
|
94
|
+
if (attempt < maxAttempts) {
|
|
95
|
+
await delay(RETRY_DELAYS_MS[attempt - 1]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
await recordChannelOutcome(channel, false);
|
|
100
|
+
throw lastError ?? new Error(`${channel} channel failed after ${maxAttempts} attempts`);
|
|
101
|
+
}
|
|
@@ -4,7 +4,6 @@ export declare const brandInputSchema: z.ZodObject<{
|
|
|
4
4
|
slug: z.ZodString;
|
|
5
5
|
description: z.ZodOptional<z.ZodString>;
|
|
6
6
|
logoURL: z.ZodOptional<z.ZodString>;
|
|
7
|
-
bannerURL: z.ZodOptional<z.ZodString>;
|
|
8
7
|
website: z.ZodOptional<z.ZodString>;
|
|
9
8
|
country: z.ZodOptional<z.ZodString>;
|
|
10
9
|
founded: z.ZodOptional<z.ZodNumber>;
|
|
@@ -18,7 +17,6 @@ export declare const brandInputSchema: z.ZodObject<{
|
|
|
18
17
|
country?: string | undefined;
|
|
19
18
|
description?: string | undefined;
|
|
20
19
|
logoURL?: string | undefined;
|
|
21
|
-
bannerURL?: string | undefined;
|
|
22
20
|
website?: string | undefined;
|
|
23
21
|
founded?: number | undefined;
|
|
24
22
|
}, {
|
|
@@ -29,7 +27,6 @@ export declare const brandInputSchema: z.ZodObject<{
|
|
|
29
27
|
isActive?: boolean | undefined;
|
|
30
28
|
displayOrder?: number | undefined;
|
|
31
29
|
logoURL?: string | undefined;
|
|
32
|
-
bannerURL?: string | undefined;
|
|
33
30
|
website?: string | undefined;
|
|
34
31
|
founded?: number | undefined;
|
|
35
32
|
}>;
|
|
@@ -38,7 +35,6 @@ export declare const brandUpdateSchema: z.ZodObject<Omit<{
|
|
|
38
35
|
slug: z.ZodOptional<z.ZodString>;
|
|
39
36
|
description: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
40
37
|
logoURL: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
41
|
-
bannerURL: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
42
38
|
website: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
43
39
|
country: z.ZodOptional<z.ZodOptional<z.ZodString>>;
|
|
44
40
|
founded: z.ZodOptional<z.ZodOptional<z.ZodNumber>>;
|
|
@@ -51,7 +47,6 @@ export declare const brandUpdateSchema: z.ZodObject<Omit<{
|
|
|
51
47
|
isActive?: boolean | undefined;
|
|
52
48
|
displayOrder?: number | undefined;
|
|
53
49
|
logoURL?: string | undefined;
|
|
54
|
-
bannerURL?: string | undefined;
|
|
55
50
|
website?: string | undefined;
|
|
56
51
|
founded?: number | undefined;
|
|
57
52
|
}, {
|
|
@@ -61,7 +56,6 @@ export declare const brandUpdateSchema: z.ZodObject<Omit<{
|
|
|
61
56
|
isActive?: boolean | undefined;
|
|
62
57
|
displayOrder?: number | undefined;
|
|
63
58
|
logoURL?: string | undefined;
|
|
64
|
-
bannerURL?: string | undefined;
|
|
65
59
|
website?: string | undefined;
|
|
66
60
|
founded?: number | undefined;
|
|
67
61
|
}>;
|
|
@@ -5,7 +5,6 @@ export const brandInputSchema = z.object({
|
|
|
5
5
|
slug: z.string().min(1).regex(/^brand-[a-z0-9-]+$/, "Slug must start with 'brand-' and contain only lowercase letters, numbers, and hyphens"),
|
|
6
6
|
description: z.string().max(BRAND_DESCRIPTION_MAX_LENGTH).optional(),
|
|
7
7
|
logoURL: z.string().url().optional(),
|
|
8
|
-
bannerURL: z.string().url().optional(),
|
|
9
8
|
website: z.string().url().optional(),
|
|
10
9
|
country: z.string().optional(),
|
|
11
10
|
founded: z.number().int().min(1800).max(new Date().getFullYear()).optional(),
|
|
@@ -81,6 +81,8 @@ export declare const bundleCreateSchema: z.ZodObject<{
|
|
|
81
81
|
* "brand" — brand discovery collection; does not touch product reverse pointers.
|
|
82
82
|
*/
|
|
83
83
|
bundleKind: z.ZodEnum<["special", "brand"]>;
|
|
84
|
+
/** Which brand this bundle belongs to, for the brand page's bundle tab — distinct from bundleQueryRule.filter.brandSlug (a dynamic-rule query filter). */
|
|
85
|
+
brandSlug: z.ZodOptional<z.ZodString>;
|
|
84
86
|
bundlePrice: z.ZodNumber;
|
|
85
87
|
bundleQueryRule: z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
86
88
|
type: z.ZodLiteral<"static">;
|
|
@@ -185,6 +187,7 @@ export declare const bundleCreateSchema: z.ZodObject<{
|
|
|
185
187
|
} | undefined;
|
|
186
188
|
description?: string | undefined;
|
|
187
189
|
slug?: string | undefined;
|
|
190
|
+
brandSlug?: string | undefined;
|
|
188
191
|
isActive?: boolean | undefined;
|
|
189
192
|
bundleItemDetails?: {
|
|
190
193
|
productId: string;
|
|
@@ -216,6 +219,7 @@ export declare const bundleCreateSchema: z.ZodObject<{
|
|
|
216
219
|
} | undefined;
|
|
217
220
|
description?: string | undefined;
|
|
218
221
|
slug?: string | undefined;
|
|
222
|
+
brandSlug?: string | undefined;
|
|
219
223
|
isActive?: boolean | undefined;
|
|
220
224
|
bundleItemDetails?: {
|
|
221
225
|
productId: string;
|
|
@@ -226,6 +230,7 @@ export declare const bundleUpdateSchema: z.ZodObject<{
|
|
|
226
230
|
name: z.ZodOptional<z.ZodString>;
|
|
227
231
|
description: z.ZodOptional<z.ZodString>;
|
|
228
232
|
bundleKind: z.ZodOptional<z.ZodEnum<["special", "brand"]>>;
|
|
233
|
+
brandSlug: z.ZodOptional<z.ZodString>;
|
|
229
234
|
bundlePrice: z.ZodOptional<z.ZodNumber>;
|
|
230
235
|
bundleQueryRule: z.ZodOptional<z.ZodDiscriminatedUnion<"type", [z.ZodObject<{
|
|
231
236
|
type: z.ZodLiteral<"static">;
|
|
@@ -311,6 +316,7 @@ export declare const bundleUpdateSchema: z.ZodObject<{
|
|
|
311
316
|
} | undefined;
|
|
312
317
|
name?: string | undefined;
|
|
313
318
|
description?: string | undefined;
|
|
319
|
+
brandSlug?: string | undefined;
|
|
314
320
|
isActive?: boolean | undefined;
|
|
315
321
|
bundleKind?: "brand" | "special" | undefined;
|
|
316
322
|
bundleProductIds?: string[] | undefined;
|
|
@@ -341,6 +347,7 @@ export declare const bundleUpdateSchema: z.ZodObject<{
|
|
|
341
347
|
} | undefined;
|
|
342
348
|
name?: string | undefined;
|
|
343
349
|
description?: string | undefined;
|
|
350
|
+
brandSlug?: string | undefined;
|
|
344
351
|
isActive?: boolean | undefined;
|
|
345
352
|
bundleKind?: "brand" | "special" | undefined;
|
|
346
353
|
bundleProductIds?: string[] | undefined;
|
|
@@ -52,6 +52,8 @@ export const bundleCreateSchema = z.object({
|
|
|
52
52
|
* "brand" — brand discovery collection; does not touch product reverse pointers.
|
|
53
53
|
*/
|
|
54
54
|
bundleKind: z.enum([BUNDLE_KIND_SPECIAL, BUNDLE_KIND_BRAND]),
|
|
55
|
+
/** Which brand this bundle belongs to, for the brand page's bundle tab — distinct from bundleQueryRule.filter.brandSlug (a dynamic-rule query filter). */
|
|
56
|
+
brandSlug: z.string().optional(),
|
|
55
57
|
bundlePrice: z.number().min(1),
|
|
56
58
|
bundleQueryRule: bundleQueryRuleSchema,
|
|
57
59
|
bundleProductIds: productIdsSchema,
|
|
@@ -64,6 +66,7 @@ export const bundleUpdateSchema = z.object({
|
|
|
64
66
|
name: z.string().min(2).max(120).optional(),
|
|
65
67
|
description: z.string().max(2000).optional(),
|
|
66
68
|
bundleKind: z.enum([BUNDLE_KIND_SPECIAL, BUNDLE_KIND_BRAND]).optional(),
|
|
69
|
+
brandSlug: z.string().optional(),
|
|
67
70
|
bundlePrice: z.number().min(1).optional(),
|
|
68
71
|
bundleQueryRule: bundleQueryRuleSchema.optional(),
|
|
69
72
|
bundleProductIds: productIdsSchema.optional(),
|