@mohasinac/appkit 4.42.18 → 4.44.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/LotteryDetailView.js +22 -1
- package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
- package/dist/_internal/server/features/categories/adapters.js +235 -0
- package/dist/_internal/server/features/checkout/actions.js +161 -22
- package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/products/list-public.d.ts +24 -0
- package/dist/_internal/server/features/products/list-public.js +10 -2
- package/dist/_internal/server/features/refunds/actions.js +1 -1
- package/dist/_internal/server/features/seo/sitemap.js +54 -15
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +6 -0
- package/dist/_internal/server/functions/scheduled.d.ts +4 -2
- package/dist/_internal/server/functions/scheduled.js +64 -6
- package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
- package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
- package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +59 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +31 -0
- package/dist/_internal/server/jobs/core/onReviewWrite.js +8 -0
- package/dist/_internal/server/jobs/core/onStoreWrite.d.ts +1 -1
- package/dist/_internal/server/jobs/core/onStoreWrite.js +8 -2
- package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.d.ts +28 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.js +125 -0
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
- package/dist/_internal/server/jobs/handlers/index.js +3 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
- package/dist/_internal/server/jobs/handlers/messages.js +5 -2
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.js +34 -0
- package/dist/_internal/shared/cache/revalidate-targets.d.ts +49 -0
- package/dist/_internal/shared/cache/revalidate-targets.js +157 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
- package/dist/_internal/shared/checkout/order-math.js +194 -7
- package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
- package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
- package/dist/_internal/shared/fees/calculator.d.ts +105 -0
- package/dist/_internal/shared/fees/calculator.js +143 -0
- package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
- package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
- package/dist/_internal/shared/templating/placeholders.js +73 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/admin/actions/admin-actions.d.ts +1 -1
- package/dist/features/admin/actions/admin-actions.js +1 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
- package/dist/features/admin/components/AdminProductEditorView.js +8 -1
- package/dist/features/admin/components/AdminProductsView.js +78 -7
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
- package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
- package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
- package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
- package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
- package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +10 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
- package/dist/features/auctions/components/AuctionLiveStatusBadge.d.ts +20 -0
- package/dist/features/auctions/components/AuctionLiveStatusBadge.js +21 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +20 -2
- package/dist/features/auctions/hooks/useIsAuctionEnded.d.ts +41 -0
- package/dist/features/auctions/hooks/useIsAuctionEnded.js +76 -0
- package/dist/features/auth/schemas/firestore.d.ts +0 -1
- package/dist/features/blog/api/route.js +16 -1
- package/dist/features/blog/schemas/firestore.d.ts +2 -2
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
- package/dist/features/categories/actions/category-actions.d.ts +2 -2
- package/dist/features/categories/actions/category-actions.js +22 -10
- package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
- package/dist/features/categories/api/[id]/description-template/route.js +252 -0
- package/dist/features/categories/api/route.js +156 -11
- package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -3
- package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +27 -1
- package/dist/features/categories/components/CategoriesIndexPageView.js +24 -33
- package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
- package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
- package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
- package/dist/features/categories/constants/listing-categories.d.ts +17 -0
- package/dist/features/categories/constants/listing-categories.js +53 -0
- package/dist/features/categories/hooks/useCategoryTree.js +28 -2
- package/dist/features/categories/repository/categories.repository.d.ts +73 -12
- package/dist/features/categories/repository/categories.repository.js +203 -42
- package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
- package/dist/features/categories/repository/price-index.repository.js +85 -0
- package/dist/features/categories/schemas/category-content.d.ts +206 -0
- package/dist/features/categories/schemas/category-content.js +28 -0
- package/dist/features/categories/schemas/firestore.d.ts +74 -5
- package/dist/features/categories/schemas/firestore.js +74 -3
- package/dist/features/categories/schemas/index.d.ts +1047 -0
- package/dist/features/categories/schemas/index.js +96 -0
- package/dist/features/categories/server.d.ts +1 -0
- package/dist/features/categories/server.js +7 -0
- package/dist/features/categories/types/index.d.ts +9 -0
- package/dist/features/events/schemas/firestore.d.ts +2 -2
- package/dist/features/faq/schemas/firestore.d.ts +2 -1
- package/dist/features/faq/schemas/firestore.js +20 -8
- package/dist/features/faq/utils/getHomepageFaqsCached.d.ts +20 -0
- package/dist/features/faq/utils/getHomepageFaqsCached.js +22 -0
- package/dist/features/homepage/components/MarketplaceHomepageView.js +13 -3
- package/dist/features/homepage/lib/live-stats.js +18 -8
- package/dist/features/media/MediaImage.js +19 -2
- package/dist/features/orders/schemas/firestore.d.ts +70 -0
- package/dist/features/orders/types/index.d.ts +55 -0
- package/dist/features/products/actions/product-actions.d.ts +0 -9
- package/dist/features/products/actions/product-actions.js +25 -0
- package/dist/features/products/columns/index.d.ts +0 -1
- package/dist/features/products/columns/index.js +5 -1
- package/dist/features/products/components/ArtStickersListView.js +7 -1
- package/dist/features/products/components/PrizeDrawsSection.js +5 -0
- package/dist/features/products/components/ProductFilters.d.ts +0 -9
- package/dist/features/products/components/ProductForm.d.ts +27 -1
- package/dist/features/products/components/ProductForm.js +26 -12
- package/dist/features/products/components/ProductsIndexPageView.js +1 -1
- package/dist/features/products/constants/sieve.d.ts +0 -6
- package/dist/features/products/constants/sieve.js +35 -2
- package/dist/features/products/repository/products.repository.d.ts +115 -1
- package/dist/features/products/repository/products.repository.js +491 -39
- package/dist/features/products/schemas/firestore.d.ts +65 -5
- package/dist/features/products/schemas/firestore.js +9 -0
- package/dist/features/products/schemas/product-features.d.ts +140 -2
- package/dist/features/products/schemas/product-features.js +62 -1
- package/dist/features/promotions/repository/coupons.repository.js +6 -1
- package/dist/features/promotions/schemas/firestore.d.ts +44 -1
- package/dist/features/promotions/schemas/firestore.js +76 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +22 -0
- package/dist/features/reviews/repository/reviews.repository.js +35 -0
- package/dist/features/seller/actions/seller-actions.js +17 -2
- package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
- package/dist/features/seller/components/PrintCenterView.js +3 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
- package/dist/features/seller/components/SellerOrdersView.js +6 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
- package/dist/features/seller/components/SellerProductShell.js +101 -9
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +108 -3
- package/dist/features/seller/types/index.d.ts +17 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
- package/dist/features/seller/utils/product-draft-mapping.js +17 -0
- package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
- package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
- package/dist/features/stores/schemas/firestore.d.ts +31 -2
- package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
- package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
- package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
- package/dist/features/tax-codes/schemas/firestore.js +82 -0
- package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
- package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
- package/dist/features/tester/actions/index.d.ts +0 -1
- package/dist/features/tester/actions/index.js +0 -1
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
- package/dist/features/tester/components/index.d.ts +0 -4
- package/dist/features/tester/components/index.js +0 -2
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
- package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
- package/dist/features/tester/schemas/firestore.d.ts +0 -6
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
- package/dist/features/tester/seed-data/authored/index.js +0 -2
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +10 -29
- package/dist/index.d.ts +21 -7
- package/dist/index.js +53 -7
- package/dist/next/routing/route-map.d.ts +2 -4
- package/dist/next/routing/route-map.js +1 -2
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +506 -0
- package/dist/seed/_helpers/brand-rows.d.ts +35 -0
- package/dist/seed/_helpers/brand-rows.js +576 -0
- package/dist/seed/_helpers/category-content.d.ts +39 -0
- package/dist/seed/_helpers/category-content.js +634 -0
- package/dist/seed/_helpers/category-forest.d.ts +2 -0
- package/dist/seed/_helpers/category-forest.js +585 -243
- package/dist/seed/_helpers/category-models.d.ts +5 -0
- package/dist/seed/_helpers/category-models.js +1364 -0
- package/dist/seed/_helpers/category-tree.js +17 -1
- package/dist/seed/_helpers/character-features.d.ts +10 -0
- package/dist/seed/_helpers/character-features.js +118 -0
- package/dist/seed/_helpers/media.d.ts +1 -1
- package/dist/seed/_helpers/media.js +44 -8
- package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
- package/dist/seed/categories-seed-data.js +147 -169
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/manifest.d.ts +1 -1
- package/dist/seed/manifest.js +5 -0
- package/dist/seed/orders-seed-data.js +5 -3
- package/dist/seed/product-features-seed-data.d.ts +1 -8
- package/dist/seed/product-features-seed-data.js +358 -154
- package/dist/seed/products-art-seed-data.js +16 -4
- package/dist/seed/products-auctions-seed-data.js +6 -6
- package/dist/seed/products-classifieds-seed-data.js +17 -7
- package/dist/seed/products-digital-codes-seed-data.js +12 -12
- package/dist/seed/products-preorders-seed-data.js +8 -8
- package/dist/seed/products-prize-draws-seed-data.js +25 -8
- package/dist/seed/products-standard-seed-data.js +10 -60
- package/dist/seed/products-stickers-seed-data.js +6 -6
- package/dist/seed/site-settings-seed-data.js +24 -0
- package/dist/seed/store-extensions-seed-data.js +10 -4
- package/dist/seed/tax-codes-seed-data.d.ts +34 -0
- package/dist/seed/tax-codes-seed-data.js +100 -0
- package/dist/seed/test-data-ttl.d.ts +29 -0
- package/dist/seed/test-data-ttl.js +31 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +6 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.js +11 -2
- package/dist/styles.css +40 -0
- package/dist/ui/components/PaginatedSelect.d.ts +14 -0
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/PaginatedSelect.style.css +40 -0
- package/dist/utils/media-url.d.ts +26 -0
- package/dist/utils/media-url.js +144 -0
- package/dist/utils/physical-location.d.ts +23 -0
- package/dist/utils/physical-location.js +52 -0
- package/dist/utils/search-txt-builders.d.ts +20 -0
- package/dist/utils/search-txt-builders.js +36 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -1
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
- package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
- package/dist/features/tester/actions/leaderboard-actions.js +0 -5
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
- package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
- package/dist/features/tester/components/TesterHubView.d.ts +0 -5
- package/dist/features/tester/components/TesterHubView.js +0 -110
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
- package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
- package/dist/features/tester/seed-data/tester-ttl.js +0 -6
|
@@ -47,6 +47,97 @@ export const bundleQueryRuleSchema = z.discriminatedUnion("type", [
|
|
|
47
47
|
limit: z.number().int().positive(),
|
|
48
48
|
}),
|
|
49
49
|
]);
|
|
50
|
+
/*
|
|
51
|
+
* ── Category-owned content (B3), mirrored here in B5 ────────────────────────
|
|
52
|
+
*
|
|
53
|
+
* 🛑 These three were added to `CategoryDocument` in B3 and NOT to this
|
|
54
|
+
* schema, which is the drift this file has a history of: `highlights` and
|
|
55
|
+
* `faqs` sat in the interface with no mirror for months, and nothing enforces
|
|
56
|
+
* parity between the two. Mirroring them is what lets the admin category
|
|
57
|
+
* editor (C2) validate a template body server-side instead of trusting the
|
|
58
|
+
* form.
|
|
59
|
+
*/
|
|
60
|
+
export const categoryDescriptionTemplateSchema = z.object({
|
|
61
|
+
id: z.string(),
|
|
62
|
+
label: z.string(),
|
|
63
|
+
variant: z.enum(["default", "new_in_box", "new_in_packet", "pre_owned", "replica"]),
|
|
64
|
+
body: z.string(),
|
|
65
|
+
/** Absent means "re-parse", never "no slots" — see the field's own doc. */
|
|
66
|
+
placeholders: z.array(z.string()).optional(),
|
|
67
|
+
isTested: z.boolean().optional(),
|
|
68
|
+
version: z.number().int().optional(),
|
|
69
|
+
});
|
|
70
|
+
export const categorySpecificationSchema = z.object({
|
|
71
|
+
name: z.string(),
|
|
72
|
+
value: z.string(),
|
|
73
|
+
unit: z.string().optional(),
|
|
74
|
+
});
|
|
75
|
+
export const categoryPriceGuidanceSchema = z.object({
|
|
76
|
+
currency: z.literal("INR"),
|
|
77
|
+
market: z
|
|
78
|
+
.object({
|
|
79
|
+
median: rupeesSchema,
|
|
80
|
+
// Optional on purpose — see the field docs in category-content.ts. The
|
|
81
|
+
// crawl measured quartiles for some verticals and a median-plus-range
|
|
82
|
+
// for others, and a required p25 would force a range to be retyped as a
|
|
83
|
+
// quartile.
|
|
84
|
+
p25: rupeesSchema.optional(),
|
|
85
|
+
p75: rupeesSchema.optional(),
|
|
86
|
+
sampleSize: z.number().int().positive().optional(),
|
|
87
|
+
asOf: firestoreDateSchema,
|
|
88
|
+
soldOutShare: z.number().min(0).max(1).optional(),
|
|
89
|
+
source: z.string().optional(),
|
|
90
|
+
})
|
|
91
|
+
.optional(),
|
|
92
|
+
/*
|
|
93
|
+
* 🛑 No `.default()` anywhere in here. An absent `sold` means "we have no
|
|
94
|
+
* sales yet" and a zeroed one would render as "things sell here for ₹0" —
|
|
95
|
+
* which is worse than silence, and is exactly what a defaulting schema would
|
|
96
|
+
* manufacture on the first quiet night.
|
|
97
|
+
*/
|
|
98
|
+
sold: z
|
|
99
|
+
.object({
|
|
100
|
+
min: rupeesSchema,
|
|
101
|
+
median: rupeesSchema,
|
|
102
|
+
max: rupeesSchema,
|
|
103
|
+
sampleSize: z.number().int().positive(),
|
|
104
|
+
windowDays: z.number().int().positive(),
|
|
105
|
+
byPath: z
|
|
106
|
+
.object({
|
|
107
|
+
standard: z.number().int().nonnegative().optional(),
|
|
108
|
+
auction: z.number().int().nonnegative().optional(),
|
|
109
|
+
offer: z.number().int().nonnegative().optional(),
|
|
110
|
+
})
|
|
111
|
+
.optional(),
|
|
112
|
+
})
|
|
113
|
+
.optional(),
|
|
114
|
+
lastSold: z
|
|
115
|
+
.object({
|
|
116
|
+
amount: rupeesSchema,
|
|
117
|
+
at: firestoreDateSchema,
|
|
118
|
+
via: z.enum(["standard", "auction", "offer"]).optional(),
|
|
119
|
+
})
|
|
120
|
+
.optional(),
|
|
121
|
+
});
|
|
122
|
+
export const categoryProductDefaultsSchema = z.object({
|
|
123
|
+
taxCodeId: z.string().optional(),
|
|
124
|
+
specifications: z.array(categorySpecificationSchema).optional(),
|
|
125
|
+
defaultFeatures: z.array(z.string()).optional(),
|
|
126
|
+
defaultCondition: z.string().optional(),
|
|
127
|
+
defaultAuthenticity: z.enum(["original", "reproduction", "unverified"]).optional(),
|
|
128
|
+
inTheBox: z.array(z.string()).optional(),
|
|
129
|
+
priceGuidance: categoryPriceGuidanceSchema.optional(),
|
|
130
|
+
seoTitleTemplate: z.string().optional(),
|
|
131
|
+
seoDescriptionTemplate: z.string().optional(),
|
|
132
|
+
seoKeywords: z.array(z.string()).optional(),
|
|
133
|
+
weightG: z.number().nonnegative().optional(),
|
|
134
|
+
dimensionsCm: z
|
|
135
|
+
.object({ l: z.number().positive(), w: z.number().positive(), h: z.number().positive() })
|
|
136
|
+
.optional(),
|
|
137
|
+
shippingPaidBy: z.string().optional(),
|
|
138
|
+
fragile: z.boolean().optional(),
|
|
139
|
+
faqTemplates: z.array(z.object({ question: z.string(), answer: z.string() })).optional(),
|
|
140
|
+
});
|
|
50
141
|
export const bundleItemDetailSchema = z.object({
|
|
51
142
|
productId: z.string(),
|
|
52
143
|
drawCount: z.number().int().nonnegative().optional(),
|
|
@@ -78,6 +169,11 @@ export const categoryFirestoreSchema = z.object({
|
|
|
78
169
|
brandFounded: z.number().int().optional(),
|
|
79
170
|
highlights: z.array(z.string()).optional(),
|
|
80
171
|
faqs: z.array(z.object({ question: z.string(), answer: z.string() })).optional(),
|
|
172
|
+
aliases: z.array(z.string()).optional(),
|
|
173
|
+
contentBody: z.string().optional(),
|
|
174
|
+
descriptionTemplates: z.array(categoryDescriptionTemplateSchema).optional(),
|
|
175
|
+
productDefaults: categoryProductDefaultsSchema.optional(),
|
|
176
|
+
searchTxt: z.array(z.string()).optional(),
|
|
81
177
|
bundleKind: z.enum(["special", "brand"]).optional(),
|
|
82
178
|
brandSlug: z.string().optional(),
|
|
83
179
|
bundlePrice: rupeesSchema.optional(),
|
|
@@ -7,3 +7,4 @@ export * from "./actions";
|
|
|
7
7
|
export { CategoriesRepository, categoriesRepository, } from "./repository/categories.repository";
|
|
8
8
|
export { GET as categoriesGET, GET, POST } from "./api/route";
|
|
9
9
|
export { categoryItemGET, categoryItemPATCH, categoryItemDELETE, } from "./api/[id]/route";
|
|
10
|
+
export { GET as categoryDescriptionTemplateGET } from "./api/[id]/description-template/route";
|
|
@@ -7,3 +7,10 @@ export * from "./actions";
|
|
|
7
7
|
export { CategoriesRepository, categoriesRepository, } from "./repository/categories.repository";
|
|
8
8
|
export { GET as categoriesGET, GET, POST } from "./api/route";
|
|
9
9
|
export { categoryItemGET, categoryItemPATCH, categoryItemDELETE, } from "./api/[id]/route";
|
|
10
|
+
/*
|
|
11
|
+
* The seller form's template read. A separate route rather than a field on
|
|
12
|
+
* `categoryItemGET` because that one returns the whole document and is not
|
|
13
|
+
* edge-cached — this is a narrow, hour-cacheable answer to one question, hit
|
|
14
|
+
* on every category change in the form.
|
|
15
|
+
*/
|
|
16
|
+
export { GET as categoryDescriptionTemplateGET } from "./api/[id]/description-template/route";
|
|
@@ -57,6 +57,15 @@ export interface CategoryItem {
|
|
|
57
57
|
metrics?: CategoryMetrics;
|
|
58
58
|
isFeatured?: boolean;
|
|
59
59
|
featuredPriority?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Whether the category is live. Declared here because `AdminCategoriesView`
|
|
62
|
+
* reads it off THIS route to render its Status column, and its fallback is
|
|
63
|
+
* `toStringValue(item.status, "Active")` — a field `CategoryDocument` does
|
|
64
|
+
* not have. So an absent `isActive` does not render "unknown", it renders
|
|
65
|
+
* **every row as "Active"**, which is the same silent-wrong-value shape the
|
|
66
|
+
* `parentId` comment in that file documents.
|
|
67
|
+
*/
|
|
68
|
+
isActive?: boolean;
|
|
60
69
|
/** @deprecated Use categoryType === "brand" */ isBrand?: boolean;
|
|
61
70
|
seo?: CategorySeo;
|
|
62
71
|
display?: CategoryDisplay;
|
|
@@ -16,9 +16,9 @@ export interface EventDocument extends BaseDocument {
|
|
|
16
16
|
title: string;
|
|
17
17
|
description: string;
|
|
18
18
|
status: EventStatus;
|
|
19
|
-
/**
|
|
19
|
+
/** Test-data flag — set on the shared admin-seeded test event; swept by testDataCleanup. */
|
|
20
20
|
isTestData?: boolean;
|
|
21
|
-
/** When isTestData, the cutoff after which
|
|
21
|
+
/** When isTestData, the cutoff after which testDataCleanup deletes this doc. */
|
|
22
22
|
testDataExpiresAt?: Date;
|
|
23
23
|
startsAt: Date;
|
|
24
24
|
endsAt: Date;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { FAQCategory, FAQAnswer, FAQStats } from "../types";
|
|
5
5
|
export type { FAQCategory, FAQAnswer, FAQStats } from "../types";
|
|
6
6
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
7
|
+
import { extractPlaceholders } from "../../../_internal/shared/templating/placeholders";
|
|
7
8
|
export interface FAQSEO {
|
|
8
9
|
slug: string;
|
|
9
10
|
metaTitle?: string;
|
|
@@ -53,7 +54,7 @@ export declare const faqQueryHelpers: {
|
|
|
53
54
|
readonly byTag: (tag: string) => readonly ["tags", "array-contains", string];
|
|
54
55
|
readonly byCreator: (userId: string) => readonly ["createdBy", "==", string];
|
|
55
56
|
};
|
|
56
|
-
export declare
|
|
57
|
+
export declare const extractVariablePlaceholders: typeof extractPlaceholders;
|
|
57
58
|
export declare function usesVariableInterpolation(faq: FAQDocument): boolean;
|
|
58
59
|
export declare function validateFAQVariables(faq: FAQDocument, siteSettingsVariables: Record<string, string | number>): string[];
|
|
59
60
|
export declare function isPopularFAQ(faq: FAQDocument): boolean;
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* FAQ Feature Firestore Document Types & Constants
|
|
3
3
|
*/
|
|
4
4
|
import { generateFAQId } from "../../../utils/id-generators";
|
|
5
|
+
// Imported from the DEFINING module, not a barrel (Root Cause #18).
|
|
6
|
+
import { extractPlaceholders, missingPlaceholders, } from "../../../_internal/shared/templating/placeholders";
|
|
5
7
|
export const FAQS_COLLECTION = "faqs";
|
|
6
8
|
export const FAQS_INDEXED_FIELDS = [
|
|
7
9
|
"slug",
|
|
@@ -74,21 +76,31 @@ export const faqQueryHelpers = {
|
|
|
74
76
|
byTag: (tag) => ["tags", "array-contains", tag],
|
|
75
77
|
byCreator: (userId) => ["createdBy", "==", userId],
|
|
76
78
|
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
/*
|
|
80
|
+
* The `{{placeholder}}` grammar moved to `_internal/shared/templating/
|
|
81
|
+
* placeholders.ts` so category description templates can share it instead of
|
|
82
|
+
* carrying a second regex (Root Cause #75).
|
|
83
|
+
*
|
|
84
|
+
* These three keep their original NAMES and signatures on purpose: they have
|
|
85
|
+
* FAQ-specific behaviour layered on the shared primitive — both gate on
|
|
86
|
+
* `useSiteSettings`, and `validateFAQVariables` merges the FAQ's own
|
|
87
|
+
* `variables` over the site-settings map. That layer is why this is a
|
|
88
|
+
* re-export-with-wrapper rather than a plain re-export, and why no FAQ call
|
|
89
|
+
* site changed.
|
|
90
|
+
*/
|
|
91
|
+
export const extractVariablePlaceholders = extractPlaceholders;
|
|
81
92
|
export function usesVariableInterpolation(faq) {
|
|
82
93
|
if (!faq.useSiteSettings)
|
|
83
94
|
return false;
|
|
84
|
-
return
|
|
95
|
+
return extractPlaceholders(faq.answer.text).length > 0;
|
|
85
96
|
}
|
|
86
97
|
export function validateFAQVariables(faq, siteSettingsVariables) {
|
|
87
98
|
if (!faq.useSiteSettings)
|
|
88
99
|
return [];
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
100
|
+
return missingPlaceholders(faq.answer.text, {
|
|
101
|
+
...siteSettingsVariables,
|
|
102
|
+
...(faq.variables || {}),
|
|
103
|
+
});
|
|
92
104
|
}
|
|
93
105
|
export function isPopularFAQ(faq) {
|
|
94
106
|
const { views, helpful, notHelpful } = faq.stats;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React.cache deduplication — one Firestore read per RSC request tree.
|
|
3
|
+
*
|
|
4
|
+
* 🛑 `getHomepageFAQs()` was called TWICE on every homepage render, from two
|
|
5
|
+
* modules that could not see each other: `src/app/[locale]/page.tsx` builds the
|
|
6
|
+
* FAQ JSON-LD from it, and `MarketplaceHomepageView` renders the FAQ section
|
|
7
|
+
* from it. Two call sites, no shared wrapper, so two real queries — and the
|
|
8
|
+
* markup and the visible content had no structural guarantee of agreeing, which
|
|
9
|
+
* is a requirement Google actually enforces.
|
|
10
|
+
*
|
|
11
|
+
* Caching the NARROWEST shared call rather than either caller is the pattern
|
|
12
|
+
* CLAUDE.md § React.cache discipline prescribes, and it is a straight copy of
|
|
13
|
+
* `getSiteSettingsGlobal` (features/admin/utils/) — which exists for exactly the
|
|
14
|
+
* same reason and is the precedent this follows.
|
|
15
|
+
*
|
|
16
|
+
* Takes no arguments on purpose: `React.cache` memoises per argument list, so a
|
|
17
|
+
* parameterised version would give each distinct call its own entry and dedupe
|
|
18
|
+
* nothing.
|
|
19
|
+
*/
|
|
20
|
+
export declare const getHomepageFaqsCached: () => Promise<import("..").FAQDocument[]>;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cache } from "react";
|
|
2
|
+
import { faqsRepository } from "../repository/faqs.repository";
|
|
3
|
+
/**
|
|
4
|
+
* React.cache deduplication — one Firestore read per RSC request tree.
|
|
5
|
+
*
|
|
6
|
+
* 🛑 `getHomepageFAQs()` was called TWICE on every homepage render, from two
|
|
7
|
+
* modules that could not see each other: `src/app/[locale]/page.tsx` builds the
|
|
8
|
+
* FAQ JSON-LD from it, and `MarketplaceHomepageView` renders the FAQ section
|
|
9
|
+
* from it. Two call sites, no shared wrapper, so two real queries — and the
|
|
10
|
+
* markup and the visible content had no structural guarantee of agreeing, which
|
|
11
|
+
* is a requirement Google actually enforces.
|
|
12
|
+
*
|
|
13
|
+
* Caching the NARROWEST shared call rather than either caller is the pattern
|
|
14
|
+
* CLAUDE.md § React.cache discipline prescribes, and it is a straight copy of
|
|
15
|
+
* `getSiteSettingsGlobal` (features/admin/utils/) — which exists for exactly the
|
|
16
|
+
* same reason and is the precedent this follows.
|
|
17
|
+
*
|
|
18
|
+
* Takes no arguments on purpose: `React.cache` memoises per argument list, so a
|
|
19
|
+
* parameterised version would give each distinct call its own entry and dedupe
|
|
20
|
+
* nothing.
|
|
21
|
+
*/
|
|
22
|
+
export const getHomepageFaqsCached = cache(() => faqsRepository.getHomepageFAQs());
|
|
@@ -2,7 +2,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
|
|
3
3
|
import { sortBy } from "@mohasinac/appkit";
|
|
4
4
|
import { Div, Main } from "../../../ui";
|
|
5
|
-
import { carouselRepository
|
|
5
|
+
import { carouselRepository } from "../../../repositories";
|
|
6
|
+
// Both are React.cache wrappers around a single repository call, shared with
|
|
7
|
+
// other readers in the same request tree (the locale layout for settings,
|
|
8
|
+
// page.tsx's FAQ JSON-LD for the FAQs). Importing the repositories directly
|
|
9
|
+
// here is what made each of those a second Firestore read.
|
|
10
|
+
import { getSiteSettingsGlobal } from "../../admin/utils/getSiteSettingsGlobal";
|
|
11
|
+
import { getHomepageFaqsCached } from "../../faq/utils/getHomepageFaqsCached";
|
|
6
12
|
import { safeRead } from "../../../errors/safe-read";
|
|
7
13
|
import { fetchLiveStats } from "../lib/live-stats";
|
|
8
14
|
import { renderSection, AnnouncementBar } from "../lib/section-renderer";
|
|
@@ -46,7 +52,10 @@ export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onB
|
|
|
46
52
|
const slides = await safeRead(() => carouselRepository.getActiveSlides(), {
|
|
47
53
|
route: "/", key: "homepage.carouselSlides", fallback: [],
|
|
48
54
|
});
|
|
49
|
-
|
|
55
|
+
// getSiteSettingsGlobal, not the repository directly — the locale layout and
|
|
56
|
+
// generateMetadata already read the same singleton through it, so this is one
|
|
57
|
+
// Firestore read for the whole request tree instead of a second one.
|
|
58
|
+
const siteSettings = await safeRead(() => getSiteSettingsGlobal(), {
|
|
50
59
|
route: "/", key: "homepage.siteSettings", fallback: null,
|
|
51
60
|
});
|
|
52
61
|
// The fallback used to be a hardcoded seasonal Pokémon TCG discount, which
|
|
@@ -58,7 +67,8 @@ export async function MarketplaceHomepageView({ adSlots, newsletterFormSlot, onB
|
|
|
58
67
|
safeRead(() => homepageSectionsRepository.getEnabledSections(), {
|
|
59
68
|
route: "/", key: "homepage.enabledSections", fallback: [],
|
|
60
69
|
}),
|
|
61
|
-
|
|
70
|
+
// Shared with the FAQ JSON-LD built in src/app/[locale]/page.tsx — one read.
|
|
71
|
+
safeRead(() => getHomepageFaqsCached(), {
|
|
62
72
|
route: "/", key: "homepage.faqs", fallback: [],
|
|
63
73
|
}),
|
|
64
74
|
]);
|
|
@@ -47,16 +47,26 @@ export async function fetchLiveStats(requests) {
|
|
|
47
47
|
.catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: total_orders query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
|
|
48
48
|
}
|
|
49
49
|
if (neededPresets.has("total_reviews") || neededPresets.has("platform_rating")) {
|
|
50
|
-
tasks.push(
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
50
|
+
tasks.push(
|
|
51
|
+
/*
|
|
52
|
+
* 🛑 One aggregation, ~1 read — was `findAll()`, an UNBOUNDED full-collection
|
|
53
|
+
* scan of `reviews` on every cold homepage render, whose entire output was
|
|
54
|
+
* these two strings. Billed one read per document, it was the largest single
|
|
55
|
+
* Firestore read on the site (Hobby free tier is 50K reads/day, and the
|
|
56
|
+
* project measured 2.1M/week).
|
|
57
|
+
*
|
|
58
|
+
* The dropped `!r.status ||` branch was dead: `status` is a REQUIRED field
|
|
59
|
+
* defaulting to `"pending"` (features/reviews/schemas/firestore.ts), so no
|
|
60
|
+
* stored review lacks it, and treating a missing status as approved would
|
|
61
|
+
* have counted unmoderated reviews anyway.
|
|
62
|
+
*/
|
|
63
|
+
reviewRepository.getApprovedSiteRatingSummary()
|
|
64
|
+
.then(({ count, average }) => {
|
|
54
65
|
if (neededPresets.has("total_reviews")) {
|
|
55
|
-
resolvedPresets.total_reviews = String(
|
|
66
|
+
resolvedPresets.total_reviews = String(count);
|
|
56
67
|
}
|
|
57
|
-
if (neededPresets.has("platform_rating") &&
|
|
58
|
-
|
|
59
|
-
resolvedPresets.platform_rating = avg.toFixed(1) + "★";
|
|
68
|
+
if (neededPresets.has("platform_rating") && average !== null) {
|
|
69
|
+
resolvedPresets.platform_rating = average.toFixed(1) + "★";
|
|
60
70
|
}
|
|
61
71
|
})
|
|
62
72
|
.catch((err) => { void normalizeError(err); serverLogger.warn("live-stats: reviews/rating query failed — stat omitted", { error: err instanceof Error ? err.message : String(err) }); }));
|
|
@@ -4,7 +4,7 @@ import Image from "next/image";
|
|
|
4
4
|
import { useEffect, useRef, useState } from "react";
|
|
5
5
|
import { Div, Row, Span } from "../../ui";
|
|
6
6
|
import { ROUNDED_MAP, SHADOW_MAP } from "../../ui/components/surface-tokens";
|
|
7
|
-
import { resolveMediaUrl } from "../../utils/media-url";
|
|
7
|
+
import { MEDIA_PROXY_PREFIX, resolveMediaUrl } from "../../utils/media-url";
|
|
8
8
|
const SIZE_HINTS = {
|
|
9
9
|
thumbnail: "(max-width: 640px) 80px, (max-width: 1024px) 96px, 112px",
|
|
10
10
|
card: "(max-width: 640px) 50vw, (max-width: 1024px) 33vw, 25vw",
|
|
@@ -32,6 +32,23 @@ const FALLBACK_ICONS = {
|
|
|
32
32
|
// icon, instead of failing permanently on the very first request.
|
|
33
33
|
const MAX_LOAD_RETRIES = 3;
|
|
34
34
|
const RETRY_BACKOFF_MS = [600, 1200, 2400];
|
|
35
|
+
/**
|
|
36
|
+
* Only `/media/<slug>` may be retried.
|
|
37
|
+
*
|
|
38
|
+
* 🛑 The retry appends `?retry=N`, which mints a BRAND-NEW CDN cache key that is
|
|
39
|
+
* guaranteed cold — so a failing image costs up to 4 cold function invocations
|
|
40
|
+
* instead of 1, on precisely the images that are already flaky. That is a real
|
|
41
|
+
* cost multiplier and it suspended this project once (HTTP 402, 2026-10-09).
|
|
42
|
+
*
|
|
43
|
+
* It is worth paying for our own Storage objects, because the post-upload race
|
|
44
|
+
* above is genuine and transient — retrying actually succeeds. It is NOT worth
|
|
45
|
+
* paying for `/api/media/ext`: there is no upload, the upstream is a third party
|
|
46
|
+
* we do not control, and an upstream that just failed will almost certainly fail
|
|
47
|
+
* again. Those fall straight through to the fallback icon on the first error.
|
|
48
|
+
*/
|
|
49
|
+
function isRetryableSrc(resolvedSrc) {
|
|
50
|
+
return resolvedSrc.startsWith(MEDIA_PROXY_PREFIX);
|
|
51
|
+
}
|
|
35
52
|
/** Resolve every URL in a comma-separated srcset string through the media proxy. */
|
|
36
53
|
function resolveSrcSet(input) {
|
|
37
54
|
return input
|
|
@@ -78,7 +95,7 @@ export function MediaImage({ src, alt, size = "card", priority = false, loading,
|
|
|
78
95
|
className ?? "",
|
|
79
96
|
].filter(Boolean).join(" ");
|
|
80
97
|
const handleError = () => {
|
|
81
|
-
if (retryCount < MAX_LOAD_RETRIES) {
|
|
98
|
+
if (resolvedSrc && isRetryableSrc(resolvedSrc) && retryCount < MAX_LOAD_RETRIES) {
|
|
82
99
|
retryTimeoutRef.current = setTimeout(() => {
|
|
83
100
|
setRetryCount((count) => count + 1);
|
|
84
101
|
}, RETRY_BACKOFF_MS[retryCount]);
|
|
@@ -180,6 +180,25 @@ export interface OrderDocumentItem {
|
|
|
180
180
|
totalPrice: number;
|
|
181
181
|
/** SB8-F â€" set when the item is a prize-draw entry; drives the reveals badge. */
|
|
182
182
|
listingType?: ListingType;
|
|
183
|
+
/**
|
|
184
|
+
* The product's LEAF category, snapshotted at order time (C1).
|
|
185
|
+
*
|
|
186
|
+
* 🛑 A SNAPSHOT, not a join, and not for performance alone. `priceIndexRollup`
|
|
187
|
+
* needs to know which category band a realised sale belongs to, and this
|
|
188
|
+
* interface carried no taxonomy field at all — `bundleCategorySlug` is a
|
|
189
|
+
* bundle key, not a category — so the only route was a full products scan
|
|
190
|
+
* per run, which answers nothing for an item whose product has since been
|
|
191
|
+
* deleted.
|
|
192
|
+
*
|
|
193
|
+
* It is also the correct answer rather than the cheap one: a product
|
|
194
|
+
* re-categorised next year must not retroactively move last year's sale into
|
|
195
|
+
* a band it was never part of. Same reasoning as the `hsnCode` / `gstRate` /
|
|
196
|
+
* `finalSale` snapshots below — a past sale's facts are fixed.
|
|
197
|
+
*
|
|
198
|
+
* Absent on every order written before C1, which is why the rollup keeps the
|
|
199
|
+
* products-map fallback rather than treating absence as "no category".
|
|
200
|
+
*/
|
|
201
|
+
categorySlug?: string;
|
|
183
202
|
/** SB8-F â€" per-item reveal status; flips through pending â†' open â†' revealed/closed. */
|
|
184
203
|
prizeRevealStatus?: "pending" | "open" | "closed" | "revealed";
|
|
185
204
|
/** Set when the buyer cancels this specific line item post-order (partial cancellation). */
|
|
@@ -358,6 +377,57 @@ export interface OrderDocument extends BaseDocument {
|
|
|
358
377
|
sgst?: number;
|
|
359
378
|
/** Inter-state: buyer and seller/store pickup address are in different states. */
|
|
360
379
|
igst?: number;
|
|
380
|
+
/**
|
|
381
|
+
* Taxable value of 0%-rated (EXEMPT) goods in this order — live plants and
|
|
382
|
+
* animals. **Not** part of `taxableAmount`, which covers positively-rated
|
|
383
|
+
* slices only.
|
|
384
|
+
*
|
|
385
|
+
* 🛑 Without it a mixed exempt/taxed order cannot reconcile its own invoice:
|
|
386
|
+
* `sumGroupGst` skipped exempt slices entirely, so `taxableAmount` excluded
|
|
387
|
+
* them while `items[]` included them, and the Taxable column could never
|
|
388
|
+
* sum to the summary. Exempt is a real, reportable state (nil-rated supply),
|
|
389
|
+
* not an absence.
|
|
390
|
+
*/
|
|
391
|
+
exemptAmount?: number;
|
|
392
|
+
/**
|
|
393
|
+
* Per-rate rollup — taxable value and tax for each distinct `gstRate` in the
|
|
394
|
+
* order.
|
|
395
|
+
*
|
|
396
|
+
* 🛑 Required for a Rule 46-compliant invoice, which asks for taxable value
|
|
397
|
+
* and tax **per rate**. The aggregate `taxableAmount`/`gstAmount`/cgst/sgst/
|
|
398
|
+
* igst fields above lose that dimension, so a mixed 5% + 18% order could not
|
|
399
|
+
* produce a compliant document without re-deriving the split downstream —
|
|
400
|
+
* i.e. a second implementation of the loop that already computed it.
|
|
401
|
+
*/
|
|
402
|
+
gstByRate?: Array<{
|
|
403
|
+
gstRate: number;
|
|
404
|
+
taxableAmount: number;
|
|
405
|
+
cgst: number;
|
|
406
|
+
sgst: number;
|
|
407
|
+
igst: number;
|
|
408
|
+
gstAmount: number;
|
|
409
|
+
}>;
|
|
410
|
+
/**
|
|
411
|
+
* OUR GST registration as it stood at the moment of supply — LetItRip is the
|
|
412
|
+
* seller of record and invoices under its own GSTIN.
|
|
413
|
+
*
|
|
414
|
+
* 🛑 Snapshotted for two independent reasons, and both matter.
|
|
415
|
+
*
|
|
416
|
+
* **Compliance**: changing our registered details must never rewrite an
|
|
417
|
+
* invoice already issued. Same discipline as `items[].hsnCode` / `gstRate` /
|
|
418
|
+
* `finalSale`, and the same reason `TaxCodeDocument` edits change future
|
|
419
|
+
* derivations only.
|
|
420
|
+
*
|
|
421
|
+
* **Access**: `gst` sits in `PRIVATE_SITE_FIELDS` — withheld from the public
|
|
422
|
+
* site-settings projection precisely because an earlier deny-list version
|
|
423
|
+
* shipped the GSTIN and registered legal name to anonymous callers (Root
|
|
424
|
+
* Cause #70). So the invoice page cannot read it from site settings, and it
|
|
425
|
+
* should not: the buyer is entitled to the GSTIN *on their own invoice*,
|
|
426
|
+
* which is an authenticated per-order read, not public data.
|
|
427
|
+
*/
|
|
428
|
+
supplierGstin?: string;
|
|
429
|
+
supplierLegalName?: string;
|
|
430
|
+
supplierAddress?: string;
|
|
361
431
|
emiEnabled?: boolean;
|
|
362
432
|
/** Number of monthly installments the buyer chose (2–6). */
|
|
363
433
|
emiTenureMonths?: number;
|
|
@@ -53,6 +53,18 @@ export interface OrderItem {
|
|
|
53
53
|
* gate reads the document, so this is a rendering hint only.
|
|
54
54
|
*/
|
|
55
55
|
finalSale?: boolean;
|
|
56
|
+
/**
|
|
57
|
+
* HSN code and GST rate, snapshotted onto the order item at purchase.
|
|
58
|
+
*
|
|
59
|
+
* 🛑 Surfaced here because the INVOICE needs them per line and could not
|
|
60
|
+
* previously get them: `orderDocumentToOrder` mapped `tax: doc.gstAmount`
|
|
61
|
+
* and nothing else tax-related, so the buyer-facing invoice rendered
|
|
62
|
+
* `["Item", "Qty", "Price"]` and one aggregate Tax row — not because the
|
|
63
|
+
* renderer was lazy, but because the adapter never handed it a rate, an HSN
|
|
64
|
+
* or a split to show. Root Cause #57's exact shape.
|
|
65
|
+
*/
|
|
66
|
+
hsnCode?: string;
|
|
67
|
+
gstRate?: 0 | 5 | 12 | 18 | 28;
|
|
56
68
|
}
|
|
57
69
|
/**
|
|
58
70
|
* One entry on an order's timeline — the client-facing shape of
|
|
@@ -100,7 +112,50 @@ export interface Order {
|
|
|
100
112
|
subtotal: number;
|
|
101
113
|
shippingCost?: number;
|
|
102
114
|
discount?: number;
|
|
115
|
+
/** Total GST — product GST plus the GST on our platform fee. */
|
|
103
116
|
tax?: number;
|
|
117
|
+
/**
|
|
118
|
+
* The GST breakdown the INVOICE needs, and which the adapter did not map
|
|
119
|
+
* until 2026-10-10 — so `/user/orders/[id]/invoice` had no GSTIN, no HSN
|
|
120
|
+
* column, no rate column and one undifferentiated "Tax (GST)" row, and was
|
|
121
|
+
* therefore not Rule 46-compliant in any respect.
|
|
122
|
+
*
|
|
123
|
+
* 🛑 `taxableAmount` excludes `exemptAmount`: a 0%-rated supply is a real
|
|
124
|
+
* reportable state, not an absence, and keeping them separate is what lets
|
|
125
|
+
* the invoice's columns reconcile against its own summary.
|
|
126
|
+
*/
|
|
127
|
+
taxableAmount?: number;
|
|
128
|
+
exemptAmount?: number;
|
|
129
|
+
cgst?: number;
|
|
130
|
+
sgst?: number;
|
|
131
|
+
igst?: number;
|
|
132
|
+
/**
|
|
133
|
+
* Taxable value and tax PER RATE — what Rule 46 actually asks for.
|
|
134
|
+
*
|
|
135
|
+
* ✅ This is also why no per-item `gstAmount` was added. Rule 46 requires the
|
|
136
|
+
* tax amount per **rate**, not per **line**; the per-line requirements are
|
|
137
|
+
* HSN, quantity, taxable value and rate, all of which the item already
|
|
138
|
+
* carries or derives from `price × quantity`. A per-item tax amount would be
|
|
139
|
+
* a denormalised mirror of what this array already holds — Root Cause #42's
|
|
140
|
+
* shape — so it was deliberately NOT added.
|
|
141
|
+
*/
|
|
142
|
+
gstByRate?: Array<{
|
|
143
|
+
gstRate: number;
|
|
144
|
+
taxableAmount: number;
|
|
145
|
+
cgst: number;
|
|
146
|
+
sgst: number;
|
|
147
|
+
igst: number;
|
|
148
|
+
gstAmount: number;
|
|
149
|
+
}>;
|
|
150
|
+
/**
|
|
151
|
+
* Our GST registration as snapshotted on this order. A tax invoice without
|
|
152
|
+
* the supplier's GSTIN is not a tax invoice — and the invoice page cannot
|
|
153
|
+
* read it from site settings, because `gst` is in `PRIVATE_SITE_FIELDS`
|
|
154
|
+
* after an earlier deny-list projection shipped it publicly (Root Cause #70).
|
|
155
|
+
*/
|
|
156
|
+
supplierGstin?: string;
|
|
157
|
+
supplierLegalName?: string;
|
|
158
|
+
supplierAddress?: string;
|
|
104
159
|
total: number;
|
|
105
160
|
currency: string;
|
|
106
161
|
/**
|
|
@@ -15,15 +15,6 @@ export interface ProductListActionParams {
|
|
|
15
15
|
export type ProductListResult = FirebaseSieveResult<ProductDocument>;
|
|
16
16
|
export declare function listProducts(params?: ProductListActionParams): Promise<ProductListResult>;
|
|
17
17
|
export declare function getProductById(id: string): Promise<ProductDocument | null>;
|
|
18
|
-
/**
|
|
19
|
-
* Featured items for the homepage's generic "Featured Products" strip.
|
|
20
|
-
*
|
|
21
|
-
* Scoped to `GENERIC_PRODUCT_LISTING_TYPES` for the same reason `/products` is:
|
|
22
|
-
* the homepage already has dedicated Live Auctions, Pre-Orders and Prize Draws
|
|
23
|
-
* sections, so leaving this unscoped put the same item in two strips on one
|
|
24
|
-
* page — and an auction rendered as a generic product card shows Buy-Now chrome
|
|
25
|
-
* instead of bid affordances.
|
|
26
|
-
*/
|
|
27
18
|
export declare function getFeaturedProducts(pageSize?: number): Promise<ProductListResult>;
|
|
28
19
|
/**
|
|
29
20
|
* The soonest-ending LIVE auctions.
|
|
@@ -73,6 +73,28 @@ function toListResult(result, pageSize) {
|
|
|
73
73
|
* page — and an auction rendered as a generic product card shows Buy-Now chrome
|
|
74
74
|
* instead of bid affordances.
|
|
75
75
|
*/
|
|
76
|
+
/**
|
|
77
|
+
* How many documents a fixed-size homepage strip may read.
|
|
78
|
+
*
|
|
79
|
+
* 🛑 These strips land on `listPublicProducts`' in-memory window path — every
|
|
80
|
+
* anonymous viewer sets `hidesTestData`, which forces it — and that path pins
|
|
81
|
+
* the fetch to 50 documents regardless of the requested `pageSize`. So
|
|
82
|
+
* `getFeaturedProducts(12)` read 51 to render 12, and the homepage does this
|
|
83
|
+
* four times: ~200 reads for 44 cards, against a 50K-reads/day free tier that
|
|
84
|
+
* the project was exceeding 6× (2.1M/week, measured 2026-10-09).
|
|
85
|
+
*
|
|
86
|
+
* Headroom is genuinely required, not optional: availability and test-data are
|
|
87
|
+
* per-row predicates applied AFTER the fetch, so a window equal to `pageSize`
|
|
88
|
+
* would under-fill the strip the moment one row is unavailable. 2× with a floor
|
|
89
|
+
* of +8 absorbs that while still cutting the read roughly in half.
|
|
90
|
+
*
|
|
91
|
+
* Deliberately NOT derived inside `listPublicProducts` from `pageSize` — a
|
|
92
|
+
* browse page must keep the full 50 so it can page deeply, and silently
|
|
93
|
+
* shrinking its window would drop rows from page 2 onward.
|
|
94
|
+
*/
|
|
95
|
+
function featuredWindow(pageSize) {
|
|
96
|
+
return Math.max(pageSize * 2, pageSize + 8);
|
|
97
|
+
}
|
|
76
98
|
export async function getFeaturedProducts(pageSize = 8) {
|
|
77
99
|
return toListResult(await listPublicProducts({
|
|
78
100
|
featured: true,
|
|
@@ -81,6 +103,7 @@ export async function getFeaturedProducts(pageSize = 8) {
|
|
|
81
103
|
sorts: sortBy("createdAt", "DESC"),
|
|
82
104
|
page: 1,
|
|
83
105
|
pageSize,
|
|
106
|
+
windowSize: featuredWindow(pageSize),
|
|
84
107
|
}), pageSize);
|
|
85
108
|
}
|
|
86
109
|
/**
|
|
@@ -103,6 +126,7 @@ export async function getFeaturedAuctions(pageSize = 6) {
|
|
|
103
126
|
sorts: sortBy(PRODUCT_FIELDS.AUCTION_END_DATE, "ASC"),
|
|
104
127
|
page: 1,
|
|
105
128
|
pageSize,
|
|
129
|
+
windowSize: featuredWindow(pageSize),
|
|
106
130
|
}), pageSize);
|
|
107
131
|
}
|
|
108
132
|
export async function getLatestProducts(pageSize = 12) {
|
|
@@ -138,6 +162,7 @@ export async function getFeaturedPreOrders(pageSize = 6) {
|
|
|
138
162
|
sorts: sortBy(PRODUCT_FIELDS.PRE_ORDER_DELIVERY_DATE, "ASC"),
|
|
139
163
|
page: 1,
|
|
140
164
|
pageSize,
|
|
165
|
+
windowSize: featuredWindow(pageSize),
|
|
141
166
|
}), pageSize);
|
|
142
167
|
}
|
|
143
168
|
export async function getLatestPreOrders(pageSize = 12) {
|
|
@@ -6,7 +6,11 @@ export const PRODUCT_SORT_VALUES = {
|
|
|
6
6
|
PRICE_LOW: "price",
|
|
7
7
|
PRICE_HIGH: "-price",
|
|
8
8
|
RATING_HIGH: "-avgRating",
|
|
9
|
-
POPULAR: "-viewCount"
|
|
9
|
+
// 🛑 `POPULAR: "-viewCount"` removed 2026-10-10 — `viewCount` is on no
|
|
10
|
+
// document (0/72 products, 0/58 categories) and nothing writes it, so a
|
|
11
|
+
// Firestore orderBy on it excludes every row. Verified: 0 results against a
|
|
12
|
+
// valid index. See STANDARD_SORT_OPTIONS for the full writeup.
|
|
13
|
+
// `RATING_HIGH` above is fine by contrast — `avgRating` is present on 72/72.
|
|
10
14
|
NAME_AZ: "title",
|
|
11
15
|
NAME_ZA: "-title",
|
|
12
16
|
};
|
|
@@ -20,5 +20,11 @@ export async function ArtStickersListView({ searchParams = {} }) {
|
|
|
20
20
|
// about the default view (Root Cause #30).
|
|
21
21
|
...defaultAvailabilityForListingTypes(ART_STICKERS_LISTING_TYPES),
|
|
22
22
|
}));
|
|
23
|
-
return (_jsxs(Main, { children: [_jsx(Section, { border: "bottom-subtle", paddingY: "b-md", padding: "t-xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", color: "primary", children: "Art & Stickers" }), _jsx(Text, { className: "mt-1", color: "muted", size: "sm", children: "Original art prints and sticker packs from independent sellers" })] }) }), _jsxs(Container, { size: "xl", padding: "x-md", children: [_jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-4 mt-4" }), _jsx(ProductsIndexListing
|
|
23
|
+
return (_jsxs(Main, { children: [_jsx(Section, { border: "bottom-subtle", paddingY: "b-md", padding: "t-xl", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "bold", color: "primary", children: "Art & Stickers" }), _jsx(Text, { className: "mt-1", color: "muted", size: "sm", children: "Original art prints and sticker packs from independent sellers" })] }) }), _jsxs(Container, { size: "xl", padding: "x-md", children: [_jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-4 mt-4" }), _jsx(ProductsIndexListing
|
|
24
|
+
// `undefined`, not `null` — React Query treats null as defined data,
|
|
25
|
+
// so a failed query froze an empty grid. See ProductsIndexPageView.
|
|
26
|
+
, {
|
|
27
|
+
// `undefined`, not `null` — React Query treats null as defined data,
|
|
28
|
+
// so a failed query froze an empty grid. See ProductsIndexPageView.
|
|
29
|
+
initialData: result ?? undefined, listingTypes: ART_STICKERS_LISTING_TYPES, typeTabs: ART_STICKERS_TYPE_FILTER_TABS, searchPlaceholder: "Search art & stickers..." }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] })] }));
|
|
24
30
|
}
|