@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,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Price Index Repository — what things actually SOLD for here, per leaf.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Why this is its own collection and not a field on `categories`
|
|
5
|
+
*
|
|
6
|
+
* The obvious design is `categories/{leafId}.productDefaults.priceGuidance.sold`,
|
|
7
|
+
* beside the seed-owned `market` band it is shown next to. That design is a
|
|
8
|
+
* runaway-trigger hazard and it is not a theoretical one.
|
|
9
|
+
*
|
|
10
|
+
* `onCategoryWrite` is a `documentWritten` trigger on `categories`, so a job
|
|
11
|
+
* writing a price band there re-triggers it once per document. That is exactly
|
|
12
|
+
* the shape of Root Cause #92, where `onShipmentHeaderWrite` wrote back to the
|
|
13
|
+
* collection it watched and produced **1,017,548 invocations in 24 hours**
|
|
14
|
+
* against a 2M/month allowance, blocked `firebase deploy` with a billing
|
|
15
|
+
* denial, and degraded production.
|
|
16
|
+
*
|
|
17
|
+
* It is also worse than that here, because `notifyRevalidate` fires on every
|
|
18
|
+
* category write — so each nightly price write would drop that category page's
|
|
19
|
+
* ISR entry, on a project that was paused for exceeding ISR writes by 15.5×.
|
|
20
|
+
*
|
|
21
|
+
* A sibling collection removes the self-write entirely. It costs one extra
|
|
22
|
+
* read on the surface that shows the band, and buys immunity.
|
|
23
|
+
*
|
|
24
|
+
* ## Shape
|
|
25
|
+
*
|
|
26
|
+
* priceIndex/{leafId} the current band
|
|
27
|
+
* priceIndex/{leafId}/daily/{YYYYMMDD} one point per day, for a series
|
|
28
|
+
*
|
|
29
|
+
* 🛑 NO `soldPrices: number[]` on the document. `CategoryDocument.metrics`
|
|
30
|
+
* already carries two unbounded arrays and a third is the 1 MiB trap. The
|
|
31
|
+
* samples stay in job memory and only the summary is written.
|
|
32
|
+
*/
|
|
33
|
+
import { BaseRepository } from "../../../providers/db-firebase";
|
|
34
|
+
/** Which acquisition path a sale came through. Counts, not amounts. */
|
|
35
|
+
export interface PriceIndexByPath {
|
|
36
|
+
standard?: number;
|
|
37
|
+
auction?: number;
|
|
38
|
+
offer?: number;
|
|
39
|
+
}
|
|
40
|
+
export interface PriceIndexDocument {
|
|
41
|
+
/** The LEAF category id. A sale rolls up to exactly one leaf. */
|
|
42
|
+
id: string;
|
|
43
|
+
currency: "INR";
|
|
44
|
+
/**
|
|
45
|
+
* Scoped by listing type WITHIN the leaf.
|
|
46
|
+
*
|
|
47
|
+
* 🛑 Load-bearing. A named-model leaf holds every listing type for that
|
|
48
|
+
* model — the bey, its stickers, its spare parts, a used one as a classified
|
|
49
|
+
* — because `listingType` and `categorySlugs` are independent axes. A ₹399
|
|
50
|
+
* sticker sheet and a ₹1,299 bey under one leaf would blend into a
|
|
51
|
+
* meaningless band, so the band is per type and the UI shows the one the
|
|
52
|
+
* seller is listing.
|
|
53
|
+
*/
|
|
54
|
+
byListingType: Record<string, PriceIndexBand>;
|
|
55
|
+
/** How many days of completed sales the bands cover. */
|
|
56
|
+
windowDays: number;
|
|
57
|
+
computedAt: Date;
|
|
58
|
+
}
|
|
59
|
+
export interface PriceIndexBand {
|
|
60
|
+
min: number;
|
|
61
|
+
median: number;
|
|
62
|
+
max: number;
|
|
63
|
+
/**
|
|
64
|
+
* 🛑 The confidence signal, and the UI must render a small one as a sample
|
|
65
|
+
* size with NO headline number — the same discipline as `truncated`
|
|
66
|
+
* rendering "50+" rather than a false total. A median of two sales is a coin
|
|
67
|
+
* flip presented as data.
|
|
68
|
+
*/
|
|
69
|
+
sampleSize: number;
|
|
70
|
+
byPath?: PriceIndexByPath;
|
|
71
|
+
lastSold?: {
|
|
72
|
+
amount: number;
|
|
73
|
+
at: Date;
|
|
74
|
+
via?: "standard" | "auction" | "offer";
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/** One day's point, for a series. */
|
|
78
|
+
export interface PriceIndexDailyPoint {
|
|
79
|
+
id: string;
|
|
80
|
+
/** `YYYYMMDD`, so the document id sorts chronologically on its own. */
|
|
81
|
+
date: string;
|
|
82
|
+
byListingType: Record<string, Pick<PriceIndexBand, "median" | "sampleSize">>;
|
|
83
|
+
computedAt: Date;
|
|
84
|
+
}
|
|
85
|
+
export declare const PRICE_INDEX_COLLECTION = "priceIndex";
|
|
86
|
+
/**
|
|
87
|
+
* Below this, a band renders as a sample size and no headline figure.
|
|
88
|
+
*
|
|
89
|
+
* Exported because the renderer and the job must agree: a threshold the job
|
|
90
|
+
* applied and the UI did not would hide bands that exist, and the reverse
|
|
91
|
+
* would publish a median of one sale as a market rate.
|
|
92
|
+
*/
|
|
93
|
+
export declare const PRICE_INDEX_MIN_CONFIDENT_SAMPLES = 3;
|
|
94
|
+
export declare class PriceIndexRepository extends BaseRepository<PriceIndexDocument> {
|
|
95
|
+
constructor();
|
|
96
|
+
getForLeaf(leafId: string): Promise<PriceIndexDocument | null>;
|
|
97
|
+
/**
|
|
98
|
+
* Write the current band for one leaf.
|
|
99
|
+
*
|
|
100
|
+
* `createWithId`/`update` rather than a blind `set`, matching every other
|
|
101
|
+
* rollup repository here — and `update` merges, so a leaf whose band for one
|
|
102
|
+
* listing type disappeared this run would keep the stale entry. Hence
|
|
103
|
+
* `byListingType` is replaced wholesale by passing the full map.
|
|
104
|
+
*/
|
|
105
|
+
setForLeaf(leafId: string, data: Pick<PriceIndexDocument, "byListingType" | "windowDays">): Promise<void>;
|
|
106
|
+
/**
|
|
107
|
+
* Append today's point.
|
|
108
|
+
*
|
|
109
|
+
* Keyed on the date, so a re-run on the same day OVERWRITES rather than
|
|
110
|
+
* double-counting — the same idempotency `pageViewRollup`'s `throughDate`
|
|
111
|
+
* buys, achieved here by the document id instead of a cursor.
|
|
112
|
+
*/
|
|
113
|
+
setDailyPoint(leafId: string, date: string, byListingType: PriceIndexDailyPoint["byListingType"]): Promise<void>;
|
|
114
|
+
}
|
|
115
|
+
export declare const priceIndexRepository: PriceIndexRepository;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Price Index Repository — what things actually SOLD for here, per leaf.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Why this is its own collection and not a field on `categories`
|
|
5
|
+
*
|
|
6
|
+
* The obvious design is `categories/{leafId}.productDefaults.priceGuidance.sold`,
|
|
7
|
+
* beside the seed-owned `market` band it is shown next to. That design is a
|
|
8
|
+
* runaway-trigger hazard and it is not a theoretical one.
|
|
9
|
+
*
|
|
10
|
+
* `onCategoryWrite` is a `documentWritten` trigger on `categories`, so a job
|
|
11
|
+
* writing a price band there re-triggers it once per document. That is exactly
|
|
12
|
+
* the shape of Root Cause #92, where `onShipmentHeaderWrite` wrote back to the
|
|
13
|
+
* collection it watched and produced **1,017,548 invocations in 24 hours**
|
|
14
|
+
* against a 2M/month allowance, blocked `firebase deploy` with a billing
|
|
15
|
+
* denial, and degraded production.
|
|
16
|
+
*
|
|
17
|
+
* It is also worse than that here, because `notifyRevalidate` fires on every
|
|
18
|
+
* category write — so each nightly price write would drop that category page's
|
|
19
|
+
* ISR entry, on a project that was paused for exceeding ISR writes by 15.5×.
|
|
20
|
+
*
|
|
21
|
+
* A sibling collection removes the self-write entirely. It costs one extra
|
|
22
|
+
* read on the surface that shows the band, and buys immunity.
|
|
23
|
+
*
|
|
24
|
+
* ## Shape
|
|
25
|
+
*
|
|
26
|
+
* priceIndex/{leafId} the current band
|
|
27
|
+
* priceIndex/{leafId}/daily/{YYYYMMDD} one point per day, for a series
|
|
28
|
+
*
|
|
29
|
+
* 🛑 NO `soldPrices: number[]` on the document. `CategoryDocument.metrics`
|
|
30
|
+
* already carries two unbounded arrays and a third is the 1 MiB trap. The
|
|
31
|
+
* samples stay in job memory and only the summary is written.
|
|
32
|
+
*/
|
|
33
|
+
import { BaseRepository } from "../../../providers/db-firebase";
|
|
34
|
+
export const PRICE_INDEX_COLLECTION = "priceIndex";
|
|
35
|
+
const DAILY_SUBCOLLECTION = "daily";
|
|
36
|
+
/**
|
|
37
|
+
* Below this, a band renders as a sample size and no headline figure.
|
|
38
|
+
*
|
|
39
|
+
* Exported because the renderer and the job must agree: a threshold the job
|
|
40
|
+
* applied and the UI did not would hide bands that exist, and the reverse
|
|
41
|
+
* would publish a median of one sale as a market rate.
|
|
42
|
+
*/
|
|
43
|
+
export const PRICE_INDEX_MIN_CONFIDENT_SAMPLES = 3;
|
|
44
|
+
export class PriceIndexRepository extends BaseRepository {
|
|
45
|
+
constructor() {
|
|
46
|
+
super(PRICE_INDEX_COLLECTION);
|
|
47
|
+
}
|
|
48
|
+
async getForLeaf(leafId) {
|
|
49
|
+
return this.findById(leafId);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Write the current band for one leaf.
|
|
53
|
+
*
|
|
54
|
+
* `createWithId`/`update` rather than a blind `set`, matching every other
|
|
55
|
+
* rollup repository here — and `update` merges, so a leaf whose band for one
|
|
56
|
+
* listing type disappeared this run would keep the stale entry. Hence
|
|
57
|
+
* `byListingType` is replaced wholesale by passing the full map.
|
|
58
|
+
*/
|
|
59
|
+
async setForLeaf(leafId, data) {
|
|
60
|
+
const payload = { currency: "INR", ...data, computedAt: new Date() };
|
|
61
|
+
const existing = await this.findById(leafId);
|
|
62
|
+
if (existing) {
|
|
63
|
+
await this.update(leafId, payload);
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
await this.createWithId(leafId, payload);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Append today's point.
|
|
71
|
+
*
|
|
72
|
+
* Keyed on the date, so a re-run on the same day OVERWRITES rather than
|
|
73
|
+
* double-counting — the same idempotency `pageViewRollup`'s `throughDate`
|
|
74
|
+
* buys, achieved here by the document id instead of a cursor.
|
|
75
|
+
*/
|
|
76
|
+
async setDailyPoint(leafId, date, byListingType) {
|
|
77
|
+
await this.db
|
|
78
|
+
.collection(PRICE_INDEX_COLLECTION)
|
|
79
|
+
.doc(leafId)
|
|
80
|
+
.collection(DAILY_SUBCOLLECTION)
|
|
81
|
+
.doc(date)
|
|
82
|
+
.set({ date, byListingType, computedAt: new Date() }, { merge: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
export const priceIndexRepository = new PriceIndexRepository();
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category-owned content — description templates and product defaults.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this lives on the CATEGORY and not on a listing template
|
|
5
|
+
*
|
|
6
|
+
* `ListingTemplateDocument` is store-scoped. The ask is that the description
|
|
7
|
+
* lives with the *category*, and the crawl proves the model: worldhobbyshop
|
|
8
|
+
* reuses TWO bodies across 1,195 listings, and tcgindia generates five
|
|
9
|
+
* templated FAQs per product. Nobody writes 1,195 descriptions by hand — our
|
|
10
|
+
* sellers do, because no category→description mechanism exists anywhere in
|
|
11
|
+
* this codebase.
|
|
12
|
+
*
|
|
13
|
+
* ## Why it costs ZERO extra reads
|
|
14
|
+
*
|
|
15
|
+
* Both hot paths already hold the category document: `deriveTaxonomy` does
|
|
16
|
+
* `cats.doc(leaf).get()`, and `CategoryDetailPageView` does `getCategoryBySlug`
|
|
17
|
+
* plus seven parallel reads — already at Rule #6's ~3-round-trip ceiling. A
|
|
18
|
+
* sidecar collection would add an eighth read to the second-most-crawled page
|
|
19
|
+
* on the site.
|
|
20
|
+
*
|
|
21
|
+
* ## Why three fields rather than one blob
|
|
22
|
+
*
|
|
23
|
+
* Each has exactly one reader: `contentBody` → the category page;
|
|
24
|
+
* `descriptionTemplates` → the seller form plus the narrow template endpoint;
|
|
25
|
+
* `productDefaults` → `deriveTaxonomy`. A single `content` object would make
|
|
26
|
+
* every one of those read the other two.
|
|
27
|
+
*/
|
|
28
|
+
import type { ProductAuthenticity } from "../../products/schemas/firestore";
|
|
29
|
+
/**
|
|
30
|
+
* Which condition a template is written for.
|
|
31
|
+
*
|
|
32
|
+
* 🛑 A KEYED SET, not one body, because worldhobbyshop demonstrably reuses
|
|
33
|
+
* **two** per category — a pre-owned body and a new-in-box body — and a single
|
|
34
|
+
* string field cannot express that. The variants are the conditions that
|
|
35
|
+
* change what the copy must SAY, not every `CONDITION_OPTIONS` value: a
|
|
36
|
+
* "pre-owned" and a "used" listing want the same disclosure paragraph.
|
|
37
|
+
*/
|
|
38
|
+
export type CategoryTemplateVariant = "default" | "new_in_box" | "new_in_packet" | "pre_owned" | "replica";
|
|
39
|
+
export interface CategoryDescriptionTemplate {
|
|
40
|
+
id: string;
|
|
41
|
+
/** What the picker shows, e.g. "Pre-owned — condition as pictured". */
|
|
42
|
+
label: string;
|
|
43
|
+
variant: CategoryTemplateVariant;
|
|
44
|
+
/** `{{placeholder}}` slots; HTML permitted, rendered via RichTextRenderer. */
|
|
45
|
+
body: string;
|
|
46
|
+
/**
|
|
47
|
+
* Slot names found in `body`, derived at write time as a convenience.
|
|
48
|
+
*
|
|
49
|
+
* 🛑 ABSENT means "re-parse", never "this body has no slots". A reader that
|
|
50
|
+
* treats `undefined` as an empty list will skip interpolation entirely on
|
|
51
|
+
* every template written before this field existed — and the failure is a
|
|
52
|
+
* published description containing a literal `{{series}}`, which is the most
|
|
53
|
+
* visible way this feature can break. Use `extractPlaceholders(body)` when
|
|
54
|
+
* it is absent.
|
|
55
|
+
*/
|
|
56
|
+
placeholders?: string[];
|
|
57
|
+
/**
|
|
58
|
+
* An admin has published a real listing from this template.
|
|
59
|
+
*
|
|
60
|
+
* Exists because of raikages, the crawl's counter-example: 114-char bodies
|
|
61
|
+
* lifted from the Beyblade Wiki, one of which ships with the literal UI
|
|
62
|
+
* string "Add to Wishlist Add to Wishlist" welded into the product body. An
|
|
63
|
+
* unreviewed template is how that happens, so the flag records review rather
|
|
64
|
+
* than assuming it.
|
|
65
|
+
*/
|
|
66
|
+
isTested?: boolean;
|
|
67
|
+
version?: number;
|
|
68
|
+
}
|
|
69
|
+
/** A spec row as the product page renders it. */
|
|
70
|
+
export interface CategorySpecification {
|
|
71
|
+
name: string;
|
|
72
|
+
value: string;
|
|
73
|
+
unit?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* What a category hands down to a product filed under it.
|
|
77
|
+
*
|
|
78
|
+
* Three behaviours, and the distinction matters more than the field list:
|
|
79
|
+
*
|
|
80
|
+
* WRITTEN tax, specifications, inTheBox, features — written server-side
|
|
81
|
+
* by `deriveTaxonomy` when the product omits them.
|
|
82
|
+
* FILL-BLANK seo* — fills only an empty field, never overwrites.
|
|
83
|
+
* ADVISORY priceGuidance — NEVER written to `price`. It renders as a hint
|
|
84
|
+
* beside the price input and nothing else.
|
|
85
|
+
*/
|
|
86
|
+
export interface CategoryProductDefaults {
|
|
87
|
+
/**
|
|
88
|
+
* Points at a `taxCodes` row. `deriveTaxonomy` resolves it and writes
|
|
89
|
+
* `gstRate`/`hsnCode` onto the product; the order item then snapshots from
|
|
90
|
+
* the product. That layering is what stops a tax-code edit retroactively
|
|
91
|
+
* re-rating an invoice already issued.
|
|
92
|
+
*/
|
|
93
|
+
taxCodeId?: string;
|
|
94
|
+
specifications?: CategorySpecification[];
|
|
95
|
+
/**
|
|
96
|
+
* Feature ids merged onto the product, never replacing what it already has.
|
|
97
|
+
* (The old `defaultTags` name went with `ProductDocument.tags`.)
|
|
98
|
+
*/
|
|
99
|
+
defaultFeatures?: string[];
|
|
100
|
+
defaultCondition?: string;
|
|
101
|
+
defaultAuthenticity?: ProductAuthenticity;
|
|
102
|
+
/** "What's in the box?" — becomes `ProductDocument.features[]` prose. */
|
|
103
|
+
inTheBox?: string[];
|
|
104
|
+
priceGuidance?: CategoryPriceGuidance;
|
|
105
|
+
seoTitleTemplate?: string;
|
|
106
|
+
seoDescriptionTemplate?: string;
|
|
107
|
+
/** Merged with whatever the product already has. */
|
|
108
|
+
seoKeywords?: string[];
|
|
109
|
+
weightG?: number;
|
|
110
|
+
dimensionsCm?: {
|
|
111
|
+
l: number;
|
|
112
|
+
w: number;
|
|
113
|
+
h: number;
|
|
114
|
+
};
|
|
115
|
+
shippingPaidBy?: string;
|
|
116
|
+
fragile?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Rendered at READ time, so fixing a wrong answer fixes every listing at
|
|
119
|
+
* once. The tcgindia model — five per product, three interpolating the
|
|
120
|
+
* name — and nobody else in the crawl of fifteen does it.
|
|
121
|
+
*/
|
|
122
|
+
faqTemplates?: {
|
|
123
|
+
question: string;
|
|
124
|
+
answer: string;
|
|
125
|
+
}[];
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Two price views, never blended.
|
|
129
|
+
*
|
|
130
|
+
* 🛑 `market` and `sold` are rendered SIDE BY SIDE — "Market ₹799–₹1,499 ·
|
|
131
|
+
* Sold here ₹850–₹1,200 (7 sales)" — and the gap between them is the most
|
|
132
|
+
* useful thing we can tell a seller, given 76–93% of competitor listings never
|
|
133
|
+
* sell at their ask. Averaging the two would launder an inference into a
|
|
134
|
+
* measurement.
|
|
135
|
+
*/
|
|
136
|
+
export interface CategoryPriceGuidance {
|
|
137
|
+
currency: "INR";
|
|
138
|
+
/**
|
|
139
|
+
* Day-1 baseline from the competitor crawl: a TYPICAL MARKET LISTING PRICE.
|
|
140
|
+
*
|
|
141
|
+
* 🛑 NOT evidence of a transaction. A competitor's sold-out row proves the
|
|
142
|
+
* item is GONE, not that it sold at that price. Seed-owned — the rollup job
|
|
143
|
+
* must never write here, or a quiet night would erase the baseline.
|
|
144
|
+
*/
|
|
145
|
+
market?: {
|
|
146
|
+
/**
|
|
147
|
+
* The one figure every measured band has. Required for exactly that
|
|
148
|
+
* reason — a band with no central value is not a band.
|
|
149
|
+
*/
|
|
150
|
+
median: number;
|
|
151
|
+
/**
|
|
152
|
+
* 🛑 OPTIONAL, and that is load-bearing. The crawl recorded quartiles for
|
|
153
|
+
* parts, launchers, TCG and figures and only a MEDIAN AND A RANGE for Hot
|
|
154
|
+
* Wheels (n=7,418, ₹99–36,500). Requiring p25/p75 would have forced a
|
|
155
|
+
* range to be retyped as quartiles, which is a different statistic
|
|
156
|
+
* presented as the one the field is named after. Absent means "not
|
|
157
|
+
* measured", and the hint renders as "around ₹850" rather than a span.
|
|
158
|
+
*/
|
|
159
|
+
p25?: number;
|
|
160
|
+
p75?: number;
|
|
161
|
+
/**
|
|
162
|
+
* 🛑 Also optional, for the same reason: the crawl's Beyblade-top table
|
|
163
|
+
* carries no per-leaf sample size at all, and the one place two sources
|
|
164
|
+
* disagreed (HMS: beybladeshopindia n=39 against worldhobbyshop n=2) is
|
|
165
|
+
* recorded precisely because an n=2 median is a coin flip presented as
|
|
166
|
+
* data. Absent must render as absent — never as a confident figure, and
|
|
167
|
+
* never as a number somebody filled in to satisfy the type.
|
|
168
|
+
*/
|
|
169
|
+
sampleSize?: number;
|
|
170
|
+
asOf: Date;
|
|
171
|
+
soldOutShare?: number;
|
|
172
|
+
/**
|
|
173
|
+
* Which crawl this came from, and any caveat about it — "worldhobbyshop",
|
|
174
|
+
* "raikages n=68 (the only component-level pricing anywhere)",
|
|
175
|
+
* "toycollectorsindia 1:64 POOLED across mainline and premium".
|
|
176
|
+
*
|
|
177
|
+
* Carried so the question "where did this number come from" is answerable
|
|
178
|
+
* from the data rather than from a markdown file nobody has open, and so a
|
|
179
|
+
* source-priority decision can be applied without re-deriving the bands.
|
|
180
|
+
*/
|
|
181
|
+
source?: string;
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* OUR completed sales only, including won auctions and accepted offers.
|
|
185
|
+
*
|
|
186
|
+
* Absent until there are some — **never zeroed**. A zero would render as a
|
|
187
|
+
* real figure meaning "things sell here for ₹0", which is worse than silence.
|
|
188
|
+
*/
|
|
189
|
+
sold?: {
|
|
190
|
+
min: number;
|
|
191
|
+
median: number;
|
|
192
|
+
max: number;
|
|
193
|
+
sampleSize: number;
|
|
194
|
+
windowDays: number;
|
|
195
|
+
byPath?: {
|
|
196
|
+
standard?: number;
|
|
197
|
+
auction?: number;
|
|
198
|
+
offer?: number;
|
|
199
|
+
};
|
|
200
|
+
};
|
|
201
|
+
lastSold?: {
|
|
202
|
+
amount: number;
|
|
203
|
+
at: Date;
|
|
204
|
+
via?: "standard" | "auction" | "offer";
|
|
205
|
+
};
|
|
206
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Category-owned content — description templates and product defaults.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this lives on the CATEGORY and not on a listing template
|
|
5
|
+
*
|
|
6
|
+
* `ListingTemplateDocument` is store-scoped. The ask is that the description
|
|
7
|
+
* lives with the *category*, and the crawl proves the model: worldhobbyshop
|
|
8
|
+
* reuses TWO bodies across 1,195 listings, and tcgindia generates five
|
|
9
|
+
* templated FAQs per product. Nobody writes 1,195 descriptions by hand — our
|
|
10
|
+
* sellers do, because no category→description mechanism exists anywhere in
|
|
11
|
+
* this codebase.
|
|
12
|
+
*
|
|
13
|
+
* ## Why it costs ZERO extra reads
|
|
14
|
+
*
|
|
15
|
+
* Both hot paths already hold the category document: `deriveTaxonomy` does
|
|
16
|
+
* `cats.doc(leaf).get()`, and `CategoryDetailPageView` does `getCategoryBySlug`
|
|
17
|
+
* plus seven parallel reads — already at Rule #6's ~3-round-trip ceiling. A
|
|
18
|
+
* sidecar collection would add an eighth read to the second-most-crawled page
|
|
19
|
+
* on the site.
|
|
20
|
+
*
|
|
21
|
+
* ## Why three fields rather than one blob
|
|
22
|
+
*
|
|
23
|
+
* Each has exactly one reader: `contentBody` → the category page;
|
|
24
|
+
* `descriptionTemplates` → the seller form plus the narrow template endpoint;
|
|
25
|
+
* `productDefaults` → `deriveTaxonomy`. A single `content` object would make
|
|
26
|
+
* every one of those read the other two.
|
|
27
|
+
*/
|
|
28
|
+
export {};
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import type { CategoryAncestor, CategoryMetrics, CategoryDisplay } from "../types";
|
|
8
8
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
9
|
+
import type { CategoryDescriptionTemplate, CategoryProductDefaults } from "./category-content";
|
|
10
|
+
export type { CategoryDescriptionTemplate, CategoryProductDefaults, CategoryPriceGuidance, CategorySpecification, CategoryTemplateVariant, } from "./category-content";
|
|
9
11
|
export type { CategoryAncestor, CategoryMetrics };
|
|
10
12
|
/**
|
|
11
13
|
* How a bundle resolves its member products.
|
|
@@ -82,9 +84,9 @@ export interface CategoryDocument extends BaseDocument {
|
|
|
82
84
|
name: string;
|
|
83
85
|
slug: string;
|
|
84
86
|
description?: string;
|
|
85
|
-
/**
|
|
87
|
+
/** Test-data flag — set on shared admin-seeded test categories/brands/bundles; swept by testDataCleanup. */
|
|
86
88
|
isTestData?: boolean;
|
|
87
|
-
/** When isTestData, the cutoff after which
|
|
89
|
+
/** When isTestData, the cutoff after which testDataCleanup deletes this doc. */
|
|
88
90
|
testDataExpiresAt?: Date;
|
|
89
91
|
rootId: string;
|
|
90
92
|
parentIds: string[];
|
|
@@ -135,6 +137,38 @@ export interface CategoryDocument extends BaseDocument {
|
|
|
135
137
|
question: string;
|
|
136
138
|
answer: string;
|
|
137
139
|
}[];
|
|
140
|
+
/**
|
|
141
|
+
* Alternate names this category is searched by — indexed into `searchTxt`
|
|
142
|
+
* and rendered nowhere.
|
|
143
|
+
*
|
|
144
|
+
* B5. A named-model leaf is the case that needs it: *Lost Longinus* is sold
|
|
145
|
+
* in the west as *Lost Luinor L2* and its product code is *B-66*, and a
|
|
146
|
+
* buyer types whichever one they learned first. `name` can only hold one of
|
|
147
|
+
* the three.
|
|
148
|
+
*
|
|
149
|
+
* 🛑 Watch the 600-token ceiling in `buildSearchTxt`, which truncates
|
|
150
|
+
* SILENTLY. A category's own tokens (name, slug, description, path, every
|
|
151
|
+
* ancestor name) come first in the array, so an unbounded alias list would
|
|
152
|
+
* evict the lineage tokens that are the whole reason ancestors are indexed.
|
|
153
|
+
* The generator caps a model leaf at the Hasbro name plus the product code.
|
|
154
|
+
*/
|
|
155
|
+
aliases?: string[];
|
|
156
|
+
/**
|
|
157
|
+
* 400–900 chars of keyword-rich HTML with sibling/parent cross-links,
|
|
158
|
+
* rendered via `<RichTextRenderer>`.
|
|
159
|
+
*
|
|
160
|
+
* 🛑 DISTINCT from `description`, which is a <=230-char blurb that also
|
|
161
|
+
* feeds `seo.description` and every card. Conflating them would put a
|
|
162
|
+
* 900-char body in a card and a 230-char stub on the page.
|
|
163
|
+
*
|
|
164
|
+
* 36 of worldhobbyshop's 46 categories carry one and it is their actual
|
|
165
|
+
* ranking mechanism; no Shopify competitor in the crawl has any.
|
|
166
|
+
*/
|
|
167
|
+
contentBody?: string;
|
|
168
|
+
/** Condition-keyed description bodies. A SET, because WHS reuses two per category. */
|
|
169
|
+
descriptionTemplates?: CategoryDescriptionTemplate[];
|
|
170
|
+
/** What this category hands down to a product filed under it. */
|
|
171
|
+
productDefaults?: CategoryProductDefaults;
|
|
138
172
|
/**
|
|
139
173
|
* "special" — admin-curated bundle; writes reverse `partOfBundleIds`
|
|
140
174
|
* pointers on member products and shows the "Bundled" badge on cards.
|
|
@@ -194,6 +228,19 @@ export interface CategoryDocument extends BaseDocument {
|
|
|
194
228
|
isActive: boolean;
|
|
195
229
|
isSearchable: boolean;
|
|
196
230
|
showOnHomepage?: boolean;
|
|
231
|
+
/**
|
|
232
|
+
* Prefix-expanded search tokens, derived on every write by
|
|
233
|
+
* `CategoriesRepository.buildSearchTxtFor` → `buildCategorySearchTxt`.
|
|
234
|
+
*
|
|
235
|
+
* 🛑 Distinct from `isSearchable`, which is an admin visibility flag. This is
|
|
236
|
+
* the index that makes a category findable AT ALL: before it existed,
|
|
237
|
+
* `CATEGORY_SEARCH_SCAN_LIMIT` was capped at 100 by `SIEVE_DEFAULTS.maxPageSize`
|
|
238
|
+
* — so raising that number did nothing — and a tier-4 model like
|
|
239
|
+
* "Lost Longinus" was unfindable in every picker.
|
|
240
|
+
*
|
|
241
|
+
* Never fed any PII: no `createdBy` (a raw uid) and no `createdByStoreName`.
|
|
242
|
+
*/
|
|
243
|
+
searchTxt?: string[];
|
|
197
244
|
createdBy: string;
|
|
198
245
|
/** Whether this category was created by admin or a store owner. */
|
|
199
246
|
createdByType?: "admin" | "store";
|
|
@@ -204,12 +251,12 @@ export interface CategoryDocument extends BaseDocument {
|
|
|
204
251
|
ancestors: CategoryAncestor[];
|
|
205
252
|
}
|
|
206
253
|
export declare const CATEGORIES_COLLECTION: "categories";
|
|
207
|
-
export declare const CATEGORIES_INDEXED_FIELDS: readonly ["slug", "rootId", "tier", "parentIds", "isLeaf", "isFeatured", "featuredPriority", "isBrand", "categoryType", "isActive", "isSearchable", "showOnHomepage", "createdBy", "createdByType", "createdByStoreId", "createdAt"];
|
|
254
|
+
export declare const CATEGORIES_INDEXED_FIELDS: readonly ["slug", "rootId", "tier", "parentIds", "isLeaf", "isFeatured", "featuredPriority", "isBrand", "categoryType", "isActive", "isSearchable", "showOnHomepage", "searchTxt", "createdBy", "createdByType", "createdByStoreId", "createdAt"];
|
|
208
255
|
export declare const MIN_ITEMS_FOR_FEATURED: 8;
|
|
209
256
|
export declare const MAX_FEATURED_CATEGORIES: 4;
|
|
210
257
|
export declare const DEFAULT_CATEGORY_DATA: Partial<CategoryDocument>;
|
|
211
|
-
export declare const CATEGORIES_PUBLIC_FIELDS: readonly ["id", "name", "slug", "description", "rootId", "parentIds", "childrenIds", "tier", "path", "order", "position", "subtreeSize", "isLeaf", "metrics.totalProductCount", "metrics.totalAuctionCount", "metrics.totalItemCount", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "ancestors"];
|
|
212
|
-
export declare const CATEGORIES_UPDATABLE_FIELDS: readonly ["name", "slug", "description", "order", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "isSearchable"];
|
|
258
|
+
export declare const CATEGORIES_PUBLIC_FIELDS: readonly ["id", "name", "slug", "description", "rootId", "parentIds", "childrenIds", "tier", "path", "order", "position", "subtreeSize", "isLeaf", "metrics.totalProductCount", "metrics.totalAuctionCount", "metrics.totalItemCount", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "ancestors", "highlights", "faqs", "contentBody", "descriptionTemplates", "productDefaults", "aliases"];
|
|
259
|
+
export declare const CATEGORIES_UPDATABLE_FIELDS: readonly ["name", "slug", "description", "order", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "isSearchable", "highlights", "faqs", "contentBody", "descriptionTemplates", "productDefaults", "aliases"];
|
|
213
260
|
export type CategoryCreateInput = Omit<CategoryDocument, "id" | "createdAt" | "updatedAt" | "metrics" | "isLeaf" | "ancestors"> & {
|
|
214
261
|
parentIds?: string[];
|
|
215
262
|
parentId?: string | null;
|
|
@@ -285,6 +332,11 @@ export declare const CATEGORY_FIELDS: {
|
|
|
285
332
|
readonly VIEW_COUNT: "viewCount";
|
|
286
333
|
readonly IS_ACTIVE: "isActive";
|
|
287
334
|
readonly IS_SEARCHABLE: "isSearchable";
|
|
335
|
+
readonly SEARCH_TXT: "searchTxt";
|
|
336
|
+
readonly CONTENT_BODY: "contentBody";
|
|
337
|
+
readonly DESCRIPTION_TEMPLATES: "descriptionTemplates";
|
|
338
|
+
readonly PRODUCT_DEFAULTS: "productDefaults";
|
|
339
|
+
readonly PRODUCT_DEFAULTS_TAX_CODE_ID: "productDefaults.taxCodeId";
|
|
288
340
|
readonly CATEGORY_TYPE: "categoryType";
|
|
289
341
|
readonly CREATED_BY: "createdBy";
|
|
290
342
|
readonly CREATED_AT: "createdAt";
|
|
@@ -293,5 +345,22 @@ export declare const CATEGORY_FIELDS: {
|
|
|
293
345
|
};
|
|
294
346
|
/**
|
|
295
347
|
* Build a category tree from a flat list of CategoryDocument objects.
|
|
348
|
+
*
|
|
349
|
+
* 🛑 ONE INDEXING PASS, then recursion — not a `.filter()` per node.
|
|
350
|
+
* This used to scan the whole array once per category to find its children,
|
|
351
|
+
* which is O(n²): 2,209 comparisons at today's 47 nodes, ~109,000 at the ~330
|
|
352
|
+
* this plan seeds, and **2.25 million** at 1,500. It runs inside
|
|
353
|
+
* `categoriesRepository.buildTree()`, i.e. on a request path. Grouping by
|
|
354
|
+
* parent id first makes it O(n log n) (the sorts) and the output is
|
|
355
|
+
* byte-identical: same roots, same child order, same depths.
|
|
356
|
+
*
|
|
357
|
+
* 🛑 The `seen` set is not defensive padding. A root is picked by
|
|
358
|
+
* `tier === 0`, and nothing stops a corrupt row from being tier 0 *and* a
|
|
359
|
+
* descendant of its own child (`parentIds` is a denormalised chain that four
|
|
360
|
+
* different write paths maintain). The old code would recurse forever on that,
|
|
361
|
+
* and a hung SSR render is worse than a failed one — it holds a function open
|
|
362
|
+
* for its whole timeout and reports nothing. A revisited node is dropped from
|
|
363
|
+
* the second position it appears in rather than throwing: a malformed edge
|
|
364
|
+
* should cost one tile, not the category page.
|
|
296
365
|
*/
|
|
297
366
|
export declare function buildCategoryTree(categories: CategoryDocument[], rootId?: string): CategoryTreeNode[];
|