@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.
- package/dist/_internal/client/features/digital-code/CodeRevealPanel.js +5 -2
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +3 -3
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +3 -2
- package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +5 -2
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +11 -3
- package/dist/_internal/client/hand-mode/HandModeProvider.js +2 -0
- package/dist/_internal/server/features/auctions/actions.js +14 -3
- package/dist/_internal/server/features/auctions/service.js +1 -1
- package/dist/_internal/server/features/blog/data.js +2 -2
- package/dist/_internal/server/features/blog/service.js +3 -3
- package/dist/_internal/server/features/brands/data.js +1 -1
- package/dist/_internal/server/features/brands/service.js +3 -3
- package/dist/_internal/server/features/bundles/actions.js +6 -1
- package/dist/_internal/server/features/bundles/data.js +21 -4
- package/dist/_internal/server/features/cart/data.js +3 -1
- package/dist/_internal/server/features/cart/service.js +13 -2
- package/dist/_internal/server/features/categories/data.js +12 -3
- package/dist/_internal/server/features/events/actions.js +17 -4
- package/dist/_internal/server/features/events/data.js +1 -1
- package/dist/_internal/server/features/events/service.js +1 -1
- package/dist/_internal/server/features/grouped/data.js +7 -2
- package/dist/_internal/server/features/item-requests/data.js +1 -1
- package/dist/_internal/server/features/lottery/actions.js +2 -2
- package/dist/_internal/server/features/lottery/data.js +1 -1
- package/dist/_internal/server/features/orders/actions.js +7 -7
- package/dist/_internal/server/features/orders/data.d.ts +1 -1
- package/dist/_internal/server/features/orders/data.js +14 -2
- package/dist/_internal/server/features/orders/service.js +1 -1
- package/dist/_internal/server/features/pre-orders/service.js +1 -1
- package/dist/_internal/server/features/prize-draws/actions.js +1 -1
- package/dist/_internal/server/features/products/data.js +7 -2
- package/dist/_internal/server/features/products/service.js +1 -1
- package/dist/_internal/server/features/promotions/actions.js +1 -1
- package/dist/_internal/server/features/promotions/data.js +1 -1
- package/dist/_internal/server/features/promotions/service.js +1 -1
- package/dist/_internal/server/features/reviews/data.d.ts +1 -1
- package/dist/_internal/server/features/reviews/data.js +6 -1
- package/dist/_internal/server/features/reviews/og-data.js +1 -1
- package/dist/_internal/server/features/shared/listing-data-factory.js +7 -2
- package/dist/_internal/server/features/stores/data.js +1 -1
- package/dist/_internal/server/features/sublisting-categories/data.js +1 -1
- package/dist/_internal/server/jobs/core/auctionSettlement.js +6 -1
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +7 -15
- package/dist/_internal/server/jobs/core/revenueRollup.js +7 -1
- package/dist/_internal/server/jobs/core/testerSandboxRefresh.js +27 -2
- package/dist/client/api/useApiQuery.js +17 -6
- package/dist/client.d.ts +1 -0
- package/dist/client.js +3 -0
- package/dist/contracts/client-realtime.d.ts +6 -0
- package/dist/contracts/realtime-session.d.ts +12 -12
- package/dist/contracts/realtime-session.js +72 -77
- package/dist/errors/index.d.ts +1 -1
- package/dist/errors/index.js +1 -1
- package/dist/features/about/components/DeveloperView.js +7 -2
- package/dist/features/about/components/PolicyPageView.js +9 -1
- package/dist/features/about/components/PublicProfileView.js +23 -5
- package/dist/features/account/components/AddressForm.js +1 -1
- package/dist/features/account/components/NotificationBell.js +4 -3
- package/dist/features/account/components/NotificationPreferencesPanel.js +1 -1
- package/dist/features/account/components/UserOffersPanel.js +1 -1
- package/dist/features/admin/components/AdminFulfillmentView.js +5 -2
- package/dist/features/admin/components/AdminMediaView.js +3 -3
- package/dist/features/analytics/types.d.ts +1 -1
- package/dist/features/analytics/types.js +13 -0
- package/dist/features/auctions/components/AuctionDetailPageView.js +6 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +3 -1
- package/dist/features/auth/actions/profile-actions.js +4 -3
- package/dist/features/auth/components/ForgotPasswordView.js +1 -1
- package/dist/features/auth/components/LoginForm.js +1 -1
- package/dist/features/auth/components/RegisterForm.js +1 -1
- package/dist/features/auth/components/ResetPasswordView.js +1 -1
- package/dist/features/auth/hooks/useAuth.js +3 -0
- package/dist/features/blog/repository/blog.repository.js +3 -1
- package/dist/features/cart/actions/cart-actions.js +13 -4
- package/dist/features/cart/utils/pending-ops.js +1 -1
- package/dist/features/categories/components/BrandDetailPageView.js +28 -12
- package/dist/features/categories/components/CategoryDetailPageView.js +37 -20
- package/dist/features/categories/repository/categories.repository.js +3 -1
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +10 -1
- package/dist/features/classified/components/ClassifiedContactSellerPanel.js +1 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.js +10 -2
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +4 -1
- package/dist/features/events/components/EventBanner.js +5 -2
- package/dist/features/events/components/EventOfferCard.js +3 -1
- package/dist/features/events/components/EventPollWidget.js +1 -1
- package/dist/features/events/components/SpinWheelView.js +3 -2
- package/dist/features/grouped/components/GroupedListingEditorView.js +2 -1
- package/dist/features/history/hooks/useHistory.js +29 -27
- package/dist/features/homepage/components/AdSlot.js +3 -1
- package/dist/features/homepage/components/GoogleReviewsSection.js +8 -2
- package/dist/features/homepage/components/MarketplaceHomepageView.js +34 -10
- package/dist/features/homepage/components/SocialFeedSection.js +4 -1
- package/dist/features/homepage/lib/live-stats.js +8 -1
- package/dist/features/homepage/repository/carousel.repository.js +2 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +4 -1
- package/dist/features/media/finalize.js +3 -1
- package/dist/features/media/modals/ImageEditor.js +1 -1
- package/dist/features/media/modals/VideoThumbnailSelector.js +5 -2
- package/dist/features/media/modals/VideoTrimModal.js +5 -2
- package/dist/features/media/upload/MediaUploadField.js +2 -2
- package/dist/features/messages/hooks/useConversation.js +18 -4
- package/dist/features/messages/hooks/useConversations.js +25 -8
- package/dist/features/orders/components/RefundRequestView.js +5 -2
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -1
- package/dist/features/pre-orders/components/PreOrderActionsClient.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +4 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +4 -1
- package/dist/features/products/components/ProductDetailPageView.js +8 -2
- package/dist/features/products/components/ShareButton.js +3 -1
- package/dist/features/products/repository/loadProductFeatures.js +10 -2
- package/dist/features/products/repository/products.repository.js +2 -1
- package/dist/features/promotions/components/CouponCard.js +3 -1
- package/dist/features/reviews/actions/review-actions.js +8 -1
- package/dist/features/reviews/components/ReviewDetailPageView.js +13 -3
- package/dist/features/reviews/components/ReviewDetailShell.js +3 -1
- package/dist/features/scams/actions/scam-actions.d.ts +9 -1
- package/dist/features/scams/actions/scam-actions.js +52 -12
- package/dist/features/scams/components/SellerTrustBadge.d.ts +1 -1
- package/dist/features/scams/components/SellerTrustBadge.js +13 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +6 -1
- package/dist/features/seller/components/FulfillmentView.js +5 -2
- package/dist/features/seller/components/PhysicalLocationModal.js +1 -1
- package/dist/features/seller/components/PrintCenterView.js +3 -1
- package/dist/features/seller/components/SellerBidsView.js +1 -1
- package/dist/features/seller/components/SellerCouponsView.js +1 -1
- package/dist/features/seller/components/SellerGoogleReviewsView.js +2 -2
- package/dist/features/seller/components/SellerOrdersView.js +3 -3
- package/dist/features/seller/components/SellerPayoutMethodsView.js +2 -2
- package/dist/features/seller/components/SellerProductShell.d.ts +5 -0
- package/dist/features/seller/components/SellerProductShell.js +15 -5
- package/dist/features/seller/components/SellerProductsView.js +24 -10
- package/dist/features/seller/components/SellerReviewsView.js +47 -16
- package/dist/features/seller/components/SellerShippingConfigsView.js +5 -4
- package/dist/features/seller/components/SellerStoreCategoriesView.js +3 -2
- package/dist/features/stores/components/StoreDetailLayoutView.d.ts +1 -1
- package/dist/features/stores/components/StoreDetailLayoutView.js +12 -3
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +16 -1
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +3 -2
- package/dist/features/wishlist/hooks/useWishlistToggle.js +8 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +5 -2
- package/dist/next/api/routeHandler.js +3 -1
- package/dist/providers/auth-firebase/session.js +3 -1
- package/dist/providers/db-firebase/admin-app-lite.js +8 -8
- package/dist/providers/db-firebase/admin.js +12 -12
- package/dist/providers/db-firebase/browser-stub.d.ts +1 -11
- package/dist/providers/db-firebase/browser-stub.js +6 -11
- package/dist/providers/db-firebase/index.d.ts +0 -1
- package/dist/providers/db-firebase/index.js +0 -1
- package/dist/providers/db-firebase/rtdb-paths.d.ts +15 -6
- package/dist/providers/db-firebase/rtdb-paths.js +15 -6
- package/dist/providers/db-firebase/rtdb-url.d.ts +27 -0
- package/dist/providers/db-firebase/rtdb-url.js +50 -0
- package/dist/providers/firebase-client/realtime.d.ts +20 -0
- package/dist/providers/firebase-client/realtime.js +34 -1
- package/dist/providers.d.ts +1 -1
- package/dist/providers.js +1 -1
- package/dist/react/hooks/use-action-dispatch.js +8 -2
- package/dist/react/hooks/useFormStatePreservation.js +3 -1
- package/dist/react/hooks/useRealtimeEvent.js +20 -9
- package/dist/ui/components/toolbar-collapse.js +1 -2
- 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
|
-
|
|
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
|
-
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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)
|
|
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<
|
|
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)
|
|
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(() => ({
|
|
@@ -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)
|
|
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) =>
|
|
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)
|
|
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")
|
|
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)
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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;
|