@mohasinac/appkit 4.43.0 → 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/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/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 +54 -0
- 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/{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/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/auth/schemas/firestore.d.ts +0 -1
- package/dist/features/blog/schemas/firestore.d.ts +2 -2
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
- 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/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 +62 -11
- package/dist/features/categories/repository/categories.repository.js +178 -35
- 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/orders/schemas/firestore.d.ts +70 -0
- package/dist/features/orders/types/index.d.ts +55 -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/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/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/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 +8 -27
- 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/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/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 +2 -1
- package/dist/server.js +5 -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/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;
|
|
@@ -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
|
/**
|
|
@@ -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
|
};
|
|
@@ -37,9 +37,6 @@ export declare const PRODUCT_ADMIN_SORT_OPTIONS: readonly [{
|
|
|
37
37
|
}, {
|
|
38
38
|
readonly value: string;
|
|
39
39
|
readonly label: "Recently Updated";
|
|
40
|
-
}, {
|
|
41
|
-
readonly value: string;
|
|
42
|
-
readonly label: "Most Viewed";
|
|
43
40
|
}];
|
|
44
41
|
export declare const PRODUCT_SELLER_SORT_OPTIONS: readonly [{
|
|
45
42
|
readonly value: string;
|
|
@@ -68,9 +65,6 @@ export declare const PRODUCT_SELLER_SORT_OPTIONS: readonly [{
|
|
|
68
65
|
}, {
|
|
69
66
|
readonly value: string;
|
|
70
67
|
readonly label: "Recently Updated";
|
|
71
|
-
}, {
|
|
72
|
-
readonly value: string;
|
|
73
|
-
readonly label: "Most Viewed";
|
|
74
68
|
}];
|
|
75
69
|
export declare const PRODUCT_PUBLIC_SORT_OPTIONS: readonly [{
|
|
76
70
|
readonly value: string;
|
|
@@ -90,9 +84,6 @@ export declare const PRODUCT_PUBLIC_SORT_OPTIONS: readonly [{
|
|
|
90
84
|
}, {
|
|
91
85
|
readonly value: string;
|
|
92
86
|
readonly label: "Name: Z–A";
|
|
93
|
-
}, {
|
|
94
|
-
readonly value: string;
|
|
95
|
-
readonly label: "Most Viewed";
|
|
96
87
|
}];
|
|
97
88
|
export declare function getProductFilterKeys(variant: ProductFilterVariant): readonly string[];
|
|
98
89
|
export declare function getProductSortOptions(variant: ProductFilterVariant): ReadonlyArray<{
|
|
@@ -33,6 +33,32 @@ export interface ProductFormProps {
|
|
|
33
33
|
product: ProductFormValue;
|
|
34
34
|
onChange: (updated: ProductFormValue) => void;
|
|
35
35
|
isReadonly?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Show the Tax & GST section (`gstRate` + `hsnCode`). **Admin only.**
|
|
38
|
+
*
|
|
39
|
+
* 🛑 Defaults to `false`, and the default is the point. LetItRip is the
|
|
40
|
+
* seller of record and invoices under its OWN GSTIN, so a supplier choosing
|
|
41
|
+
* the tax rate on goods we invoice is a compliance hole, not a convenience.
|
|
42
|
+
* The rate is a tax determination: it belongs to the category (via
|
|
43
|
+
* `productDefaults.taxCodeId`, resolved server-side by `deriveTaxonomy`) and
|
|
44
|
+
* to the admin who maintains the tax codes.
|
|
45
|
+
*
|
|
46
|
+
* There is also a one-click foot-gun behind it. The select renders
|
|
47
|
+
* `String(gstRate ?? 0)`, so an undetermined rate DISPLAYS as "0%" — and
|
|
48
|
+
* because `0` is a deliberate exemption while `undefined` means "nobody has
|
|
49
|
+
* said" (see `ProductDocument.gstRate`), a seller who touches that control
|
|
50
|
+
* at all, even re-picking the 0% already shown, writes a hard `0` and
|
|
51
|
+
* **permanently suppresses the category derivation**: `deriveTaxonomy` only
|
|
52
|
+
* derives when `input.gstRate == null`.
|
|
53
|
+
*
|
|
54
|
+
* 🛑 The display default does NOT corrupt the field on its own. `FormField`'s
|
|
55
|
+
* select fires `onChange` from a real DOM event only, and the draft is
|
|
56
|
+
* seeded from the stored document — so an untouched save preserves
|
|
57
|
+
* `undefined`. Verified rather than assumed; an earlier plan revision
|
|
58
|
+
* asserted every save converted "nobody has said" into "exempt", and that
|
|
59
|
+
* was not true of this code.
|
|
60
|
+
*/
|
|
61
|
+
canEditTax?: boolean;
|
|
36
62
|
/**
|
|
37
63
|
* Render a custom rich-text description editor.
|
|
38
64
|
* If omitted, a standard textarea field is used.
|
|
@@ -76,5 +102,5 @@ export interface ProductFormProps {
|
|
|
76
102
|
disabled: boolean;
|
|
77
103
|
}) => React.ReactNode;
|
|
78
104
|
}
|
|
79
|
-
export declare function ProductForm({ product, onChange, isReadonly, renderDescriptionEditor, renderCategorySelector, renderBrandSelector, renderStoreAddressSelector, onMediaAbort, currencyPrefix, renderGroupSettings, renderGroupJoinField, }: ProductFormProps): React.JSX.Element;
|
|
105
|
+
export declare function ProductForm({ product, onChange, isReadonly, canEditTax, renderDescriptionEditor, renderCategorySelector, renderBrandSelector, renderStoreAddressSelector, onMediaAbort, currencyPrefix, renderGroupSettings, renderGroupJoinField, }: ProductFormProps): React.JSX.Element;
|
|
80
106
|
export {};
|
|
@@ -50,7 +50,9 @@ function readFileAsDataUrl(file) {
|
|
|
50
50
|
reader.readAsDataURL(file);
|
|
51
51
|
});
|
|
52
52
|
}
|
|
53
|
-
export function ProductForm({ product, onChange, isReadonly = false,
|
|
53
|
+
export function ProductForm({ product, onChange, isReadonly = false,
|
|
54
|
+
/* Defaults to false — the seller path must not reach it. See the prop's docs. */
|
|
55
|
+
canEditTax = false, renderDescriptionEditor, renderCategorySelector, renderBrandSelector, renderStoreAddressSelector, onMediaAbort, currencyPrefix = "", renderGroupSettings, renderGroupJoinField, }) {
|
|
54
56
|
const t = useTranslations("adminProducts");
|
|
55
57
|
const { upload } = useMediaUpload();
|
|
56
58
|
const galleryIndexRef = useRef(0);
|
|
@@ -179,17 +181,29 @@ export function ProductForm({ product, onChange, isReadonly = false, renderDescr
|
|
|
179
181
|
minOfferPercent: e.target.checked
|
|
180
182
|
? product.minOfferPercent ?? DEFAULT_MIN_OFFER_PERCENT
|
|
181
183
|
: undefined,
|
|
182
|
-
}), disabled: isReadonly })] }), product.allowOffers && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formAllowOffersHelp"), children: t("formAllowOffersHelp") }), _jsx(FormField, { name: "minOfferPercent", label: t("formMinOfferPercent"), type: "number", value: String(product.minOfferPercent ?? ""), onChange: (value) => update({ minOfferPercent: Number(value) }), disabled: isReadonly, placeholder: String(DEFAULT_MIN_OFFER_PERCENT) })] })), _jsx(Heading, { level: 4, className: "mt-4", children: t("sectionTaxGst") }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "gstRate", label: t("formGstRate"), type: "select",
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
184
|
+
}), disabled: isReadonly })] }), product.allowOffers && (_jsxs(_Fragment, { children: [_jsx(Alert, { variant: "info", title: t("formAllowOffersHelp"), children: t("formAllowOffersHelp") }), _jsx(FormField, { name: "minOfferPercent", label: t("formMinOfferPercent"), type: "number", value: String(product.minOfferPercent ?? ""), onChange: (value) => update({ minOfferPercent: Number(value) }), disabled: isReadonly, placeholder: String(DEFAULT_MIN_OFFER_PERCENT) })] })), canEditTax ? (_jsxs(_Fragment, { children: [_jsx(Heading, { level: 4, className: "mt-4", children: t("sectionTaxGst") }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "gstRate", label: t("formGstRate"), type: "select",
|
|
185
|
+
/*
|
|
186
|
+
* 🛑 "" is the UNSET option, and it is not cosmetic.
|
|
187
|
+
* `ProductDocument.gstRate` documents that `undefined` and `0`
|
|
188
|
+
* mean different things — zero is a deliberate exemption (live
|
|
189
|
+
* plants and animals), undefined is "nobody has said" — and
|
|
190
|
+
* `deriveTaxonomy` only derives the category's rate while it is
|
|
191
|
+
* `== null`. Collapsing unset to "0%", which this control did
|
|
192
|
+
* until 2026-10-10, made an undetermined rate read as exempt and
|
|
193
|
+
* put a hard `0` one click away.
|
|
194
|
+
*/
|
|
195
|
+
value: product.gstRate == null ? "" : String(product.gstRate), onChange: (value) => update({
|
|
196
|
+
gstRate: value === ""
|
|
197
|
+
? undefined
|
|
198
|
+
: Number(value),
|
|
199
|
+
}), disabled: isReadonly, options: [
|
|
200
|
+
{ value: "", label: t("formGstRateUnset") },
|
|
201
|
+
{ value: "0", label: "0%" },
|
|
202
|
+
{ value: "5", label: "5%" },
|
|
203
|
+
{ value: "12", label: "12%" },
|
|
204
|
+
{ value: "18", label: "18%" },
|
|
205
|
+
{ value: "28", label: "28%" },
|
|
206
|
+
] }), _jsx(FormField, { name: "hsnCode", label: t("formHsnCode"), type: "text", value: product.hsnCode ?? "", onChange: (value) => update({ hsnCode: value }), disabled: isReadonly, placeholder: "e.g. 9503" })] })] })) : null, _jsx(Heading, { level: 4, className: "mt-4", children: t("sectionConditionShipping") }), _jsxs(FormGroup, { columns: 2, children: [_jsx(FormField, { name: "condition", label: t("formCondition"), type: "select", value: product.condition || "new", onChange: (value) => update({
|
|
193
207
|
condition: value,
|
|
194
208
|
}), disabled: isReadonly, options: [
|
|
195
209
|
{ value: "new", label: t("formConditionNew") },
|
|
@@ -57,9 +57,6 @@ export declare const STANDARD_SORT_OPTIONS: readonly [{
|
|
|
57
57
|
}, {
|
|
58
58
|
readonly value: string;
|
|
59
59
|
readonly label: "Recently Updated";
|
|
60
|
-
}, {
|
|
61
|
-
readonly value: string;
|
|
62
|
-
readonly label: "Most Viewed";
|
|
63
60
|
}];
|
|
64
61
|
export declare const STANDARD_PUBLIC_SORT_OPTIONS: readonly [{
|
|
65
62
|
readonly value: string;
|
|
@@ -79,9 +76,6 @@ export declare const STANDARD_PUBLIC_SORT_OPTIONS: readonly [{
|
|
|
79
76
|
}, {
|
|
80
77
|
readonly value: string;
|
|
81
78
|
readonly label: "Name: Z–A";
|
|
82
|
-
}, {
|
|
83
|
-
readonly value: string;
|
|
84
|
-
readonly label: "Most Viewed";
|
|
85
79
|
}];
|
|
86
80
|
export declare const AUCTION_SORT_OPTIONS: readonly [{
|
|
87
81
|
readonly value: string;
|
|
@@ -68,7 +68,31 @@ export const STANDARD_SORT_OPTIONS = [
|
|
|
68
68
|
{ value: sortBy(PRODUCT_FIELDS.FEATURED), label: "Featured First" },
|
|
69
69
|
{ value: sortBy(PRODUCT_FIELDS.IS_PROMOTED), label: "Promoted First" },
|
|
70
70
|
{ value: sortBy(PRODUCT_FIELDS.UPDATED_AT), label: "Recently Updated" },
|
|
71
|
-
|
|
71
|
+
/*
|
|
72
|
+
* 🛑 "Most Viewed" REMOVED 2026-10-10 — it returned zero products, always.
|
|
73
|
+
*
|
|
74
|
+
* It sorted by `viewCount`, and **no document has that field**: measured on
|
|
75
|
+
* production, 0 of 72 published products and 0 of 58 categories carry it.
|
|
76
|
+
* Nothing writes it either — `incrementViewCount` was removed from the
|
|
77
|
+
* render path on 2026-08-31 (correctly: a Firestore write per render) and
|
|
78
|
+
* both repositories' copies have had zero callers since.
|
|
79
|
+
*
|
|
80
|
+
* A Firestore `orderBy` EXCLUDES every document that lacks the ordering
|
|
81
|
+
* field, so this was Root Cause #100 verbatim. Confirmed against the real
|
|
82
|
+
* query shape — `status==published` + `listingType==standard`:
|
|
83
|
+
*
|
|
84
|
+
* no sort -> 16 rows
|
|
85
|
+
* orderBy title -> 16 rows
|
|
86
|
+
* orderBy viewCount -> 0 rows <- and the index EXISTS
|
|
87
|
+
*
|
|
88
|
+
* So it was not a missing index. It was a served query, costing a real
|
|
89
|
+
* round trip, guaranteed to return nothing — and picking it emptied the
|
|
90
|
+
* catalogue with no error anywhere.
|
|
91
|
+
*
|
|
92
|
+
* Real view counts arrive from the `pageViews` collection (already
|
|
93
|
+
* collected on ~30 surfaces) as `metrics.views7d`/`views30d`, rolled up by
|
|
94
|
+
* the daily job. A sort can come back then, against a field that exists.
|
|
95
|
+
*/
|
|
72
96
|
];
|
|
73
97
|
export const STANDARD_PUBLIC_SORT_OPTIONS = [
|
|
74
98
|
STANDARD_SORT_OPTIONS[0],
|
|
@@ -77,7 +101,16 @@ export const STANDARD_PUBLIC_SORT_OPTIONS = [
|
|
|
77
101
|
STANDARD_SORT_OPTIONS[3],
|
|
78
102
|
STANDARD_SORT_OPTIONS[4],
|
|
79
103
|
STANDARD_SORT_OPTIONS[5],
|
|
80
|
-
|
|
104
|
+
/*
|
|
105
|
+
* 🛑 `STANDARD_SORT_OPTIONS[9]` removed — it WAS "Most Viewed", and its
|
|
106
|
+
* presence here means the zero-result sort was offered on the PUBLIC
|
|
107
|
+
* products page, not only in admin. A shopper picking it emptied the
|
|
108
|
+
* catalogue.
|
|
109
|
+
*
|
|
110
|
+
* Indices 6–8 (Featured First / Promoted First / Recently Updated) are
|
|
111
|
+
* omitted deliberately and always have been — this list is a curated public
|
|
112
|
+
* subset, not a prefix.
|
|
113
|
+
*/
|
|
81
114
|
];
|
|
82
115
|
// ---------------------------------------------------------------------------
|
|
83
116
|
// Auctions — auction-specific base puts auctionEndDate primary
|