@mohasinac/appkit 4.47.0 → 4.48.1
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/.build-stamp.json +4 -4
- package/dist/_internal/server/features/checkout/expand-order-items.js +3 -0
- package/dist/_internal/server/features/messages/actions.d.ts +68 -0
- package/dist/_internal/server/features/messages/actions.js +249 -0
- package/dist/_internal/server/features/parts/buildable.d.ts +47 -0
- package/dist/_internal/server/features/parts/buildable.js +159 -0
- package/dist/_internal/server/features/products/list-public.d.ts +17 -0
- package/dist/_internal/server/features/products/list-public.js +92 -5
- package/dist/_internal/server/features/products/og.d.ts +7 -0
- package/dist/_internal/server/features/products/og.js +38 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +6 -0
- package/dist/_internal/server/jobs/core/cleanupRtdbEvents.js +28 -0
- package/dist/_internal/server/jobs/core/notificationPrune.js +87 -3
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +36 -1
- package/dist/_internal/shared/actions/action-registry.js +8 -0
- package/dist/_internal/shared/features/notifications/action-url.js +8 -0
- package/dist/_internal/shared/features/notifications/email-eligibility.js +3 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +4 -0
- package/dist/client.d.ts +10 -0
- package/dist/client.js +8 -0
- package/dist/constants/api-endpoints.d.ts +48 -0
- package/dist/constants/api-endpoints.js +30 -0
- package/dist/constants/notification-types.d.ts +1 -1
- package/dist/features/about/components/GstExplainerView.d.ts +28 -0
- package/dist/features/about/components/GstExplainerView.js +59 -0
- package/dist/features/about/components/GuidesHubView.d.ts +28 -0
- package/dist/features/about/components/GuidesHubView.js +58 -0
- package/dist/features/about/components/PublicProfileView.js +5 -0
- package/dist/features/about/components/SpotAFakeView.d.ts +29 -0
- package/dist/features/about/components/SpotAFakeView.js +49 -0
- package/dist/features/admin/actions/notification-actions.js +4 -0
- package/dist/features/admin/schemas/firestore.d.ts +13 -3
- package/dist/features/admin/schemas/firestore.js +8 -0
- package/dist/features/blog/repository/blog.repository.js +21 -7
- package/dist/features/categories/api/route.js +41 -1
- package/dist/features/categories/components/BreadcrumbTrail.d.ts +39 -17
- package/dist/features/categories/components/BreadcrumbTrail.js +46 -50
- package/dist/features/categories/components/CategoryDetailPageView.js +92 -38
- package/dist/features/categories/components/ContextualTrail.d.ts +62 -0
- package/dist/features/categories/components/ContextualTrail.js +81 -0
- package/dist/features/categories/components/LastCategoryBeacon.d.ts +18 -0
- package/dist/features/categories/components/LastCategoryBeacon.js +33 -0
- package/dist/features/categories/repository/categories.repository.d.ts +21 -0
- package/dist/features/categories/repository/categories.repository.js +36 -3
- package/dist/features/categories/repository/price-index.repository.d.ts +25 -0
- package/dist/features/email/notification-templates.js +4 -0
- package/dist/features/events/repository/events.repository.js +10 -3
- package/dist/features/faq/constants/read-caps.d.ts +29 -0
- package/dist/features/faq/constants/read-caps.js +29 -0
- package/dist/features/faq/repository/faqs.repository.d.ts +0 -2
- package/dist/features/faq/repository/faqs.repository.js +19 -16
- package/dist/features/homepage/components/AdSlot.js +10 -4
- package/dist/features/homepage/hooks/useActiveAd.d.ts +18 -1
- package/dist/features/homepage/hooks/useActiveAd.js +42 -2
- package/dist/features/homepage/lib/section-renderer.js +12 -2
- package/dist/features/layout/AppLayoutShell.d.ts +4 -1
- package/dist/features/layout/AppLayoutShell.js +2 -2
- package/dist/features/layout/TitleBarLayout.d.ts +4 -1
- package/dist/features/layout/TitleBarLayout.js +8 -4
- package/dist/features/media/AvatarUpload.js +8 -1
- package/dist/features/media/MediaAvatar.js +1 -1
- package/dist/features/media/MediaImage.js +3 -1
- package/dist/features/media/MediaLightbox.js +4 -1
- package/dist/features/media/MediaPickerModal.js +6 -1
- package/dist/features/media/MediaVideo.js +3 -1
- package/dist/features/messages/components/ConversationThreadView.d.ts +3 -0
- package/dist/features/messages/components/ConversationThreadView.js +75 -0
- package/dist/features/messages/components/ConversationsInboxView.d.ts +1 -0
- package/dist/features/messages/components/ConversationsInboxView.js +39 -0
- package/dist/features/messages/components/MessageSellerButton.d.ts +17 -0
- package/dist/features/messages/components/MessageSellerButton.js +33 -0
- package/dist/features/messages/hooks/useConversationChannel.d.ts +24 -0
- package/dist/features/messages/hooks/useConversationChannel.js +89 -0
- package/dist/features/messages/hooks/useMessages.d.ts +347 -0
- package/dist/features/messages/hooks/useMessages.js +113 -0
- package/dist/features/messages/repository/conversations.repository.d.ts +44 -0
- package/dist/features/messages/repository/conversations.repository.js +173 -0
- package/dist/features/messages/repository/messages.repository.d.ts +42 -0
- package/dist/features/messages/repository/messages.repository.js +142 -0
- package/dist/features/messages/schemas/firestore.d.ts +99 -0
- package/dist/features/messages/schemas/firestore.js +46 -0
- package/dist/features/orders/repository/orders.repository.js +21 -7
- package/dist/features/orders/schemas/firestore.d.ts +11 -0
- package/dist/features/products/components/CityCollectiblesView.d.ts +31 -0
- package/dist/features/products/components/CityCollectiblesView.js +82 -0
- package/dist/features/products/components/MarketPriceAnchor.d.ts +45 -0
- package/dist/features/products/components/MarketPriceAnchor.js +62 -0
- package/dist/features/products/components/ProductDetailPageView.js +71 -5
- package/dist/features/products/constants/meetup-cities.d.ts +47 -0
- package/dist/features/products/constants/meetup-cities.js +49 -0
- package/dist/features/products/repository/category-assignment.d.ts +82 -11
- package/dist/features/products/repository/category-assignment.js +93 -12
- package/dist/features/products/repository/products.repository.d.ts +12 -1
- package/dist/features/products/repository/products.repository.js +66 -34
- package/dist/features/products/schemas/firestore.d.ts +24 -2
- package/dist/features/products/schemas/firestore.js +13 -1
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/promotions/repository/coupons.repository.js +11 -4
- package/dist/features/reviews/repository/reviews.repository.js +11 -4
- package/dist/features/scams/repository/scammer.repository.js +21 -7
- package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
- package/dist/features/seller/components/SellerProductShell.js +2 -2
- package/dist/features/seller/repository/offer.repository.js +21 -6
- package/dist/features/seller/schemas/product-form.d.ts +3 -3
- package/dist/features/seller/schemas/product-form.js +1 -1
- package/dist/features/server-errors/repository/server-errors.repository.js +11 -0
- package/dist/features/server-errors/schemas/firestore.d.ts +17 -0
- package/dist/features/server-errors/schemas/firestore.js +2 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +17 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +31 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +1 -1
- package/dist/features/store-extensions/schemas/firestore.js +4 -0
- package/dist/features/stores/repository/store.repository.js +11 -3
- package/dist/features/tester/seed-data/authored/community-support__messages.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/community-support__messages.js +200 -0
- package/dist/features/tester/seed-data/authored/index.js +2 -0
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +64 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.js +1 -1
- package/dist/next/routing/route-map.d.ts +14 -0
- package/dist/next/routing/route-map.js +26 -0
- package/dist/providers/db-firebase/rtdb-paths.d.ts +8 -0
- package/dist/providers/db-firebase/rtdb-paths.js +8 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +4 -0
- package/dist/schemas/registry.d.ts +3 -3
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-schemas.d.ts +17 -0
- package/dist/security/pii-schemas.js +15 -7
- package/dist/seed/_helpers/category-content-generated.d.ts +3 -3
- package/dist/seed/_helpers/category-content-generated.js +23 -7
- package/dist/seed/_helpers/category-models.d.ts +1 -1
- package/dist/seed/_helpers/category-models.js +25 -1
- package/dist/seed/_helpers/category-names.d.ts +54 -0
- package/dist/seed/_helpers/category-names.js +118 -0
- package/dist/seed/_helpers/character-features.d.ts +1 -1
- package/dist/seed/_helpers/character-features.js +5 -4
- package/dist/seed/_helpers/final-sale-fixtures.js +1 -1
- package/dist/seed/categories-seed-data.js +55 -1
- package/dist/seed/grouped-listings-seed-data.js +2 -2
- package/dist/seed/products-art-seed-data.js +2 -1
- package/dist/seed/products-auctions-seed-data.js +42 -22
- package/dist/seed/products-classifieds-seed-data.js +21 -3
- package/dist/seed/products-digital-codes-seed-data.js +2 -1
- package/dist/seed/products-live-items-seed-data.js +2 -1
- package/dist/seed/products-preorders-seed-data.js +26 -11
- package/dist/seed/products-prize-draws-seed-data.js +8 -1
- package/dist/seed/products-standard-seed-data.js +26 -25
- package/dist/seed/products-stickers-seed-data.js +12 -11
- package/dist/server.d.ts +8 -0
- package/dist/server.js +15 -0
- package/dist/styles.css +113 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/sunset.js +17 -0
- package/dist/tokens/tokens.css +5 -0
- package/dist/ui/components/Breadcrumb.d.ts +8 -1
- package/dist/ui/components/Breadcrumb.js +2 -2
- package/dist/ui/components/Breadcrumb.style.css +37 -0
- package/dist/ui/components/Details.d.ts +11 -1
- package/dist/ui/components/Details.js +2 -2
- package/dist/ui/components/ImageMosaic.d.ts +43 -0
- package/dist/ui/components/ImageMosaic.js +46 -0
- package/dist/ui/components/ImageMosaic.style.css +69 -0
- package/dist/ui/components/SplitTile.d.ts +34 -0
- package/dist/ui/components/SplitTile.js +28 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +4 -0
- package/dist/utils/search-txt-query.d.ts +77 -0
- package/dist/utils/search-txt-query.js +50 -28
- package/package.json +1 -1
|
@@ -11,23 +11,70 @@ export declare const MAX_CATEGORY_ASSIGNMENTS = 4;
|
|
|
11
11
|
/**
|
|
12
12
|
* 🛑 THE FLIP. `false` = the leaf/non-leaf rule REPORTS; `true` = it refuses.
|
|
13
13
|
*
|
|
14
|
-
* It ships `false` and is flipped only once the data is clean
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* the data fail, because those also go through `deriveTaxonomy`.
|
|
14
|
+
* It ships `false` and is flipped only once the data is clean. Enforcing first
|
|
15
|
+
* would make the whole catalogue unsaveable — and worse, it would make the very
|
|
16
|
+
* seed writes that fix the data fail, because those also go through
|
|
17
|
+
* `deriveTaxonomy`.
|
|
19
18
|
*
|
|
20
19
|
* This is the G3a lesson, which cost a symmetric rule its credibility: the plan
|
|
21
20
|
* specified "a lot takes a non-leaf while singles take leaves", measurement
|
|
22
21
|
* rejected the second half, and the half-rule has been live since. The rule is
|
|
23
22
|
* right; enforcing it before its data is not.
|
|
24
23
|
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
24
|
+
* ## 🛑 The offender count: FOUR answers, one measurement
|
|
25
|
+
*
|
|
26
|
+
* This comment asserted "**70 of the 71** hand-authored products file at a
|
|
27
|
+
* non-leaf, and not one of them is a lot". Both halves were wrong.
|
|
28
|
+
*
|
|
29
|
+
* | Claim | Where it came from |
|
|
30
|
+
* |---|---|
|
|
31
|
+
* | 58 of 70 | an early hand count |
|
|
32
|
+
* | 70 of 71 | this comment, until 2026-10-13 |
|
|
33
|
+
* | 0 | a first scripted run that measured NOTHING — see below |
|
|
34
|
+
* | **24** | `scripts/measure-leaf-offenders.mjs`, running the real rule |
|
|
35
|
+
*
|
|
36
|
+
* The real figure was **24** (23 `single-at-branch` + **1 `lot-at-leaf`**, so
|
|
37
|
+
* the "not one is a lot" half was false too), now **14** after re-filing the
|
|
38
|
+
* ten that had an exact existing leaf.
|
|
39
|
+
*
|
|
40
|
+
* The `0` is the instructive one: that script reimplemented
|
|
41
|
+
* `readAssignments`' precedence inline as "`categoryIds`, else `category`" —
|
|
42
|
+
* its first two branches only. The seed writes **neither**; it writes
|
|
43
|
+
* `categorySlugs`, and the real function takes its HEAD. So it found no
|
|
44
|
+
* assignments on any product, examined nothing, and reported the flip safe.
|
|
45
|
+
*
|
|
46
|
+
* ## Flip procedure — a ratchet, not a judgement call
|
|
47
|
+
*
|
|
48
|
+
* node scripts/measure-leaf-offenders.mjs # must print 0 and exit 0
|
|
49
|
+
*
|
|
50
|
+
* Never flip on a hand count, and never on a zero the script produced while
|
|
51
|
+
* reporting `unassigned === products` — it exits 2 for exactly that case now,
|
|
52
|
+
* because a zero out of a corpus nothing could be read from is not a clean
|
|
53
|
+
* result.
|
|
54
|
+
*
|
|
55
|
+
* ## ✅ FLIPPED 2026-10-14, on a measurement
|
|
56
|
+
*
|
|
57
|
+
* products : 71 · unassigned : 0 · categories : 1177 · offenders : 0
|
|
58
|
+
*
|
|
59
|
+
* `unassigned: 0` is the half that makes the zero mean something: every
|
|
60
|
+
* product resolved to a real assignment and every one of those satisfied the
|
|
61
|
+
* rule. Getting there took three kinds of fix, and the mix is worth knowing
|
|
62
|
+
* because it is NOT what the plan predicted:
|
|
63
|
+
*
|
|
64
|
+
* - **11 re-filed** to a leaf that already existed. Nine were also in the
|
|
65
|
+
* wrong GENERATION, which several products' own copy already contradicted.
|
|
66
|
+
* - **4 corpus rows promoted** out of `review` with verified facts and
|
|
67
|
+
* stated provenance (`VERIFIED_RESOLUTIONS` in `parse-bey-corpus.mjs`).
|
|
68
|
+
* - **10 fixtures retitled**, because they named products that do not
|
|
69
|
+
* exist — a Burst "B-215 Hells Scythe" (every HellsScythe is Beyblade X),
|
|
70
|
+
* a "Wizard Fafnir 4-60F" (a Burst layer with an X part code), a ring
|
|
71
|
+
* BOOSTER and a release WAVE, neither of which can ever have a model leaf.
|
|
72
|
+
*
|
|
73
|
+
* 🛑 If this ever needs to go back to `false`, the honest reason is "the data
|
|
74
|
+
* regressed", and the measurement will say so before the flag does. Re-run the
|
|
75
|
+
* script rather than reasoning about it.
|
|
29
76
|
*/
|
|
30
|
-
export declare const ENFORCE_LEAF_FILING =
|
|
77
|
+
export declare const ENFORCE_LEAF_FILING = true;
|
|
31
78
|
/** A category document, reduced to the fields assignment actually needs. */
|
|
32
79
|
export interface ResolvedCategory {
|
|
33
80
|
id: string;
|
|
@@ -73,8 +120,30 @@ export declare function readAssignments(input: {
|
|
|
73
120
|
category?: string;
|
|
74
121
|
categorySlugs?: string[];
|
|
75
122
|
}): string[];
|
|
123
|
+
/**
|
|
124
|
+
* 🛑 THE LEAF RULE DOES NOT APPLY TO EVERY LISTING TYPE, and measuring the data
|
|
125
|
+
* is what showed that — the rule was wrong, not the catalogue.
|
|
126
|
+
*
|
|
127
|
+
* Of the 70 non-leaf singles, **31 are types that are not an instance of a
|
|
128
|
+
* named model at all**: 8 app/coaching redemption codes, 8 city classifieds
|
|
129
|
+
* (collections, kits, spare-part bundles), 6 sticker sheets, 5 art prints and
|
|
130
|
+
* 4 live animals or plants. "Beyblade X App — Starter Pack Code" is not a
|
|
131
|
+
* Dragoon G. A golden retriever puppy has no model leaf and never will.
|
|
132
|
+
*
|
|
133
|
+
* So the rule is scoped by the SAME predicate that already answers this
|
|
134
|
+
* question for the Parts tab: does a listing of this type HAVE a part
|
|
135
|
+
* composition — i.e. IS it the assembly, or does it merely depict one. Reusing
|
|
136
|
+
* it rather than writing a second list is the point: a tenth listing type must
|
|
137
|
+
* answer the question once, not twice, and two hand-written enumerations of one
|
|
138
|
+
* union is Root Cause #61.
|
|
139
|
+
*
|
|
140
|
+
* `classified` and `live` stay `true` there, and that is deliberate: a
|
|
141
|
+
* classified CAN be a single bey, and the seeded ones that are collections are
|
|
142
|
+
* `isLot: true`, which the lot branch already permits at a non-leaf.
|
|
143
|
+
*/
|
|
144
|
+
export declare function leafRuleApplies(listingType: string | undefined): boolean;
|
|
76
145
|
/** Which assignments break the leaf rule, and why. Reporting shape, no throw. */
|
|
77
|
-
export declare function findLeafRuleOffenders(resolved: ResolvedCategory[], isLot: boolean): {
|
|
146
|
+
export declare function findLeafRuleOffenders(resolved: ResolvedCategory[], isLot: boolean, listingType?: string): {
|
|
78
147
|
id: string;
|
|
79
148
|
name?: string;
|
|
80
149
|
reason: "lot-at-leaf" | "single-at-branch" | "not-indexed";
|
|
@@ -88,6 +157,8 @@ export declare function findLeafRuleOffenders(resolved: ResolvedCategory[], isLo
|
|
|
88
157
|
*/
|
|
89
158
|
export declare function assertAssignmentShape(resolved: ResolvedCategory[], isLot: boolean, opts?: {
|
|
90
159
|
enforceLeafFiling?: boolean;
|
|
160
|
+
/** Scopes the SINGLE half of the leaf rule — see `leafRuleApplies`. */
|
|
161
|
+
listingType?: string;
|
|
91
162
|
warn?: (msg: string, meta?: object) => void;
|
|
92
163
|
}): void;
|
|
93
164
|
/**
|
|
@@ -47,6 +47,7 @@
|
|
|
47
47
|
* @tag sideEffects:none
|
|
48
48
|
*/
|
|
49
49
|
import { ConflictError, ValidationError } from "../../../errors/index.js";
|
|
50
|
+
import { LISTING_TYPE_HAS_COMPOSITION } from "../../parts/schemas/firestore.js";
|
|
50
51
|
/**
|
|
51
52
|
* 🛑 A CAP, because the closure is `array-contains`-indexed and Firestore's
|
|
52
53
|
* `array-contains-any` takes at most 30 comparison values.
|
|
@@ -60,23 +61,70 @@ export const MAX_CATEGORY_ASSIGNMENTS = 4;
|
|
|
60
61
|
/**
|
|
61
62
|
* 🛑 THE FLIP. `false` = the leaf/non-leaf rule REPORTS; `true` = it refuses.
|
|
62
63
|
*
|
|
63
|
-
* It ships `false` and is flipped only once the data is clean
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* the data fail, because those also go through `deriveTaxonomy`.
|
|
64
|
+
* It ships `false` and is flipped only once the data is clean. Enforcing first
|
|
65
|
+
* would make the whole catalogue unsaveable — and worse, it would make the very
|
|
66
|
+
* seed writes that fix the data fail, because those also go through
|
|
67
|
+
* `deriveTaxonomy`.
|
|
68
68
|
*
|
|
69
69
|
* This is the G3a lesson, which cost a symmetric rule its credibility: the plan
|
|
70
70
|
* specified "a lot takes a non-leaf while singles take leaves", measurement
|
|
71
71
|
* rejected the second half, and the half-rule has been live since. The rule is
|
|
72
72
|
* right; enforcing it before its data is not.
|
|
73
73
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
74
|
+
* ## 🛑 The offender count: FOUR answers, one measurement
|
|
75
|
+
*
|
|
76
|
+
* This comment asserted "**70 of the 71** hand-authored products file at a
|
|
77
|
+
* non-leaf, and not one of them is a lot". Both halves were wrong.
|
|
78
|
+
*
|
|
79
|
+
* | Claim | Where it came from |
|
|
80
|
+
* |---|---|
|
|
81
|
+
* | 58 of 70 | an early hand count |
|
|
82
|
+
* | 70 of 71 | this comment, until 2026-10-13 |
|
|
83
|
+
* | 0 | a first scripted run that measured NOTHING — see below |
|
|
84
|
+
* | **24** | `scripts/measure-leaf-offenders.mjs`, running the real rule |
|
|
85
|
+
*
|
|
86
|
+
* The real figure was **24** (23 `single-at-branch` + **1 `lot-at-leaf`**, so
|
|
87
|
+
* the "not one is a lot" half was false too), now **14** after re-filing the
|
|
88
|
+
* ten that had an exact existing leaf.
|
|
89
|
+
*
|
|
90
|
+
* The `0` is the instructive one: that script reimplemented
|
|
91
|
+
* `readAssignments`' precedence inline as "`categoryIds`, else `category`" —
|
|
92
|
+
* its first two branches only. The seed writes **neither**; it writes
|
|
93
|
+
* `categorySlugs`, and the real function takes its HEAD. So it found no
|
|
94
|
+
* assignments on any product, examined nothing, and reported the flip safe.
|
|
95
|
+
*
|
|
96
|
+
* ## Flip procedure — a ratchet, not a judgement call
|
|
97
|
+
*
|
|
98
|
+
* node scripts/measure-leaf-offenders.mjs # must print 0 and exit 0
|
|
99
|
+
*
|
|
100
|
+
* Never flip on a hand count, and never on a zero the script produced while
|
|
101
|
+
* reporting `unassigned === products` — it exits 2 for exactly that case now,
|
|
102
|
+
* because a zero out of a corpus nothing could be read from is not a clean
|
|
103
|
+
* result.
|
|
104
|
+
*
|
|
105
|
+
* ## ✅ FLIPPED 2026-10-14, on a measurement
|
|
106
|
+
*
|
|
107
|
+
* products : 71 · unassigned : 0 · categories : 1177 · offenders : 0
|
|
108
|
+
*
|
|
109
|
+
* `unassigned: 0` is the half that makes the zero mean something: every
|
|
110
|
+
* product resolved to a real assignment and every one of those satisfied the
|
|
111
|
+
* rule. Getting there took three kinds of fix, and the mix is worth knowing
|
|
112
|
+
* because it is NOT what the plan predicted:
|
|
113
|
+
*
|
|
114
|
+
* - **11 re-filed** to a leaf that already existed. Nine were also in the
|
|
115
|
+
* wrong GENERATION, which several products' own copy already contradicted.
|
|
116
|
+
* - **4 corpus rows promoted** out of `review` with verified facts and
|
|
117
|
+
* stated provenance (`VERIFIED_RESOLUTIONS` in `parse-bey-corpus.mjs`).
|
|
118
|
+
* - **10 fixtures retitled**, because they named products that do not
|
|
119
|
+
* exist — a Burst "B-215 Hells Scythe" (every HellsScythe is Beyblade X),
|
|
120
|
+
* a "Wizard Fafnir 4-60F" (a Burst layer with an X part code), a ring
|
|
121
|
+
* BOOSTER and a release WAVE, neither of which can ever have a model leaf.
|
|
122
|
+
*
|
|
123
|
+
* 🛑 If this ever needs to go back to `false`, the honest reason is "the data
|
|
124
|
+
* regressed", and the measurement will say so before the flag does. Re-run the
|
|
125
|
+
* script rather than reasoning about it.
|
|
78
126
|
*/
|
|
79
|
-
export const ENFORCE_LEAF_FILING =
|
|
127
|
+
export const ENFORCE_LEAF_FILING = true;
|
|
80
128
|
/**
|
|
81
129
|
* The authored assignment list, from whichever field the caller used.
|
|
82
130
|
*
|
|
@@ -123,9 +171,42 @@ export function readAssignments(input) {
|
|
|
123
171
|
}
|
|
124
172
|
return out;
|
|
125
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* 🛑 THE LEAF RULE DOES NOT APPLY TO EVERY LISTING TYPE, and measuring the data
|
|
176
|
+
* is what showed that — the rule was wrong, not the catalogue.
|
|
177
|
+
*
|
|
178
|
+
* Of the 70 non-leaf singles, **31 are types that are not an instance of a
|
|
179
|
+
* named model at all**: 8 app/coaching redemption codes, 8 city classifieds
|
|
180
|
+
* (collections, kits, spare-part bundles), 6 sticker sheets, 5 art prints and
|
|
181
|
+
* 4 live animals or plants. "Beyblade X App — Starter Pack Code" is not a
|
|
182
|
+
* Dragoon G. A golden retriever puppy has no model leaf and never will.
|
|
183
|
+
*
|
|
184
|
+
* So the rule is scoped by the SAME predicate that already answers this
|
|
185
|
+
* question for the Parts tab: does a listing of this type HAVE a part
|
|
186
|
+
* composition — i.e. IS it the assembly, or does it merely depict one. Reusing
|
|
187
|
+
* it rather than writing a second list is the point: a tenth listing type must
|
|
188
|
+
* answer the question once, not twice, and two hand-written enumerations of one
|
|
189
|
+
* union is Root Cause #61.
|
|
190
|
+
*
|
|
191
|
+
* `classified` and `live` stay `true` there, and that is deliberate: a
|
|
192
|
+
* classified CAN be a single bey, and the seeded ones that are collections are
|
|
193
|
+
* `isLot: true`, which the lot branch already permits at a non-leaf.
|
|
194
|
+
*/
|
|
195
|
+
export function leafRuleApplies(listingType) {
|
|
196
|
+
if (!listingType)
|
|
197
|
+
return true;
|
|
198
|
+
return LISTING_TYPE_HAS_COMPOSITION[listingType] !== false;
|
|
199
|
+
}
|
|
126
200
|
/** Which assignments break the leaf rule, and why. Reporting shape, no throw. */
|
|
127
|
-
export function findLeafRuleOffenders(resolved, isLot) {
|
|
201
|
+
export function findLeafRuleOffenders(resolved, isLot, listingType) {
|
|
128
202
|
const out = [];
|
|
203
|
+
/*
|
|
204
|
+
* The LOT half still applies to every type — filing a lot of twelve beys
|
|
205
|
+
* under one model's leaf is a false claim about the items whatever the
|
|
206
|
+
* listing type is. Only the SINGLE half is scoped.
|
|
207
|
+
*/
|
|
208
|
+
if (!isLot && !leafRuleApplies(listingType))
|
|
209
|
+
return out;
|
|
129
210
|
for (const c of resolved) {
|
|
130
211
|
if (isLot) {
|
|
131
212
|
if (c.isLeaf === true)
|
|
@@ -179,7 +260,7 @@ export function assertAssignmentShape(resolved, isLot, opts = {}) {
|
|
|
179
260
|
throw new ValidationError(`${named} are in the same category tree, so a listing cannot be filed under both. ` +
|
|
180
261
|
`Pick one, or pick the category above them that covers both.`);
|
|
181
262
|
}
|
|
182
|
-
const offenders = findLeafRuleOffenders(resolved, isLot);
|
|
263
|
+
const offenders = findLeafRuleOffenders(resolved, isLot, opts.listingType);
|
|
183
264
|
if (offenders.length === 0)
|
|
184
265
|
return;
|
|
185
266
|
/*
|
|
@@ -190,7 +190,18 @@ export declare class ProductRepository extends BaseRepository<ProductDocument> {
|
|
|
190
190
|
* stock decrement or a status flip pays none of it.
|
|
191
191
|
*/
|
|
192
192
|
private applyCategoryDefaults;
|
|
193
|
-
/**
|
|
193
|
+
/**
|
|
194
|
+
* The keys whose presence means a write could change the product's taxonomy.
|
|
195
|
+
*
|
|
196
|
+
* 🛑 `categoryIds` is the ASSIGNED field — the one a human picks and the one
|
|
197
|
+
* `readAssignments` prefers over the `category` scalar. It was absent from
|
|
198
|
+
* this list, so a write naming only `categoryIds` skipped `deriveTaxonomy`
|
|
199
|
+
* entirely: the ordered closure, the `category` mirror and `categoryNames`
|
|
200
|
+
* were never re-derived, and `syncCategoryCounts` compared a document to
|
|
201
|
+
* itself and moved nothing. The write returned 200 and the product's own
|
|
202
|
+
* category pages did not change. Same shape as Root Cause #101 — a field
|
|
203
|
+
* accepted on the way in whose derivation never runs.
|
|
204
|
+
*/
|
|
194
205
|
private static readonly TAXONOMY_KEYS;
|
|
195
206
|
update(id: string, data: Partial<ProductDocument>): Promise<ProductDocument>;
|
|
196
207
|
/**
|
|
@@ -13,7 +13,7 @@ import { parseSearchTxtQuery } from "../../../utils/search-txt.js";
|
|
|
13
13
|
// The shared search plan/refine the other eleven migrated collections use.
|
|
14
14
|
// `planSearchTxt` carries the `empty` guard that keeps a punctuation-only query
|
|
15
15
|
// from returning the whole catalogue.
|
|
16
|
-
import { planSearchTxt,
|
|
16
|
+
import { planSearchTxt, beginSearchWindow, emptySearchResult, } from "../../../utils/search-txt-query.js";
|
|
17
17
|
// One definition of "which fields feed searchTxt", shared with the seed
|
|
18
18
|
// wrapper. Three copies existed and two were missing the same three fields.
|
|
19
19
|
import { buildProductSearchTxt } from "../../../utils/search-txt-builders.js";
|
|
@@ -359,6 +359,7 @@ export class ProductRepository extends BaseRepository {
|
|
|
359
359
|
if (resolved.length > 0) {
|
|
360
360
|
assertAssignmentShape(resolved, input.isLot === true, {
|
|
361
361
|
enforceLeafFiling: ENFORCE_LEAF_FILING,
|
|
362
|
+
listingType: input.listingType,
|
|
362
363
|
warn: (msg, meta) => serverLogger.warn(msg, { productTitle: input.title, ...meta }),
|
|
363
364
|
});
|
|
364
365
|
/*
|
|
@@ -520,15 +521,20 @@ export class ProductRepository extends BaseRepository {
|
|
|
520
521
|
return; // an exempt item has nothing to gross up
|
|
521
522
|
out.price = grossFromTaxable(input.price, effectiveRate);
|
|
522
523
|
/*
|
|
523
|
-
* 🛑
|
|
524
|
-
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
524
|
+
* 🛑 The strike-through price is grossed up TOO, now that it is a real
|
|
525
|
+
* field (F1 #25 — it was `compareAtPrice` in the form, `originalPrice` in
|
|
526
|
+
* both renderers, and nowhere in the document).
|
|
527
|
+
*
|
|
528
|
+
* It has to move with `price` or the discount it advertises is wrong: an
|
|
529
|
+
* ex-tax ₹1,000 "was" beside an inc-tax ₹1,050 "now" renders as a price
|
|
530
|
+
* RISE, and an ex-tax pair converted on one side only overstates the
|
|
531
|
+
* saving by the GST rate. The guard is the same one `price` uses — only a
|
|
532
|
+
* patch that carries the field is converted, so an unrelated save cannot
|
|
533
|
+
* re-convert an already-converted figure.
|
|
531
534
|
*/
|
|
535
|
+
if (input.originalPrice != null && input.originalPrice > 0) {
|
|
536
|
+
out.originalPrice = grossFromTaxable(input.originalPrice, effectiveRate);
|
|
537
|
+
}
|
|
532
538
|
serverLogger.info("[gst] grossed up an ex-tax price for a pricesIncludeGst:false store", {
|
|
533
539
|
storeId: input.storeId,
|
|
534
540
|
rate: effectiveRate,
|
|
@@ -1196,7 +1202,11 @@ export class ProductRepository extends BaseRepository {
|
|
|
1196
1202
|
if (searchTxt.length > 0) {
|
|
1197
1203
|
// Firestore permits a single array operator per query, so when a search
|
|
1198
1204
|
// and a category filter are both present the SEARCH takes the clause and
|
|
1199
|
-
// the category is refined by the caller
|
|
1205
|
+
// the category is refined by the caller — which, since the `also`
|
|
1206
|
+
// predicate in `list()`, is now true rather than merely asserted. This
|
|
1207
|
+
// comment claimed it for as long as the gate below has existed and no
|
|
1208
|
+
// caller did it, so a search inside a category returned the whole
|
|
1209
|
+
// catalogue.
|
|
1200
1210
|
//
|
|
1201
1211
|
// Two shapes here have each been reverted once and are both guarded by
|
|
1202
1212
|
// scripts/audit-search-semantics.mjs:
|
|
@@ -1270,25 +1280,34 @@ export class ProductRepository extends BaseRepository {
|
|
|
1270
1280
|
* `SEARCH_REFINE_WINDOW`, and when that saturates the result carries
|
|
1271
1281
|
* `truncated` so the total renders as a floor rather than a false total.
|
|
1272
1282
|
*/
|
|
1273
|
-
const isMultiTerm = plan.rest.length > 0;
|
|
1274
|
-
const requestedPage = Math.max(1, Number(model?.page ?? 1) || 1);
|
|
1275
|
-
const requestedPageSize = Math.max(1, Number(model?.pageSize ?? 20) || 20);
|
|
1276
|
-
const result = await this.sieveQuery(
|
|
1277
1283
|
/*
|
|
1278
|
-
* The
|
|
1279
|
-
*
|
|
1280
|
-
* `
|
|
1281
|
-
*
|
|
1284
|
+
* ── The category filter has to be refined for the SAME reason ───────────
|
|
1285
|
+
*
|
|
1286
|
+
* 🛑 `buildScopedQuery` gates `categoriesIn` on `searchTxt.length === 0`,
|
|
1287
|
+
* because Firestore permits one array operator per query and the search
|
|
1288
|
+
* term takes it. Its comment said "the category is refined by the caller"
|
|
1289
|
+
* — and NO caller did, for as long as the gate has existed. So searching
|
|
1290
|
+
* inside a category silently returned matches from the whole catalogue:
|
|
1291
|
+
* a plausible, populated, wrong grid with a 200 (Root Cause #62's family).
|
|
1292
|
+
*
|
|
1293
|
+
* This is that caller. The refine rides the same bounded window the
|
|
1294
|
+
* multi-term refine already uses, so the cost is one read either way and
|
|
1295
|
+
* there is no second pagination implementation to disagree with the first.
|
|
1282
1296
|
*/
|
|
1283
|
-
|
|
1297
|
+
const categoriesIn = opts?.categoriesIn ?? [];
|
|
1298
|
+
const needsCategoryRefine = plan.head !== null && categoriesIn.length > 0;
|
|
1299
|
+
const wanted = new Set(categoriesIn);
|
|
1300
|
+
/*
|
|
1301
|
+
* The window, the raised `maxPageSize` and the page slice are all one
|
|
1302
|
+
* decision, so they come from one helper — `beginSearchWindow`. Keeping
|
|
1303
|
+
* them hand-written here is what let the other ten repositories each get
|
|
1304
|
+
* two of the three.
|
|
1305
|
+
*/
|
|
1306
|
+
const win = beginSearchWindow(model, plan, { alsoRefining: needsCategoryRefine });
|
|
1307
|
+
const result = await this.sieveQuery(win.model, ProductRepository.SIEVE_FIELDS, {
|
|
1284
1308
|
baseQuery: this.buildScopedQuery(opts),
|
|
1285
1309
|
defaultPageSize: 20,
|
|
1286
|
-
|
|
1287
|
-
* 🛑 `maxPageSize` has to rise with the window or Sieve clamps the
|
|
1288
|
-
* request back to 100 — and the window would silently be a page again,
|
|
1289
|
-
* which is the original bug with a larger constant.
|
|
1290
|
-
*/
|
|
1291
|
-
maxPageSize: isMultiTerm ? SEARCH_REFINE_WINDOW : 100,
|
|
1310
|
+
maxPageSize: win.maxPageSize(100),
|
|
1292
1311
|
aliases: ProductRepository.FILTER_ALIASES,
|
|
1293
1312
|
});
|
|
1294
1313
|
// Only the most selective term became the `array-contains` clause, because
|
|
@@ -1303,14 +1322,15 @@ export class ProductRepository extends BaseRepository {
|
|
|
1303
1322
|
// this page only. That is the known post-pagination-filter debt, not a new
|
|
1304
1323
|
// invention — reporting the pre-filter total would promise pages that
|
|
1305
1324
|
// render empty.
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1325
|
+
/*
|
|
1326
|
+
* 🛑 The category predicate matches against the CLOSURE, not the leaf.
|
|
1327
|
+
* `categorySlugs` carries the full ancestor chain, so a category page
|
|
1328
|
+
* passing its own id matches every product in its subtree — the same
|
|
1329
|
+
* semantics the pushed-down `array-contains-any` has, which is what makes
|
|
1330
|
+
* the refined result identical to the un-searched one.
|
|
1331
|
+
*/
|
|
1332
|
+
return win.finish(result, needsCategoryRefine
|
|
1333
|
+
? (row) => (row.categorySlugs ?? []).some((slug) => wanted.has(slug))
|
|
1314
1334
|
: undefined);
|
|
1315
1335
|
}
|
|
1316
1336
|
/**
|
|
@@ -1412,8 +1432,20 @@ export class ProductRepository extends BaseRepository {
|
|
|
1412
1432
|
}
|
|
1413
1433
|
}
|
|
1414
1434
|
ProductRepository.CACHE_TTL_MS = 30000;
|
|
1415
|
-
/**
|
|
1435
|
+
/**
|
|
1436
|
+
* The keys whose presence means a write could change the product's taxonomy.
|
|
1437
|
+
*
|
|
1438
|
+
* 🛑 `categoryIds` is the ASSIGNED field — the one a human picks and the one
|
|
1439
|
+
* `readAssignments` prefers over the `category` scalar. It was absent from
|
|
1440
|
+
* this list, so a write naming only `categoryIds` skipped `deriveTaxonomy`
|
|
1441
|
+
* entirely: the ordered closure, the `category` mirror and `categoryNames`
|
|
1442
|
+
* were never re-derived, and `syncCategoryCounts` compared a document to
|
|
1443
|
+
* itself and moved nothing. The write returned 200 and the product's own
|
|
1444
|
+
* category pages did not change. Same shape as Root Cause #101 — a field
|
|
1445
|
+
* accepted on the way in whose derivation never runs.
|
|
1446
|
+
*/
|
|
1416
1447
|
ProductRepository.TAXONOMY_KEYS = [
|
|
1448
|
+
"categoryIds",
|
|
1417
1449
|
"categorySlugs",
|
|
1418
1450
|
"category",
|
|
1419
1451
|
"brand",
|
|
@@ -217,6 +217,28 @@ export interface ProductDocument extends BaseDocument {
|
|
|
217
217
|
brand?: string;
|
|
218
218
|
brandSlug?: string;
|
|
219
219
|
price: number;
|
|
220
|
+
/**
|
|
221
|
+
* The strike-through "was ₹X", for a listing the seller is discounting.
|
|
222
|
+
*
|
|
223
|
+
* 🛑 F1 #25, and it was TWO OWNERS of one value rather than a missing field.
|
|
224
|
+
* `SellerProductShell` renders an input called `compareAtPrice` and the eight
|
|
225
|
+
* store edit pages read `(product as any).compareAtPrice` — a property no
|
|
226
|
+
* document has ever carried — while BOTH public renderers (`ProductGrid`,
|
|
227
|
+
* `ProductDetailPageView`) read `originalPrice` and hand it to `GatedPrice`
|
|
228
|
+
* as `originalAmount`, which is what draws the strike-through and derives
|
|
229
|
+
* the discount %. The consumer write schema already accepted `originalPrice`
|
|
230
|
+
* too. So the authoring half wrote one name, the rendering half read
|
|
231
|
+
* another, and the document declared neither: a seller set a sale price,
|
|
232
|
+
* saw a 200, and nothing ever showed it.
|
|
233
|
+
*
|
|
234
|
+
* Declared here under the name the renderers and the write schema already
|
|
235
|
+
* use, rather than adding a second field beside them (Root Cause #75).
|
|
236
|
+
*
|
|
237
|
+
* 🛑 It is a MONEY figure, so it is grossed up with `price` for a
|
|
238
|
+
* `pricesIncludeGst: false` store — a "was" price that silently meant
|
|
239
|
+
* ex-tax while the live price meant inc-tax would overstate the discount.
|
|
240
|
+
*/
|
|
241
|
+
originalPrice?: number;
|
|
220
242
|
currency: string;
|
|
221
243
|
stockQuantity: number;
|
|
222
244
|
availableQuantity: number;
|
|
@@ -648,8 +670,8 @@ export declare const ProductStatusValues: {
|
|
|
648
670
|
export declare const PRODUCT_COLLECTION: "products";
|
|
649
671
|
export declare const PRODUCT_INDEXED_FIELDS: readonly ["storeId", "status", "category", "categoryIds", "featured", "listingType", "isPromoted", "isOnSale", "isSold", "searchTxt", "createdAt", "barcodeId", "physicalLocation.zone"];
|
|
650
672
|
export declare const DEFAULT_PRODUCT_DATA: Partial<ProductDocument>;
|
|
651
|
-
export declare const PRODUCT_PUBLIC_FIELDS: readonly ["allowOffers", "minOfferPercent", "id", "title", "description", "category", "categoryIds", "categoryIdNames", "subcategory", "brand", "price", "currency", "stockQuantity", "availableQuantity", "images", "status", "storeName", "featured", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "isLot", "lotSize", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "views7d", "views30d", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "listingType", "auctionEndDate", "startingBid", "currentBid", "bidCount", "reservePrice", "buyNowPrice", "minBidIncrement", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderCurrentCount", "preOrderProductionStatus", "preOrderCancellable", "condition", "insurance", "insuranceCost", "shippingPaidBy", "isPromoted", "isOnSale", "isSold", "slug", "seoTitle", "seoDescription", "seoKeywords", "viewCount", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "createdAt"];
|
|
652
|
-
export declare const PRODUCT_UPDATABLE_FIELDS: readonly ["title", "description", "category", "categoryIds", "subcategory", "brand", "price", "stockQuantity", "images", "status", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "isLot", "lotSize", "pickupAddressId", "condition", "insurance", "shippingPaidBy", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "reservePrice", "buyNowPrice", "minBidIncrement", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "listingType", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderProductionStatus", "preOrderCancellable", "isOnSale", "isSold", "allowShipBeforeEmiComplete", "gstRate", "hsnCode", "authenticity", "printMeta", "seoTitle", "seoDescription", "seoKeywords", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "barcodeId"];
|
|
673
|
+
export declare const PRODUCT_PUBLIC_FIELDS: readonly ["allowOffers", "minOfferPercent", "id", "title", "description", "category", "categoryIds", "categoryIdNames", "subcategory", "brand", "price", "originalPrice", "currency", "stockQuantity", "availableQuantity", "images", "status", "storeName", "featured", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "isLot", "lotSize", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "views7d", "views30d", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "listingType", "auctionEndDate", "startingBid", "currentBid", "bidCount", "reservePrice", "buyNowPrice", "minBidIncrement", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderCurrentCount", "preOrderProductionStatus", "preOrderCancellable", "condition", "insurance", "insuranceCost", "shippingPaidBy", "isPromoted", "isOnSale", "isSold", "slug", "seoTitle", "seoDescription", "seoKeywords", "viewCount", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "createdAt"];
|
|
674
|
+
export declare const PRODUCT_UPDATABLE_FIELDS: readonly ["title", "description", "category", "categoryIds", "subcategory", "brand", "price", "originalPrice", "stockQuantity", "images", "status", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "isLot", "lotSize", "pickupAddressId", "condition", "insurance", "shippingPaidBy", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "reservePrice", "buyNowPrice", "minBidIncrement", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "listingType", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderProductionStatus", "preOrderCancellable", "isOnSale", "isSold", "allowShipBeforeEmiComplete", "gstRate", "hsnCode", "authenticity", "printMeta", "seoTitle", "seoDescription", "seoKeywords", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "barcodeId"];
|
|
653
675
|
export type ProductCreateInput = Omit<ProductDocument, "id" | "createdAt" | "updatedAt" | "availableQuantity" | "bidCount" | "currentBid" | "auctionOriginalEndDate">;
|
|
654
676
|
export type ProductUpdateInput = Partial<Pick<ProductDocument, (typeof PRODUCT_UPDATABLE_FIELDS)[number]>>;
|
|
655
677
|
export type ProductAdminUpdateInput = Partial<Omit<ProductDocument, "id" | "createdAt">>;
|
|
@@ -84,6 +84,13 @@ export const PRODUCT_PUBLIC_FIELDS = [
|
|
|
84
84
|
"subcategory",
|
|
85
85
|
"brand",
|
|
86
86
|
"price",
|
|
87
|
+
/*
|
|
88
|
+
* The strike-through "was ₹X". Public because that is the only place it
|
|
89
|
+
* means anything — `GatedPrice` renders it as `originalAmount` and derives
|
|
90
|
+
* the discount % from it, inside the guest gate, so it is published on
|
|
91
|
+
* exactly the same terms as `price` itself.
|
|
92
|
+
*/
|
|
93
|
+
"originalPrice",
|
|
87
94
|
"currency",
|
|
88
95
|
"stockQuantity",
|
|
89
96
|
"availableQuantity",
|
|
@@ -192,6 +199,9 @@ export const PRODUCT_UPDATABLE_FIELDS = [
|
|
|
192
199
|
"subcategory",
|
|
193
200
|
"brand",
|
|
194
201
|
"price",
|
|
202
|
+
/* The strike-through "was" price — see the declaration on ProductDocument
|
|
203
|
+
* for why it is this name and not `compareAtPrice` (F1 #25). */
|
|
204
|
+
"originalPrice",
|
|
195
205
|
"stockQuantity",
|
|
196
206
|
"images",
|
|
197
207
|
"status",
|
|
@@ -205,7 +215,9 @@ export const PRODUCT_UPDATABLE_FIELDS = [
|
|
|
205
215
|
* `.passthrough()`, so a field absent from this list is dropped before it
|
|
206
216
|
* reaches Firestore while the write still returns 200 — exactly how a
|
|
207
217
|
* UI-created product lost its `categorySlugs` chain (Root Cause #101) and how
|
|
208
|
-
*
|
|
218
|
+
* the strike-through price was silently discarded until F1 #25 was closed —
|
|
219
|
+
* the form wrote `compareAtPrice`, nothing declared it, and `originalPrice`
|
|
220
|
+
* (which both renderers read) was not in this list either. */
|
|
209
221
|
"isLot",
|
|
210
222
|
"lotSize",
|
|
211
223
|
"pickupAddressId",
|
|
@@ -406,6 +406,7 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
406
406
|
description?: string | undefined;
|
|
407
407
|
keywords?: string[] | undefined;
|
|
408
408
|
} | undefined;
|
|
409
|
+
originalPrice?: number | undefined;
|
|
409
410
|
allowShipBeforeEmiComplete?: boolean | undefined;
|
|
410
411
|
printMeta?: {
|
|
411
412
|
size?: string | undefined;
|
|
@@ -459,7 +460,6 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
459
460
|
}[] | undefined;
|
|
460
461
|
prizeGithubFileUrl?: string | undefined;
|
|
461
462
|
inStock?: boolean | undefined;
|
|
462
|
-
originalPrice?: number | undefined;
|
|
463
463
|
sellerAvatar?: string | undefined;
|
|
464
464
|
stockCount?: number | undefined;
|
|
465
465
|
attributes?: Record<string, string> | undefined;
|
|
@@ -545,6 +545,7 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
545
545
|
description?: string | undefined;
|
|
546
546
|
keywords?: string[] | undefined;
|
|
547
547
|
} | undefined;
|
|
548
|
+
originalPrice?: number | undefined;
|
|
548
549
|
allowShipBeforeEmiComplete?: boolean | undefined;
|
|
549
550
|
printMeta?: {
|
|
550
551
|
size?: string | undefined;
|
|
@@ -598,7 +599,6 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
598
599
|
}[] | undefined;
|
|
599
600
|
prizeGithubFileUrl?: string | undefined;
|
|
600
601
|
inStock?: boolean | undefined;
|
|
601
|
-
originalPrice?: number | undefined;
|
|
602
602
|
sellerAvatar?: string | undefined;
|
|
603
603
|
stockCount?: number | undefined;
|
|
604
604
|
attributes?: Record<string, string> | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { planSearchTxt,
|
|
1
|
+
import { planSearchTxt, beginSearchWindow, emptySearchResult, } from "../../../utils/search-txt-query.js";
|
|
2
2
|
import { buildCouponSearchTxt } from "../../../utils/search-txt-builders.js";
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
4
4
|
import { serverLogger } from "../../../monitoring/server-logger.js";
|
|
@@ -500,12 +500,19 @@ export class CouponsRepository extends BaseRepository {
|
|
|
500
500
|
if (plan.head) {
|
|
501
501
|
baseQuery = baseQuery.where("searchTxt", "array-contains", plan.head);
|
|
502
502
|
}
|
|
503
|
-
|
|
503
|
+
/*
|
|
504
|
+
* One bounded window, one raised `maxPageSize`, one page slice — see
|
|
505
|
+
* `beginSearchWindow`. Hand-writing the three is how the other ten
|
|
506
|
+
* repositories each ended up with two of them, which reads as a working
|
|
507
|
+
* multi-word search and silently cannot reach page 2.
|
|
508
|
+
*/
|
|
509
|
+
const win = beginSearchWindow(model, plan);
|
|
510
|
+
const result = await this.sieveQuery(win.model, CouponsRepository.SIEVE_FIELDS, {
|
|
504
511
|
baseQuery,
|
|
505
512
|
defaultPageSize: 50,
|
|
506
|
-
maxPageSize: 200,
|
|
513
|
+
maxPageSize: win.maxPageSize(200),
|
|
507
514
|
});
|
|
508
|
-
return
|
|
515
|
+
return win.finish(result);
|
|
509
516
|
}
|
|
510
517
|
/**
|
|
511
518
|
* Cloud Functions: find active coupons whose validity period has expired.
|
|
@@ -4,7 +4,7 @@ import { REVIEW_MAX_RATING, REVIEW_MIN_RATING } from "../../../_internal/shared/
|
|
|
4
4
|
import { decryptPiiFields, REVIEW_PII_FIELDS, REVIEW_PII_INDEX_MAP, } from "../../../security/index.js";
|
|
5
5
|
import { createReviewId, REVIEW_COLLECTION, } from "../schemas/index.js";
|
|
6
6
|
import { buildReviewSearchTxt } from "../../../utils/search-txt-builders.js";
|
|
7
|
-
import { planSearchTxt,
|
|
7
|
+
import { planSearchTxt, beginSearchWindow, emptySearchResult, } from "../../../utils/search-txt-query.js";
|
|
8
8
|
const REVIEW_FIELDS = {
|
|
9
9
|
/** Word-prefix search tokens. Derived on write by `buildSearchTxtFor`. */
|
|
10
10
|
SEARCH_TXT: "searchTxt",
|
|
@@ -362,14 +362,21 @@ class ReviewRepository extends BaseRepository {
|
|
|
362
362
|
if (plan.head) {
|
|
363
363
|
baseQuery = baseQuery.where(REVIEW_FIELDS.SEARCH_TXT, "array-contains", plan.head);
|
|
364
364
|
}
|
|
365
|
-
|
|
365
|
+
/*
|
|
366
|
+
* One bounded window, one raised `maxPageSize`, one page slice — see
|
|
367
|
+
* `beginSearchWindow`. Hand-writing the three is how the other ten
|
|
368
|
+
* repositories each ended up with two of them, which reads as a working
|
|
369
|
+
* multi-word search and silently cannot reach page 2.
|
|
370
|
+
*/
|
|
371
|
+
const win = beginSearchWindow(model, plan);
|
|
372
|
+
const result = await this.sieveQuery(win.model, ReviewRepository.SIEVE_FIELDS, {
|
|
366
373
|
baseQuery,
|
|
367
374
|
defaultPageSize: 50,
|
|
368
|
-
maxPageSize: 200,
|
|
375
|
+
maxPageSize: win.maxPageSize(200),
|
|
369
376
|
});
|
|
370
377
|
// Refine BEFORE decrypting: the refine reads `searchTxt`, which is never
|
|
371
378
|
// encrypted, and decrypting rows that are about to be dropped is waste.
|
|
372
|
-
return this.decryptSieveResult(
|
|
379
|
+
return this.decryptSieveResult(win.finish(result));
|
|
373
380
|
}
|
|
374
381
|
}
|
|
375
382
|
ReviewRepository.SIEVE_FIELDS = {
|