@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.
Files changed (220) hide show
  1. package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
  2. package/dist/_internal/server/features/categories/adapters.js +235 -0
  3. package/dist/_internal/server/features/checkout/actions.js +161 -22
  4. package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
  5. package/dist/_internal/server/features/orders/adapters.js +21 -0
  6. package/dist/_internal/server/features/refunds/actions.js +1 -1
  7. package/dist/_internal/server/features/seo/sitemap.js +54 -15
  8. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  9. package/dist/_internal/server/features/stores/adapters.js +6 -0
  10. package/dist/_internal/server/functions/scheduled.d.ts +4 -2
  11. package/dist/_internal/server/functions/scheduled.js +64 -6
  12. package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
  13. package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
  14. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
  15. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
  16. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
  17. package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
  18. package/dist/_internal/server/jobs/core/onCategoryWrite.js +54 -0
  19. package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
  20. package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
  21. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
  22. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
  23. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
  24. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
  25. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
  26. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
  27. package/dist/_internal/server/jobs/handlers/index.js +3 -1
  28. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
  29. package/dist/_internal/server/jobs/handlers/messages.js +5 -2
  30. package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
  31. package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
  32. package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
  33. package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
  34. package/dist/_internal/shared/actions/action-registry.js +34 -0
  35. package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
  36. package/dist/_internal/shared/checkout/order-math.js +194 -7
  37. package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
  38. package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
  39. package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
  40. package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
  41. package/dist/_internal/shared/fees/calculator.d.ts +105 -0
  42. package/dist/_internal/shared/fees/calculator.js +143 -0
  43. package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
  44. package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
  45. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
  46. package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
  47. package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
  48. package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
  49. package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
  50. package/dist/_internal/shared/templating/placeholders.js +73 -0
  51. package/dist/client.d.ts +2 -0
  52. package/dist/client.js +1 -0
  53. package/dist/constants/api-endpoints.d.ts +42 -0
  54. package/dist/constants/api-endpoints.js +14 -0
  55. package/dist/features/admin/actions/admin-actions.d.ts +1 -1
  56. package/dist/features/admin/actions/admin-actions.js +1 -1
  57. package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
  58. package/dist/features/admin/components/AdminProductEditorView.js +8 -1
  59. package/dist/features/admin/components/AdminProductsView.js +78 -7
  60. package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
  61. package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
  62. package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
  63. package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
  64. package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
  65. package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
  66. package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
  67. package/dist/features/auth/schemas/firestore.d.ts +0 -1
  68. package/dist/features/blog/schemas/firestore.d.ts +2 -2
  69. package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
  70. package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
  71. package/dist/features/categories/api/[id]/description-template/route.js +252 -0
  72. package/dist/features/categories/api/route.js +156 -11
  73. package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
  74. package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
  75. package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
  76. package/dist/features/categories/constants/listing-categories.d.ts +17 -0
  77. package/dist/features/categories/constants/listing-categories.js +53 -0
  78. package/dist/features/categories/hooks/useCategoryTree.js +28 -2
  79. package/dist/features/categories/repository/categories.repository.d.ts +62 -11
  80. package/dist/features/categories/repository/categories.repository.js +178 -35
  81. package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
  82. package/dist/features/categories/repository/price-index.repository.js +85 -0
  83. package/dist/features/categories/schemas/category-content.d.ts +206 -0
  84. package/dist/features/categories/schemas/category-content.js +28 -0
  85. package/dist/features/categories/schemas/firestore.d.ts +74 -5
  86. package/dist/features/categories/schemas/firestore.js +74 -3
  87. package/dist/features/categories/schemas/index.d.ts +1047 -0
  88. package/dist/features/categories/schemas/index.js +96 -0
  89. package/dist/features/categories/server.d.ts +1 -0
  90. package/dist/features/categories/server.js +7 -0
  91. package/dist/features/categories/types/index.d.ts +9 -0
  92. package/dist/features/events/schemas/firestore.d.ts +2 -2
  93. package/dist/features/faq/schemas/firestore.d.ts +2 -1
  94. package/dist/features/faq/schemas/firestore.js +20 -8
  95. package/dist/features/orders/schemas/firestore.d.ts +70 -0
  96. package/dist/features/orders/types/index.d.ts +55 -0
  97. package/dist/features/products/columns/index.d.ts +0 -1
  98. package/dist/features/products/columns/index.js +5 -1
  99. package/dist/features/products/components/ProductFilters.d.ts +0 -9
  100. package/dist/features/products/components/ProductForm.d.ts +27 -1
  101. package/dist/features/products/components/ProductForm.js +26 -12
  102. package/dist/features/products/constants/sieve.d.ts +0 -6
  103. package/dist/features/products/constants/sieve.js +35 -2
  104. package/dist/features/products/repository/products.repository.d.ts +115 -1
  105. package/dist/features/products/repository/products.repository.js +491 -39
  106. package/dist/features/products/schemas/firestore.d.ts +65 -5
  107. package/dist/features/products/schemas/firestore.js +9 -0
  108. package/dist/features/products/schemas/product-features.d.ts +140 -2
  109. package/dist/features/products/schemas/product-features.js +62 -1
  110. package/dist/features/promotions/repository/coupons.repository.js +6 -1
  111. package/dist/features/promotions/schemas/firestore.d.ts +44 -1
  112. package/dist/features/promotions/schemas/firestore.js +76 -2
  113. package/dist/features/seller/actions/seller-actions.js +17 -2
  114. package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
  115. package/dist/features/seller/components/PrintCenterView.js +3 -2
  116. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
  117. package/dist/features/seller/components/SellerOrdersView.js +6 -1
  118. package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
  119. package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
  120. package/dist/features/seller/components/SellerProductShell.js +101 -9
  121. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
  122. package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
  123. package/dist/features/seller/components/SellerProductsView.js +108 -3
  124. package/dist/features/seller/types/index.d.ts +17 -0
  125. package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
  126. package/dist/features/seller/utils/product-draft-mapping.js +17 -0
  127. package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
  128. package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
  129. package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
  130. package/dist/features/stores/schemas/firestore.d.ts +31 -2
  131. package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
  132. package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
  133. package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
  134. package/dist/features/tax-codes/schemas/firestore.js +82 -0
  135. package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
  136. package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
  137. package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
  138. package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
  139. package/dist/features/tester/actions/index.d.ts +0 -1
  140. package/dist/features/tester/actions/index.js +0 -1
  141. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
  142. package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
  143. package/dist/features/tester/components/index.d.ts +0 -4
  144. package/dist/features/tester/components/index.js +0 -2
  145. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
  146. package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
  147. package/dist/features/tester/schemas/firestore.d.ts +0 -6
  148. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
  149. package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
  150. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
  151. package/dist/features/tester/seed-data/authored/index.js +0 -2
  152. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
  153. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +8 -27
  154. package/dist/index.d.ts +21 -7
  155. package/dist/index.js +53 -7
  156. package/dist/next/routing/route-map.d.ts +2 -4
  157. package/dist/next/routing/route-map.js +1 -2
  158. package/dist/repositories/index.d.ts +1 -0
  159. package/dist/repositories/index.js +1 -0
  160. package/dist/schemas/registry.d.ts +506 -0
  161. package/dist/seed/_helpers/brand-rows.d.ts +35 -0
  162. package/dist/seed/_helpers/brand-rows.js +576 -0
  163. package/dist/seed/_helpers/category-content.d.ts +39 -0
  164. package/dist/seed/_helpers/category-content.js +634 -0
  165. package/dist/seed/_helpers/category-forest.d.ts +2 -0
  166. package/dist/seed/_helpers/category-forest.js +585 -243
  167. package/dist/seed/_helpers/category-models.d.ts +5 -0
  168. package/dist/seed/_helpers/category-models.js +1364 -0
  169. package/dist/seed/_helpers/category-tree.js +17 -1
  170. package/dist/seed/_helpers/character-features.d.ts +10 -0
  171. package/dist/seed/_helpers/character-features.js +118 -0
  172. package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
  173. package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
  174. package/dist/seed/categories-seed-data.js +147 -169
  175. package/dist/seed/index.d.ts +1 -0
  176. package/dist/seed/index.js +1 -0
  177. package/dist/seed/manifest.d.ts +1 -1
  178. package/dist/seed/manifest.js +5 -0
  179. package/dist/seed/product-features-seed-data.d.ts +1 -8
  180. package/dist/seed/product-features-seed-data.js +358 -154
  181. package/dist/seed/products-art-seed-data.js +16 -4
  182. package/dist/seed/products-auctions-seed-data.js +6 -6
  183. package/dist/seed/products-classifieds-seed-data.js +17 -7
  184. package/dist/seed/products-digital-codes-seed-data.js +12 -12
  185. package/dist/seed/products-preorders-seed-data.js +8 -8
  186. package/dist/seed/products-prize-draws-seed-data.js +25 -8
  187. package/dist/seed/products-standard-seed-data.js +10 -60
  188. package/dist/seed/products-stickers-seed-data.js +6 -6
  189. package/dist/seed/site-settings-seed-data.js +24 -0
  190. package/dist/seed/store-extensions-seed-data.js +10 -4
  191. package/dist/seed/tax-codes-seed-data.d.ts +34 -0
  192. package/dist/seed/tax-codes-seed-data.js +100 -0
  193. package/dist/seed/test-data-ttl.d.ts +29 -0
  194. package/dist/seed/test-data-ttl.js +31 -0
  195. package/dist/server-entry.d.ts +1 -1
  196. package/dist/server-entry.js +6 -1
  197. package/dist/server.d.ts +2 -1
  198. package/dist/server.js +5 -2
  199. package/dist/styles.css +40 -0
  200. package/dist/ui/components/PaginatedSelect.d.ts +14 -0
  201. package/dist/ui/components/PaginatedSelect.js +0 -0
  202. package/dist/ui/components/PaginatedSelect.style.css +40 -0
  203. package/dist/utils/physical-location.d.ts +23 -0
  204. package/dist/utils/physical-location.js +52 -0
  205. package/dist/utils/search-txt-builders.d.ts +20 -0
  206. package/dist/utils/search-txt-builders.js +36 -0
  207. package/package.json +1 -1
  208. package/scripts/seed-cli.mjs +4 -1
  209. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
  210. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
  211. package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
  212. package/dist/features/tester/actions/leaderboard-actions.js +0 -5
  213. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
  214. package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
  215. package/dist/features/tester/components/TesterHubView.d.ts +0 -5
  216. package/dist/features/tester/components/TesterHubView.js +0 -110
  217. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
  218. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
  219. package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
  220. package/dist/features/tester/seed-data/tester-ttl.js +0 -6
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Tax codes — Firestore schema
3
+ *
4
+ * An HSN code and its GST rate, as an ENTITY rather than two fields repeated
5
+ * across every category. Changing a rate becomes one edit instead of ~330.
6
+ *
7
+ * ## 🛑 The reference is for AUTHORING. The scalars stay for CORRECTNESS.
8
+ *
9
+ * `CategoryProductDefaults.taxCodeId` points here, and `deriveTaxonomy`
10
+ * resolves it to `ProductDocument.gstRate` / `.hsnCode` **on the product**;
11
+ * the order item then snapshots from the product, exactly as it already does.
12
+ *
13
+ * That layering is not redundancy. Without it, editing a tax code would
14
+ * retroactively change the GST on orders already invoiced — a compliance
15
+ * problem, not a modelling preference. It is the same reason
16
+ * `OrderDocumentItem` already snapshots `hsnCode`/`gstRate`.
17
+ *
18
+ * a category edit -> changes future listings
19
+ * a tax-code edit -> changes future derivations
20
+ * nothing -> rewrites a past sale
21
+ *
22
+ * ## Why this exists at all
23
+ *
24
+ * GST has never been charged on goods here. Recorded in our own source
25
+ * (`_internal/server/features/checkout/actions.ts`): *"0 of 40 live orders had
26
+ * a `gstAmount`, and the order page's Tax row had never rendered."* And
27
+ * `gstRate` is set on zero of the current products.
28
+ *
29
+ * id === slug, prefix "tax-".
30
+ */
31
+ export const TAX_CODES_COLLECTION = "taxCodes";
32
+ export const TAX_CODE_PREFIX = "tax-";
33
+ export const GST_RATE_VALUES = [0, 5, 12, 18, 28];
34
+ export const TAX_CODE_UPDATABLE_FIELDS = [
35
+ "hsnCode",
36
+ "label",
37
+ "description",
38
+ "gstRate",
39
+ "chapter",
40
+ "effectiveFrom",
41
+ "isActive",
42
+ "notes",
43
+ ];
44
+ export const TAX_CODE_FIELDS = {
45
+ ID: "id",
46
+ HSN_CODE: "hsnCode",
47
+ LABEL: "label",
48
+ GST_RATE: "gstRate",
49
+ CHAPTER: "chapter",
50
+ IS_ACTIVE: "isActive",
51
+ EFFECTIVE_FROM: "effectiveFrom",
52
+ CREATED_AT: "createdAt",
53
+ UPDATED_AT: "updatedAt",
54
+ };
55
+ export const TAX_CODE_SIEVE_FIELDS = {
56
+ label: { canFilter: true, canSort: true },
57
+ hsnCode: { canFilter: true, canSort: true },
58
+ gstRate: { canFilter: true, canSort: true },
59
+ chapter: { canFilter: true, canSort: false },
60
+ isActive: { canFilter: true, canSort: false },
61
+ createdAt: { canFilter: false, canSort: true },
62
+ };
63
+ /**
64
+ * Derive the document id from the HSN code.
65
+ *
66
+ * Deterministic, so a seed run is idempotent (Root Cause #25) and so the same
67
+ * HSN cannot be entered twice under two ids — which would give two rows one
68
+ * admin edits and the other serves.
69
+ */
70
+ export function createTaxCodeId(hsnCode) {
71
+ const digits = hsnCode.replace(/\D/g, "");
72
+ return digits ? `${TAX_CODE_PREFIX}hsn-${digits}` : `${TAX_CODE_PREFIX}exempt-0`;
73
+ }
74
+ /**
75
+ * India's HSN codes are 4, 6 or 8 digits. Checked rather than trusted because
76
+ * the inline-create path in the seller form reaches the same route as the
77
+ * admin editor, and a malformed code propagates onto every product derived
78
+ * from the category that points at it.
79
+ */
80
+ export function isValidHsnCode(hsnCode) {
81
+ return /^(\d{4}|\d{6}|\d{8})$/.test(hsnCode.trim());
82
+ }
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ /** The GST slabs, as the rate select offers them. */
3
+ export declare const GST_RATE_OPTIONS: {
4
+ value: string;
5
+ label: string;
6
+ }[];
7
+ /**
8
+ * Chapter options.
9
+ *
10
+ * 🛑 The two non-9503 chapters are offered because the merchandise exists
11
+ * (printed cards and stickers; playing cards), and their labels say the rate is
12
+ * unverified rather than implying one. Picking a chapter does not set a rate.
13
+ */
14
+ export declare const HSN_CHAPTER_OPTIONS: {
15
+ value: string;
16
+ label: string;
17
+ }[];
18
+ /**
19
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
20
+ * by schema instance and every zod wrapper returns a new one.
21
+ */
22
+ export declare const taxCodeFormSchema: z.ZodEffects<z.ZodObject<{
23
+ label: z.ZodString;
24
+ hsnCode: z.ZodOptional<z.ZodString>;
25
+ gstRate: z.ZodEffects<z.ZodNumber, number, number>;
26
+ chapter: z.ZodOptional<z.ZodEnum<["9503", "4911", "9504", "exempt"]>>;
27
+ description: z.ZodOptional<z.ZodString>;
28
+ notes: z.ZodOptional<z.ZodString>;
29
+ isActive: z.ZodBoolean;
30
+ }, "strip", z.ZodTypeAny, {
31
+ label: string;
32
+ isActive: boolean;
33
+ gstRate: number;
34
+ description?: string | undefined;
35
+ notes?: string | undefined;
36
+ hsnCode?: string | undefined;
37
+ chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
38
+ }, {
39
+ label: string;
40
+ isActive: boolean;
41
+ gstRate: number;
42
+ description?: string | undefined;
43
+ notes?: string | undefined;
44
+ hsnCode?: string | undefined;
45
+ chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
46
+ }>, {
47
+ label: string;
48
+ isActive: boolean;
49
+ gstRate: number;
50
+ description?: string | undefined;
51
+ notes?: string | undefined;
52
+ hsnCode?: string | undefined;
53
+ chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
54
+ }, {
55
+ label: string;
56
+ isActive: boolean;
57
+ gstRate: number;
58
+ description?: string | undefined;
59
+ notes?: string | undefined;
60
+ hsnCode?: string | undefined;
61
+ chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
62
+ }>;
63
+ export type TaxCodeFormValues = z.infer<typeof taxCodeFormSchema>;
@@ -0,0 +1,152 @@
1
+ /*
2
+ * WHAT: The tax-code editor's schema — the field set, annotated for
3
+ * `buildSectionsFromSchema`, with the HSN-format rule as a refinement
4
+ * rather than a hint under the input.
5
+ *
6
+ * ## 🛑 Why the HSN rule is a refinement and not placeholder text
7
+ *
8
+ * A malformed HSN does not fail loudly. It propagates: a category points at
9
+ * the tax code, `deriveTaxonomy` writes `hsnCode` onto every product derived
10
+ * from that category, and the order item snapshots it onto a GST invoice.
11
+ * Nothing in that chain validates the string again, and the invoice is the
12
+ * first place a human might notice — by which point the sale is recorded.
13
+ *
14
+ * So the format is checked at the one place it enters the system. The same
15
+ * route serves the inline-create path in the seller form, which is the busiest
16
+ * surface and the one most likely to be used in a hurry.
17
+ *
18
+ * ## The exemption row is the reason `hsnCode` is not simply required
19
+ *
20
+ * A 0% row (live plants, live animals) has no HSN. Requiring one would force
21
+ * an author to invent a code to express an exemption, and an invented HSN on
22
+ * an invoice is exactly the failure above. So the refinement is conditional:
23
+ * blank is legal only at rate 0.
24
+ *
25
+ * 🛑 The two option arrays are exported for the VIEW to pass as
26
+ * `options: { gstRate: GST_RATE_OPTIONS, chapter: HSN_CHAPTER_OPTIONS }` —
27
+ * `FieldUiMeta` has no `options` key, and `annotate` would silently accept
28
+ * neither. That split follows `AdminBundleEditorView`, which supplies
29
+ * `BUNDLE_RULE_TYPE_OPTIONS` the same way: a select's VALUES are schema, its
30
+ * option list is often runtime data (brands, categories) and so belongs to
31
+ * whoever can fetch it.
32
+ *
33
+ * EXPORTS:
34
+ * taxCodeFormSchema, type TaxCodeFormValues, GST_RATE_OPTIONS,
35
+ * HSN_CHAPTER_OPTIONS
36
+ *
37
+ * @tag domain:tax-codes
38
+ * @tag layer:schema
39
+ * @tag pattern:none
40
+ * @tag access:isomorphic
41
+ * @tag consumers:AdminTaxCodeEditorView
42
+ * @tag sideEffects:none
43
+ */
44
+ import { z } from "zod";
45
+ import { annotate } from "../../shell/field-ui-meta";
46
+ import { GST_RATE_VALUES, isValidHsnCode } from "./firestore";
47
+ /** The GST slabs, as the rate select offers them. */
48
+ export const GST_RATE_OPTIONS = GST_RATE_VALUES.map((r) => ({
49
+ value: String(r),
50
+ label: r === 0 ? "0% — exempt" : `${r}%`,
51
+ }));
52
+ /**
53
+ * Chapter options.
54
+ *
55
+ * 🛑 The two non-9503 chapters are offered because the merchandise exists
56
+ * (printed cards and stickers; playing cards), and their labels say the rate is
57
+ * unverified rather than implying one. Picking a chapter does not set a rate.
58
+ */
59
+ export const HSN_CHAPTER_OPTIONS = [
60
+ { value: "9503", label: "9503 — Toys (verified)" },
61
+ { value: "4911", label: "4911 — Printed matter: cards, stickers, posters (rate unverified)" },
62
+ { value: "9504", label: "9504 — Playing cards, board games (rate unverified)" },
63
+ { value: "exempt", label: "Exempt — no HSN" },
64
+ ];
65
+ /**
66
+ * 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
67
+ * by schema instance and every zod wrapper returns a new one.
68
+ */
69
+ export const taxCodeFormSchema = z
70
+ .object({
71
+ label: annotate(z
72
+ .string()
73
+ .min(1, "Give the code a label an admin will recognise in a picker.")
74
+ .max(150, "Keep the label under 150 characters."), {
75
+ section: "basics",
76
+ sectionLabel: "Tax code",
77
+ sectionRequired: true,
78
+ quick: true,
79
+ order: 1,
80
+ label: "Label",
81
+ help: 'What the picker shows, e.g. "Toys, non-electronic (HSN 9503 0020) — 5%".',
82
+ }),
83
+ /*
84
+ * A STRING, not a number: leading zeros are significant in an HSN code
85
+ * (`04` is not `4`) and a numeric input would eat them. Digits-only is
86
+ * enforced by the refinement below, against the real 4/6/8-digit rule.
87
+ */
88
+ hsnCode: annotate(z.string().trim().max(8, "An HSN code is at most 8 digits.").optional(), {
89
+ section: "basics",
90
+ quick: true,
91
+ order: 2,
92
+ row: "pair",
93
+ label: "HSN code",
94
+ help: "4, 6 or 8 digits. Leave blank only for a 0% exemption.",
95
+ }),
96
+ gstRate: annotate(z.coerce
97
+ .number()
98
+ .refine((v) => GST_RATE_VALUES.includes(v), {
99
+ message: "Pick one of the GST slabs: 0, 5, 12, 18 or 28.",
100
+ }), {
101
+ section: "basics",
102
+ sectionRequired: true,
103
+ quick: true,
104
+ order: 3,
105
+ row: "pair",
106
+ kind: "select",
107
+ label: "GST rate",
108
+ }),
109
+ chapter: annotate(z.enum(["9503", "4911", "9504", "exempt"]).optional(), {
110
+ section: "basics",
111
+ order: 4,
112
+ kind: "select",
113
+ label: "Chapter",
114
+ help: "Groups the picker. Only chapter 9503's rates are verified.",
115
+ }),
116
+ description: annotate(z.string().max(500).optional(), { section: "detail", sectionLabel: "Detail", order: 1, kind: "textarea", label: "Description" }),
117
+ notes: annotate(z.string().max(1000).optional(), {
118
+ section: "detail",
119
+ order: 2,
120
+ kind: "textarea",
121
+ label: "Notes",
122
+ help: "Why this rate, and what it covers. The thing a future admin will want.",
123
+ }),
124
+ isActive: annotate(z.boolean(), {
125
+ section: "detail",
126
+ order: 3,
127
+ label: "Active",
128
+ help: "An inactive code is never resolved onto a product, and disappears from the picker.",
129
+ }),
130
+ })
131
+ .superRefine((values, ctx) => {
132
+ const hsn = values.hsnCode?.trim() ?? "";
133
+ // Blank is legal ONLY at 0%. See the header: forcing an HSN on an exemption
134
+ // is what gets a code invented, and an invented code reaches an invoice.
135
+ if (hsn === "") {
136
+ if (values.gstRate !== 0) {
137
+ ctx.addIssue({
138
+ code: z.ZodIssueCode.custom,
139
+ path: ["hsnCode"],
140
+ message: "An HSN code is required for any rate above 0%. Leave it blank only to record an exemption.",
141
+ });
142
+ }
143
+ return;
144
+ }
145
+ if (!isValidHsnCode(hsn)) {
146
+ ctx.addIssue({
147
+ code: z.ZodIssueCode.custom,
148
+ path: ["hsnCode"],
149
+ message: "An HSN code must be exactly 4, 6 or 8 digits — no spaces or dots.",
150
+ });
151
+ }
152
+ });
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Zod validators for the `/api/admin/tax-codes` route handlers.
3
+ *
4
+ * Separate from `tax-code-form.ts`, which is the EDITOR's schema: that one is
5
+ * annotated for `buildSectionsFromSchema`, takes strings from inputs, and
6
+ * coerces. These are the wire contracts, and they are `.strict()`.
7
+ *
8
+ * ## 🛑 `.strict()`, not `z.object()` and not `.passthrough()`
9
+ *
10
+ * `z.object()` STRIPS unknown keys, which is how a PATCH returns 200 having
11
+ * written nothing (Root Cause #40's shape, and the admin grouped-listings
12
+ * route's documented bug). `.passthrough()` writes whatever arrives, which is
13
+ * how a form overwrote booked lottery slots. `.strict()` 400s on a field the
14
+ * contract does not name — the caller is told, and nothing silently vanishes.
15
+ *
16
+ * ## The id is NOT accepted on update
17
+ *
18
+ * `createTaxCodeId` derives the id from the HSN code, so accepting an id (or
19
+ * re-deriving one on update) would let an edit orphan every
20
+ * `CategoryProductDefaults.taxCodeId` pointing at the old one — with a GST
21
+ * invoice at the end of that chain. To correct a wrong HSN: deactivate, create
22
+ * the right row, repoint the categories.
23
+ */
24
+ import { z } from "zod";
25
+ /**
26
+ * 🛑 `z.custom<GstRate>`, not `z.number().refine(...)`.
27
+ *
28
+ * A `.refine()` validates at runtime and does NOT narrow the inferred type —
29
+ * the payload still types as `number`, so `repository.create(body)` fails to
30
+ * compile against `gstRate: GstRate`, and the obvious unblock is a cast. A
31
+ * cast at a request boundary is where this codebase's worst bugs live (an
32
+ * `ActionResult` envelope spread as a product, a raw `StoreDocument` published
33
+ * as HTML), so the type is narrowed here instead.
34
+ *
35
+ * And `z.custom` rather than a hand-written `z.union([z.literal(0), …])`
36
+ * because the slab list then has ONE definition — `GST_RATE_VALUES`, which
37
+ * `ProductDocument.gstRate` and the form's option list also derive from. A
38
+ * literal union here would be a second enumeration of the same union, which is
39
+ * Root Cause #61's shape.
40
+ */
41
+ export declare const GST_RATE_ENUM: z.ZodType<0 | 5 | 12 | 18 | 28, z.ZodTypeDef, 0 | 5 | 12 | 18 | 28>;
42
+ export declare const HSN_CHAPTER_ENUM: z.ZodEnum<["9503", "4911", "9504", "exempt"]>;
43
+ /**
44
+ * 🛑 `hsnCode` is OPTIONAL here and the blank-only-at-0% rule lives in the
45
+ * repository, not this schema. Deliberate: the same rule has to hold for the
46
+ * seed path and any future importer, neither of which goes through a Zod
47
+ * request schema. A rule enforced only at the HTTP edge is a rule with a side
48
+ * door.
49
+ */
50
+ export declare const taxCodeCreateSchema: z.ZodObject<{
51
+ hsnCode: z.ZodDefault<z.ZodString>;
52
+ label: z.ZodString;
53
+ description: z.ZodOptional<z.ZodString>;
54
+ gstRate: z.ZodType<0 | 5 | 12 | 18 | 28, z.ZodTypeDef, 0 | 5 | 12 | 18 | 28>;
55
+ chapter: z.ZodOptional<z.ZodEnum<["9503", "4911", "9504", "exempt"]>>;
56
+ effectiveFrom: z.ZodOptional<z.ZodDate>;
57
+ isActive: z.ZodDefault<z.ZodBoolean>;
58
+ notes: z.ZodOptional<z.ZodString>;
59
+ }, "strict", z.ZodTypeAny, {
60
+ label: string;
61
+ isActive: boolean;
62
+ gstRate: 0 | 5 | 12 | 18 | 28;
63
+ hsnCode: string;
64
+ description?: string | undefined;
65
+ notes?: string | undefined;
66
+ chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
67
+ effectiveFrom?: Date | undefined;
68
+ }, {
69
+ label: string;
70
+ gstRate: 0 | 5 | 12 | 18 | 28;
71
+ description?: string | undefined;
72
+ isActive?: boolean | undefined;
73
+ notes?: string | undefined;
74
+ hsnCode?: string | undefined;
75
+ chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
76
+ effectiveFrom?: Date | undefined;
77
+ }>;
78
+ export type TaxCodeCreatePayload = z.infer<typeof taxCodeCreateSchema>;
79
+ export declare const taxCodeUpdateSchema: z.ZodObject<{
80
+ hsnCode: z.ZodOptional<z.ZodString>;
81
+ label: z.ZodOptional<z.ZodString>;
82
+ description: z.ZodOptional<z.ZodString>;
83
+ gstRate: z.ZodOptional<z.ZodType<0 | 5 | 12 | 18 | 28, z.ZodTypeDef, 0 | 5 | 12 | 18 | 28>>;
84
+ chapter: z.ZodOptional<z.ZodEnum<["9503", "4911", "9504", "exempt"]>>;
85
+ effectiveFrom: z.ZodOptional<z.ZodDate>;
86
+ isActive: z.ZodOptional<z.ZodBoolean>;
87
+ notes: z.ZodOptional<z.ZodString>;
88
+ }, "strict", z.ZodTypeAny, {
89
+ label?: string | undefined;
90
+ description?: string | undefined;
91
+ isActive?: boolean | undefined;
92
+ notes?: string | undefined;
93
+ gstRate?: 0 | 5 | 12 | 18 | 28 | undefined;
94
+ hsnCode?: string | undefined;
95
+ chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
96
+ effectiveFrom?: Date | undefined;
97
+ }, {
98
+ label?: string | undefined;
99
+ description?: string | undefined;
100
+ isActive?: boolean | undefined;
101
+ notes?: string | undefined;
102
+ gstRate?: 0 | 5 | 12 | 18 | 28 | undefined;
103
+ hsnCode?: string | undefined;
104
+ chapter?: "9503" | "4911" | "9504" | "exempt" | undefined;
105
+ effectiveFrom?: Date | undefined;
106
+ }>;
107
+ export type TaxCodeUpdatePayload = z.infer<typeof taxCodeUpdateSchema>;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Zod validators for the `/api/admin/tax-codes` route handlers.
3
+ *
4
+ * Separate from `tax-code-form.ts`, which is the EDITOR's schema: that one is
5
+ * annotated for `buildSectionsFromSchema`, takes strings from inputs, and
6
+ * coerces. These are the wire contracts, and they are `.strict()`.
7
+ *
8
+ * ## 🛑 `.strict()`, not `z.object()` and not `.passthrough()`
9
+ *
10
+ * `z.object()` STRIPS unknown keys, which is how a PATCH returns 200 having
11
+ * written nothing (Root Cause #40's shape, and the admin grouped-listings
12
+ * route's documented bug). `.passthrough()` writes whatever arrives, which is
13
+ * how a form overwrote booked lottery slots. `.strict()` 400s on a field the
14
+ * contract does not name — the caller is told, and nothing silently vanishes.
15
+ *
16
+ * ## The id is NOT accepted on update
17
+ *
18
+ * `createTaxCodeId` derives the id from the HSN code, so accepting an id (or
19
+ * re-deriving one on update) would let an edit orphan every
20
+ * `CategoryProductDefaults.taxCodeId` pointing at the old one — with a GST
21
+ * invoice at the end of that chain. To correct a wrong HSN: deactivate, create
22
+ * the right row, repoint the categories.
23
+ */
24
+ import { z } from "zod";
25
+ import { GST_RATE_VALUES } from "./firestore";
26
+ const LABEL_MAX = 150;
27
+ const DESCRIPTION_MAX = 500;
28
+ const NOTES_MAX = 1000;
29
+ /**
30
+ * 🛑 `z.custom<GstRate>`, not `z.number().refine(...)`.
31
+ *
32
+ * A `.refine()` validates at runtime and does NOT narrow the inferred type —
33
+ * the payload still types as `number`, so `repository.create(body)` fails to
34
+ * compile against `gstRate: GstRate`, and the obvious unblock is a cast. A
35
+ * cast at a request boundary is where this codebase's worst bugs live (an
36
+ * `ActionResult` envelope spread as a product, a raw `StoreDocument` published
37
+ * as HTML), so the type is narrowed here instead.
38
+ *
39
+ * And `z.custom` rather than a hand-written `z.union([z.literal(0), …])`
40
+ * because the slab list then has ONE definition — `GST_RATE_VALUES`, which
41
+ * `ProductDocument.gstRate` and the form's option list also derive from. A
42
+ * literal union here would be a second enumeration of the same union, which is
43
+ * Root Cause #61's shape.
44
+ */
45
+ export const GST_RATE_ENUM = z.custom((v) => typeof v === "number" && GST_RATE_VALUES.includes(v), { message: "gstRate must be one of 0, 5, 12, 18 or 28." });
46
+ export const HSN_CHAPTER_ENUM = z.enum(["9503", "4911", "9504", "exempt"]);
47
+ /**
48
+ * 🛑 `hsnCode` is OPTIONAL here and the blank-only-at-0% rule lives in the
49
+ * repository, not this schema. Deliberate: the same rule has to hold for the
50
+ * seed path and any future importer, neither of which goes through a Zod
51
+ * request schema. A rule enforced only at the HTTP edge is a rule with a side
52
+ * door.
53
+ */
54
+ export const taxCodeCreateSchema = z
55
+ .object({
56
+ /*
57
+ * Omitted becomes `""`, not `undefined`, because `TaxCodeDocument.hsnCode`
58
+ * is REQUIRED and the exemption row's value genuinely is the empty string.
59
+ * An optional-into-required field here would force a cast at the one call
60
+ * site, and "no HSN" and "nobody filled this in" would become
61
+ * indistinguishable — exactly the `undefined`-vs-`0` distinction `gstRate`
62
+ * depends on, one field over.
63
+ */
64
+ hsnCode: z.string().trim().max(8).default(""),
65
+ label: z.string().trim().min(1).max(LABEL_MAX),
66
+ description: z.string().max(DESCRIPTION_MAX).optional(),
67
+ gstRate: GST_RATE_ENUM,
68
+ chapter: HSN_CHAPTER_ENUM.optional(),
69
+ effectiveFrom: z.coerce.date().optional(),
70
+ isActive: z.boolean().default(true),
71
+ notes: z.string().max(NOTES_MAX).optional(),
72
+ })
73
+ .strict();
74
+ export const taxCodeUpdateSchema = z
75
+ .object({
76
+ hsnCode: z.string().trim().max(8).optional(),
77
+ label: z.string().trim().min(1).max(LABEL_MAX).optional(),
78
+ description: z.string().max(DESCRIPTION_MAX).optional(),
79
+ gstRate: GST_RATE_ENUM.optional(),
80
+ chapter: HSN_CHAPTER_ENUM.optional(),
81
+ effectiveFrom: z.coerce.date().optional(),
82
+ isActive: z.boolean().optional(),
83
+ notes: z.string().max(NOTES_MAX).optional(),
84
+ })
85
+ .strict();
@@ -1,2 +1 @@
1
1
  export * from "./checklist-item-actions";
2
- export * from "./leaderboard-actions";
@@ -1,2 +1 @@
1
1
  export * from "./checklist-item-actions";
2
- export * from "./leaderboard-actions";
@@ -4,7 +4,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query";
4
4
  import { Button, Card, Details, Div, Row, Stack, Summary, Text } from "../../../ui";
5
5
  import { MediaImage } from "../../media/MediaImage";
6
6
  import { apiClient } from "../../../http";
7
- import { useApiMutation } from "../../../client";
7
+ import { useApiMutation } from "../../../client/api/useApiMutation";
8
8
  import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
9
9
  import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
10
10
  const REPORT_QUERY_KEY = ["admin", "tester-feedback", "report"];
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { useApiMutation } from "../../../client";
3
+ import { useApiMutation } from "../../../client/api/useApiMutation";
4
4
  import { SIEVE_OP, sieveFilter } from "../../../utils/sieve-builder";
5
5
  import { sortBy } from "../../../constants/sort";
6
6
  import { TESTER_CHECKLIST_RESPONSE_FIELDS } from "../schemas/firestore";
@@ -1,5 +1,3 @@
1
- export { TesterHubView } from "./TesterHubView";
2
- export type { TesterHubViewProps } from "./TesterHubView";
3
1
  export { TesterChecklistStepRow } from "./TesterChecklistStepRow";
4
2
  export type { TesterChecklistStepRowProps, TesterChecklistStepItem } from "./TesterChecklistStepRow";
5
3
  export { TesterFeedbackChart } from "./TesterFeedbackChart";
@@ -9,5 +7,3 @@ export { AdminTesterFeedbackReportView } from "./AdminTesterFeedbackReportView";
9
7
  export { AdminTesterFeedbackIssuesView } from "./AdminTesterFeedbackIssuesView";
10
8
  export { AdminTesterFeedbackListView } from "./AdminTesterFeedbackListView";
11
9
  export type { AdminTesterFeedbackListViewProps } from "./AdminTesterFeedbackListView";
12
- export { BugHunterLeaderboardView } from "./BugHunterLeaderboardView";
13
- export type { BugHunterLeaderboardViewProps } from "./BugHunterLeaderboardView";
@@ -1,8 +1,6 @@
1
- export { TesterHubView } from "./TesterHubView";
2
1
  export { TesterChecklistStepRow } from "./TesterChecklistStepRow";
3
2
  export { TesterFeedbackChart } from "./TesterFeedbackChart";
4
3
  export { AdminTesterFeedbackView } from "./AdminTesterFeedbackView";
5
4
  export { AdminTesterFeedbackReportView } from "./AdminTesterFeedbackReportView";
6
5
  export { AdminTesterFeedbackIssuesView } from "./AdminTesterFeedbackIssuesView";
7
6
  export { AdminTesterFeedbackListView } from "./AdminTesterFeedbackListView";
8
- export { BugHunterLeaderboardView } from "./BugHunterLeaderboardView";
@@ -1,6 +1,6 @@
1
1
  import { BaseRepository } from "../../../providers/db-firebase";
2
2
  import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
3
- import { type TesterChecklistItemDocument, type TesterChecklistItemCreateInput, type TesterChecklistItemUpdateInput, type BugHunterLeaderboardEntry } from "../schemas/firestore";
3
+ import { type TesterChecklistItemDocument, type TesterChecklistItemCreateInput, type TesterChecklistItemUpdateInput } from "../schemas/firestore";
4
4
  export declare class TesterChecklistItemRepository extends BaseRepository<TesterChecklistItemDocument> {
5
5
  static readonly SIEVE_FIELDS: FirebaseSieveFields;
6
6
  constructor();
@@ -15,24 +15,5 @@ export declare class TesterChecklistItemRepository extends BaseRepository<Tester
15
15
  /** Reopens a fixed, bug-confirmed case as a new, active version for retest.
16
16
  * The old item stays disabled forever with its bug-hunter credit intact. */
17
17
  reopenAsNewVersion(oldItemId: string): Promise<TesterChecklistItemDocument>;
18
- /**
19
- * Ids of automated (non-human) accounts, excluded from the PUBLIC leaderboard.
20
- *
21
- * Read as a bare collection query rather than through `userRepository` on purpose:
22
- * no repository in this codebase imports another, and this needs a key set, not
23
- * user documents. `.select()` with no fields returns refs only, so the read is as
24
- * cheap as Firestore allows, and `isBot == true` is a single-field equality served
25
- * by the automatic index — no composite index to declare.
26
- */
27
- private botHunterIds;
28
- /** Single-query, in-memory aggregation of bug credits per hunter — mirrors
29
- * EventEntryRepository.getLeaderboard()'s shape. Includes old/disabled/
30
- * superseded items, since bug credit is permanent.
31
- *
32
- * Bot accounts are aggregated out (not merely ranked last): the board exists to
33
- * credit people, and a runner working all 943 cases would otherwise dominate it.
34
- * The credit still lives on the item, so admin triage and the item's own
35
- * `bugHunterName` are unaffected. */
36
- getBugHunterLeaderboard(limit?: number): Promise<BugHunterLeaderboardEntry[]>;
37
18
  }
38
19
  export declare const testerChecklistItemRepository: TesterChecklistItemRepository;
@@ -1,7 +1,6 @@
1
1
  import { BaseRepository, parseSieveDateValue } from "../../../providers/db-firebase";
2
2
  import { buildSearchTxt } from "../../../utils/search-txt";
3
3
  import { DatabaseError } from "../../../errors";
4
- import { USER_COLLECTION } from "../../auth/schemas/firestore";
5
4
  import { TESTER_CHECKLIST_ITEM_COLLECTION, TESTER_CHECKLIST_ITEM_FIELDS, createChecklistItemId, } from "../schemas/firestore";
6
5
  export class TesterChecklistItemRepository extends BaseRepository {
7
6
  constructor() {
@@ -87,57 +86,6 @@ export class TesterChecklistItemRepository extends BaseRepository {
87
86
  await this.update(old.id, { supersededByItemId: newId });
88
87
  return newItem;
89
88
  }
90
- /**
91
- * Ids of automated (non-human) accounts, excluded from the PUBLIC leaderboard.
92
- *
93
- * Read as a bare collection query rather than through `userRepository` on purpose:
94
- * no repository in this codebase imports another, and this needs a key set, not
95
- * user documents. `.select()` with no fields returns refs only, so the read is as
96
- * cheap as Firestore allows, and `isBot == true` is a single-field equality served
97
- * by the automatic index — no composite index to declare.
98
- */
99
- async botHunterIds() {
100
- const snapshot = await this.db
101
- .collection(USER_COLLECTION)
102
- .where("isBot", "==", true)
103
- .select()
104
- .get();
105
- return new Set(snapshot.docs.map((d) => d.id));
106
- }
107
- /** Single-query, in-memory aggregation of bug credits per hunter — mirrors
108
- * EventEntryRepository.getLeaderboard()'s shape. Includes old/disabled/
109
- * superseded items, since bug credit is permanent.
110
- *
111
- * Bot accounts are aggregated out (not merely ranked last): the board exists to
112
- * credit people, and a runner working all 943 cases would otherwise dominate it.
113
- * The credit still lives on the item, so admin triage and the item's own
114
- * `bugHunterName` are unaffected. */
115
- async getBugHunterLeaderboard(limit = 50) {
116
- const [snapshot, botIds] = await Promise.all([
117
- this.db
118
- .collection(this.collection)
119
- .where(TESTER_CHECKLIST_ITEM_FIELDS.BUG_CONFIRMED, "==", true)
120
- .get(),
121
- this.botHunterIds(),
122
- ]);
123
- const byHunter = new Map();
124
- for (const doc of snapshot.docs) {
125
- const item = this.mapDoc(doc);
126
- if (!item.bugHunterId || botIds.has(item.bugHunterId))
127
- continue;
128
- const entry = byHunter.get(item.bugHunterId) ?? {
129
- name: item.bugHunterName ?? "Unknown tester",
130
- count: 0,
131
- };
132
- entry.count += 1;
133
- byHunter.set(item.bugHunterId, entry);
134
- }
135
- return Array.from(byHunter.entries())
136
- .map(([hunterId, v]) => ({ hunterId, hunterName: v.name, bugCount: v.count }))
137
- .sort((a, b) => b.bugCount - a.bugCount)
138
- .slice(0, limit)
139
- .map((entry, index) => ({ ...entry, rank: index + 1 }));
140
- }
141
89
  }
142
90
  TesterChecklistItemRepository.SIEVE_FIELDS = {
143
91
  groupKey: { canFilter: true, canSort: true },
@@ -164,12 +164,6 @@ export declare const testerChecklistItemQueryHelpers: {
164
164
  readonly active: () => readonly ["isActive", "==", true];
165
165
  readonly confirmedBugs: () => readonly ["bugConfirmed", "==", true];
166
166
  };
167
- export interface BugHunterLeaderboardEntry {
168
- rank: number;
169
- hunterId: string;
170
- hunterName: string;
171
- bugCount: number;
172
- }
173
167
  export declare function slugifyChecklistLabel(label: string): string;
174
168
  export declare function createChecklistItemId(groupKey: string, pageKey: string, label: string): string;
175
169
  /**