@mohasinac/appkit 4.30.18 → 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/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,
|
|
@@ -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;
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { normalizeError } from "../errors/normalize";
|
|
2
|
-
import { getClientRealtimeProvider } from "./client-realtime";
|
|
2
|
+
import { getClientRealtimeProvider, } from "./client-realtime";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
4
|
+
* PER-SCOPE, reference-counted sign-in for the client realtime channels.
|
|
5
|
+
*
|
|
6
|
+
* Each claim scope gets its own Firebase app (via `provider.forScope`), because
|
|
7
|
+
* Firebase Auth state is per-app and two channels legitimately need two
|
|
8
|
+
* different claim sets at the same time.
|
|
5
9
|
*
|
|
6
10
|
* WHY THIS EXISTS — two distinct bugs, one cause.
|
|
7
11
|
*
|
|
@@ -21,98 +25,89 @@ import { getClientRealtimeProvider } from "./client-realtime";
|
|
|
21
25
|
* bulk token (carrying `bulkJobId`), killing an in-flight job subscription.
|
|
22
26
|
* Whoever finished last broke whoever was still working.
|
|
23
27
|
*
|
|
24
|
-
*
|
|
25
|
-
* consumer may decide to sign out
|
|
26
|
-
*
|
|
28
|
+
* (2) needed BOTH halves, and only one was done at first:
|
|
29
|
+
* - sign-OUT: no consumer may decide to sign out. Each takes a lease; the
|
|
30
|
+
* session ends only when the last lease for that scope is gone.
|
|
31
|
+
* - sign-IN: ref-counting cannot help here, because nothing is being shared
|
|
32
|
+
* by agreement — a bulk job and a message thread need different claims
|
|
33
|
+
* simultaneously. The answer is separate apps, not coordination.
|
|
27
34
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
35
|
+
* 🛑 Subscribe through `lease.provider`, never `getClientRealtimeProvider()`.
|
|
36
|
+
* The global one is the root app; the lease's is the app your scope actually
|
|
37
|
+
* authenticated on.
|
|
30
38
|
*/
|
|
31
39
|
/** Refresh a little early — a token that expires mid-subscription is a silent drop. */
|
|
32
40
|
const EXPIRY_SKEW_MS = 60000;
|
|
33
|
-
let leases = 0;
|
|
34
|
-
let currentScope = null;
|
|
35
|
-
let expiresAt = 0;
|
|
36
|
-
let inFlight = null;
|
|
37
41
|
/**
|
|
38
|
-
*
|
|
42
|
+
* One entry per claim scope, each on its OWN Firebase app.
|
|
39
43
|
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
44
|
+
* 🛑 This used to be three module-level singletons — `currentScope`,
|
|
45
|
+
* `expiresAt`, `leases` — describing ONE shared app. That shape could not
|
|
46
|
+
* express the real situation: two channels legitimately need two different
|
|
47
|
+
* claim sets at the same time, and Firebase Auth state is per-app, so every
|
|
48
|
+
* sign-in replaced the previous one's claims. Ref-counting only stopped a
|
|
49
|
+
* finishing channel from signing the others OUT; it could not stop a starting
|
|
50
|
+
* channel from signing itself IN over them.
|
|
46
51
|
*/
|
|
52
|
+
const scopes = new Map();
|
|
47
53
|
export async function acquireRealtimeSession(scope, getToken) {
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
|
|
54
|
+
const root = getClientRealtimeProvider();
|
|
55
|
+
let state = scopes.get(scope);
|
|
56
|
+
if (!state) {
|
|
57
|
+
// `forScope` is optional on the contract: a provider that cannot isolate
|
|
58
|
+
// falls back to the shared app, which is the old behaviour rather than a
|
|
59
|
+
// crash.
|
|
60
|
+
state = {
|
|
61
|
+
leases: 0,
|
|
62
|
+
expiresAt: 0,
|
|
63
|
+
inFlight: null,
|
|
64
|
+
provider: root.forScope?.(scope) ?? root,
|
|
65
|
+
};
|
|
66
|
+
scopes.set(scope, state);
|
|
67
|
+
}
|
|
68
|
+
state.leases++;
|
|
69
|
+
if (Date.now() > state.expiresAt - EXPIRY_SKEW_MS) {
|
|
70
|
+
// Collapse concurrent acquires for the SAME scope — several hooks mount in
|
|
71
|
+
// one tick and would otherwise each fetch a token and race to sign in.
|
|
72
|
+
state.inFlight ?? (state.inFlight = (async () => {
|
|
55
73
|
try {
|
|
56
74
|
const token = await getToken();
|
|
57
|
-
await
|
|
58
|
-
|
|
59
|
-
expiresAt = token.expiresAt;
|
|
75
|
+
await state.provider.signInWithToken(token.customToken);
|
|
76
|
+
state.expiresAt = token.expiresAt;
|
|
60
77
|
}
|
|
61
78
|
finally {
|
|
62
|
-
inFlight = null;
|
|
79
|
+
state.inFlight = null;
|
|
63
80
|
}
|
|
64
81
|
})());
|
|
65
|
-
await inFlight;
|
|
82
|
+
await state.inFlight;
|
|
66
83
|
}
|
|
67
84
|
let released = false;
|
|
68
|
-
return
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
* `useRealtimeEvent` is the case: its token carries a job-scoped `bulkJobId`
|
|
89
|
-
* claim it receives from the caller, so it cannot use `acquireRealtimeSession`'s
|
|
90
|
-
* fetcher. It still must not be able to sign the shared app out from under
|
|
91
|
-
* another channel, which is precisely what its `cleanup()` used to do.
|
|
92
|
-
*/
|
|
93
|
-
export function acquireExternalRealtimeLease() {
|
|
94
|
-
leases++;
|
|
95
|
-
// Its own sign-in has replaced whatever scope was active; forget the cached
|
|
96
|
-
// one so the next acquire() re-authenticates rather than trusting a token
|
|
97
|
-
// that is no longer installed.
|
|
98
|
-
currentScope = null;
|
|
99
|
-
expiresAt = 0;
|
|
100
|
-
let released = false;
|
|
101
|
-
return () => {
|
|
102
|
-
if (released)
|
|
103
|
-
return;
|
|
104
|
-
released = true;
|
|
105
|
-
leases = Math.max(0, leases - 1);
|
|
106
|
-
if (leases > 0)
|
|
107
|
-
return;
|
|
108
|
-
getClientRealtimeProvider()
|
|
109
|
-
.signOut()
|
|
110
|
-
.catch((err) => {
|
|
111
|
-
void normalizeError(err);
|
|
112
|
-
});
|
|
85
|
+
return {
|
|
86
|
+
provider: state.provider,
|
|
87
|
+
release: () => {
|
|
88
|
+
if (released)
|
|
89
|
+
return; // React StrictMode double-invokes cleanups
|
|
90
|
+
released = true;
|
|
91
|
+
const cur = scopes.get(scope);
|
|
92
|
+
if (!cur)
|
|
93
|
+
return;
|
|
94
|
+
cur.leases = Math.max(0, cur.leases - 1);
|
|
95
|
+
if (cur.leases > 0)
|
|
96
|
+
return;
|
|
97
|
+
// Last holder of THIS scope. Signing out touches only this scope's app,
|
|
98
|
+
// so it cannot disturb any other channel.
|
|
99
|
+
cur.expiresAt = 0;
|
|
100
|
+
scopes.delete(scope);
|
|
101
|
+
cur.provider.signOut().catch((err) => {
|
|
102
|
+
void normalizeError(err);
|
|
103
|
+
});
|
|
104
|
+
},
|
|
113
105
|
};
|
|
114
106
|
}
|
|
115
|
-
/** Test/diagnostic only. */
|
|
107
|
+
/** Test/diagnostic only — total leases across every scope. */
|
|
116
108
|
export function activeRealtimeLeases() {
|
|
117
|
-
|
|
109
|
+
let total = 0;
|
|
110
|
+
for (const s of scopes.values())
|
|
111
|
+
total += s.leases;
|
|
112
|
+
return total;
|
|
118
113
|
}
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ export { DatabaseError } from "./database-error";
|
|
|
12
12
|
export { RazorpayUnreachableError } from "./razorpay-unreachable";
|
|
13
13
|
export { handleApiError, logError, isAppError } from "./error-handler";
|
|
14
14
|
export { mapToHttpError, HTTP_ERROR_CODES, type MappedError, type HttpErrorCode, } from "./error-mapping";
|
|
15
|
-
export { ERROR_DISPLAY_MAP, getErrorDisplay, type ErrorDisplayEntry, } from "./error-display-map";
|
|
15
|
+
export { ERROR_DISPLAY_MAP, getErrorDisplay, toUserMessage, GENERIC_USER_MESSAGE, type ErrorDisplayEntry, } from "./error-display-map";
|
|
16
16
|
export { safeRead, installDegradedReadReporter, type DegradedReadReport, } from "./safe-read";
|
|
17
17
|
export { normalizeError, getErrorMessage, isAuthError, isApiNormalized, isAppNormalized, isFirebaseAuthNormalized, isFirebaseFirestoreNormalized, isFirebaseStorageNormalized, isNativeNormalized, isNetworkNormalized, isUnknownNormalized, isZodNormalized, } from "./normalize";
|
|
18
18
|
export type { NormalizedError, NormalizedApiError, NormalizedAppError, NormalizedFirebaseAuthError, NormalizedFirebaseFirestoreError, NormalizedFirebaseStorageError, NormalizedNativeError, NormalizedNetworkError, NormalizedUnknownThrownValue, NormalizedZodError, } from "./normalize";
|
package/dist/errors/index.js
CHANGED
|
@@ -17,7 +17,7 @@ export { RazorpayUnreachableError } from "./razorpay-unreachable";
|
|
|
17
17
|
export { handleApiError, logError, isAppError } from "./error-handler";
|
|
18
18
|
// Code→HTTP mapper (server) — and the i18n / inline-field map (client-safe)
|
|
19
19
|
export { mapToHttpError, HTTP_ERROR_CODES, } from "./error-mapping";
|
|
20
|
-
export { ERROR_DISPLAY_MAP, getErrorDisplay, } from "./error-display-map";
|
|
20
|
+
export { ERROR_DISPLAY_MAP, getErrorDisplay, toUserMessage, GENERIC_USER_MESSAGE, } from "./error-display-map";
|
|
21
21
|
// safeRead is server-only (uses the persisted log reporter)
|
|
22
22
|
export { safeRead, installDegradedReadReporter, } from "./safe-read";
|
|
23
23
|
// W3 — catch-clause normalizer. Every catch (e: unknown) funnels through
|