@mohasinac/appkit 4.30.17 → 4.30.19

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.
Files changed (163) hide show
  1. package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +5 -2
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +3 -3
  3. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +3 -2
  4. package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +5 -2
  5. package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +11 -3
  6. package/dist/_internal/client/hand-mode/HandModeProvider.js +2 -0
  7. package/dist/_internal/server/features/auctions/actions.js +14 -3
  8. package/dist/_internal/server/features/auctions/service.js +1 -1
  9. package/dist/_internal/server/features/blog/data.js +2 -2
  10. package/dist/_internal/server/features/blog/service.js +3 -3
  11. package/dist/_internal/server/features/brands/data.js +1 -1
  12. package/dist/_internal/server/features/brands/service.js +3 -3
  13. package/dist/_internal/server/features/bundles/actions.js +6 -1
  14. package/dist/_internal/server/features/bundles/data.js +21 -4
  15. package/dist/_internal/server/features/cart/data.js +3 -1
  16. package/dist/_internal/server/features/cart/service.js +13 -2
  17. package/dist/_internal/server/features/categories/data.js +12 -3
  18. package/dist/_internal/server/features/events/actions.js +17 -4
  19. package/dist/_internal/server/features/events/data.js +1 -1
  20. package/dist/_internal/server/features/events/service.js +1 -1
  21. package/dist/_internal/server/features/grouped/data.js +7 -2
  22. package/dist/_internal/server/features/item-requests/data.js +1 -1
  23. package/dist/_internal/server/features/lottery/actions.js +2 -2
  24. package/dist/_internal/server/features/lottery/data.js +1 -1
  25. package/dist/_internal/server/features/orders/actions.js +7 -7
  26. package/dist/_internal/server/features/orders/data.d.ts +1 -1
  27. package/dist/_internal/server/features/orders/data.js +14 -2
  28. package/dist/_internal/server/features/orders/service.js +1 -1
  29. package/dist/_internal/server/features/pre-orders/service.js +1 -1
  30. package/dist/_internal/server/features/prize-draws/actions.js +1 -1
  31. package/dist/_internal/server/features/products/data.js +7 -2
  32. package/dist/_internal/server/features/products/service.js +1 -1
  33. package/dist/_internal/server/features/promotions/actions.js +1 -1
  34. package/dist/_internal/server/features/promotions/data.js +1 -1
  35. package/dist/_internal/server/features/promotions/service.js +1 -1
  36. package/dist/_internal/server/features/reviews/data.d.ts +1 -1
  37. package/dist/_internal/server/features/reviews/data.js +6 -1
  38. package/dist/_internal/server/features/reviews/og-data.js +1 -1
  39. package/dist/_internal/server/features/shared/listing-data-factory.js +7 -2
  40. package/dist/_internal/server/features/stores/data.js +1 -1
  41. package/dist/_internal/server/features/sublisting-categories/data.js +1 -1
  42. package/dist/_internal/server/jobs/core/auctionSettlement.js +6 -1
  43. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +7 -15
  44. package/dist/_internal/server/jobs/core/revenueRollup.js +7 -1
  45. package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +27 -2
  46. package/dist/client/api/useApiQuery.js +17 -6
  47. package/dist/client.d.ts +1 -0
  48. package/dist/client.js +3 -0
  49. package/dist/contracts/client-realtime.d.ts +6 -0
  50. package/dist/contracts/realtime-session.d.ts +12 -12
  51. package/dist/contracts/realtime-session.js +72 -77
  52. package/dist/errors/index.d.ts +1 -1
  53. package/dist/errors/index.js +1 -1
  54. package/dist/features/about/components/DeveloperView.js +7 -2
  55. package/dist/features/about/components/PolicyPageView.js +9 -1
  56. package/dist/features/about/components/PublicProfileView.js +23 -5
  57. package/dist/features/account/components/AddressForm.js +1 -1
  58. package/dist/features/account/components/NotificationBell.js +4 -3
  59. package/dist/features/account/components/NotificationPreferencesPanel.js +1 -1
  60. package/dist/features/account/components/UserOffersPanel.js +1 -1
  61. package/dist/features/admin/components/AdminFulfillmentView.js +5 -2
  62. package/dist/features/admin/components/AdminMediaView.js +3 -3
  63. package/dist/features/analytics/types.d.ts +1 -1
  64. package/dist/features/analytics/types.js +13 -0
  65. package/dist/features/auctions/components/AuctionDetailPageView.js +6 -2
  66. package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -1
  67. package/dist/features/auth/actions/profile-actions.js +4 -3
  68. package/dist/features/auth/components/ForgotPasswordView.js +1 -1
  69. package/dist/features/auth/components/LoginForm.js +1 -1
  70. package/dist/features/auth/components/RegisterForm.js +1 -1
  71. package/dist/features/auth/components/ResetPasswordView.js +1 -1
  72. package/dist/features/auth/hooks/useAuth.js +3 -0
  73. package/dist/features/blog/repository/blog.repository.js +3 -1
  74. package/dist/features/cart/actions/cart-actions.js +13 -4
  75. package/dist/features/cart/utils/pending-ops.js +1 -1
  76. package/dist/features/categories/components/BrandDetailPageView.js +28 -12
  77. package/dist/features/categories/components/CategoryDetailPageView.js +37 -20
  78. package/dist/features/categories/repository/categories.repository.js +3 -1
  79. package/dist/features/checkout/actions/checkout-value-otp-actions.js +10 -1
  80. package/dist/features/classified/components/ClassifiedContactSellerPanel.js +1 -1
  81. package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -2
  82. package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +4 -1
  83. package/dist/features/events/components/EventBanner.js +5 -2
  84. package/dist/features/events/components/EventOfferCard.js +3 -1
  85. package/dist/features/events/components/EventPollWidget.js +1 -1
  86. package/dist/features/events/components/SpinWheelView.js +3 -2
  87. package/dist/features/grouped/components/GroupedListingEditorView.js +2 -1
  88. package/dist/features/history/hooks/useHistory.js +29 -27
  89. package/dist/features/homepage/components/AdSlot.js +3 -1
  90. package/dist/features/homepage/components/GoogleReviewsSection.js +8 -2
  91. package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -10
  92. package/dist/features/homepage/components/SocialFeedSection.js +4 -1
  93. package/dist/features/homepage/lib/live-stats.js +8 -1
  94. package/dist/features/homepage/repository/carousel.repository.js +2 -1
  95. package/dist/features/live/components/LiveItemDetailPageView.js +4 -1
  96. package/dist/features/media/finalize.js +3 -1
  97. package/dist/features/media/modals/ImageEditor.js +1 -1
  98. package/dist/features/media/modals/VideoThumbnailSelector.js +5 -2
  99. package/dist/features/media/modals/VideoTrimModal.js +5 -2
  100. package/dist/features/media/upload/MediaUploadField.js +2 -2
  101. package/dist/features/messages/hooks/useConversation.js +18 -4
  102. package/dist/features/messages/hooks/useConversations.js +25 -8
  103. package/dist/features/orders/components/RefundRequestView.js +5 -2
  104. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -1
  105. package/dist/features/pre-orders/components/PreOrderActionsClient.js +1 -1
  106. package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -1
  107. package/dist/features/products/components/PrizeDrawDetailPageView.js +4 -1
  108. package/dist/features/products/components/ProductDetailPageView.js +8 -2
  109. package/dist/features/products/components/ShareButton.js +3 -1
  110. package/dist/features/products/repository/loadProductFeatures.js +10 -2
  111. package/dist/features/products/repository/products.repository.js +2 -1
  112. package/dist/features/promotions/components/CouponCard.js +3 -1
  113. package/dist/features/reviews/actions/review-actions.js +8 -1
  114. package/dist/features/reviews/components/ReviewDetailPageView.js +13 -3
  115. package/dist/features/reviews/components/ReviewDetailShell.js +3 -1
  116. package/dist/features/scams/actions/scam-actions.d.ts +9 -1
  117. package/dist/features/scams/actions/scam-actions.js +52 -12
  118. package/dist/features/scams/components/SellerTrustBadge.d.ts +1 -1
  119. package/dist/features/scams/components/SellerTrustBadge.js +13 -0
  120. package/dist/features/scams/repository/scammer.repository.js +2 -1
  121. package/dist/features/seller/actions/offer-actions.js +6 -1
  122. package/dist/features/seller/components/FulfillmentView.js +5 -2
  123. package/dist/features/seller/components/PhysicalLocationModal.js +1 -1
  124. package/dist/features/seller/components/PrintCenterView.js +3 -1
  125. package/dist/features/seller/components/SellerBidsView.js +1 -1
  126. package/dist/features/seller/components/SellerCouponsView.js +1 -1
  127. package/dist/features/seller/components/SellerGoogleReviewsView.js +2 -2
  128. package/dist/features/seller/components/SellerOrdersView.js +3 -3
  129. package/dist/features/seller/components/SellerPayoutMethodsView.js +2 -2
  130. package/dist/features/seller/components/SellerProductShell.d.ts +5 -0
  131. package/dist/features/seller/components/SellerProductShell.js +15 -5
  132. package/dist/features/seller/components/SellerProductsView.js +24 -10
  133. package/dist/features/seller/components/SellerReviewsView.js +47 -16
  134. package/dist/features/seller/components/SellerShippingConfigsView.js +5 -4
  135. package/dist/features/seller/components/SellerStoreCategoriesView.js +3 -2
  136. package/dist/features/stores/components/StoreDetailLayoutView.d.ts +1 -1
  137. package/dist/features/stores/components/StoreDetailLayoutView.js +12 -3
  138. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +16 -1
  139. package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
  140. package/dist/features/wishlist/hooks/useWishlistToggle.js +8 -1
  141. package/dist/index.d.ts +1 -1
  142. package/dist/index.js +5 -2
  143. package/dist/next/api/routeHandler.js +3 -1
  144. package/dist/providers/auth-firebase/session.js +3 -1
  145. package/dist/providers/db-firebase/admin-app-lite.js +8 -8
  146. package/dist/providers/db-firebase/admin.js +12 -12
  147. package/dist/providers/db-firebase/browser-stub.d.ts +1 -11
  148. package/dist/providers/db-firebase/browser-stub.js +6 -11
  149. package/dist/providers/db-firebase/index.d.ts +0 -1
  150. package/dist/providers/db-firebase/index.js +0 -1
  151. package/dist/providers/db-firebase/rtdb-paths.d.ts +15 -6
  152. package/dist/providers/db-firebase/rtdb-paths.js +15 -6
  153. package/dist/providers/db-firebase/rtdb-url.d.ts +27 -0
  154. package/dist/providers/db-firebase/rtdb-url.js +50 -0
  155. package/dist/providers/firebase-client/realtime.d.ts +20 -0
  156. package/dist/providers/firebase-client/realtime.js +34 -1
  157. package/dist/providers.d.ts +1 -1
  158. package/dist/providers.js +1 -1
  159. package/dist/react/hooks/use-action-dispatch.js +8 -2
  160. package/dist/react/hooks/useFormStatePreservation.js +3 -1
  161. package/dist/react/hooks/useRealtimeEvent.js +20 -9
  162. package/dist/ui/components/toolbar-collapse.js +1 -2
  163. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { normalizeError } from "../../../../errors/normalize";
4
+ import { toUserMessage } from "../../../../errors/error-display-map";
4
5
  import { useState } from "react";
5
6
  import { Button, Div, Row, Span, Stack, Text } from "../../../../ui";
6
7
  const __P = {
@@ -19,8 +20,10 @@ export function CodeRevealPanel({ orderId, redemptionInstructions, fetchCode, })
19
20
  setRevealed(data);
20
21
  }
21
22
  catch (e) {
22
- void normalizeError(e);
23
- setError(e instanceof Error ? e.message : "Could not retrieve code");
23
+ const normalized = normalizeError(e);
24
+ setError(toUserMessage(normalized.code, undefined, {
25
+ fallback: "Could not retrieve your code. Please try again.",
26
+ }));
24
27
  }
25
28
  finally {
26
29
  setPending(false);
@@ -69,7 +69,7 @@ function useResponsiveDrawer(storageKey) {
69
69
  localStorage.setItem(storageKey, "true");
70
70
  }
71
71
  catch (_err) {
72
- void normalizeError(_err);
72
+ void normalizeError(_err); /* localStorage unavailable (private-browse / quota) — the sidebar still opens, only the preference is not remembered */
73
73
  }
74
74
  setDesktopOpen(true);
75
75
  }
@@ -84,7 +84,7 @@ function useResponsiveDrawer(storageKey) {
84
84
  localStorage.setItem(storageKey, "false");
85
85
  }
86
86
  catch (_err) {
87
- void normalizeError(_err);
87
+ void normalizeError(_err); /* localStorage unavailable (private-browse / quota) — the sidebar still closes, only the preference is not remembered */
88
88
  }
89
89
  setDesktopOpen(false);
90
90
  }
@@ -105,7 +105,7 @@ function useResponsiveDrawer(storageKey) {
105
105
  localStorage.setItem(storageKey, String(next));
106
106
  }
107
107
  catch (_err) {
108
- void normalizeError(_err);
108
+ void normalizeError(_err); /* localStorage unavailable (private-browse / quota) — the toggle still applies, only the preference is not remembered */
109
109
  }
110
110
  return next;
111
111
  });
@@ -2,6 +2,7 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { useState, useTransition } from "react";
4
4
  import { normalizeError } from "../../../../errors/normalize";
5
+ import { toUserMessage } from "../../../../errors/error-display-map";
5
6
  import { Stack, Row, Text, Heading, Section, Container, Button, DataTable, } from "../../../../ui";
6
7
  import { Form, FieldInput, FieldSelect, Input } from "../../../../ui";
7
8
  import { lotteryConfigFormSchema } from "../../../shared/features/lottery/config-form";
@@ -77,8 +78,8 @@ export function LotteryAdminEditView({ eventId, initialData, onSubmit, }) {
77
78
  await onSubmit({ title, description, type: "lottery", lotteryConfig: parsed.data });
78
79
  }
79
80
  catch (err) {
80
- void normalizeError(err);
81
- setError(err instanceof Error ? err.message : "Failed to save lottery");
81
+ const e = normalizeError(err);
82
+ setError(toUserMessage(e.code, undefined, { fallback: "Failed to save lottery." }));
82
83
  }
83
84
  });
84
85
  };
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { normalizeError } from "../../../../../errors/normalize";
4
+ import { toUserMessage } from "../../../../../errors/error-display-map";
4
5
  import * as React from "react";
5
6
  import { Button, Div, Heading, Input, Li, Section, Span, Table, Tbody, Td, Text, Th, Thead, Tr, Ul } from "@mohasinac/appkit/client";
6
7
  import { Select } from "@mohasinac/appkit/ui";
@@ -29,8 +30,10 @@ export function AnalysisRunnerView() {
29
30
  setReport(body.data);
30
31
  }
31
32
  catch (err) {
32
- void normalizeError(err);
33
- setError(err instanceof Error ? err.message : String(err));
33
+ const e = normalizeError(err);
34
+ setError(toUserMessage(e.code, undefined, {
35
+ fallback: "Could not run the analysis. Check the server logs for detail.",
36
+ }));
34
37
  }
35
38
  finally {
36
39
  setRunning(false);
@@ -4,6 +4,7 @@ import * as React from "react";
4
4
  import { Button, Div, Heading, Span, Table, Tbody, Td, Text, Th, Thead, Tr } from "@mohasinac/appkit/client";
5
5
  import { ADMIN_ENDPOINTS } from "../../../../../constants/api-endpoints";
6
6
  import { normalizeError } from "../../../../../errors/normalize";
7
+ import { toUserMessage } from "../../../../../errors/error-display-map";
7
8
  const BORDER_STYLE = "1px solid var(--appkit-color-border)";
8
9
  const COLUMNS = [
9
10
  { key: "timestampFmt", label: "When", width: "180px" },
@@ -38,8 +39,10 @@ export function CloudLogsListView({ title, subtitle, initialEntries, initialNext
38
39
  setNextPageToken(body.data.nextPageToken);
39
40
  }
40
41
  catch (err) {
41
- void normalizeError(err);
42
- setError(err instanceof Error ? err.message : String(err));
42
+ const e = normalizeError(err);
43
+ setError(toUserMessage(e.code, undefined, {
44
+ fallback: "Could not load more log entries. Please try again.",
45
+ }));
43
46
  }
44
47
  finally {
45
48
  setLoadingMore(false);
@@ -51,5 +54,10 @@ export function CloudLogsListView({ title, subtitle, initialEntries, initialNext
51
54
  borderBottom: BORDER_STYLE,
52
55
  width: c.width,
53
56
  whiteSpace: "nowrap",
54
- }, children: c.label }, c.key))) }) }), _jsx(Tbody, { children: entries.length === 0 ? (_jsx(Tr, { children: _jsx(Td, { colSpan: COLUMNS.length, style: { padding: "2rem", textAlign: "center", color: "var(--appkit-color-text-muted)" }, children: "No log entries in the selected window." }) })) : (entries.map((e) => (_jsxs(Tr, { style: { borderBottom: "1px solid var(--appkit-color-border-subtle)" }, children: [_jsx(Td, { style: { padding: "0.5rem 0.75rem", verticalAlign: "top", whiteSpace: "nowrap" }, children: fmt(e.timestamp) }), _jsx(Td, { style: { padding: "0.5rem 0.75rem", verticalAlign: "top" }, children: e.severity }), _jsx(Td, { style: { padding: "0.5rem 0.75rem", verticalAlign: "top" }, children: e.resourceType }), _jsx(Td, { style: { padding: "0.5rem 0.75rem", verticalAlign: "top", fontFamily: "monospace" }, children: e.message })] }, e.id)))) })] }) }), _jsx(Div, { style: { marginTop: "1rem", textAlign: "center" }, children: nextPageToken ? (_jsx(Button, { type: "button", onClick: loadMore, isLoading: loadingMore, children: "Load more" })) : entries.length > 0 ? (_jsx(Text, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "No more entries." })) : null })] }));
57
+ }, children: c.label }, c.key))) }) }), _jsx(Tbody, { children: entries.length === 0 ? (_jsx(Tr, { children: _jsx(Td, { colSpan: COLUMNS.length, style: { padding: "2rem", textAlign: "center", color: "var(--appkit-color-text-muted)" }, children: "No log entries in the selected window." }) })) : (
58
+ // Named `entry`, not `e`: this is a Cloud Logging record, not a
59
+ // caught error, and rendering its `.message` is the entire point
60
+ // of this admin viewer. The short name read as the defect
61
+ // audit-raw-error-text exists to catch.
62
+ entries.map((entry) => (_jsxs(Tr, { style: { borderBottom: "1px solid var(--appkit-color-border-subtle)" }, children: [_jsx(Td, { style: { padding: "0.5rem 0.75rem", verticalAlign: "top", whiteSpace: "nowrap" }, children: fmt(entry.timestamp) }), _jsx(Td, { style: { padding: "0.5rem 0.75rem", verticalAlign: "top" }, children: entry.severity }), _jsx(Td, { style: { padding: "0.5rem 0.75rem", verticalAlign: "top" }, children: entry.resourceType }), _jsx(Td, { style: { padding: "0.5rem 0.75rem", verticalAlign: "top", fontFamily: "monospace" }, children: entry.message })] }, entry.id)))) })] }) }), _jsx(Div, { style: { marginTop: "1rem", textAlign: "center" }, children: nextPageToken ? (_jsx(Button, { type: "button", onClick: loadMore, isLoading: loadingMore, children: "Load more" })) : entries.length > 0 ? (_jsx(Text, { style: { color: "var(--appkit-color-text-muted)", fontSize: "0.85rem" }, children: "No more entries." })) : null })] }));
55
63
  }
@@ -54,6 +54,8 @@ export function HandModeProvider({ initialHand, children }) {
54
54
  }
55
55
  }
56
56
  catch (_err) {
57
+ // localStorage unavailable (private-browse) — fall through to the value
58
+ // readHandPreference() already resolved; there is no preference to honour.
57
59
  void normalizeError(_err);
58
60
  }
59
61
  return stored;
@@ -1,5 +1,6 @@
1
1
  "use server";
2
2
  import { wrapAction } from "@mohasinac/appkit/server";
3
+ import { safeRead } from "../../../../errors/safe-read";
3
4
  import { bidRepository, productRepository, userRepository, siteSettingsRepository } from "../../../../repositories";
4
5
  import { requireRoleUser } from "../../../../providers/auth-firebase/helpers";
5
6
  import { placeBidActionSchema } from "../../../shared/features/auctions/schema";
@@ -16,10 +17,20 @@ export async function placeBidAction(input) {
16
17
  assertNotAuctionOwner(product, user.uid);
17
18
  const settings = await siteSettingsRepository.getSingleton();
18
19
  assertBidAmount(product, amount, settings.auctionConfig?.bidIncrementTiers ?? []);
19
- const profile = await userRepository.findById(user.uid).catch(() => null);
20
+ const profile = await safeRead(() => userRepository.findById(user.uid), {
21
+ route: "auction:placeBid",
22
+ key: "auctions.placeBid.bidderProfile",
23
+ fallback: null,
24
+ });
20
25
  const userName = profile?.displayName ?? user.name ?? "Anonymous";
21
- // Mark all current bids for this auction as outbid, then set new winning bid
22
- await bidRepository.setWinningBid("__placeholder__", auctionId).catch(() => null);
26
+ // (Removed: a `setWinningBid("__placeholder__", auctionId)` call whose
27
+ // rejection was swallowed. It accomplished NOTHING. setWinningBid builds a
28
+ // single batch — mark every bid for the product `outbid`, then update the
29
+ // winning bid's own doc — and `__placeholder__` is not a real bid id, so
30
+ // `batch.commit()` always threw and DISCARDED the whole batch, outbid
31
+ // marking included. It cost a full-collection query and an exception per
32
+ // bid placed, and the real `setWinningBid(bid.id, …)` below already marks
33
+ // every other bid for this product as outbid.)
23
34
  const bid = await bidRepository.create({
24
35
  productId: auctionId,
25
36
  productTitle: product.title ?? auctionId,
@@ -4,7 +4,7 @@ import { AuctionNotFoundError, AuctionEndedError, BidTooLowError, BidOnOwnAuctio
4
4
  import { AUCTION_SNIPING_WINDOW_SECONDS, AUCTION_DEFAULT_EXTENSION_MINUTES, resolveMinBid, } from "../../../shared/features/auctions/config";
5
5
  /** Assert an auction exists, is published, and is still active. Returns the product. */
6
6
  export async function assertAuctionActive(auctionId) {
7
- const product = await productRepository.findByIdOrSlug(auctionId).catch(() => null);
7
+ const product = await productRepository.findByIdOrSlug(auctionId);
8
8
  if (!product || !isAuctionListing(product))
9
9
  throw new AuctionNotFoundError(auctionId);
10
10
  const endDate = product.auctionEndDate instanceof Date
@@ -1,8 +1,8 @@
1
1
  import { cache } from "react";
2
2
  import { blogRepository } from "../../../../repositories";
3
3
  export const getBlogPostForDetail = cache(async (slug) => {
4
- return (await blogRepository.findBySlug(slug).catch(() => undefined)) ?? null;
4
+ return (await blogRepository.findBySlug(slug)) ?? null;
5
5
  });
6
6
  export const getBlogPostById = cache(async (id) => {
7
- return (await blogRepository.findById(id).catch(() => undefined)) ?? null;
7
+ return (await blogRepository.findById(id)) ?? null;
8
8
  });
@@ -1,14 +1,14 @@
1
1
  import { blogRepository } from "../../../../repositories";
2
2
  import { BlogPostNotFoundError, BlogSlugConflictError } from "../../../shared/features/blog/errors";
3
3
  export async function assertBlogPostExists(slugOrId) {
4
- const post = await blogRepository.findBySlug(slugOrId).catch(() => null)
5
- ?? await blogRepository.findById(slugOrId).catch(() => null);
4
+ const post = await blogRepository.findBySlug(slugOrId)
5
+ ?? await blogRepository.findById(slugOrId);
6
6
  if (!post)
7
7
  throw new BlogPostNotFoundError(slugOrId);
8
8
  return post;
9
9
  }
10
10
  export async function assertBlogSlugUnique(slug) {
11
- const existing = await blogRepository.findBySlug(slug).catch(() => null);
11
+ const existing = await blogRepository.findBySlug(slug);
12
12
  if (existing)
13
13
  throw new BlogSlugConflictError(slug);
14
14
  }
@@ -6,7 +6,7 @@ import { categoriesRepository } from "../../../../repositories";
6
6
  * `BrandDetailPageView` consume the same `CategoryDocument` shape.
7
7
  */
8
8
  export const getBrandForDetail = cache(async (slug) => {
9
- return categoriesRepository.findBySlugAndType(slug, "brand").catch(() => null);
9
+ return categoriesRepository.findBySlugAndType(slug, "brand");
10
10
  });
11
11
  /**
12
12
  * @deprecated Identical to getBrandForDetail after SB-UNI-C. Kept as an
@@ -1,14 +1,14 @@
1
1
  import { categoriesRepository } from "../../../../repositories";
2
2
  import { BrandNotFoundError, BrandSlugConflictError } from "../../../shared/features/brands/errors";
3
3
  export async function assertBrandExists(slugOrId) {
4
- const brand = (await categoriesRepository.findBySlugAndType(slugOrId, "brand").catch(() => null)) ??
5
- (await categoriesRepository.findById(slugOrId).catch(() => null));
4
+ const brand = (await categoriesRepository.findBySlugAndType(slugOrId, "brand")) ??
5
+ (await categoriesRepository.findById(slugOrId));
6
6
  if (!brand || brand.categoryType !== "brand")
7
7
  throw new BrandNotFoundError(slugOrId);
8
8
  return brand;
9
9
  }
10
10
  export async function assertBrandSlugUnique(slug) {
11
- const existing = await categoriesRepository.findBySlugAndType(slug, "brand").catch(() => null);
11
+ const existing = await categoriesRepository.findBySlugAndType(slug, "brand");
12
12
  if (existing)
13
13
  throw new BrandSlugConflictError(slug);
14
14
  }
@@ -13,6 +13,7 @@
13
13
  * helpers can fan out stock decrements to member products.
14
14
  */
15
15
  import { NotFoundError, ValidationError } from "../../../../errors";
16
+ import { safeRead } from "../../../../errors/safe-read";
16
17
  import { categoriesRepository } from "../../../../repositories";
17
18
  import { cartRepository } from "../../../../features/cart/repository/cart.repository";
18
19
  import { assertCanAddNewItems } from "../../../../features/cart/actions/cart-actions";
@@ -55,7 +56,11 @@ quantity = 1) {
55
56
  // treating that as a stock quantity would silently change how much a
56
57
  // prize-draw member decrements, which the stock fan-out has never done.
57
58
  const memberIds = bundle.bundleProductIds ?? [];
58
- const memberDocs = await Promise.all(memberIds.map((id) => productRepository.findById(id).catch(() => null)));
59
+ const memberDocs = await Promise.all(memberIds.map((id) => safeRead(() => productRepository.findById(id), {
60
+ route: "/bundles",
61
+ key: "bundles.addBundleToCart.member",
62
+ fallback: null,
63
+ })));
59
64
  const groupMembers = memberDocs
60
65
  .filter((p) => p !== null)
61
66
  .map((p) => ({
@@ -8,6 +8,7 @@
8
8
  import { cache } from "react";
9
9
  import { categoriesRepository } from "../../../../repositories";
10
10
  import { productRepository } from "../../../../repositories";
11
+ import { safeRead } from "../../../../errors/safe-read";
11
12
  /**
12
13
  * Fetch a single bundle by slug for the public detail page. Returns null
13
14
  * when the slug is empty or no matching `categoryType:"bundle"` row exists.
@@ -43,7 +44,11 @@ export const listBundleMembers = cache(async (bundle, _opts) => {
43
44
  const ids = resolveBundleMemberIds(bundle);
44
45
  if (ids.length === 0)
45
46
  return [];
46
- const results = await Promise.all(ids.map((id) => productRepository.findById(id).catch(() => null)));
47
+ const results = await Promise.all(ids.map((id) => safeRead(() => productRepository.findById(id), {
48
+ route: "/bundles",
49
+ key: "bundles.listBundleMembers.member",
50
+ fallback: null,
51
+ })));
47
52
  return results.filter((p) => p !== null);
48
53
  });
49
54
  /**
@@ -58,7 +63,11 @@ export const listBundleMembers = cache(async (bundle, _opts) => {
58
63
  export async function resolveBundleOriginalTotal(productIds) {
59
64
  if (productIds.length === 0)
60
65
  return undefined;
61
- const results = await Promise.all(productIds.map((id) => productRepository.findById(id).catch(() => null)));
66
+ const results = await Promise.all(productIds.map((id) => safeRead(() => productRepository.findById(id), {
67
+ route: "/bundles",
68
+ key: "bundles.resolveBundleOriginalTotal.member",
69
+ fallback: null,
70
+ })));
62
71
  const prices = [];
63
72
  for (const p of results) {
64
73
  if (!p || typeof p.price !== "number")
@@ -89,7 +98,11 @@ export async function resolveBundleOriginalTotal(productIds) {
89
98
  export async function findBundleMemberStores(productIds) {
90
99
  if (productIds.length === 0)
91
100
  return [];
92
- const results = await Promise.all(productIds.map((id) => productRepository.findById(id).catch(() => null)));
101
+ const results = await Promise.all(productIds.map((id) => safeRead(() => productRepository.findById(id), {
102
+ route: "/bundles",
103
+ key: "bundles.findBundleMemberStores.member",
104
+ fallback: null,
105
+ })));
93
106
  const stores = new Set();
94
107
  for (const p of results) {
95
108
  // An unresolvable member says nothing about seller spread — the price
@@ -118,7 +131,11 @@ export async function findBundleMemberStores(productIds) {
118
131
  export async function resolveBundleCategorySlugs(productIds) {
119
132
  if (productIds.length === 0)
120
133
  return [];
121
- const results = await Promise.all(productIds.map((id) => productRepository.findById(id).catch(() => null)));
134
+ const results = await Promise.all(productIds.map((id) => safeRead(() => productRepository.findById(id), {
135
+ route: "/bundles",
136
+ key: "bundles.resolveBundleCategorySlugs.member",
137
+ fallback: null,
138
+ })));
122
139
  const slugs = new Set();
123
140
  for (const p of results) {
124
141
  for (const slug of p?.categorySlugs ?? [])
@@ -1,5 +1,7 @@
1
1
  import { cache } from "react";
2
2
  import { cartRepository } from "../../../../repositories";
3
3
  export const getCartForUser = cache(async (userId) => {
4
- return (await cartRepository.findByUserId(userId).catch(() => undefined)) ?? null;
4
+ // No catch: this cart IS the page. Degrading a Firestore failure to null
5
+ // renders an EMPTY CART to someone whose cart is full — money path.
6
+ return (await cartRepository.findByUserId(userId)) ?? null;
5
7
  });
@@ -1,8 +1,13 @@
1
1
  import { cartRepository } from "../../../../repositories";
2
2
  import { CART_MAX_ITEMS } from "../../../shared/features/cart/config";
3
3
  import { CartFullError, CartQuantityError } from "../../../shared/features/cart/errors";
4
+ import { normalizeError } from "../../../../errors/normalize";
5
+ import { serverLogger } from "../../../../monitoring/server-logger";
4
6
  export async function assertCartCapacity(userId, addingQty) {
5
- const cart = await cartRepository.findByUserId(userId).catch(() => null);
7
+ // No catch. Swallowing to null made `currentItemCount` 0, so a Firestore
8
+ // failure silently DISABLED the cart cap and let the add through — a guard
9
+ // that fails open. A throw is caught by the action envelope and recorded.
10
+ const cart = await cartRepository.findByUserId(userId);
6
11
  const currentItemCount = cart ? (cart.items ?? []).length : 0;
7
12
  if (currentItemCount + addingQty > CART_MAX_ITEMS) {
8
13
  throw new CartFullError(CART_MAX_ITEMS);
@@ -20,6 +25,12 @@ export async function upsertCartItem(userId, item) {
20
25
  }
21
26
  export async function mergeGuestItems(userId, guestItems) {
22
27
  for (const item of guestItems) {
23
- await cartRepository.addItem(userId, item).catch(() => null);
28
+ // A WRITE, not a read — safeRead does not apply. Guest-merge is
29
+ // best-effort per item so one bad line cannot lose the whole cart, but a
30
+ // dropped item must be visible rather than silent.
31
+ await cartRepository.addItem(userId, item).catch((err) => {
32
+ void normalizeError(err);
33
+ serverLogger.warn("cart.mergeGuestCart: item dropped", { userId, err });
34
+ });
24
35
  }
25
36
  }
@@ -1,17 +1,22 @@
1
1
  "use server";
2
2
  import { cache } from "react";
3
3
  import { categoriesRepository } from "../../../../repositories";
4
+ import { safeRead } from "../../../../errors/safe-read";
4
5
  import { CATEGORIES_FEATURED_LIMIT, CATEGORIES_MENU_LIMIT, CATEGORIES_SITEMAP_LIMIT } from "../../../shared/features/categories/config";
5
6
  /** Full category document by slug — deduped per request via React.cache(). */
6
7
  export const getCategoryForDetail = cache(async (slug) => {
7
- const category = (await categoriesRepository.getCategoryBySlug(slug).catch(() => undefined)) ?? null;
8
+ const category = (await categoriesRepository.getCategoryBySlug(slug)) ?? null;
8
9
  if (category)
9
10
  void categoriesRepository.incrementViewCount(category.id);
10
11
  return category;
11
12
  });
12
13
  /** Flat list of all categories at tier 1 (roots) — for nav/sitemap. */
13
14
  export const listRootCategories = cache(async () => {
14
- return categoriesRepository.getCategoriesByTier(1).catch(() => []);
15
+ return safeRead(() => categoriesRepository.getCategoriesByTier(1), {
16
+ route: "/categories",
17
+ key: "categories.listRootCategories",
18
+ fallback: [],
19
+ });
15
20
  });
16
21
  /** Featured categories for homepage display (showOnHomepage: true). */
17
22
  export const listFeaturedCategories = cache(async () => {
@@ -29,7 +34,11 @@ export const listMenuCategories = cache(async () => {
29
34
  });
30
35
  /** Full category tree rooted at a given rootId — for sidebar navigation. */
31
36
  export const getCategoryTree = cache(async (rootId) => {
32
- return categoriesRepository.buildTree(rootId).catch(() => []);
37
+ return safeRead(() => categoriesRepository.buildTree(rootId), {
38
+ route: "/categories",
39
+ key: "categories.getCategoryTree",
40
+ fallback: [],
41
+ });
33
42
  });
34
43
  /** Flat category list for sitemap generation. */
35
44
  export const listSitemapCategories = cache(async () => {
@@ -6,6 +6,8 @@ import { eventInputSchema, eventUpdateSchema, registerForEventSchema } from "../
6
6
  import { assertEventActive } from "./service";
7
7
  import { AlreadyRegisteredError, EventNotFoundError } from "../../../shared/features/events/errors";
8
8
  import { ValidationError } from "../../../shared/errors/index";
9
+ import { normalizeError } from "../../../../errors/normalize";
10
+ import { serverLogger } from "../../../../monitoring/server-logger";
9
11
  export async function createEventAction(input) {
10
12
  return wrapAction(async () => {
11
13
  const user = await requireRoleUser(["admin", "moderator"]);
@@ -23,7 +25,7 @@ export async function createEventAction(input) {
23
25
  export async function updateEventAction(eventId, input) {
24
26
  return wrapAction(async () => {
25
27
  await requireRoleUser(["admin", "moderator"]);
26
- const event = await eventRepository.findById(eventId).catch(() => null);
28
+ const event = await eventRepository.findById(eventId);
27
29
  if (!event)
28
30
  throw new EventNotFoundError(eventId);
29
31
  const parsed = eventUpdateSchema.safeParse(input);
@@ -52,15 +54,26 @@ export async function registerForEventAction(input) {
52
54
  reviewStatus: "pending",
53
55
  submittedAt: new Date(),
54
56
  });
55
- // Increment entry count
56
- await eventRepository.incrementTotalEntries(eventId).catch(() => null);
57
+ // Increment entry count. The entry document is already written and is the
58
+ // authoritative record, so a failed counter bump must not fail the
59
+ // registration — but it leaves `stats.totalEntries` under-reporting, and
60
+ // the capacity check in assertEventActive reads that mirror, so it has to
61
+ // be visible to an operator rather than dropped (Root Cause #42).
62
+ await eventRepository.incrementTotalEntries(eventId).catch((err) => {
63
+ void normalizeError(err);
64
+ serverLogger.warn("events: incrementTotalEntries failed — stats.totalEntries is now stale", {
65
+ eventId,
66
+ error: err instanceof Error ? err.message : String(err),
67
+ });
68
+ return null;
69
+ });
57
70
  return entry;
58
71
  });
59
72
  }
60
73
  export async function cancelEventRegistrationAction(entryId) {
61
74
  return wrapAction(async () => {
62
75
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
63
- const entry = await eventEntryRepository.findById(entryId).catch(() => null);
76
+ const entry = await eventEntryRepository.findById(entryId);
64
77
  if (!entry || entry.userId !== user.uid) {
65
78
  throw new ValidationError("Registration not found or does not belong to you");
66
79
  }
@@ -1,5 +1,5 @@
1
1
  import { cache } from "react";
2
2
  import { eventRepository } from "../../../../repositories";
3
3
  export const getEventForDetail = cache(async (id) => {
4
- return eventRepository.findById(id).catch(() => null);
4
+ return eventRepository.findById(id);
5
5
  });
@@ -1,7 +1,7 @@
1
1
  import { eventRepository } from "../../../../repositories";
2
2
  import { EventNotFoundError, EventNotActiveError, EventEndedError, EventFullError, } from "../../../shared/features/events/errors";
3
3
  export async function assertEventActive(eventId) {
4
- const event = await eventRepository.findById(eventId).catch(() => null);
4
+ const event = await eventRepository.findById(eventId);
5
5
  if (!event)
6
6
  throw new EventNotFoundError(eventId);
7
7
  const status = event.status;
@@ -1,6 +1,7 @@
1
1
  import { cache } from "react";
2
2
  import { getAdminDb } from "../../../../providers/db-firebase";
3
3
  import { normalizeError } from "../../../../errors/normalize";
4
+ import { safeRead } from "../../../../errors/safe-read";
4
5
  import { serverLogger } from "../../../../monitoring/server-logger";
5
6
  import { GROUPED_LISTINGS_COLLECTION, } from "../../../../features/grouped/schemas/firestore";
6
7
  import { productRepository, groupedListingsRepository } from "../../../../repositories";
@@ -70,7 +71,7 @@ export const getGroupedListingWithItems = cache(async (slug) => {
70
71
  return null;
71
72
  const items = [];
72
73
  for (const productId of doc.productIds) {
73
- const product = await productRepository.findByIdOrSlug(productId).catch(() => null);
74
+ const product = await safeRead(() => productRepository.findByIdOrSlug(productId), { route: "/groups", key: "grouped.getGroupedListingWithItems.member", fallback: null });
74
75
  if (product)
75
76
  items.push(product);
76
77
  }
@@ -128,7 +129,11 @@ export const getGroupsWithItemsForProduct = cache(async (productId) => {
128
129
  const groups = await getGroupsForProduct(productId);
129
130
  return Promise.all(groups.map(async (group) => {
130
131
  const otherIds = group.productIds.filter((id) => id !== productId);
131
- const items = await Promise.all(otherIds.map((id) => productRepository.findByIdOrSlug(id).catch(() => null)));
132
+ const items = await Promise.all(otherIds.map((id) => safeRead(() => productRepository.findByIdOrSlug(id), {
133
+ route: "/products",
134
+ key: "grouped.getGroupsWithItemsForProduct.member",
135
+ fallback: null,
136
+ })));
132
137
  return { ...group, items: items.filter((p) => p !== null) };
133
138
  }));
134
139
  });
@@ -16,5 +16,5 @@ export const getItemRequestForDetail = cache(async (id, _opts) => {
16
16
  void _opts;
17
17
  if (!id)
18
18
  return null;
19
- return (await itemRequestsRepository.findById(id).catch(() => null)) ?? null;
19
+ return (await itemRequestsRepository.findById(id)) ?? null;
20
20
  });
@@ -39,7 +39,7 @@ export async function submitLotteryPullAction(input) {
39
39
  // Load the event/product to get the lotteryConfig
40
40
  let lotteryConfig;
41
41
  if (data.sourceType === "event") {
42
- const event = await eventRepository.findById(sourceId).catch(() => null);
42
+ const event = await eventRepository.findById(sourceId);
43
43
  if (!event || event.type !== "lottery") {
44
44
  throw new LotteryError("LOTTERY_WINDOW_CLOSED", "Lottery event not found or not of type lottery.");
45
45
  }
@@ -52,7 +52,7 @@ export async function submitLotteryPullAction(input) {
52
52
  lotteryConfig = event.lotteryConfig;
53
53
  }
54
54
  else {
55
- const product = await productRepository.findById(sourceId).catch(() => null);
55
+ const product = await productRepository.findById(sourceId);
56
56
  if (!product || product.listingType !== "prize-draw") {
57
57
  throw new LotteryError("LOTTERY_WINDOW_CLOSED", "Prize-draw product not found.");
58
58
  }
@@ -4,7 +4,7 @@ import { lotteryEntryRepository } from "../../../../features/lottery/repository/
4
4
  import { toClientLotteryConfig } from "./adapters";
5
5
  /** Fetch a lottery-type event with price/weight stripped for client use. */
6
6
  export const getLotteryEventCached = cache(async (id) => {
7
- const event = await eventRepository.findById(id).catch(() => null);
7
+ const event = await eventRepository.findById(id);
8
8
  if (!event || event.type !== "lottery")
9
9
  return null;
10
10
  return {
@@ -53,7 +53,7 @@ export async function updateOrderStatusAction(input) {
53
53
  const parsed = updateOrderStatusSchema.safeParse(input);
54
54
  if (!parsed.success)
55
55
  throw new ValidationError(parsed.error.issues[0]?.message ?? "Invalid input");
56
- const order = await orderRepository.findById(parsed.data.orderId).catch(() => null);
56
+ const order = await orderRepository.findById(parsed.data.orderId);
57
57
  if (!order)
58
58
  throw new OrderNotFoundError(parsed.data.orderId);
59
59
  if (!isAdminUser(user) && order.storeId !== user.uid) {
@@ -126,7 +126,7 @@ function notifyAdminsOfPaymentProof(order) {
126
126
  export async function attachPaymentProofAction(orderId, proof) {
127
127
  return wrapAction(async () => {
128
128
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
129
- const order = await orderRepository.findById(orderId).catch(() => null);
129
+ const order = await orderRepository.findById(orderId);
130
130
  if (!order)
131
131
  throw new OrderNotFoundError(orderId);
132
132
  if (!isAdminUser(user) && order.userId !== user.uid)
@@ -187,7 +187,7 @@ export async function adminVerifyPaymentAction(orderId) {
187
187
  if (!isAdminUser(user) && !isModeratorUser(user)) {
188
188
  throw new ValidationError("Only admin or moderator can verify payments");
189
189
  }
190
- const order = await orderRepository.findById(orderId).catch(() => null);
190
+ const order = await orderRepository.findById(orderId);
191
191
  if (!order)
192
192
  throw new OrderNotFoundError(orderId);
193
193
  if (order.paymentStatus === "paid")
@@ -226,7 +226,7 @@ export async function adminRequestProofReuploadAction(orderId, note) {
226
226
  }
227
227
  if (!note.trim())
228
228
  throw new ValidationError(REASON_REQUIRED_MSG);
229
- const order = await orderRepository.findById(orderId).catch(() => null);
229
+ const order = await orderRepository.findById(orderId);
230
230
  if (!order)
231
231
  throw new OrderNotFoundError(orderId);
232
232
  if (order.paymentStatus === "paid") {
@@ -275,7 +275,7 @@ export async function adminRejectPaymentAsFraudAction(orderId, note) {
275
275
  }
276
276
  if (!note.trim())
277
277
  throw new ValidationError(REASON_REQUIRED_MSG);
278
- const order = await orderRepository.findById(orderId).catch(() => null);
278
+ const order = await orderRepository.findById(orderId);
279
279
  if (!order)
280
280
  throw new OrderNotFoundError(orderId);
281
281
  if (order.paymentStatus === "paid") {
@@ -317,13 +317,13 @@ export async function raiseOrderDisputeAction(orderId, reason) {
317
317
  const user = await requireRoleUser(["buyer", "seller", "admin"]);
318
318
  if (!reason.trim())
319
319
  throw new ValidationError(REASON_REQUIRED_MSG);
320
- const order = await orderRepository.findById(orderId).catch(() => null);
320
+ const order = await orderRepository.findById(orderId);
321
321
  if (!order)
322
322
  throw new OrderNotFoundError(orderId);
323
323
  const isBuyer = order.userId === user.uid;
324
324
  let isSeller = false;
325
325
  if (!isBuyer && !isAdminUser(user) && !isModeratorUser(user) && order.storeId) {
326
- const store = await storeRepository.findById(order.storeId).catch(() => null);
326
+ const store = await storeRepository.findById(order.storeId);
327
327
  isSeller = store?.ownerId === user.uid;
328
328
  }
329
329
  if (!isBuyer && !isSeller && !isAdminUser(user) && !isModeratorUser(user)) {
@@ -7,4 +7,4 @@ export declare const getOrdersForBuyer: (userId: string, page?: number, pageSize
7
7
  totalPages: number;
8
8
  hasMore: boolean;
9
9
  }>;
10
- export declare const getRecentOrdersForBuyer: (userId: string) => Promise<never[] | import("../../../..").OrderDocument[]>;
10
+ export declare const getRecentOrdersForBuyer: (userId: string) => Promise<import("../../../..").OrderDocument[]>;