@mohasinac/appkit 4.42.18 → 4.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/lottery/LotteryDetailView.js +22 -1
- package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
- package/dist/_internal/server/features/categories/adapters.js +235 -0
- package/dist/_internal/server/features/checkout/actions.js +161 -22
- package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/products/list-public.d.ts +24 -0
- package/dist/_internal/server/features/products/list-public.js +10 -2
- package/dist/_internal/server/features/refunds/actions.js +1 -1
- package/dist/_internal/server/features/seo/sitemap.js +54 -15
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +6 -0
- package/dist/_internal/server/functions/scheduled.d.ts +4 -2
- package/dist/_internal/server/functions/scheduled.js +64 -6
- package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
- package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
- package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +59 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +31 -0
- package/dist/_internal/server/jobs/core/onReviewWrite.js +8 -0
- package/dist/_internal/server/jobs/core/onStoreWrite.d.ts +1 -1
- package/dist/_internal/server/jobs/core/onStoreWrite.js +8 -2
- package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.d.ts +28 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.js +125 -0
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
- package/dist/_internal/server/jobs/handlers/index.js +3 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
- package/dist/_internal/server/jobs/handlers/messages.js +5 -2
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.js +34 -0
- package/dist/_internal/shared/cache/revalidate-targets.d.ts +49 -0
- package/dist/_internal/shared/cache/revalidate-targets.js +157 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
- package/dist/_internal/shared/checkout/order-math.js +194 -7
- package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
- package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
- package/dist/_internal/shared/fees/calculator.d.ts +105 -0
- package/dist/_internal/shared/fees/calculator.js +143 -0
- package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
- package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
- package/dist/_internal/shared/templating/placeholders.js +73 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/admin/actions/admin-actions.d.ts +1 -1
- package/dist/features/admin/actions/admin-actions.js +1 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
- package/dist/features/admin/components/AdminProductEditorView.js +8 -1
- package/dist/features/admin/components/AdminProductsView.js +78 -7
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
- package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
- package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
- package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
- package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
- package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +10 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
- package/dist/features/auctions/components/AuctionLiveStatusBadge.d.ts +20 -0
- package/dist/features/auctions/components/AuctionLiveStatusBadge.js +21 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +20 -2
- package/dist/features/auctions/hooks/useIsAuctionEnded.d.ts +41 -0
- package/dist/features/auctions/hooks/useIsAuctionEnded.js +76 -0
- package/dist/features/auth/schemas/firestore.d.ts +0 -1
- package/dist/features/blog/api/route.js +16 -1
- package/dist/features/blog/schemas/firestore.d.ts +2 -2
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
- package/dist/features/categories/actions/category-actions.d.ts +2 -2
- package/dist/features/categories/actions/category-actions.js +22 -10
- package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
- package/dist/features/categories/api/[id]/description-template/route.js +252 -0
- package/dist/features/categories/api/route.js +156 -11
- package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -3
- package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +27 -1
- package/dist/features/categories/components/CategoriesIndexPageView.js +24 -33
- package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
- package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
- package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
- package/dist/features/categories/constants/listing-categories.d.ts +17 -0
- package/dist/features/categories/constants/listing-categories.js +53 -0
- package/dist/features/categories/hooks/useCategoryTree.js +28 -2
- package/dist/features/categories/repository/categories.repository.d.ts +73 -12
- package/dist/features/categories/repository/categories.repository.js +203 -42
- package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
- package/dist/features/categories/repository/price-index.repository.js +85 -0
- package/dist/features/categories/schemas/category-content.d.ts +206 -0
- package/dist/features/categories/schemas/category-content.js +28 -0
- package/dist/features/categories/schemas/firestore.d.ts +74 -5
- package/dist/features/categories/schemas/firestore.js +74 -3
- package/dist/features/categories/schemas/index.d.ts +1047 -0
- package/dist/features/categories/schemas/index.js +96 -0
- package/dist/features/categories/server.d.ts +1 -0
- package/dist/features/categories/server.js +7 -0
- package/dist/features/categories/types/index.d.ts +9 -0
- package/dist/features/events/schemas/firestore.d.ts +2 -2
- package/dist/features/faq/schemas/firestore.d.ts +2 -1
- package/dist/features/faq/schemas/firestore.js +20 -8
- package/dist/features/faq/utils/getHomepageFaqsCached.d.ts +20 -0
- package/dist/features/faq/utils/getHomepageFaqsCached.js +22 -0
- package/dist/features/homepage/components/MarketplaceHomepageView.js +13 -3
- package/dist/features/homepage/lib/live-stats.js +18 -8
- package/dist/features/media/MediaImage.js +19 -2
- package/dist/features/orders/schemas/firestore.d.ts +70 -0
- package/dist/features/orders/types/index.d.ts +55 -0
- package/dist/features/products/actions/product-actions.d.ts +0 -9
- package/dist/features/products/actions/product-actions.js +25 -0
- package/dist/features/products/columns/index.d.ts +0 -1
- package/dist/features/products/columns/index.js +5 -1
- package/dist/features/products/components/ArtStickersListView.js +7 -1
- package/dist/features/products/components/PrizeDrawsSection.js +5 -0
- package/dist/features/products/components/ProductFilters.d.ts +0 -9
- package/dist/features/products/components/ProductForm.d.ts +27 -1
- package/dist/features/products/components/ProductForm.js +26 -12
- package/dist/features/products/components/ProductsIndexPageView.js +1 -1
- package/dist/features/products/constants/sieve.d.ts +0 -6
- package/dist/features/products/constants/sieve.js +35 -2
- package/dist/features/products/repository/products.repository.d.ts +115 -1
- package/dist/features/products/repository/products.repository.js +491 -39
- package/dist/features/products/schemas/firestore.d.ts +65 -5
- package/dist/features/products/schemas/firestore.js +9 -0
- package/dist/features/products/schemas/product-features.d.ts +140 -2
- package/dist/features/products/schemas/product-features.js +62 -1
- package/dist/features/promotions/repository/coupons.repository.js +6 -1
- package/dist/features/promotions/schemas/firestore.d.ts +44 -1
- package/dist/features/promotions/schemas/firestore.js +76 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +22 -0
- package/dist/features/reviews/repository/reviews.repository.js +35 -0
- package/dist/features/seller/actions/seller-actions.js +17 -2
- package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
- package/dist/features/seller/components/PrintCenterView.js +3 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
- package/dist/features/seller/components/SellerOrdersView.js +6 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
- package/dist/features/seller/components/SellerProductShell.js +101 -9
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +108 -3
- package/dist/features/seller/types/index.d.ts +17 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
- package/dist/features/seller/utils/product-draft-mapping.js +17 -0
- package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
- package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
- package/dist/features/stores/schemas/firestore.d.ts +31 -2
- package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
- package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
- package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
- package/dist/features/tax-codes/schemas/firestore.js +82 -0
- package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
- package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
- package/dist/features/tester/actions/index.d.ts +0 -1
- package/dist/features/tester/actions/index.js +0 -1
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
- package/dist/features/tester/components/index.d.ts +0 -4
- package/dist/features/tester/components/index.js +0 -2
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
- package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
- package/dist/features/tester/schemas/firestore.d.ts +0 -6
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
- package/dist/features/tester/seed-data/authored/index.js +0 -2
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +10 -29
- package/dist/index.d.ts +21 -7
- package/dist/index.js +53 -7
- package/dist/next/routing/route-map.d.ts +2 -4
- package/dist/next/routing/route-map.js +1 -2
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +506 -0
- package/dist/seed/_helpers/brand-rows.d.ts +35 -0
- package/dist/seed/_helpers/brand-rows.js +576 -0
- package/dist/seed/_helpers/category-content.d.ts +39 -0
- package/dist/seed/_helpers/category-content.js +634 -0
- package/dist/seed/_helpers/category-forest.d.ts +2 -0
- package/dist/seed/_helpers/category-forest.js +585 -243
- package/dist/seed/_helpers/category-models.d.ts +5 -0
- package/dist/seed/_helpers/category-models.js +1364 -0
- package/dist/seed/_helpers/category-tree.js +17 -1
- package/dist/seed/_helpers/character-features.d.ts +10 -0
- package/dist/seed/_helpers/character-features.js +118 -0
- package/dist/seed/_helpers/media.d.ts +1 -1
- package/dist/seed/_helpers/media.js +44 -8
- package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
- package/dist/seed/categories-seed-data.js +147 -169
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/manifest.d.ts +1 -1
- package/dist/seed/manifest.js +5 -0
- package/dist/seed/orders-seed-data.js +5 -3
- package/dist/seed/product-features-seed-data.d.ts +1 -8
- package/dist/seed/product-features-seed-data.js +358 -154
- package/dist/seed/products-art-seed-data.js +16 -4
- package/dist/seed/products-auctions-seed-data.js +6 -6
- package/dist/seed/products-classifieds-seed-data.js +17 -7
- package/dist/seed/products-digital-codes-seed-data.js +12 -12
- package/dist/seed/products-preorders-seed-data.js +8 -8
- package/dist/seed/products-prize-draws-seed-data.js +25 -8
- package/dist/seed/products-standard-seed-data.js +10 -60
- package/dist/seed/products-stickers-seed-data.js +6 -6
- package/dist/seed/site-settings-seed-data.js +24 -0
- package/dist/seed/store-extensions-seed-data.js +10 -4
- package/dist/seed/tax-codes-seed-data.d.ts +34 -0
- package/dist/seed/tax-codes-seed-data.js +100 -0
- package/dist/seed/test-data-ttl.d.ts +29 -0
- package/dist/seed/test-data-ttl.js +31 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +6 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.js +11 -2
- package/dist/styles.css +40 -0
- package/dist/ui/components/PaginatedSelect.d.ts +14 -0
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/PaginatedSelect.style.css +40 -0
- package/dist/utils/media-url.d.ts +26 -0
- package/dist/utils/media-url.js +144 -0
- package/dist/utils/physical-location.d.ts +23 -0
- package/dist/utils/physical-location.js +52 -0
- package/dist/utils/search-txt-builders.d.ts +20 -0
- package/dist/utils/search-txt-builders.js +36 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -1
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
- package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
- package/dist/features/tester/actions/leaderboard-actions.js +0 -5
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
- package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
- package/dist/features/tester/components/TesterHubView.d.ts +0 -5
- package/dist/features/tester/components/TesterHubView.js +0 -110
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
- package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
- package/dist/features/tester/seed-data/tester-ttl.js +0 -6
|
@@ -1,15 +1,45 @@
|
|
|
1
1
|
import { normalizeError } from "../../../errors/normalize";
|
|
2
2
|
import { increment, arrayUnion, arrayRemove, } from "../../../contracts/field-ops";
|
|
3
3
|
import { DatabaseError } from "../../../errors";
|
|
4
|
-
import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase";
|
|
4
|
+
import { BaseRepository, prepareForFirestore, parseSieveDateValue, applySieveToFirestore, } from "../../../providers/db-firebase";
|
|
5
|
+
import { buildCategorySearchTxt } from "../../../utils/search-txt-builders";
|
|
6
|
+
import { planSearchTxt, refineSearchTxt, emptySearchResult, } from "../../../utils/search-txt-query";
|
|
5
7
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
6
8
|
import { CATEGORY_FIELDS, CATEGORIES_COLLECTION, MIN_ITEMS_FOR_FEATURED, buildCategoryTree, calculateCategoryFields, canBeFeatured, createCategoryId, isValidCategoryMove, } from "../schemas";
|
|
7
9
|
export class CategoriesRepository extends BaseRepository {
|
|
8
10
|
constructor() {
|
|
9
11
|
super(CATEGORIES_COLLECTION);
|
|
10
12
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
/** Derived on every write path via `applyWriteHooks`. */
|
|
14
|
+
buildSearchTxtFor(data) {
|
|
15
|
+
return buildCategorySearchTxt(data);
|
|
16
|
+
}
|
|
17
|
+
async list(model, opts) {
|
|
18
|
+
/*
|
|
19
|
+
* `q` is a searchTxt pushdown, NOT a Sieve filter.
|
|
20
|
+
*
|
|
21
|
+
* `plan.head` becomes one `array-contains` on the longest term and
|
|
22
|
+
* `plan.rest` is AND-refined in memory — the same two-part shape the other
|
|
23
|
+
* ten migrated collections use. Going through `planSearchTxt` rather than
|
|
24
|
+
* hand-rolling it is what buys the `empty` guard: a query that normalises
|
|
25
|
+
* to no usable token (`"!!!"`, `"—"`) must return NOTHING, because
|
|
26
|
+
* emitting no clause at all returns the whole collection and calls it a
|
|
27
|
+
* search result. That is the live bug `products` still has (A2).
|
|
28
|
+
*/
|
|
29
|
+
const plan = planSearchTxt(opts?.search);
|
|
30
|
+
if (plan.empty)
|
|
31
|
+
return emptySearchResult();
|
|
32
|
+
if (!plan.head) {
|
|
33
|
+
return this.sieveQuery(model, CategoriesRepository.SIEVE_FIELDS);
|
|
34
|
+
}
|
|
35
|
+
const baseQuery = this.getCollection().where(CATEGORY_FIELDS.SEARCH_TXT, "array-contains", plan.head);
|
|
36
|
+
const result = await applySieveToFirestore({
|
|
37
|
+
baseQuery,
|
|
38
|
+
model,
|
|
39
|
+
fields: CategoriesRepository.SIEVE_FIELDS,
|
|
40
|
+
mapDoc: (snap) => this.mapDoc(snap),
|
|
41
|
+
});
|
|
42
|
+
return refineSearchTxt(result, plan.rest);
|
|
13
43
|
}
|
|
14
44
|
async createWithHierarchy(input) {
|
|
15
45
|
try {
|
|
@@ -112,14 +142,26 @@ export class CategoriesRepository extends BaseRepository {
|
|
|
112
142
|
throw new DatabaseError(`Failed to retrieve leaf categories: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
113
143
|
}
|
|
114
144
|
}
|
|
115
|
-
|
|
145
|
+
/**
|
|
146
|
+
* @param limit 0 = unbounded (the historical behaviour). When > 0 the bound is
|
|
147
|
+
* pushed into Firestore instead of slicing after the read.
|
|
148
|
+
*
|
|
149
|
+
* 🛑 The limit is applied with HEADROOM, not exactly, because `!isBrand` is a
|
|
150
|
+
* post-fetch filter: brand rows live in this same collection (they are
|
|
151
|
+
* `CategoryDocument`s discriminated by `isBrand`), so a bare `.limit(n)` could
|
|
152
|
+
* return n brand rows, filter all of them out, and yield an empty tier. The
|
|
153
|
+
* caller still slices to its own limit.
|
|
154
|
+
*/
|
|
155
|
+
async getCategoriesByTier(tier, limit = 0) {
|
|
116
156
|
try {
|
|
117
|
-
|
|
157
|
+
let query = this.db
|
|
118
158
|
.collection(this.collection)
|
|
119
159
|
.where(CATEGORY_FIELDS.TIER, "==", tier)
|
|
120
160
|
.where(CATEGORY_FIELDS.IS_ACTIVE, "==", true)
|
|
121
|
-
.orderBy(CATEGORY_FIELDS.ORDER, "asc")
|
|
122
|
-
|
|
161
|
+
.orderBy(CATEGORY_FIELDS.ORDER, "asc");
|
|
162
|
+
if (limit > 0)
|
|
163
|
+
query = query.limit(limit * 4);
|
|
164
|
+
const snapshot = await query.get();
|
|
123
165
|
return snapshot.docs
|
|
124
166
|
.map((doc) => this.mapDoc(doc))
|
|
125
167
|
.filter((category) => !category.isBrand);
|
|
@@ -129,21 +171,139 @@ export class CategoriesRepository extends BaseRepository {
|
|
|
129
171
|
throw new DatabaseError(`Failed to retrieve categories by tier: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
130
172
|
}
|
|
131
173
|
}
|
|
132
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Every category sharing a root, tier-then-order.
|
|
176
|
+
*
|
|
177
|
+
* 🛑 `limit` is for DISPLAY callers only, and it is a real truncation.
|
|
178
|
+
* `buildTree(rootId)` must pass nothing: a tree built from a truncated node
|
|
179
|
+
* list silently drops whole branches, which is the exact failure
|
|
180
|
+
* `getDescendants`'s docstring was rewritten to avoid. A display grid of
|
|
181
|
+
* sibling categories, by contrast, renders ~20 tiles and has no reason to
|
|
182
|
+
* read 1,500 documents to do it.
|
|
183
|
+
*
|
|
184
|
+
* Paginated rather than one unbounded `.get()`: the previous form read the
|
|
185
|
+
* whole root subtree into memory on every category page render, which at the
|
|
186
|
+
* ~330-node forest this plan seeds is ~9% of a 50K/day Firestore budget for
|
|
187
|
+
* ONE page view.
|
|
188
|
+
*/
|
|
189
|
+
/**
|
|
190
|
+
* Several categories by id, in ONE round trip, in the order asked for.
|
|
191
|
+
*
|
|
192
|
+
* The ancestor-walk primitive. A five-tier tree means a leaf has up to four
|
|
193
|
+
* ancestors, and resolving an inherited description template or an inherited
|
|
194
|
+
* `productDefaults` means reading them nearest-first until the answer is
|
|
195
|
+
* found — which as four sequential `findById` calls is four round trips, on
|
|
196
|
+
* a route the seller form hits on every category change. Rule #6 budgets
|
|
197
|
+
* about three for a whole request.
|
|
198
|
+
*
|
|
199
|
+
* 🛑 Order is PRESERVED and missing ids are DROPPED, not nulled. Callers
|
|
200
|
+
* walk the result looking for the first document that declares a thing, so
|
|
201
|
+
* "nearest first" has to survive the round trip — Firestore's `getAll`
|
|
202
|
+
* already answers in argument order, and this keeps that guarantee explicit
|
|
203
|
+
* rather than incidental. A caller needing to know WHICH id was missing
|
|
204
|
+
* should compare lengths; none does, because an ancestor that no longer
|
|
205
|
+
* exists is a data problem for the reconcile job, not a reason to fail a
|
|
206
|
+
* form.
|
|
207
|
+
*
|
|
208
|
+
* Bounded at 30 to match `array-contains-any`'s cap, so this cannot become
|
|
209
|
+
* the unbounded read that `getCategoriesByRootId` had to be rewritten to
|
|
210
|
+
* avoid being.
|
|
211
|
+
*/
|
|
212
|
+
async findByIds(ids) {
|
|
213
|
+
const unique = Array.from(new Set(ids.filter(Boolean))).slice(0, 30);
|
|
214
|
+
if (unique.length === 0)
|
|
215
|
+
return [];
|
|
133
216
|
try {
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
.
|
|
138
|
-
.
|
|
139
|
-
|
|
140
|
-
|
|
217
|
+
const refs = unique.map((id) => this.db.collection(this.collection).doc(id));
|
|
218
|
+
const snaps = await this.db.getAll(...refs);
|
|
219
|
+
return snaps
|
|
220
|
+
.filter((s) => s.exists)
|
|
221
|
+
.map((s) => this.mapDoc(s));
|
|
222
|
+
}
|
|
223
|
+
catch (err) {
|
|
224
|
+
const e = normalizeError(err);
|
|
225
|
+
/*
|
|
226
|
+
* 🛑 The raw `.message` is NOT interpolated into the thrown error — only
|
|
227
|
+
* the normalised `kind` and a count.
|
|
228
|
+
*
|
|
229
|
+
* Several older methods in this file do interpolate it, and
|
|
230
|
+
* `audit-raw-error-text` flagged this one for trying. The reasoning is
|
|
231
|
+
* Root Cause #86's: a thrown value's message is written for a developer,
|
|
232
|
+
* and this one reaches a user through the route's 500 branch — which is
|
|
233
|
+
* how a Node `MODULE_NOT_FOUND` with a full require stack ended up
|
|
234
|
+
* rendered inside the "Place your bid" modal.
|
|
235
|
+
*
|
|
236
|
+
* The ids are counted rather than listed. A category id is not
|
|
237
|
+
* sensitive, but 30 of them in one line is noise against Hobby's ~4 KB/s
|
|
238
|
+
* log budget.
|
|
239
|
+
*
|
|
240
|
+
* (`normalizeError` has to be the FIRST statement, not merely nearby:
|
|
241
|
+
* `audit-catch-normalize` looks within a few lines of the catch, and the
|
|
242
|
+
* first draft put this comment above the call and tripped it.)
|
|
243
|
+
*/
|
|
244
|
+
throw new DatabaseError(`Failed to read categories by id (${unique.length} requested): ${e.kind}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async getCategoriesByRootId(rootId, opts) {
|
|
248
|
+
try {
|
|
249
|
+
const cap = opts?.limit;
|
|
250
|
+
const pageSize = cap ? Math.min(cap, 300) : 300;
|
|
251
|
+
const out = [];
|
|
252
|
+
let cursor = null;
|
|
253
|
+
for (;;) {
|
|
254
|
+
let q = this.db
|
|
255
|
+
.collection(this.collection)
|
|
256
|
+
.where(CATEGORY_FIELDS.ROOT_ID, "==", rootId)
|
|
257
|
+
.orderBy(CATEGORY_FIELDS.TIER, "asc")
|
|
258
|
+
.orderBy(CATEGORY_FIELDS.ORDER, "asc")
|
|
259
|
+
.limit(pageSize);
|
|
260
|
+
if (cursor)
|
|
261
|
+
q = q.startAfter(cursor);
|
|
262
|
+
const page = await q.get();
|
|
263
|
+
if (page.empty)
|
|
264
|
+
break;
|
|
265
|
+
for (const doc of page.docs)
|
|
266
|
+
out.push(this.mapDoc(doc));
|
|
267
|
+
if (cap && out.length >= cap)
|
|
268
|
+
return out.slice(0, cap);
|
|
269
|
+
if (page.size < pageSize)
|
|
270
|
+
break;
|
|
271
|
+
cursor = page.docs[page.docs.length - 1];
|
|
272
|
+
}
|
|
273
|
+
return out;
|
|
141
274
|
}
|
|
142
275
|
catch (error) {
|
|
143
276
|
void normalizeError(error);
|
|
144
277
|
throw new DatabaseError(`Failed to retrieve categories by rootId: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
145
278
|
}
|
|
146
279
|
}
|
|
280
|
+
/**
|
|
281
|
+
* Categories whose `productDefaults.taxCodeId` points at `taxCodeId`.
|
|
282
|
+
*
|
|
283
|
+
* 🛑 Exists so DELETE on a tax code can REFUSE while it is referenced.
|
|
284
|
+
* `taxCodeId` is a plain string with no referential integrity, and a
|
|
285
|
+
* dangling reference does not error — `findResolvable` answers `null`,
|
|
286
|
+
* whose correct handling is "leave the product's gstRate alone", so every
|
|
287
|
+
* subsequent listing under that category silently derives NO TAX. It does
|
|
288
|
+
* not fail; it under-charges, until someone reads an invoice.
|
|
289
|
+
*
|
|
290
|
+
* One equality on a dot-path — served by the automatic index, no composite
|
|
291
|
+
* to declare. Bounded at 200: the answer is used to NAME referrers in a 409
|
|
292
|
+
* message, and the first five are what the admin reads.
|
|
293
|
+
*/
|
|
294
|
+
async findByTaxCodeId(taxCodeId) {
|
|
295
|
+
try {
|
|
296
|
+
const snap = await this.getCollection()
|
|
297
|
+
.where(CATEGORY_FIELDS.PRODUCT_DEFAULTS_TAX_CODE_ID, "==", taxCodeId)
|
|
298
|
+
.limit(200)
|
|
299
|
+
.get();
|
|
300
|
+
return snap.docs.map((d) => this.mapDoc(d));
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
void normalizeError(error);
|
|
304
|
+
throw new DatabaseError(`Failed to find categories by tax code: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
147
307
|
async getChildren(parentId) {
|
|
148
308
|
try {
|
|
149
309
|
const snapshot = await this.db
|
|
@@ -161,17 +321,16 @@ export class CategoriesRepository extends BaseRepository {
|
|
|
161
321
|
throw new DatabaseError(`Failed to retrieve children categories: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
162
322
|
}
|
|
163
323
|
}
|
|
164
|
-
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
324
|
+
/*
|
|
325
|
+
* 🛑 `getDescendantIds` was DELETED (2026-10-10). It was
|
|
326
|
+
* `getDescendants(id).then(d => d.map(x => x.id))`, and its only caller —
|
|
327
|
+
* CategoryDetailPageView — already held the documents from its own
|
|
328
|
+
* `getDescendants` call in the same render. So it re-ran the identical
|
|
329
|
+
* paginated subtree query and discarded everything but the ids: ~330 wasted
|
|
330
|
+
* reads per page view at the forest size this plan seeds, and two answers
|
|
331
|
+
* that could disagree whenever one read failed and the other did not.
|
|
332
|
+
* Call `getDescendants` and `.map(d => d.id)` at the one site that needs it.
|
|
170
333
|
*/
|
|
171
|
-
async getDescendantIds(categoryId) {
|
|
172
|
-
const docs = await this.getDescendants(categoryId);
|
|
173
|
-
return docs.map((d) => d.id);
|
|
174
|
-
}
|
|
175
334
|
/**
|
|
176
335
|
* Every descendant DOCUMENT at any depth.
|
|
177
336
|
*
|
|
@@ -230,12 +389,18 @@ export class CategoriesRepository extends BaseRepository {
|
|
|
230
389
|
}
|
|
231
390
|
async getBrandCategories(limit = 0) {
|
|
232
391
|
try {
|
|
233
|
-
|
|
392
|
+
// The limit is pushed into Firestore rather than sliced after the read.
|
|
393
|
+
// It used to fetch EVERY brand and `.slice()` — so `limit` reduced what
|
|
394
|
+
// the caller saw but not what was billed. Exact here (no post-filter to
|
|
395
|
+
// under-fill), unlike `getCategoriesByTier` above.
|
|
396
|
+
let query = this.db
|
|
234
397
|
.collection(this.collection)
|
|
235
398
|
.where("isBrand", "==", true)
|
|
236
399
|
.where(CATEGORY_FIELDS.IS_ACTIVE, "==", true)
|
|
237
|
-
.orderBy(CATEGORY_FIELDS.ORDER, "asc")
|
|
238
|
-
|
|
400
|
+
.orderBy(CATEGORY_FIELDS.ORDER, "asc");
|
|
401
|
+
if (limit > 0)
|
|
402
|
+
query = query.limit(limit);
|
|
403
|
+
const snapshot = await query.get();
|
|
239
404
|
const brands = snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
240
405
|
return limit > 0 ? brands.slice(0, limit) : brands;
|
|
241
406
|
}
|
|
@@ -678,24 +843,19 @@ export class CategoriesRepository extends BaseRepository {
|
|
|
678
843
|
.get();
|
|
679
844
|
return snap.docs.map((d) => this.mapDoc(d));
|
|
680
845
|
}
|
|
846
|
+
/*
|
|
847
|
+
* 🛑 `incrementViewCount` DELETED 2026-10-10 — zero callers since
|
|
848
|
+
* 2026-08-31, when it was removed from the category-detail render path (a
|
|
849
|
+
* Firestore write per render, and a second counter racing `pageViews`).
|
|
850
|
+
*
|
|
851
|
+
* Measured: `viewCount` is absent from all 58 category documents. Deleting
|
|
852
|
+
* the writer nobody calls is what stops the dead field reading as live. See
|
|
853
|
+
* the products repository for the same removal.
|
|
854
|
+
*/
|
|
681
855
|
/**
|
|
682
856
|
* SB-UNI-B — derive the canonical `sublisting-{slug}` ID from a
|
|
683
857
|
* human-entered category name.
|
|
684
858
|
*/
|
|
685
|
-
async incrementViewCount(categoryId) {
|
|
686
|
-
try {
|
|
687
|
-
await this.db
|
|
688
|
-
.collection(this.collection)
|
|
689
|
-
.doc(categoryId)
|
|
690
|
-
.update({ [CATEGORY_FIELDS.VIEW_COUNT]: increment(1) });
|
|
691
|
-
// Fire-and-forget analytics: a lost view increment changes nothing that
|
|
692
|
-
// renders, so it must never break the category read path it rides along
|
|
693
|
-
// with — the count is approximate by design.
|
|
694
|
-
}
|
|
695
|
-
catch (_err) {
|
|
696
|
-
void normalizeError(_err);
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
859
|
generateSublistingId(name) {
|
|
700
860
|
const base = name
|
|
701
861
|
.toLowerCase()
|
|
@@ -712,6 +872,7 @@ CategoriesRepository.SIEVE_FIELDS = {
|
|
|
712
872
|
isActive: { canFilter: true, canSort: false },
|
|
713
873
|
isFeatured: { canFilter: true, canSort: false },
|
|
714
874
|
isBrand: { canFilter: true, canSort: false },
|
|
875
|
+
searchTxt: { canFilter: true, canSort: false },
|
|
715
876
|
categoryType: { canFilter: true, canSort: false },
|
|
716
877
|
isSearchable: { canFilter: true, canSort: false },
|
|
717
878
|
parentId: { canFilter: true, canSort: false, path: "parentIds" },
|
|
@@ -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();
|