@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
|
@@ -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();
|
|
@@ -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
|
|
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
|
/**
|