@mohasinac/appkit 4.27.0 → 4.28.0
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/lottery/LotteryAdminEditView.d.ts +16 -16
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +29 -6
- package/dist/_internal/client/features/lottery/LotteryListView.js +13 -1
- package/dist/_internal/server/features/checkout/actions.js +6 -4
- package/dist/_internal/server/features/classified/metadata.js +3 -1
- package/dist/_internal/server/features/digital-code/metadata.js +3 -1
- package/dist/_internal/server/features/live/metadata.js +3 -1
- package/dist/_internal/server/features/refunds/actions.js +3 -2
- package/dist/_internal/server/features/reviews/actions.js +3 -2
- package/dist/_internal/server/features/seo/sitemap.js +189 -40
- package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
- package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
- package/dist/_internal/shared/actions/action-registry.js +14 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
- package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
- package/dist/_internal/shared/features/checkout/config.js +6 -3
- package/dist/client.d.ts +32 -2
- package/dist/client.js +37 -2
- package/dist/constants/api-endpoints.d.ts +21 -3
- package/dist/constants/api-endpoints.js +14 -1
- package/dist/constants/field-names.d.ts +2 -2
- package/dist/constants/field-names.js +2 -2
- package/dist/core/newsletter.repository.js +14 -1
- package/dist/features/about/components/FAQPageView.js +1 -1
- package/dist/features/about/components/FeesView.js +1 -1
- package/dist/features/account/components/AddressForm.d.ts +2 -0
- package/dist/features/account/components/AddressForm.js +5 -1
- package/dist/features/account/components/UserBidsView.js +1 -1
- package/dist/features/account/components/UserSidebar.js +1 -1
- package/dist/features/account/components/UserSupportView.js +24 -2
- package/dist/features/account/hooks/useAddresses.d.ts +29 -1
- package/dist/features/account/hooks/useAddresses.js +4 -1
- package/dist/features/account/hooks/useProfile.d.ts +1 -1
- package/dist/features/account/hooks/useProfile.js +1 -1
- package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
- package/dist/features/addresses/repository/addresses.repository.js +19 -0
- package/dist/features/admin/actions/chat-actions.js +5 -1
- package/dist/features/admin/components/AdminAdsView.js +1 -1
- package/dist/features/admin/components/AdminBidsView.js +6 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
- package/dist/features/admin/components/AdminCarouselView.js +1 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
- package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/admin/components/AdminViewCards.js +2 -2
- package/dist/features/admin/components/DataTable.js +2 -2
- package/dist/features/admin/schemas/firestore.js +9 -3
- package/dist/features/auctions/actions/bid-actions.js +45 -4
- package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
- package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
- package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/hooks/useAuth.d.ts +23 -1
- package/dist/features/auth/hooks/useAuth.js +27 -1
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +3 -0
- package/dist/features/auth/repository/session.repository.d.ts +16 -1
- package/dist/features/auth/repository/session.repository.js +23 -0
- package/dist/features/auth/repository/user.repository.js +13 -2
- package/dist/features/blog/components/BlogListView.js +2 -2
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
- package/dist/features/cart/components/index.d.ts +1 -1
- package/dist/features/cart/components/index.js +1 -1
- package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
- package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
- package/dist/features/cart/utils/line-total.d.ts +25 -0
- package/dist/features/cart/utils/line-total.js +26 -0
- package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
- package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
- package/dist/features/categories/components/CategoryTree.js +2 -2
- package/dist/features/contact/components/ContactForm.js +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
- package/dist/features/events/components/AdminEventEditorView.js +16 -9
- package/dist/features/events/components/AdminEventEntriesView.js +7 -13
- package/dist/features/events/components/EventRafflesSection.js +14 -2
- package/dist/features/events/components/EventStatusBadge.js +1 -1
- package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
- package/dist/features/events/components/event-entry-detail-fields.js +13 -0
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
- package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
- package/dist/features/faq/repository/faqs.repository.js +64 -54
- package/dist/features/faq/schemas/firestore.d.ts +3 -2
- package/dist/features/faq/schemas/firestore.js +2 -2
- package/dist/features/faq/schemas/index.d.ts +3 -3
- package/dist/features/faq/schemas/index.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +2 -2
- package/dist/features/forms/Toggle.js +1 -1
- package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
- package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
- package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
- package/dist/features/homepage/components/BrandsSection.js +1 -1
- package/dist/features/homepage/components/CTABannerSection.js +1 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
- package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/homepage/lib/section-renderer.js +2 -1
- package/dist/features/layout/AppLayoutShell.js +5 -5
- package/dist/features/layout/BottomActions.js +5 -5
- package/dist/features/layout/BottomSheet.js +1 -1
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
- package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
- package/dist/features/lottery/schemas/config-write.d.ts +141 -0
- package/dist/features/lottery/schemas/config-write.js +139 -0
- package/dist/features/media/MediaPickerModal.js +3 -1
- package/dist/features/media/MediaVideo.js +4 -2
- package/dist/features/media/components/MediaSlider.js +1 -1
- package/dist/features/orders/components/OrdersList.js +2 -2
- package/dist/features/payments/repository/payout.repository.d.ts +11 -1
- package/dist/features/payments/repository/payout.repository.js +14 -6
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
- package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
- package/dist/features/products/columns/productTableColumns.js +3 -3
- package/dist/features/products/components/CompareOverlay.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
- package/dist/features/products/components/PrizeDrawsSection.js +5 -1
- package/dist/features/products/components/ProductDetailActions.js +11 -7
- package/dist/features/products/components/ProductDetailPageView.js +3 -3
- package/dist/features/products/components/ProductFeatureBadges.js +2 -2
- package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
- package/dist/features/products/components/ProductFilters.js +15 -8
- package/dist/features/products/components/ProductGrid.js +8 -17
- package/dist/features/products/components/ProductTabs.js +7 -4
- package/dist/features/products/components/ProductTabsShell.js +2 -7
- package/dist/features/products/constants/action-defs.d.ts +21 -4
- package/dist/features/products/constants/action-defs.js +7 -3
- package/dist/features/products/hooks/useRelatedProducts.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -1
- package/dist/features/products/repository/products.repository.js +27 -13
- package/dist/features/products/schemas/firestore.d.ts +3 -2
- package/dist/features/products/schemas/firestore.js +1 -1
- package/dist/features/reviews/actions/review-actions.d.ts +8 -1
- package/dist/features/reviews/actions/review-actions.js +14 -3
- package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
- package/dist/features/reviews/components/ReviewModal.js +1 -1
- package/dist/features/reviews/repository/reviews.repository.d.ts +25 -1
- package/dist/features/reviews/repository/reviews.repository.js +53 -13
- package/dist/features/reviews/schemas/firestore.d.ts +6 -0
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/components/Search.js +3 -3
- package/dist/features/search/components/SearchResultsSection.js +1 -1
- package/dist/features/seller/components/SellerAddressesView.js +3 -1
- package/dist/features/seller/components/SellerBidsView.js +6 -1
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/shell/SectionForm.d.ts +21 -1
- package/dist/features/shell/SectionForm.js +15 -3
- package/dist/features/shell/build-sections.js +10 -1
- package/dist/features/shell/field-ui-meta.d.ts +21 -0
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
- package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
- package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
- package/dist/features/stores/actions/store-query-actions.js +25 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
- package/dist/features/stores/components/StoreHeader.js +1 -1
- package/dist/features/stores/components/StoreNavTabs.js +20 -8
- package/dist/features/support/repository/support.repository.d.ts +12 -0
- package/dist/features/support/repository/support.repository.js +15 -0
- package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
- package/dist/features/support/schemas/ticket-create-form.js +82 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.js +2 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +138 -1
- package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
- package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
- package/dist/features/wishlist/components/index.d.ts +2 -1
- package/dist/features/wishlist/components/index.js +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js +19 -4
- package/dist/monitoring/error-tracking.js +39 -5
- package/dist/monitoring/server-logger.js +70 -32
- package/dist/next/components/ErrorView.js +1 -1
- package/dist/next/routing/route-map.d.ts +39 -7
- package/dist/next/routing/route-map.js +34 -4
- package/dist/providers/db-firebase/base.repository.d.ts +30 -0
- package/dist/providers/db-firebase/base.repository.js +56 -9
- package/dist/react/ErrorBoundary.js +9 -1
- package/dist/react/contexts/SessionContext.js +6 -2
- package/dist/react/hooks/useAuthGate.d.ts +22 -0
- package/dist/react/hooks/useAuthGate.js +86 -7
- package/dist/schemas/registry.d.ts +3 -3
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-encrypt.d.ts +13 -5
- package/dist/security/pii-encrypt.js +132 -6
- package/dist/security/pii-schemas.d.ts +20 -4
- package/dist/security/pii-schemas.js +23 -5
- package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
- package/dist/seed/_helpers/product-search-txt.js +33 -0
- package/dist/seed/faq-seed-data.d.ts +16 -0
- package/dist/seed/faq-seed-data.js +22 -1
- package/dist/seed/products-art-seed-data.js +49 -7
- package/dist/seed/products-auctions-seed-data.js +12 -4
- package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
- package/dist/seed/products-classifieds-seed-data.js +65 -9
- package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
- package/dist/seed/products-digital-codes-seed-data.js +65 -9
- package/dist/seed/products-live-items-seed-data.d.ts +5 -0
- package/dist/seed/products-live-items-seed-data.js +33 -5
- package/dist/seed/products-preorders-seed-data.d.ts +1 -1
- package/dist/seed/products-preorders-seed-data.js +6 -2
- package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
- package/dist/seed/products-prize-draws-seed-data.js +57 -8
- package/dist/seed/products-standard-seed-data.js +6 -2
- package/dist/seed/products-stickers-seed-data.js +49 -7
- package/dist/seed/site-settings-seed-data.js +5 -3
- package/dist/seo/json-ld.d.ts +44 -2
- package/dist/seo/json-ld.js +57 -11
- package/dist/styles.css +368 -90
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.js +1 -1
- package/dist/tokens/themes/default-dark.js +6 -0
- package/dist/tokens/themes/default-light.js +5 -0
- package/dist/tokens/tokens.css +84 -0
- package/dist/ui/DataTable.js +1 -1
- package/dist/ui/DataTable.style.css +3 -3
- package/dist/ui/components/BulkActionBar.style.css +1 -3
- package/dist/ui/components/Button.js +15 -6
- package/dist/ui/components/Dropdown.style.css +1 -4
- package/dist/ui/components/FilterChipGroup.js +1 -1
- package/dist/ui/components/FilterDrawer.style.css +1 -1
- package/dist/ui/components/Icon.d.ts +53 -0
- package/dist/ui/components/Icon.js +41 -0
- package/dist/ui/components/IconButton.d.ts +13 -0
- package/dist/ui/components/IconButton.js +13 -0
- package/dist/ui/components/IconButton.style.css +18 -10
- package/dist/ui/components/ListingLayout.style.css +2 -10
- package/dist/ui/components/ListingToolbar.js +6 -6
- package/dist/ui/components/Modal.js +1 -1
- package/dist/ui/components/PaginatedSelect.style.css +3 -3
- package/dist/ui/components/Pagination.style.css +1 -6
- package/dist/ui/components/RichTextEditor.js +1 -1
- package/dist/ui/components/RowActionMenu.style.css +1 -1
- package/dist/ui/components/SideDrawer.style.css +1 -1
- package/dist/ui/components/Slider.js +1 -1
- package/dist/ui/components/TabBarShell.d.ts +78 -0
- package/dist/ui/components/TabBarShell.js +95 -0
- package/dist/ui/components/Tabs.js +12 -8
- package/dist/ui/components/Tabs.style.css +249 -41
- package/dist/ui/components/ViewToggle.style.css +1 -5
- package/dist/ui/components/tabs-overflow.d.ts +17 -0
- package/dist/ui/components/tabs-overflow.js +142 -0
- package/dist/ui/icons/icon-registry.d.ts +38 -0
- package/dist/ui/icons/icon-registry.js +37 -0
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/media-url.d.ts +27 -4
- package/dist/utils/media-url.js +37 -6
- package/dist/utils/search-txt.d.ts +71 -0
- package/dist/utils/search-txt.js +118 -0
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from "react";
|
|
2
|
+
import type { LotteryConfigWriteInput } from "../../../../features/lottery/schemas/config-write";
|
|
2
3
|
interface LotterySlotRow {
|
|
3
4
|
slotNumber: number;
|
|
4
5
|
name: string;
|
|
@@ -6,26 +7,25 @@ interface LotterySlotRow {
|
|
|
6
7
|
/** Prize photo — rendered publicly in the prize collage on the detail page. */
|
|
7
8
|
image?: string;
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* What this form hands its caller.
|
|
12
|
+
*
|
|
13
|
+
* `lotteryConfig` is the WRITE input — deliberately not `LotteryConfig`. It
|
|
14
|
+
* carries no `isBooked`, no `bookedBy*` and no `weight`, because an admin
|
|
15
|
+
* editing a lottery is describing prizes, not attendance. The stored config
|
|
16
|
+
* gains those in `mergeLotteryConfig`, server-side, from the slots that are
|
|
17
|
+
* already there.
|
|
18
|
+
*
|
|
19
|
+
* `totalSlots` is absent for the same reason: derived from `slots.length` by
|
|
20
|
+
* the merge, never sent. A caller-supplied count that disagrees with the array
|
|
21
|
+
* is the mirror-drift trap (Root Cause #42), and the count is what the
|
|
22
|
+
* fullness check reads.
|
|
23
|
+
*/
|
|
9
24
|
interface LotteryEventFormData {
|
|
10
25
|
title: string;
|
|
11
26
|
description?: string;
|
|
12
27
|
type: "lottery";
|
|
13
|
-
lotteryConfig:
|
|
14
|
-
slots: Array<{
|
|
15
|
-
slotNumber: number;
|
|
16
|
-
name: string;
|
|
17
|
-
image?: string;
|
|
18
|
-
price: number;
|
|
19
|
-
weight: number;
|
|
20
|
-
isBooked: boolean;
|
|
21
|
-
}>;
|
|
22
|
-
totalSlots: number;
|
|
23
|
-
pricingMode: "uniform" | "variable";
|
|
24
|
-
uniformPrice?: number;
|
|
25
|
-
drawWindowDurationMinutes: number;
|
|
26
|
-
maxPullsPerTransaction: number;
|
|
27
|
-
maxPullsPerUser: number;
|
|
28
|
-
};
|
|
28
|
+
lotteryConfig: LotteryConfigWriteInput;
|
|
29
29
|
}
|
|
30
30
|
interface LotteryAdminEditViewProps {
|
|
31
31
|
eventId?: string;
|
|
@@ -5,6 +5,7 @@ import { normalizeError } from "../../../../errors/normalize";
|
|
|
5
5
|
import { Stack, Row, Text, Heading, Section, Container, Button, DataTable, } from "../../../../ui";
|
|
6
6
|
import { Form, FieldInput, FieldSelect, Input } from "../../../../ui";
|
|
7
7
|
import { lotteryConfigFormSchema } from "../../../shared/features/lottery/config-form";
|
|
8
|
+
import { lotteryConfigWriteSchema } from "../../../../features/lottery/schemas/config-write";
|
|
8
9
|
import { FormErrorSummary } from "../../../../ui/forms/FormErrorSummary";
|
|
9
10
|
export function LotteryAdminEditView({ eventId, initialData, onSubmit, }) {
|
|
10
11
|
const [isPending, startTransition] = useTransition();
|
|
@@ -35,23 +36,45 @@ export function LotteryAdminEditView({ eventId, initialData, onSubmit, }) {
|
|
|
35
36
|
setError(null);
|
|
36
37
|
startTransition(async () => {
|
|
37
38
|
try {
|
|
38
|
-
|
|
39
|
+
/*
|
|
40
|
+
* 🛑 This used to send `isBooked: false` and `weight: 0` on every slot,
|
|
41
|
+
* and drop `bookedByUserId` / `bookedByDisplayName` /
|
|
42
|
+
* `bookedByUserLotteryNumber` entirely — against a `.passthrough()`
|
|
43
|
+
* PATCH, which made the first save of a live lottery erase every
|
|
44
|
+
* purchased slot, silently, with a 200.
|
|
45
|
+
*
|
|
46
|
+
* The write shape now has NO booking fields at all and is `.strict()`,
|
|
47
|
+
* so a slot's buyer is not something this form can express, let alone
|
|
48
|
+
* overwrite. `PUT /lottery-config` merges by `slotNumber` against the
|
|
49
|
+
* stored config and is the only writer.
|
|
50
|
+
*/
|
|
51
|
+
const candidate = {
|
|
39
52
|
slots: slots.map((s) => ({
|
|
40
53
|
slotNumber: s.slotNumber,
|
|
41
54
|
name: s.name,
|
|
42
55
|
image: s.image?.trim() || undefined,
|
|
43
56
|
price: s.price,
|
|
44
|
-
weight: 0,
|
|
45
|
-
isBooked: false,
|
|
46
57
|
})),
|
|
47
|
-
totalSlots: slots.length,
|
|
48
58
|
pricingMode,
|
|
49
|
-
uniformPrice: pricingMode === "uniform"
|
|
59
|
+
uniformPrice: pricingMode === "uniform"
|
|
60
|
+
? Math.round(parseFloat(uniformPrice) * 100) / 100 || 0
|
|
61
|
+
: undefined,
|
|
50
62
|
drawWindowDurationMinutes: parseInt(drawWindowMinutes, 10) || 5,
|
|
51
63
|
maxPullsPerTransaction: parseInt(maxPullsPerTx, 10) || 1,
|
|
52
64
|
maxPullsPerUser: parseInt(maxPullsPerUser, 10) || 1,
|
|
53
65
|
};
|
|
54
|
-
|
|
66
|
+
/*
|
|
67
|
+
* The schema was passed to `<Form>` and never actually run — so the
|
|
68
|
+
* `parseFloat(x) || 0` coercions above, which it exists to catch, were
|
|
69
|
+
* still live. Parsed here so a bad slot is refused on the client
|
|
70
|
+
* rather than discovered as a 400.
|
|
71
|
+
*/
|
|
72
|
+
const parsed = lotteryConfigWriteSchema.safeParse(candidate);
|
|
73
|
+
if (!parsed.success) {
|
|
74
|
+
setError(parsed.error.issues[0]?.message ?? "Check the lottery settings.");
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
await onSubmit({ title, description, type: "lottery", lotteryConfig: parsed.data });
|
|
55
78
|
}
|
|
56
79
|
catch (err) {
|
|
57
80
|
void normalizeError(err);
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { Stack, Row, Text, Heading, Badge, Card, Section, Container, Anchor, Div, Grid, } from "../../../../ui";
|
|
4
4
|
import { MediaImage } from "../../../../features/media/MediaImage";
|
|
5
|
+
import { ROUTES } from "../../../../next/routing/route-map";
|
|
5
6
|
/**
|
|
6
7
|
* Card grid of lotteries.
|
|
7
8
|
* Admin sees all statuses; public sees active only.
|
|
@@ -17,7 +18,18 @@ export function LotteryListView({ items, adminMode = false }) {
|
|
|
17
18
|
const claimed = event.lotteryConfig?.slots.filter((s) => s.isBooked).length ?? 0;
|
|
18
19
|
const total = event.lotteryConfig?.totalSlots ?? 0;
|
|
19
20
|
const slotsLeft = total - claimed;
|
|
20
|
-
return (_jsx(Anchor, {
|
|
21
|
+
return (_jsx(Anchor, {
|
|
22
|
+
/*
|
|
23
|
+
* In adminMode this used to link to the PUBLIC lottery page,
|
|
24
|
+
* so an admin on the admin list had no route to slot
|
|
25
|
+
* configuration at all — which went unnoticed because the
|
|
26
|
+
* editor was orphaned and there was nowhere to link TO.
|
|
27
|
+
* Nested anchors are invalid, so the card itself changes
|
|
28
|
+
* target rather than gaining a second link.
|
|
29
|
+
*/
|
|
30
|
+
href: adminMode
|
|
31
|
+
? String(ROUTES.ADMIN.LOTTERY_CONFIG(event.id))
|
|
32
|
+
: `/lottery/${event.id}`, tone: "brand", rounded: "2xl", className: "block focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", "aria-label": `${event.title} lottery`, children: _jsxs(Card, { variant: "default", padding: "none", animate: "hoverLift", className: "overflow-hidden", children: [event.coverImageUrl || event.coverImage?.url ? (_jsx(Div, { className: "relative aspect-video overflow-hidden", children: _jsx(MediaImage, { src: event.coverImageUrl || event.coverImage?.url || "", alt: event.title, size: "card" }) })) : (_jsx(Div, { className: "aspect-video", surface: "muted", align: "center", justify: "center", children: _jsx(Text, { size: "3xl", children: "\uD83C\uDFB0" }) })), _jsxs(Stack, { gap: "xs", padding: "md", children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, children: [_jsx(Badge, { variant: event.status === "active"
|
|
21
33
|
? "success"
|
|
22
34
|
: event.status === "ended"
|
|
23
35
|
? "danger"
|
|
@@ -80,7 +80,7 @@ async function claimDigitalCodeForOrder(db, productId, orderId, userId, opts) {
|
|
|
80
80
|
userName: opts.userName ?? "there",
|
|
81
81
|
productTitle: opts.productTitle ?? "your product",
|
|
82
82
|
orderId,
|
|
83
|
-
}).catch((e) => serverLogger.error("claimDigitalCode: email failed", e));
|
|
83
|
+
}).catch((e) => serverLogger.error("claimDigitalCode: email failed", { error: normalizeError(e).message }));
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
86
|
/**
|
|
@@ -550,7 +550,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
550
550
|
userEmail: userEmail || undefined,
|
|
551
551
|
userName,
|
|
552
552
|
productTitle: firstItem.productTitle,
|
|
553
|
-
}).catch((e) => serverLogger.error("claimDigitalCode", e));
|
|
553
|
+
}).catch((e) => serverLogger.error("claimDigitalCode", { error: normalizeError(e).message }));
|
|
554
554
|
}
|
|
555
555
|
for (const code of groupCouponCodes) {
|
|
556
556
|
const entry = couponUsageAccumulator.get(code);
|
|
@@ -892,8 +892,10 @@ export async function previewCheckoutPricing(input) {
|
|
|
892
892
|
// A preview never throws — it just shows the lane the buyer can actually
|
|
893
893
|
// check out, matching what the checkout page's active tab displays.
|
|
894
894
|
(!previewLane || laneOf(item) === previewLane));
|
|
895
|
+
// A fresh object, not the module singleton — an in-process caller mutating
|
|
896
|
+
// the returned preview would otherwise poison every later empty result.
|
|
895
897
|
if (cartItems.length === 0)
|
|
896
|
-
return EMPTY_PRICING_PREVIEW;
|
|
898
|
+
return { ...EMPTY_PRICING_PREVIEW, stores: [] };
|
|
897
899
|
// Best-effort product lookup — a preview never throws on a since-unpublished
|
|
898
900
|
// product, it just falls back to the item's cart-snapshot price for that
|
|
899
901
|
// line (same fallback cartRepository.getSubtotal() implicitly relies on).
|
|
@@ -1268,7 +1270,7 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1268
1270
|
userEmail: userEmail || undefined,
|
|
1269
1271
|
userName,
|
|
1270
1272
|
productTitle: firstItem.productTitle,
|
|
1271
|
-
}).catch((e) => serverLogger.error("claimDigitalCode", e));
|
|
1273
|
+
}).catch((e) => serverLogger.error("claimDigitalCode", { error: normalizeError(e).message }));
|
|
1272
1274
|
}
|
|
1273
1275
|
for (const code of groupCouponCodes) {
|
|
1274
1276
|
const entry = couponUsageAccumulator.get(code);
|
|
@@ -21,7 +21,9 @@ export function buildClassifiedMetadata(product, opts) {
|
|
|
21
21
|
: product.description?.slice(0, 160) ?? "";
|
|
22
22
|
const image = opts?.ogImageUrl ?? (product.mainImage || product.images?.[0] || null);
|
|
23
23
|
const siteUrl = opts?.siteUrl?.replace(/\/+$/, "");
|
|
24
|
-
|
|
24
|
+
// Prefer `slug` — the route is `/classified/[slug]`, so a canonical built from
|
|
25
|
+
// `id` names a URL the router does not serve whenever the two differ.
|
|
26
|
+
const canonical = siteUrl ? `${siteUrl}/classified/${product.slug ?? product.id}` : undefined;
|
|
25
27
|
return {
|
|
26
28
|
title,
|
|
27
29
|
description,
|
|
@@ -14,7 +14,9 @@ export function buildDigitalCodeMetadata(product, opts) {
|
|
|
14
14
|
: product.description?.slice(0, 160) ?? "";
|
|
15
15
|
const image = opts?.ogImageUrl ?? (product.mainImage || product.images?.[0] || null);
|
|
16
16
|
const siteUrl = opts?.siteUrl?.replace(/\/+$/, "");
|
|
17
|
-
|
|
17
|
+
// Prefer `slug` — the route is `/digital-codes/[slug]`, so a canonical built
|
|
18
|
+
// from `id` names a URL the router does not serve whenever the two differ.
|
|
19
|
+
const canonical = siteUrl ? `${siteUrl}/digital-codes/${product.slug ?? product.id}` : undefined;
|
|
18
20
|
return {
|
|
19
21
|
title,
|
|
20
22
|
description,
|
|
@@ -17,7 +17,9 @@ export function buildLiveItemMetadata(product, opts) {
|
|
|
17
17
|
: product.description?.slice(0, 160) ?? "";
|
|
18
18
|
const image = opts?.ogImageUrl ?? (product.mainImage || product.images?.[0] || null);
|
|
19
19
|
const siteUrl = opts?.siteUrl?.replace(/\/+$/, "");
|
|
20
|
-
|
|
20
|
+
// Prefer `slug` — the route is `/live/[slug]`, so a canonical built from `id`
|
|
21
|
+
// names a URL the router does not serve whenever the two differ.
|
|
22
|
+
const canonical = siteUrl ? `${siteUrl}/live/${product.slug ?? product.id}` : undefined;
|
|
21
23
|
return {
|
|
22
24
|
title,
|
|
23
25
|
description,
|
|
@@ -16,6 +16,7 @@ import { wrapAction } from "@mohasinac/appkit/server";
|
|
|
16
16
|
*/
|
|
17
17
|
import { randomUUID } from "crypto";
|
|
18
18
|
import { getProviders } from "../../../../contracts/registry";
|
|
19
|
+
import { normalizeError } from "../../../../errors/normalize";
|
|
19
20
|
import { rupeesToPaise } from "../../../../providers/payment-razorpay/index";
|
|
20
21
|
import { orderRepository } from "../../../..";
|
|
21
22
|
import { ORDER_FIELDS } from "../../../../constants/field-names";
|
|
@@ -65,7 +66,7 @@ function revokeAvailableDigitalCodes(orderId, items) {
|
|
|
65
66
|
.limit(1)
|
|
66
67
|
.get()
|
|
67
68
|
.then((snap) => revokeFirstSnapDoc(snap))
|
|
68
|
-
.catch((e) => serverLogger.error("refund: code query failed", e));
|
|
69
|
+
.catch((e) => serverLogger.error("refund: code query failed", { error: normalizeError(e).message }));
|
|
69
70
|
}
|
|
70
71
|
}
|
|
71
72
|
function revokeFirstSnapDoc(snap) {
|
|
@@ -73,7 +74,7 @@ function revokeFirstSnapDoc(snap) {
|
|
|
73
74
|
return;
|
|
74
75
|
snap.docs[0].ref
|
|
75
76
|
.update({ status: "revoked", updatedAt: new Date() })
|
|
76
|
-
.catch((e) => serverLogger.error("refund: code revoke failed", e));
|
|
77
|
+
.catch((e) => serverLogger.error("refund: code revoke failed", { error: normalizeError(e).message }));
|
|
77
78
|
}
|
|
78
79
|
export async function processRefundAction(input) {
|
|
79
80
|
return wrapAction(async () => {
|
|
@@ -71,8 +71,9 @@ export async function deleteReviewAction(input) {
|
|
|
71
71
|
}
|
|
72
72
|
export async function markReviewHelpfulAction(reviewId) {
|
|
73
73
|
return wrapAction(async () => {
|
|
74
|
-
await requireRoleUser(["buyer", "seller", "admin"]);
|
|
74
|
+
const user = await requireRoleUser(["buyer", "seller", "admin"]);
|
|
75
75
|
await getReviewOrThrow(reviewId);
|
|
76
|
-
|
|
76
|
+
// Keyed on the voter so a repeated call is a no-op rather than another +1.
|
|
77
|
+
return reviewRepository.voteHelpful(reviewId, user.uid);
|
|
77
78
|
});
|
|
78
79
|
}
|
|
@@ -14,6 +14,50 @@ const PRODUCT_STATUS = "status";
|
|
|
14
14
|
const PRODUCT_STATUS_PUBLISHED = "published";
|
|
15
15
|
const PRODUCT_SLUG = "slug";
|
|
16
16
|
const PRODUCT_UPDATED_AT = "updatedAt";
|
|
17
|
+
/**
|
|
18
|
+
* Tester-sandbox fixtures must never reach the public sitemap. They are created
|
|
19
|
+
* and destroyed on a cycle by `testerSandboxCleanup` / `testerSandboxRefresh`,
|
|
20
|
+
* so anything Google indexes from them 404s shortly after. 34 of 182 sitemap
|
|
21
|
+
* URLs were disposable test fixtures before this filter existed.
|
|
22
|
+
*
|
|
23
|
+
* 🛑 This MUST be an in-memory filter, never a Firestore inequality on the
|
|
24
|
+
* field. An inequality silently excludes every document that does not have the
|
|
25
|
+
* field at all — which is every real product, store, category and post, since
|
|
26
|
+
* the flag is optional and was added later. Such a query returns ONLY test
|
|
27
|
+
* data. Same trap documented in CLAUDE.md § Tester QA Program.
|
|
28
|
+
*
|
|
29
|
+
* The field must therefore appear in each fetcher's `.select(...)` list, or it
|
|
30
|
+
* comes back undefined and nothing is filtered.
|
|
31
|
+
*
|
|
32
|
+
* The flag is not product-specific — it is set on categories, stores, blog
|
|
33
|
+
* posts and events too — but `PRODUCT_FIELDS` is where the canonical constant
|
|
34
|
+
* lives, so alias it rather than reintroducing a raw string.
|
|
35
|
+
*/
|
|
36
|
+
const TEST_DATA_FIELD = PRODUCT_FIELDS.IS_TEST_DATA;
|
|
37
|
+
const isTestDoc = (data) => data.isTestData === true;
|
|
38
|
+
/**
|
|
39
|
+
* A sitemap section that throws returns `[]` so a single broken query degrades
|
|
40
|
+
* the sitemap instead of 500-ing the whole route. That trade-off is right, but
|
|
41
|
+
* it is also why `fetchCategoryUrls` could return zero URLs for months without
|
|
42
|
+
* anyone noticing — it was logged at `warn` and looked identical to "this site
|
|
43
|
+
* genuinely has no categories".
|
|
44
|
+
*
|
|
45
|
+
* Log at `error`, and always name the section, so a section dropping to zero is
|
|
46
|
+
* greppable in production logs. `buildSitemap` additionally logs a per-section
|
|
47
|
+
* count digest on every build (see below), and `scripts/deploy.mjs` fails the
|
|
48
|
+
* deploy when any section is empty.
|
|
49
|
+
*
|
|
50
|
+
* NOTE: callers must still call `normalizeError(err)` themselves, in the catch
|
|
51
|
+
* block, BEFORE delegating here. `audit-catch-normalize` requires that call to
|
|
52
|
+
* be lexically inside the `catch` — a helper that normalises on the caller's
|
|
53
|
+
* behalf reads as an unnormalised catch and fails the audit. Keeping the call at
|
|
54
|
+
* the catch site is also what guarantees `unknown` never escapes into downstream
|
|
55
|
+
* logic, which is the rule's actual point.
|
|
56
|
+
*/
|
|
57
|
+
function sitemapSectionFailed(label, err) {
|
|
58
|
+
serverLogger.error(`sitemap: section "${label}" failed and returned 0 URLs`, { section: label, error: err });
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
17
61
|
function staticPages(baseUrl) {
|
|
18
62
|
const page = (path, changeFreq, priority) => ({ url: `${baseUrl}${path}`, lastModified: new Date(), changeFrequency: changeFreq, priority });
|
|
19
63
|
return [
|
|
@@ -70,10 +114,12 @@ async function fetchProductUrls(baseUrl) {
|
|
|
70
114
|
.collection(PRODUCT_COLLECTION)
|
|
71
115
|
.where(PRODUCT_STATUS, "==", PRODUCT_STATUS_PUBLISHED)
|
|
72
116
|
.where("listingType", "in", PRODUCT_DETAIL_LISTING_TYPES)
|
|
73
|
-
.select(PRODUCT_SLUG, PRODUCT_UPDATED_AT)
|
|
117
|
+
.select(PRODUCT_SLUG, PRODUCT_UPDATED_AT, TEST_DATA_FIELD)
|
|
74
118
|
.limit(5000)
|
|
75
119
|
.get();
|
|
76
|
-
return snap.docs
|
|
120
|
+
return snap.docs
|
|
121
|
+
.filter((doc) => !isTestDoc(doc.data()))
|
|
122
|
+
.map((doc) => {
|
|
77
123
|
const data = doc.data();
|
|
78
124
|
const slug = data[PRODUCT_SLUG] ?? doc.id;
|
|
79
125
|
return {
|
|
@@ -86,8 +132,7 @@ async function fetchProductUrls(baseUrl) {
|
|
|
86
132
|
}
|
|
87
133
|
catch (err) {
|
|
88
134
|
void normalizeError(err);
|
|
89
|
-
|
|
90
|
-
return [];
|
|
135
|
+
return sitemapSectionFailed("product", err);
|
|
91
136
|
}
|
|
92
137
|
}
|
|
93
138
|
async function fetchListingTypeUrls(baseUrl, listingType, buildUrl, label) {
|
|
@@ -97,10 +142,12 @@ async function fetchListingTypeUrls(baseUrl, listingType, buildUrl, label) {
|
|
|
97
142
|
.collection(PRODUCT_COLLECTION)
|
|
98
143
|
.where(PRODUCT_STATUS, "==", PRODUCT_STATUS_PUBLISHED)
|
|
99
144
|
.where("listingType", "==", listingType)
|
|
100
|
-
.select(PRODUCT_SLUG, PRODUCT_UPDATED_AT)
|
|
145
|
+
.select(PRODUCT_SLUG, PRODUCT_UPDATED_AT, TEST_DATA_FIELD)
|
|
101
146
|
.limit(2000)
|
|
102
147
|
.get();
|
|
103
|
-
return snap.docs
|
|
148
|
+
return snap.docs
|
|
149
|
+
.filter((doc) => !isTestDoc(doc.data()))
|
|
150
|
+
.map((doc) => {
|
|
104
151
|
const data = doc.data();
|
|
105
152
|
const slug = data[PRODUCT_SLUG] ?? doc.id;
|
|
106
153
|
return {
|
|
@@ -113,8 +160,7 @@ async function fetchListingTypeUrls(baseUrl, listingType, buildUrl, label) {
|
|
|
113
160
|
}
|
|
114
161
|
catch (err) {
|
|
115
162
|
void normalizeError(err);
|
|
116
|
-
|
|
117
|
-
return [];
|
|
163
|
+
return sitemapSectionFailed(label, err);
|
|
118
164
|
}
|
|
119
165
|
}
|
|
120
166
|
const fetchPreOrderUrls = (baseUrl) => fetchListingTypeUrls(baseUrl, "pre-order", (slug) => ROUTES.PUBLIC.PRE_ORDER_DETAIL(slug), "pre-order");
|
|
@@ -129,10 +175,12 @@ async function fetchAuctionUrls(baseUrl) {
|
|
|
129
175
|
.collection(PRODUCT_COLLECTION)
|
|
130
176
|
.where(PRODUCT_STATUS, "==", PRODUCT_STATUS_PUBLISHED)
|
|
131
177
|
.where("listingType", "==", "auction")
|
|
132
|
-
.select(PRODUCT_SLUG, PRODUCT_UPDATED_AT)
|
|
178
|
+
.select(PRODUCT_SLUG, PRODUCT_UPDATED_AT, TEST_DATA_FIELD)
|
|
133
179
|
.limit(2000)
|
|
134
180
|
.get();
|
|
135
|
-
return snap.docs
|
|
181
|
+
return snap.docs
|
|
182
|
+
.filter((doc) => !isTestDoc(doc.data()))
|
|
183
|
+
.map((doc) => {
|
|
136
184
|
const data = doc.data();
|
|
137
185
|
const slug = data[PRODUCT_SLUG] ?? doc.id;
|
|
138
186
|
return {
|
|
@@ -145,8 +193,7 @@ async function fetchAuctionUrls(baseUrl) {
|
|
|
145
193
|
}
|
|
146
194
|
catch (err) {
|
|
147
195
|
void normalizeError(err);
|
|
148
|
-
|
|
149
|
-
return [];
|
|
196
|
+
return sitemapSectionFailed("auction", err);
|
|
150
197
|
}
|
|
151
198
|
}
|
|
152
199
|
async function fetchEventUrls(baseUrl) {
|
|
@@ -155,10 +202,12 @@ async function fetchEventUrls(baseUrl) {
|
|
|
155
202
|
const snap = await db
|
|
156
203
|
.collection(EVENTS_COLLECTION)
|
|
157
204
|
.where(EVENT_FIELDS.STATUS, "==", EVENT_FIELDS.STATUS_VALUES.ACTIVE)
|
|
158
|
-
.select(EVENT_FIELDS.UPDATED_AT, EVENT_FIELDS.SLUG)
|
|
205
|
+
.select(EVENT_FIELDS.UPDATED_AT, EVENT_FIELDS.SLUG, TEST_DATA_FIELD)
|
|
159
206
|
.limit(500)
|
|
160
207
|
.get();
|
|
161
|
-
return snap.docs
|
|
208
|
+
return snap.docs
|
|
209
|
+
.filter((doc) => !isTestDoc(doc.data()))
|
|
210
|
+
.map((doc) => {
|
|
162
211
|
const data = doc.data();
|
|
163
212
|
const slug = typeof data[EVENT_FIELDS.SLUG] === "string" ? data[EVENT_FIELDS.SLUG] : undefined;
|
|
164
213
|
return {
|
|
@@ -171,15 +220,25 @@ async function fetchEventUrls(baseUrl) {
|
|
|
171
220
|
}
|
|
172
221
|
catch (err) {
|
|
173
222
|
void normalizeError(err);
|
|
174
|
-
|
|
175
|
-
return [];
|
|
223
|
+
return sitemapSectionFailed("event", err);
|
|
176
224
|
}
|
|
177
225
|
}
|
|
178
|
-
// categories is a discriminated-union collection
|
|
179
|
-
//
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
//
|
|
226
|
+
// `categories` is a discriminated-union collection. Each discriminator renders
|
|
227
|
+
// at a different public route, so each needs its own filtered query; querying
|
|
228
|
+
// the collection without a categoryType filter (an older bug here) mapped
|
|
229
|
+
// brand/bundle rows onto the wrong /categories/{slug} URL.
|
|
230
|
+
//
|
|
231
|
+
// 🛑 The union is `"category" | "sublisting" | "brand" | "bundle"` — see
|
|
232
|
+
// CategoryType in features/categories/types/index.ts. There is NO "listing"
|
|
233
|
+
// value and there never was. This function used to be called with "listing"
|
|
234
|
+
// for the plain-category case, which matched zero documents, so ~47 category
|
|
235
|
+
// pages were missing from the sitemap for as long as that call existed — with
|
|
236
|
+
// no error, because a zero-row query is indistinguishable from "no categories".
|
|
237
|
+
//
|
|
238
|
+
// It is still only safe to use this helper for discriminators that are actually
|
|
239
|
+
// STORED. A plain listing category OMITS `categoryType` entirely (that is the
|
|
240
|
+
// convention every seeded listing category follows), so no equality can select
|
|
241
|
+
// it — see fetchCategoryUrls below, which inverts the test instead.
|
|
183
242
|
async function fetchCategoryTypeUrls(baseUrl, categoryType, buildUrl, label) {
|
|
184
243
|
try {
|
|
185
244
|
const db = getAdminDb();
|
|
@@ -187,10 +246,12 @@ async function fetchCategoryTypeUrls(baseUrl, categoryType, buildUrl, label) {
|
|
|
187
246
|
.collection(CATEGORIES_COLLECTION)
|
|
188
247
|
.where(CATEGORY_FIELDS.CATEGORY_TYPE, "==", categoryType)
|
|
189
248
|
.where(CATEGORY_FIELDS.IS_ACTIVE, "==", true)
|
|
190
|
-
.select(CATEGORY_FIELDS.SLUG, CATEGORY_FIELDS.UPDATED_AT)
|
|
249
|
+
.select(CATEGORY_FIELDS.SLUG, CATEGORY_FIELDS.UPDATED_AT, TEST_DATA_FIELD)
|
|
191
250
|
.limit(500)
|
|
192
251
|
.get();
|
|
193
|
-
return snap.docs
|
|
252
|
+
return snap.docs
|
|
253
|
+
.filter((doc) => !isTestDoc(doc.data()))
|
|
254
|
+
.map((doc) => {
|
|
194
255
|
const data = doc.data();
|
|
195
256
|
const slug = data[CATEGORY_FIELDS.SLUG] ?? doc.id;
|
|
196
257
|
return {
|
|
@@ -203,11 +264,57 @@ async function fetchCategoryTypeUrls(baseUrl, categoryType, buildUrl, label) {
|
|
|
203
264
|
}
|
|
204
265
|
catch (err) {
|
|
205
266
|
void normalizeError(err);
|
|
206
|
-
|
|
207
|
-
|
|
267
|
+
return sitemapSectionFailed(label, err);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Plain listing categories — the /categories/{slug} pages.
|
|
272
|
+
*
|
|
273
|
+
* These CANNOT be selected by an equality on `categoryType`, because a plain
|
|
274
|
+
* listing category omits the field entirely rather than storing "category".
|
|
275
|
+
* `.where(CATEGORY_TYPE, "==", <anything>)` therefore excludes every one of
|
|
276
|
+
* them, and a Firestore `!=` would be worse still — it silently drops documents
|
|
277
|
+
* that lack the field, which is exactly the set we want.
|
|
278
|
+
*
|
|
279
|
+
* So: select on `isActive` alone (the one predicate that IS stored on every
|
|
280
|
+
* row) and reject the three discriminated kinds in memory. `categoryType` must
|
|
281
|
+
* stay in the `.select()` list or it comes back undefined and nothing is
|
|
282
|
+
* rejected.
|
|
283
|
+
*/
|
|
284
|
+
const NON_LISTING_CATEGORY_TYPES = new Set(["brand", "bundle", "sublisting"]);
|
|
285
|
+
async function fetchCategoryUrls(baseUrl) {
|
|
286
|
+
try {
|
|
287
|
+
const db = getAdminDb();
|
|
288
|
+
const snap = await db
|
|
289
|
+
.collection(CATEGORIES_COLLECTION)
|
|
290
|
+
.where(CATEGORY_FIELDS.IS_ACTIVE, "==", true)
|
|
291
|
+
.select(CATEGORY_FIELDS.SLUG, CATEGORY_FIELDS.UPDATED_AT, CATEGORY_FIELDS.CATEGORY_TYPE, TEST_DATA_FIELD)
|
|
292
|
+
.limit(500)
|
|
293
|
+
.get();
|
|
294
|
+
return snap.docs
|
|
295
|
+
.filter((doc) => {
|
|
296
|
+
const data = doc.data();
|
|
297
|
+
if (isTestDoc(data))
|
|
298
|
+
return false;
|
|
299
|
+
const kind = data[CATEGORY_FIELDS.CATEGORY_TYPE];
|
|
300
|
+
return typeof kind !== "string" || !NON_LISTING_CATEGORY_TYPES.has(kind);
|
|
301
|
+
})
|
|
302
|
+
.map((doc) => {
|
|
303
|
+
const data = doc.data();
|
|
304
|
+
const slug = data[CATEGORY_FIELDS.SLUG] ?? doc.id;
|
|
305
|
+
return {
|
|
306
|
+
url: `${baseUrl}${ROUTES.PUBLIC.CATEGORY_DETAIL(slug)}`,
|
|
307
|
+
lastModified: data[CATEGORY_FIELDS.UPDATED_AT]?.toDate?.() ?? new Date(),
|
|
308
|
+
changeFrequency: "weekly",
|
|
309
|
+
priority: 0.7,
|
|
310
|
+
};
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
catch (err) {
|
|
314
|
+
void normalizeError(err);
|
|
315
|
+
return sitemapSectionFailed("category", err);
|
|
208
316
|
}
|
|
209
317
|
}
|
|
210
|
-
const fetchCategoryUrls = (baseUrl) => fetchCategoryTypeUrls(baseUrl, "listing", (slug) => ROUTES.PUBLIC.CATEGORY_DETAIL(slug), "category");
|
|
211
318
|
const fetchBrandUrls = (baseUrl) => fetchCategoryTypeUrls(baseUrl, "brand", (slug) => ROUTES.PUBLIC.BRAND_DETAIL(slug), "brand");
|
|
212
319
|
const fetchBundleUrls = (baseUrl) => fetchCategoryTypeUrls(baseUrl, "bundle", (slug) => ROUTES.PUBLIC.BUNDLE_DETAIL(slug), "bundle");
|
|
213
320
|
/**
|
|
@@ -228,7 +335,9 @@ async function fetchGroupedListingUrls(baseUrl) {
|
|
|
228
335
|
}
|
|
229
336
|
catch (err) {
|
|
230
337
|
void normalizeError(err);
|
|
231
|
-
|
|
338
|
+
// This catch used to swallow silently — no log at all, so a failure here was
|
|
339
|
+
// indistinguishable from "this site has no grouped listings".
|
|
340
|
+
return sitemapSectionFailed("grouped-listing", err);
|
|
232
341
|
}
|
|
233
342
|
}
|
|
234
343
|
async function fetchBlogPostUrls(baseUrl) {
|
|
@@ -237,10 +346,12 @@ async function fetchBlogPostUrls(baseUrl) {
|
|
|
237
346
|
const snap = await db
|
|
238
347
|
.collection(BLOG_POSTS_COLLECTION)
|
|
239
348
|
.where(BLOG_POST_FIELDS.STATUS, "==", BLOG_POST_FIELDS.STATUS_VALUES.PUBLISHED)
|
|
240
|
-
.select(BLOG_POST_FIELDS.SLUG, BLOG_POST_FIELDS.PUBLISHED_AT, BLOG_POST_FIELDS.UPDATED_AT)
|
|
349
|
+
.select(BLOG_POST_FIELDS.SLUG, BLOG_POST_FIELDS.PUBLISHED_AT, BLOG_POST_FIELDS.UPDATED_AT, TEST_DATA_FIELD)
|
|
241
350
|
.limit(1000)
|
|
242
351
|
.get();
|
|
243
|
-
return snap.docs
|
|
352
|
+
return snap.docs
|
|
353
|
+
.filter((doc) => !isTestDoc(doc.data()))
|
|
354
|
+
.map((doc) => {
|
|
244
355
|
const data = doc.data();
|
|
245
356
|
const slug = data[BLOG_POST_FIELDS.SLUG] ?? doc.id;
|
|
246
357
|
const lastModified = data[BLOG_POST_FIELDS.UPDATED_AT]?.toDate?.() ??
|
|
@@ -256,8 +367,7 @@ async function fetchBlogPostUrls(baseUrl) {
|
|
|
256
367
|
}
|
|
257
368
|
catch (err) {
|
|
258
369
|
void normalizeError(err);
|
|
259
|
-
|
|
260
|
-
return [];
|
|
370
|
+
return sitemapSectionFailed("blog-post", err);
|
|
261
371
|
}
|
|
262
372
|
}
|
|
263
373
|
async function fetchStoreUrls(baseUrl) {
|
|
@@ -267,10 +377,12 @@ async function fetchStoreUrls(baseUrl) {
|
|
|
267
377
|
.collection(STORE_COLLECTION)
|
|
268
378
|
.where(STORE_FIELDS.STATUS, "==", STORE_FIELDS.STATUS_VALUES.ACTIVE)
|
|
269
379
|
.where(STORE_FIELDS.IS_PUBLIC, "==", true)
|
|
270
|
-
.select(STORE_FIELDS.STORE_SLUG, STORE_FIELDS.UPDATED_AT)
|
|
380
|
+
.select(STORE_FIELDS.STORE_SLUG, STORE_FIELDS.UPDATED_AT, TEST_DATA_FIELD)
|
|
271
381
|
.limit(1000)
|
|
272
382
|
.get();
|
|
273
|
-
return snap.docs
|
|
383
|
+
return snap.docs
|
|
384
|
+
.filter((doc) => !isTestDoc(doc.data()))
|
|
385
|
+
.map((doc) => {
|
|
274
386
|
const data = doc.data();
|
|
275
387
|
const slug = data[STORE_FIELDS.STORE_SLUG] ?? doc.id;
|
|
276
388
|
return {
|
|
@@ -283,8 +395,7 @@ async function fetchStoreUrls(baseUrl) {
|
|
|
283
395
|
}
|
|
284
396
|
catch (err) {
|
|
285
397
|
void normalizeError(err);
|
|
286
|
-
|
|
287
|
-
return [];
|
|
398
|
+
return sitemapSectionFailed("store", err);
|
|
288
399
|
}
|
|
289
400
|
}
|
|
290
401
|
async function fetchScammerUrls(baseUrl) {
|
|
@@ -293,10 +404,12 @@ async function fetchScammerUrls(baseUrl) {
|
|
|
293
404
|
const snap = await db
|
|
294
405
|
.collection(SCAMMER_COLLECTION)
|
|
295
406
|
.where(PRODUCT_FIELDS.STATUS, "==", "verified")
|
|
296
|
-
.select("seo", "updatedAt")
|
|
407
|
+
.select("seo", "updatedAt", TEST_DATA_FIELD)
|
|
297
408
|
.limit(2000)
|
|
298
409
|
.get();
|
|
299
|
-
return snap.docs
|
|
410
|
+
return snap.docs
|
|
411
|
+
.filter((doc) => !isTestDoc(doc.data()))
|
|
412
|
+
.map((doc) => {
|
|
300
413
|
const data = doc.data();
|
|
301
414
|
const slug = data.seo?.slug ?? doc.id;
|
|
302
415
|
return {
|
|
@@ -309,8 +422,7 @@ async function fetchScammerUrls(baseUrl) {
|
|
|
309
422
|
}
|
|
310
423
|
catch (err) {
|
|
311
424
|
void normalizeError(err);
|
|
312
|
-
|
|
313
|
-
return [];
|
|
425
|
+
return sitemapSectionFailed("scammer", err);
|
|
314
426
|
}
|
|
315
427
|
}
|
|
316
428
|
export async function buildSitemap({ baseUrl }) {
|
|
@@ -331,8 +443,45 @@ export async function buildSitemap({ baseUrl }) {
|
|
|
331
443
|
fetchStoreUrls(baseUrl),
|
|
332
444
|
fetchScammerUrls(baseUrl),
|
|
333
445
|
]);
|
|
446
|
+
const staticUrls = staticPages(baseUrl);
|
|
447
|
+
// Per-section count digest. A section that drops to zero is otherwise
|
|
448
|
+
// indistinguishable from a site that genuinely has none of that thing — which
|
|
449
|
+
// is exactly how `category: 0` survived unnoticed while every other section
|
|
450
|
+
// worked (the query filtered on a `categoryType` value that does not exist).
|
|
451
|
+
//
|
|
452
|
+
// `scripts/deploy.mjs` asserts the same counts against the deployed sitemap
|
|
453
|
+
// and fails the deploy on an empty section; this log is the production-side
|
|
454
|
+
// record for when a section empties out later, between deploys.
|
|
455
|
+
const sections = {
|
|
456
|
+
static: staticUrls.length,
|
|
457
|
+
category: categoryUrls.length,
|
|
458
|
+
brand: brandUrls.length,
|
|
459
|
+
bundle: bundleUrls.length,
|
|
460
|
+
group: groupedListingUrls.length,
|
|
461
|
+
blog: blogUrls.length,
|
|
462
|
+
product: productUrls.length,
|
|
463
|
+
auction: auctionUrls.length,
|
|
464
|
+
preOrder: preOrderUrls.length,
|
|
465
|
+
prizeDraw: prizeDrawUrls.length,
|
|
466
|
+
classified: classifiedUrls.length,
|
|
467
|
+
digitalCode: digitalCodeUrls.length,
|
|
468
|
+
live: liveUrls.length,
|
|
469
|
+
event: eventUrls.length,
|
|
470
|
+
store: storeUrls.length,
|
|
471
|
+
scammer: scammerUrls.length,
|
|
472
|
+
};
|
|
473
|
+
const empty = Object.entries(sections)
|
|
474
|
+
.filter(([, n]) => n === 0)
|
|
475
|
+
.map(([name]) => name);
|
|
476
|
+
const total = Object.values(sections).reduce((a, b) => a + b, 0);
|
|
477
|
+
if (empty.length > 0) {
|
|
478
|
+
serverLogger.warn(`sitemap: ${empty.length} section(s) produced 0 URLs`, { empty, sections, total });
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
serverLogger.info("sitemap: built", { sections, total });
|
|
482
|
+
}
|
|
334
483
|
return [
|
|
335
|
-
...
|
|
484
|
+
...staticUrls,
|
|
336
485
|
...categoryUrls,
|
|
337
486
|
...brandUrls,
|
|
338
487
|
...bundleUrls,
|