@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,7 +1,12 @@
1
1
  import { cache } from "react";
2
2
  import { orderRepository } from "../../../../repositories";
3
+ import { safeRead } from "../../../../errors/safe-read";
3
4
  export const getOrderForDetail = cache(async (orderId) => {
4
- return (await orderRepository.findById(orderId).catch(() => undefined)) ?? null;
5
+ // No catch: this order IS the page. Swallowing a Firestore failure to null
6
+ // renders "order not found" for a datastore outage — the caller cannot tell
7
+ // the two apart, which is Root Cause #59 on the money path. A throw reaches
8
+ // the route/error boundary and is now recorded.
9
+ return (await orderRepository.findById(orderId)) ?? null;
5
10
  });
6
11
  export const getOrdersForBuyer = cache(async (userId, page = 1, pageSize = 20) => {
7
12
  return orderRepository.listForUser(userId, { page, pageSize }).catch(() => ({
@@ -14,5 +19,12 @@ export const getOrdersForBuyer = cache(async (userId, page = 1, pageSize = 20) =
14
19
  }));
15
20
  });
16
21
  export const getRecentOrdersForBuyer = cache(async (userId) => {
17
- return orderRepository.findRecentByUser(userId).catch(() => []);
22
+ // Optional: a recent-orders rail. Degrading to empty is right, but it must
23
+ // be VISIBLE — safeRead records a DEGRADED_READ row instead of returning a
24
+ // value indistinguishable from "no recent orders".
25
+ return safeRead(() => orderRepository.findRecentByUser(userId), {
26
+ route: "/user/orders",
27
+ key: "orders.findRecentByUser",
28
+ fallback: [],
29
+ });
18
30
  });
@@ -2,7 +2,7 @@ import { orderRepository } from "../../../../repositories";
2
2
  import { OrderNotFoundError, OrderCancelError, OrderOwnershipError, OrderReturnWindowError, } from "../../../shared/features/orders/errors";
3
3
  import { ORDER_CANCELLABLE_STATUSES, ORDER_RETURN_WINDOW_MS, } from "../../../shared/features/orders/config";
4
4
  export async function assertOrderOwnership(orderId, userId) {
5
- const order = await orderRepository.findById(orderId).catch(() => null);
5
+ const order = await orderRepository.findById(orderId);
6
6
  if (!order)
7
7
  throw new OrderNotFoundError(orderId);
8
8
  if (order.userId !== userId)
@@ -4,7 +4,7 @@ import { roundRupees } from "../../../../utils/number.formatter";
4
4
  import { PreOrderNotFoundError, PreOrderSoldOutError, PreOrderNotOpenError, } from "../../../shared/features/pre-orders/errors";
5
5
  /** Assert a pre-order exists, is published, and has remaining capacity. */
6
6
  export async function assertPreOrderAvailable(preOrderId, requestedQty = 1) {
7
- const product = await productRepository.findByIdOrSlug(preOrderId).catch(() => null);
7
+ const product = await productRepository.findByIdOrSlug(preOrderId);
8
8
  if (!product || !isPreOrderListing(product))
9
9
  throw new PreOrderNotFoundError(preOrderId);
10
10
  if (product.status !== "published")
@@ -9,7 +9,7 @@ export async function enterPrizeDrawAction(input) {
9
9
  const { prizeDrawId } = input;
10
10
  if (!prizeDrawId)
11
11
  throw new Error("prizeDrawId is required.");
12
- const product = await productRepository.findByIdOrSlug(prizeDrawId).catch(() => null);
12
+ const product = await productRepository.findByIdOrSlug(prizeDrawId);
13
13
  if (!product)
14
14
  throw new Error("Prize draw not found.");
15
15
  assertPrizeDrawOpen(product);
@@ -8,6 +8,7 @@ import { productRepository } from "../../../../repositories";
8
8
  import { reviewRepository } from "../../../../repositories";
9
9
  import { getAdminDb } from "../../../../providers/db-firebase";
10
10
  import { normalizeError } from "../../../../errors/normalize";
11
+ import { safeRead } from "../../../../errors/safe-read";
11
12
  import { serverLogger } from "../../../../monitoring/server-logger";
12
13
  import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firestore";
13
14
  import { PRODUCTS_SITEMAP_LIMIT } from "../../../shared/features/products/config";
@@ -23,7 +24,7 @@ import { REVIEWS_DETAIL_PAGE_SIZE } from "../../../shared/features/reviews/confi
23
24
  // ---------------------------------------------------------------------------
24
25
  /** Fetch a single product by slug or id, deduped per request. */
25
26
  export const getProductForDetail = cache(async (slugOrId) => {
26
- const product = (await productRepository.findByIdOrSlug(slugOrId).catch(() => undefined)) ?? null;
27
+ const product = (await productRepository.findByIdOrSlug(slugOrId)) ?? null;
27
28
  // Fire-and-forget viewCount increment — must not affect response time
28
29
  if (product)
29
30
  void productRepository.incrementViewCount(product.id);
@@ -31,7 +32,11 @@ export const getProductForDetail = cache(async (slugOrId) => {
31
32
  });
32
33
  /** Fetch the first page of approved reviews for a product, deduped per request. */
33
34
  export const getReviewsForProduct = cache(async (productId, limit = REVIEWS_DETAIL_PAGE_SIZE) => {
34
- return reviewRepository.findApprovedByProduct(productId, limit).catch(() => []);
35
+ return safeRead(() => reviewRepository.findApprovedByProduct(productId, limit), {
36
+ route: "/products",
37
+ key: "products.getReviewsForProduct",
38
+ fallback: [],
39
+ });
35
40
  });
36
41
  /** Firestore review doc → client Review shape. Shared by every listing-type detail page's reviews tab. */
37
42
  export function toReview(doc) {
@@ -12,7 +12,7 @@ const ALLOWED_STATUS_TRANSITIONS = {
12
12
  };
13
13
  /** Assert a product exists and belongs to the given seller. Throws on failure. */
14
14
  export async function assertProductOwnership(productId, sellerId) {
15
- const product = await productRepository.findById(productId).catch(() => null);
15
+ const product = await productRepository.findById(productId);
16
16
  if (!product)
17
17
  throw new ProductNotFoundError(productId);
18
18
  if (product.storeId !== sellerId)
@@ -33,7 +33,7 @@ export async function createCouponAction(input) {
33
33
  export async function updateCouponAction(couponId, input) {
34
34
  return wrapAction(async () => {
35
35
  await requireRoleUser(["admin", "seller"]);
36
- const coupon = await couponsRepository.findById(couponId).catch(() => null);
36
+ const coupon = await couponsRepository.findById(couponId);
37
37
  if (!coupon)
38
38
  throw new CouponNotFoundError(couponId);
39
39
  const parsed = updateCouponSchema.safeParse(input);
@@ -1,5 +1,5 @@
1
1
  import { cache } from "react";
2
2
  import { couponsRepository } from "../../../../repositories";
3
3
  export const getCouponByCode = cache(async (code) => {
4
- return (await couponsRepository.getCouponByCode(code).catch(() => undefined)) ?? null;
4
+ return (await couponsRepository.getCouponByCode(code)) ?? null;
5
5
  });
@@ -2,7 +2,7 @@ import { couponsRepository } from "../../../../repositories";
2
2
  import { roundRupees } from "../../../../utils/number.formatter";
3
3
  import { CouponNotFoundError, CouponExpiredError, CouponUsageLimitError, CouponPerUserLimitError, CouponMinPurchaseError, CouponScopeError, } from "../../../shared/features/promotions/errors";
4
4
  export async function validateCoupon(input, userId) {
5
- const coupon = await couponsRepository.getCouponByCode(input.code).catch(() => null);
5
+ const coupon = await couponsRepository.getCouponByCode(input.code);
6
6
  if (!coupon)
7
7
  throw new CouponNotFoundError(input.code);
8
8
  if (!coupon.validity?.isActive)
@@ -1,4 +1,4 @@
1
- export declare const getReviewsForProduct: (productId: string, limit?: number) => Promise<never[] | import("../../../..").ReviewDocument[]>;
1
+ export declare const getReviewsForProduct: (productId: string, limit?: number) => Promise<import("../../../..").ReviewDocument[]>;
2
2
  export declare const getReviewsForStore: (storeId: string, page?: number, pageSize?: number) => Promise<import("../../../..").FirebaseSieveResult<import("../../../..").ReviewDocument> | {
3
3
  items: never[];
4
4
  total: number;
@@ -1,8 +1,13 @@
1
1
  import { cache } from "react";
2
2
  import { reviewRepository, orderRepository } from "../../../../repositories";
3
+ import { safeRead } from "../../../../errors/safe-read";
3
4
  import { REVIEWS_DETAIL_PAGE_SIZE } from "../../../shared/features/reviews/config";
4
5
  export const getReviewsForProduct = cache(async (productId, limit = REVIEWS_DETAIL_PAGE_SIZE) => {
5
- return reviewRepository.findApprovedByProduct(productId, limit).catch(() => []);
6
+ return safeRead(() => reviewRepository.findApprovedByProduct(productId, limit), {
7
+ route: "/products",
8
+ key: "reviews.getReviewsForProduct",
9
+ fallback: [],
10
+ });
6
11
  });
7
12
  export const getReviewsForStore = cache(async (storeId, page = 1, pageSize = 20) => {
8
13
  return reviewRepository.listForStore(storeId, { page, pageSize }).catch(() => ({
@@ -3,5 +3,5 @@ import { reviewRepository } from "../../../../repositories";
3
3
  export const getReviewById = cache(async (id) => {
4
4
  if (!id)
5
5
  return null;
6
- return reviewRepository.findById(id).catch(() => null);
6
+ return reviewRepository.findById(id);
7
7
  });
@@ -1,5 +1,6 @@
1
1
  import { cache } from "react";
2
2
  import { productRepository } from "../../../../repositories";
3
+ import { safeRead } from "../../../../errors/safe-read";
3
4
  import { loadProductFeaturesForStore } from "../../../../features/products/repository/loadProductFeatures";
4
5
  // Returns a React.cache()-wrapped fetch gated on one listing type.
5
6
  // The guard prevents cross-type URL collisions (e.g. /auctions/classified-slug returning wrong product).
@@ -7,7 +8,7 @@ export function makeGetListingForDetail(type) {
7
8
  return cache(async (slugOrId) => {
8
9
  if (!slugOrId)
9
10
  return null;
10
- const product = await productRepository.findByIdOrSlug(slugOrId).catch(() => undefined);
11
+ const product = await productRepository.findByIdOrSlug(slugOrId);
11
12
  if (!product || product.listingType !== type)
12
13
  return null;
13
14
  return product;
@@ -15,7 +16,11 @@ export function makeGetListingForDetail(type) {
15
16
  }
16
17
  // Product-feature badges are keyed by store, not listing type.
17
18
  // Per-type data.ts files re-export this under a type-specific name.
18
- export const getProductFeaturesForStore = cache(async (storeId) => loadProductFeaturesForStore(storeId).catch(() => []));
19
+ export const getProductFeaturesForStore = cache(async (storeId) => safeRead(() => loadProductFeaturesForStore(storeId), {
20
+ route: "/products",
21
+ key: "products.getProductFeaturesForStore",
22
+ fallback: [],
23
+ }));
19
24
  // Returns a React.cache()-wrapped function for store SSR first-page data.
20
25
  // Export names in stores/data.ts stay identical so consumers need no changes.
21
26
  export function makeGetStoreListingsInitial(type, pageSize) {
@@ -5,7 +5,7 @@ import { STORES_PRODUCTS_PAGE_SIZE, STORES_SITEMAP_LIMIT, } from "../../../share
5
5
  import { makeGetStoreListingsInitial } from "../shared/listing-data-factory";
6
6
  /** Full store document by slug — deduped per request via React.cache(). */
7
7
  export const getStoreForDetail = cache(async (slug) => {
8
- return (await storeRepository.findBySlug(slug).catch(() => undefined)) ?? null;
8
+ return (await storeRepository.findBySlug(slug)) ?? null;
9
9
  });
10
10
  // Collapse of three near-identical 14-line functions via factory — export names unchanged.
11
11
  export const listStoreProductsInitial = makeGetStoreListingsInitial("standard", STORES_PRODUCTS_PAGE_SIZE);
@@ -1,5 +1,5 @@
1
1
  import { cache } from "react";
2
2
  import { categoriesRepository } from "../../../../repositories";
3
3
  export const getSublistingCategoryForDetail = cache(async (slug) => {
4
- return categoriesRepository.findBySlugAndType(slug, "sublisting").catch(() => null);
4
+ return categoriesRepository.findBySlugAndType(slug, "sublisting");
5
5
  });
@@ -1,6 +1,7 @@
1
1
  import { bidRepository, cartRepository, productRepository, storeRepository, } from "../../../../repositories";
2
2
  import { sendNotification } from "../../../../features/admin/actions/notification-actions";
3
3
  import { normalizeError } from "../../../../errors/normalize";
4
+ import { safeRead } from "../../../../errors/safe-read";
4
5
  import { AUCTION_MESSAGES, BID_MESSAGES } from "../handlers/messages";
5
6
  import { ROUTES } from "../../../../next/routing/route-map";
6
7
  import { CART_LANE, LANE_CHECKOUT_WINDOW_MS } from "../../../shared/checkout/lanes";
@@ -94,7 +95,11 @@ async function settleAuction(ctx, product) {
94
95
  productRepository.updateStatusInBatch(batch, product.id, "archived");
95
96
  await batch.commit();
96
97
  const reserveStore = product.storeId
97
- ? await storeRepository.findById(product.storeId).catch(() => null)
98
+ ? await safeRead(() => storeRepository.findById(product.storeId), {
99
+ route: "job:auctionSettlement",
100
+ key: "auctionSettlement.reserveNotMet.store",
101
+ fallback: null,
102
+ })
98
103
  : null;
99
104
  if (reserveStore?.ownerId) {
100
105
  await sendNotification({
@@ -1,5 +1,3 @@
1
- import { normalizeError } from "../../../../errors/normalize";
2
- import { getAdminRealtimeDb } from "../../../../providers/db-firebase";
3
1
  import { decryptPii } from "../../../../security/index";
4
2
  import { sendNotification } from "../../../../features/admin/actions/notification-actions";
5
3
  import { ORDER_MESSAGES } from "../handlers/messages";
@@ -58,19 +56,13 @@ export async function handleOrderStatusChange(input, ctx) {
58
56
  userEmail,
59
57
  orderWhatsappAddonPaid: after.whatsappNotifyAddon === true,
60
58
  });
61
- try {
62
- await getAdminRealtimeDb().ref(`notifications/${after.userId}`).push({
63
- type: config.type,
64
- title: config.title,
65
- message: messageText,
66
- timestamp: Date.now(),
67
- read: false,
68
- });
69
- }
70
- catch (rtdbError) {
71
- void normalizeError(rtdbError);
72
- ctx.logger.error("Realtime DB push failed (non-fatal)", rtdbError);
73
- }
59
+ // The RTDB mirror that used to live here has been removed. `sendNotification`
60
+ // above is the real path (in-app + email + WhatsApp); the extra
61
+ // `notifications/{uid}` push was a duplicate that NOTHING read — no client
62
+ // subscription to that node has ever existed — and nothing pruned, so it
63
+ // accumulated one node per order transition per user forever. Its rule's
64
+ // `.type` validator also only allowed `info|success|warning|error`, none of
65
+ // which are the values written here (`order_confirmed`, `order_shipped`, …).
74
66
  ctx.logger.info(`Order ${orderId} status → ${newStatus}`, { userId: after.userId });
75
67
  }
76
68
  catch (error) {
@@ -12,7 +12,13 @@
12
12
  import { orderRepository } from "../../../../repositories";
13
13
  import { analyticsRollupRepository } from "../../../../repositories";
14
14
  export async function runRevenueRollup(ctx) {
15
- const deliveredOrders = await orderRepository.findByStatus("delivered").catch(() => []);
15
+ // No `.catch(() => [])` here on purpose: an empty list is a legitimate
16
+ // outcome that writes `totalRevenue: 0`, so swallowing a query failure would
17
+ // overwrite yesterday's correct rollup with a confident zero that the
18
+ // dashboard has no way to distinguish from "no delivered orders". Let it
19
+ // throw — the job runner records the failure and the stale-but-correct
20
+ // singleton stays put.
21
+ const deliveredOrders = await orderRepository.findByStatus("delivered");
16
22
  const totalRevenue = deliveredOrders.reduce((sum, order) => sum + (Number(order.totalPrice ?? 0) || 0), 0);
17
23
  await analyticsRollupRepository.setDashboardRollup({
18
24
  totalRevenue,
@@ -1,5 +1,7 @@
1
1
  import { batchDelete, getTestDataRefs } from "../handlers/_helpers";
2
2
  import { BID_FIELDS } from "../../../../constants/field-names";
3
+ import { encryptPiiFields } from "../../../../security/pii-encrypt";
4
+ import { BID_PII_FIELDS, OFFER_PII_FIELDS, } from "../../../../security/pii-schemas";
3
5
  import { SANDBOX_COLLECTIONS } from "./testerSandboxCleanup";
4
6
  // Direct imports from each defining module (not the seed-data barrel) —
5
7
  // this is internal appkit code, not part of the package's public API surface.
@@ -10,6 +12,29 @@ import { blogTesterSeedData } from "../../../../features/tester/seed-data/blog-t
10
12
  import { eventsTesterSeedData } from "../../../../features/tester/seed-data/events-tester-seed-data";
11
13
  import { offersTesterSeedData } from "../../../../features/tester/seed-data/offers-tester-seed-data";
12
14
  import { bidsTesterSeedData } from "../../../../features/tester/seed-data/bids-tester-seed-data";
15
+ /**
16
+ * PII per sandbox collection — the same map `seed-cli.mjs` applies.
17
+ *
18
+ * This job wrote every document with a bare `batch.set(...)`, while its own
19
+ * comment claimed to mirror seed-cli's loadGeneric. loadGeneric ENCRYPTS;
20
+ * this did not. So the 4-hourly refresh re-seeded sandbox PII in plaintext and
21
+ * quietly undid the backfill for those fixtures — caught by reading the raw
22
+ * documents after a deploy, where `bids` had 37 encrypted rows and one
23
+ * plaintext one that had not existed at backfill time.
24
+ *
25
+ * A scheduled job writing straight to Firestore bypasses `applyWriteHooks`
26
+ * exactly like the hand-rolled repository overrides did. Same defect, one
27
+ * level up.
28
+ */
29
+ const SANDBOX_PII = {
30
+ offers: OFFER_PII_FIELDS,
31
+ bids: BID_PII_FIELDS,
32
+ products: ["sellerName", "sellerEmail"],
33
+ };
34
+ function encryptForSandbox(collection, data) {
35
+ const fields = SANDBOX_PII[collection];
36
+ return fields ? encryptPiiFields(data, [...fields]) : data;
37
+ }
13
38
  const SEED_BY_COLLECTION = {
14
39
  categories: categoriesTesterSeedData,
15
40
  stores: storesTesterSeedData,
@@ -44,7 +69,7 @@ export async function runTesterSandboxRefresh(ctx) {
44
69
  // merge:true mirrors seed-cli.mjs's loadGeneric — the seed payload
45
70
  // carries every canonical field, so this fully overwrites anything
46
71
  // a tester edited through the UI.
47
- batch.set(ctx.db.collection(collection).doc(id), data, { merge: true });
72
+ batch.set(ctx.db.collection(collection).doc(id), encryptForSandbox(collection, data), { merge: true });
48
73
  reverted++;
49
74
  }
50
75
  await batch.commit();
@@ -66,7 +91,7 @@ export async function runTesterSandboxRefresh(ctx) {
66
91
  const { id, ...data } = bid;
67
92
  if (!id)
68
93
  continue;
69
- bidBatch.set(ctx.db.collection("bids").doc(id), data, { merge: true });
94
+ bidBatch.set(ctx.db.collection("bids").doc(id), encryptForSandbox("bids", data), { merge: true });
70
95
  reverted++;
71
96
  }
72
97
  await bidBatch.commit();
@@ -4,7 +4,13 @@ import * as React from "react";
4
4
  import { useQuery, } from "@tanstack/react-query";
5
5
  import { useToast } from "../../ui/components/Toast";
6
6
  import { ApiError } from "./ApiError";
7
- import { getErrorDisplay } from "../../errors/error-display-map";
7
+ import { toUserMessage } from "../../errors/error-display-map";
8
+ /**
9
+ * Appended to the "stale data" warning. Authored copy, never derived from the
10
+ * failure — a background refetch that dies on a `MODULE_NOT_FOUND` must not
11
+ * print a require stack into a toast (audit-raw-error-text).
12
+ */
13
+ const REFRESH_HINT = "Please check your connection and try again.";
8
14
  export function useApiQuery(options) {
9
15
  const { showToast } = useToast();
10
16
  const { silentBackgroundFailures, translate, reportClientError, ...rest } = options;
@@ -22,9 +28,10 @@ export function useApiQuery(options) {
22
28
  return;
23
29
  const err = result.error;
24
30
  if (err instanceof ApiError) {
25
- const display = getErrorDisplay(err.code);
26
- const message = translate?.(display.messageKey) ?? err.message;
27
- showToast(`Couldn't refresh latest data: ${message}`, "warning");
31
+ const message = toUserMessage(err.code, translate, {
32
+ fallback: REFRESH_HINT,
33
+ });
34
+ showToast(`Couldn't refresh latest data. ${message}`, "warning");
28
35
  if (reportClientError) {
29
36
  try {
30
37
  reportClientError({
@@ -39,8 +46,12 @@ export function useApiQuery(options) {
39
46
  }
40
47
  }
41
48
  else {
42
- const message = err instanceof Error ? err.message : "Unknown error";
43
- showToast(`Couldn't refresh latest data: ${message}`, "warning");
49
+ // No stable code on a non-ApiError rejection, so there is nothing to
50
+ // resolve — go straight to the authored hint rather than the raw text.
51
+ const message = toUserMessage(undefined, translate, {
52
+ fallback: REFRESH_HINT,
53
+ });
54
+ showToast(`Couldn't refresh latest data. ${message}`, "warning");
44
55
  }
45
56
  }, [
46
57
  result.isError,
package/dist/client.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  export { HTTP_ERROR_CODES, mapToHttpError } from "./errors/error-mapping";
2
2
  export type { HttpErrorCode, MappedError } from "./errors/error-mapping";
3
3
  export { ERROR_DISPLAY_MAP, getErrorDisplay } from "./errors/error-display-map";
4
+ export { toUserMessage, GENERIC_USER_MESSAGE } from "./errors/error-display-map";
4
5
  export type { ErrorDisplayEntry } from "./errors/error-display-map";
5
6
  export { RazorpayUnreachableError } from "./errors/razorpay-unreachable";
6
7
  export type { ActionResult } from "./utils/action-result";
package/dist/client.js CHANGED
@@ -4,6 +4,9 @@
4
4
  // Used by useApiMutation / useApiQuery / Button surface-error to route ApiError → toast or inline field error.
5
5
  export { HTTP_ERROR_CODES, mapToHttpError } from "./errors/error-mapping";
6
6
  export { ERROR_DISPLAY_MAP, getErrorDisplay } from "./errors/error-display-map";
7
+ // toUserMessage — the only sanctioned path from a failure to user-facing copy.
8
+ // It terminates in a constant, so a thrown value's own .message never surfaces.
9
+ export { toUserMessage, GENERIC_USER_MESSAGE } from "./errors/error-display-map";
7
10
  export { RazorpayUnreachableError } from "./errors/razorpay-unreachable";
8
11
  export { isOk, unwrap } from "./utils/action-result";
9
12
  // [CLIENT-SAFE] Client error class + surface-error router + reporter primitives.
@@ -13,6 +13,12 @@ export interface RealtimeSnapshot {
13
13
  /** Unsubscribe function returned by subscribe */
14
14
  export type Unsubscribe = () => void;
15
15
  export interface IClientRealtimeProvider {
16
+ /**
17
+ * A provider on its own Firebase app for this claim scope, so concurrent
18
+ * channels do not overwrite each other's auth. Optional: a provider that
19
+ * cannot isolate returns itself, keeping the old shared-app behaviour.
20
+ */
21
+ forScope?(scope: string): IClientRealtimeProvider;
16
22
  /** Authenticate the realtime connection with a custom token */
17
23
  signInWithToken(token: string): Promise<void>;
18
24
  /** Sign out of the realtime connection */
@@ -1,3 +1,4 @@
1
+ import { type IClientRealtimeProvider } from "./client-realtime";
1
2
  export interface RealtimeToken {
2
3
  customToken: string;
3
4
  expiresAt: number;
@@ -12,16 +13,15 @@ export interface RealtimeToken {
12
13
  * @returns a release function. **Always call it** — the session is only torn
13
14
  * down when every lease is released.
14
15
  */
15
- export declare function acquireRealtimeSession(scope: string, getToken: () => Promise<RealtimeToken>): Promise<() => void>;
16
- /**
17
- * Take a lease WITHOUT signing in — for a consumer that manages its own
18
- * sign-in but must still participate in the reference count.
19
- *
20
- * `useRealtimeEvent` is the case: its token carries a job-scoped `bulkJobId`
21
- * claim it receives from the caller, so it cannot use `acquireRealtimeSession`'s
22
- * fetcher. It still must not be able to sign the shared app out from under
23
- * another channel, which is precisely what its `cleanup()` used to do.
24
- */
25
- export declare function acquireExternalRealtimeLease(): () => void;
26
- /** Test/diagnostic only. */
16
+ export interface RealtimeLease {
17
+ /**
18
+ * Subscribe through THIS provider, not the global one — it is bound to the
19
+ * app this scope signed in on. Using the global provider would read from an
20
+ * app with someone else's claims.
21
+ */
22
+ provider: IClientRealtimeProvider;
23
+ release: () => void;
24
+ }
25
+ export declare function acquireRealtimeSession(scope: string, getToken: () => Promise<RealtimeToken>): Promise<RealtimeLease>;
26
+ /** Test/diagnostic only — total leases across every scope. */
27
27
  export declare function activeRealtimeLeases(): number;