@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.
Files changed (263) hide show
  1. package/dist/_internal/client/features/lottery/LotteryDetailView.js +22 -1
  2. package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
  3. package/dist/_internal/server/features/categories/adapters.js +235 -0
  4. package/dist/_internal/server/features/checkout/actions.js +161 -22
  5. package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
  6. package/dist/_internal/server/features/orders/adapters.js +21 -0
  7. package/dist/_internal/server/features/products/list-public.d.ts +24 -0
  8. package/dist/_internal/server/features/products/list-public.js +10 -2
  9. package/dist/_internal/server/features/refunds/actions.js +1 -1
  10. package/dist/_internal/server/features/seo/sitemap.js +54 -15
  11. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  12. package/dist/_internal/server/features/stores/adapters.js +6 -0
  13. package/dist/_internal/server/functions/scheduled.d.ts +4 -2
  14. package/dist/_internal/server/functions/scheduled.js +64 -6
  15. package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
  16. package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
  17. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
  18. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
  19. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
  20. package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
  21. package/dist/_internal/server/jobs/core/onCategoryWrite.js +59 -0
  22. package/dist/_internal/server/jobs/core/onProductWrite.js +31 -0
  23. package/dist/_internal/server/jobs/core/onReviewWrite.js +8 -0
  24. package/dist/_internal/server/jobs/core/onStoreWrite.d.ts +1 -1
  25. package/dist/_internal/server/jobs/core/onStoreWrite.js +8 -2
  26. package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
  27. package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
  28. package/dist/_internal/server/jobs/core/revalidateNotify.d.ts +28 -0
  29. package/dist/_internal/server/jobs/core/revalidateNotify.js +125 -0
  30. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
  31. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
  32. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
  33. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
  34. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
  35. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
  36. package/dist/_internal/server/jobs/handlers/index.js +3 -1
  37. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
  38. package/dist/_internal/server/jobs/handlers/messages.js +5 -2
  39. package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
  40. package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
  41. package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
  42. package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
  43. package/dist/_internal/shared/actions/action-registry.js +34 -0
  44. package/dist/_internal/shared/cache/revalidate-targets.d.ts +49 -0
  45. package/dist/_internal/shared/cache/revalidate-targets.js +157 -0
  46. package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
  47. package/dist/_internal/shared/checkout/order-math.js +194 -7
  48. package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
  49. package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
  50. package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
  51. package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
  52. package/dist/_internal/shared/fees/calculator.d.ts +105 -0
  53. package/dist/_internal/shared/fees/calculator.js +143 -0
  54. package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
  55. package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
  56. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
  57. package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
  58. package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
  59. package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
  60. package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
  61. package/dist/_internal/shared/templating/placeholders.js +73 -0
  62. package/dist/client.d.ts +2 -0
  63. package/dist/client.js +1 -0
  64. package/dist/constants/api-endpoints.d.ts +42 -0
  65. package/dist/constants/api-endpoints.js +14 -0
  66. package/dist/features/admin/actions/admin-actions.d.ts +1 -1
  67. package/dist/features/admin/actions/admin-actions.js +1 -1
  68. package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
  69. package/dist/features/admin/components/AdminProductEditorView.js +8 -1
  70. package/dist/features/admin/components/AdminProductsView.js +78 -7
  71. package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
  72. package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
  73. package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
  74. package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
  75. package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
  76. package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
  77. package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
  78. package/dist/features/auctions/components/AuctionBottomActions.js +10 -1
  79. package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
  80. package/dist/features/auctions/components/AuctionLiveStatusBadge.d.ts +20 -0
  81. package/dist/features/auctions/components/AuctionLiveStatusBadge.js +21 -0
  82. package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
  83. package/dist/features/auctions/components/PlaceBidFormClient.js +20 -2
  84. package/dist/features/auctions/hooks/useIsAuctionEnded.d.ts +41 -0
  85. package/dist/features/auctions/hooks/useIsAuctionEnded.js +76 -0
  86. package/dist/features/auth/schemas/firestore.d.ts +0 -1
  87. package/dist/features/blog/api/route.js +16 -1
  88. package/dist/features/blog/schemas/firestore.d.ts +2 -2
  89. package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
  90. package/dist/features/categories/actions/category-actions.d.ts +2 -2
  91. package/dist/features/categories/actions/category-actions.js +22 -10
  92. package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
  93. package/dist/features/categories/api/[id]/description-template/route.js +252 -0
  94. package/dist/features/categories/api/route.js +156 -11
  95. package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -3
  96. package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
  97. package/dist/features/categories/components/CategoriesIndexPageView.d.ts +27 -1
  98. package/dist/features/categories/components/CategoriesIndexPageView.js +24 -33
  99. package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
  100. package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
  101. package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
  102. package/dist/features/categories/constants/listing-categories.d.ts +17 -0
  103. package/dist/features/categories/constants/listing-categories.js +53 -0
  104. package/dist/features/categories/hooks/useCategoryTree.js +28 -2
  105. package/dist/features/categories/repository/categories.repository.d.ts +73 -12
  106. package/dist/features/categories/repository/categories.repository.js +203 -42
  107. package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
  108. package/dist/features/categories/repository/price-index.repository.js +85 -0
  109. package/dist/features/categories/schemas/category-content.d.ts +206 -0
  110. package/dist/features/categories/schemas/category-content.js +28 -0
  111. package/dist/features/categories/schemas/firestore.d.ts +74 -5
  112. package/dist/features/categories/schemas/firestore.js +74 -3
  113. package/dist/features/categories/schemas/index.d.ts +1047 -0
  114. package/dist/features/categories/schemas/index.js +96 -0
  115. package/dist/features/categories/server.d.ts +1 -0
  116. package/dist/features/categories/server.js +7 -0
  117. package/dist/features/categories/types/index.d.ts +9 -0
  118. package/dist/features/events/schemas/firestore.d.ts +2 -2
  119. package/dist/features/faq/schemas/firestore.d.ts +2 -1
  120. package/dist/features/faq/schemas/firestore.js +20 -8
  121. package/dist/features/faq/utils/getHomepageFaqsCached.d.ts +20 -0
  122. package/dist/features/faq/utils/getHomepageFaqsCached.js +22 -0
  123. package/dist/features/homepage/components/MarketplaceHomepageView.js +13 -3
  124. package/dist/features/homepage/lib/live-stats.js +18 -8
  125. package/dist/features/media/MediaImage.js +19 -2
  126. package/dist/features/orders/schemas/firestore.d.ts +70 -0
  127. package/dist/features/orders/types/index.d.ts +55 -0
  128. package/dist/features/products/actions/product-actions.d.ts +0 -9
  129. package/dist/features/products/actions/product-actions.js +25 -0
  130. package/dist/features/products/columns/index.d.ts +0 -1
  131. package/dist/features/products/columns/index.js +5 -1
  132. package/dist/features/products/components/ArtStickersListView.js +7 -1
  133. package/dist/features/products/components/PrizeDrawsSection.js +5 -0
  134. package/dist/features/products/components/ProductFilters.d.ts +0 -9
  135. package/dist/features/products/components/ProductForm.d.ts +27 -1
  136. package/dist/features/products/components/ProductForm.js +26 -12
  137. package/dist/features/products/components/ProductsIndexPageView.js +1 -1
  138. package/dist/features/products/constants/sieve.d.ts +0 -6
  139. package/dist/features/products/constants/sieve.js +35 -2
  140. package/dist/features/products/repository/products.repository.d.ts +115 -1
  141. package/dist/features/products/repository/products.repository.js +491 -39
  142. package/dist/features/products/schemas/firestore.d.ts +65 -5
  143. package/dist/features/products/schemas/firestore.js +9 -0
  144. package/dist/features/products/schemas/product-features.d.ts +140 -2
  145. package/dist/features/products/schemas/product-features.js +62 -1
  146. package/dist/features/promotions/repository/coupons.repository.js +6 -1
  147. package/dist/features/promotions/schemas/firestore.d.ts +44 -1
  148. package/dist/features/promotions/schemas/firestore.js +76 -2
  149. package/dist/features/reviews/repository/reviews.repository.d.ts +22 -0
  150. package/dist/features/reviews/repository/reviews.repository.js +35 -0
  151. package/dist/features/seller/actions/seller-actions.js +17 -2
  152. package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
  153. package/dist/features/seller/components/PrintCenterView.js +3 -2
  154. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
  155. package/dist/features/seller/components/SellerOrdersView.js +6 -1
  156. package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
  157. package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
  158. package/dist/features/seller/components/SellerProductShell.js +101 -9
  159. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
  160. package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
  161. package/dist/features/seller/components/SellerProductsView.js +108 -3
  162. package/dist/features/seller/types/index.d.ts +17 -0
  163. package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
  164. package/dist/features/seller/utils/product-draft-mapping.js +17 -0
  165. package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
  166. package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
  167. package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
  168. package/dist/features/stores/schemas/firestore.d.ts +31 -2
  169. package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
  170. package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
  171. package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
  172. package/dist/features/tax-codes/schemas/firestore.js +82 -0
  173. package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
  174. package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
  175. package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
  176. package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
  177. package/dist/features/tester/actions/index.d.ts +0 -1
  178. package/dist/features/tester/actions/index.js +0 -1
  179. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
  180. package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
  181. package/dist/features/tester/components/index.d.ts +0 -4
  182. package/dist/features/tester/components/index.js +0 -2
  183. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
  184. package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
  185. package/dist/features/tester/schemas/firestore.d.ts +0 -6
  186. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
  187. package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
  188. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
  189. package/dist/features/tester/seed-data/authored/index.js +0 -2
  190. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
  191. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +10 -29
  192. package/dist/index.d.ts +21 -7
  193. package/dist/index.js +53 -7
  194. package/dist/next/routing/route-map.d.ts +2 -4
  195. package/dist/next/routing/route-map.js +1 -2
  196. package/dist/repositories/index.d.ts +1 -0
  197. package/dist/repositories/index.js +1 -0
  198. package/dist/schemas/registry.d.ts +506 -0
  199. package/dist/seed/_helpers/brand-rows.d.ts +35 -0
  200. package/dist/seed/_helpers/brand-rows.js +576 -0
  201. package/dist/seed/_helpers/category-content.d.ts +39 -0
  202. package/dist/seed/_helpers/category-content.js +634 -0
  203. package/dist/seed/_helpers/category-forest.d.ts +2 -0
  204. package/dist/seed/_helpers/category-forest.js +585 -243
  205. package/dist/seed/_helpers/category-models.d.ts +5 -0
  206. package/dist/seed/_helpers/category-models.js +1364 -0
  207. package/dist/seed/_helpers/category-tree.js +17 -1
  208. package/dist/seed/_helpers/character-features.d.ts +10 -0
  209. package/dist/seed/_helpers/character-features.js +118 -0
  210. package/dist/seed/_helpers/media.d.ts +1 -1
  211. package/dist/seed/_helpers/media.js +44 -8
  212. package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
  213. package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
  214. package/dist/seed/categories-seed-data.js +147 -169
  215. package/dist/seed/index.d.ts +1 -0
  216. package/dist/seed/index.js +1 -0
  217. package/dist/seed/manifest.d.ts +1 -1
  218. package/dist/seed/manifest.js +5 -0
  219. package/dist/seed/orders-seed-data.js +5 -3
  220. package/dist/seed/product-features-seed-data.d.ts +1 -8
  221. package/dist/seed/product-features-seed-data.js +358 -154
  222. package/dist/seed/products-art-seed-data.js +16 -4
  223. package/dist/seed/products-auctions-seed-data.js +6 -6
  224. package/dist/seed/products-classifieds-seed-data.js +17 -7
  225. package/dist/seed/products-digital-codes-seed-data.js +12 -12
  226. package/dist/seed/products-preorders-seed-data.js +8 -8
  227. package/dist/seed/products-prize-draws-seed-data.js +25 -8
  228. package/dist/seed/products-standard-seed-data.js +10 -60
  229. package/dist/seed/products-stickers-seed-data.js +6 -6
  230. package/dist/seed/site-settings-seed-data.js +24 -0
  231. package/dist/seed/store-extensions-seed-data.js +10 -4
  232. package/dist/seed/tax-codes-seed-data.d.ts +34 -0
  233. package/dist/seed/tax-codes-seed-data.js +100 -0
  234. package/dist/seed/test-data-ttl.d.ts +29 -0
  235. package/dist/seed/test-data-ttl.js +31 -0
  236. package/dist/server-entry.d.ts +1 -1
  237. package/dist/server-entry.js +6 -1
  238. package/dist/server.d.ts +4 -1
  239. package/dist/server.js +11 -2
  240. package/dist/styles.css +40 -0
  241. package/dist/ui/components/PaginatedSelect.d.ts +14 -0
  242. package/dist/ui/components/PaginatedSelect.js +0 -0
  243. package/dist/ui/components/PaginatedSelect.style.css +40 -0
  244. package/dist/utils/media-url.d.ts +26 -0
  245. package/dist/utils/media-url.js +144 -0
  246. package/dist/utils/physical-location.d.ts +23 -0
  247. package/dist/utils/physical-location.js +52 -0
  248. package/dist/utils/search-txt-builders.d.ts +20 -0
  249. package/dist/utils/search-txt-builders.js +36 -0
  250. package/package.json +1 -1
  251. package/scripts/seed-cli.mjs +4 -1
  252. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
  253. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
  254. package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
  255. package/dist/features/tester/actions/leaderboard-actions.js +0 -5
  256. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
  257. package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
  258. package/dist/features/tester/components/TesterHubView.d.ts +0 -5
  259. package/dist/features/tester/components/TesterHubView.js +0 -110
  260. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
  261. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
  262. package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
  263. package/dist/features/tester/seed-data/tester-ttl.js +0 -6
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Category-owned content — description templates and product defaults.
3
+ *
4
+ * ## Why this lives on the CATEGORY and not on a listing template
5
+ *
6
+ * `ListingTemplateDocument` is store-scoped. The ask is that the description
7
+ * lives with the *category*, and the crawl proves the model: worldhobbyshop
8
+ * reuses TWO bodies across 1,195 listings, and tcgindia generates five
9
+ * templated FAQs per product. Nobody writes 1,195 descriptions by hand — our
10
+ * sellers do, because no category→description mechanism exists anywhere in
11
+ * this codebase.
12
+ *
13
+ * ## Why it costs ZERO extra reads
14
+ *
15
+ * Both hot paths already hold the category document: `deriveTaxonomy` does
16
+ * `cats.doc(leaf).get()`, and `CategoryDetailPageView` does `getCategoryBySlug`
17
+ * plus seven parallel reads — already at Rule #6's ~3-round-trip ceiling. A
18
+ * sidecar collection would add an eighth read to the second-most-crawled page
19
+ * on the site.
20
+ *
21
+ * ## Why three fields rather than one blob
22
+ *
23
+ * Each has exactly one reader: `contentBody` → the category page;
24
+ * `descriptionTemplates` → the seller form plus the narrow template endpoint;
25
+ * `productDefaults` → `deriveTaxonomy`. A single `content` object would make
26
+ * every one of those read the other two.
27
+ */
28
+ import type { ProductAuthenticity } from "../../products/schemas/firestore";
29
+ /**
30
+ * Which condition a template is written for.
31
+ *
32
+ * 🛑 A KEYED SET, not one body, because worldhobbyshop demonstrably reuses
33
+ * **two** per category — a pre-owned body and a new-in-box body — and a single
34
+ * string field cannot express that. The variants are the conditions that
35
+ * change what the copy must SAY, not every `CONDITION_OPTIONS` value: a
36
+ * "pre-owned" and a "used" listing want the same disclosure paragraph.
37
+ */
38
+ export type CategoryTemplateVariant = "default" | "new_in_box" | "new_in_packet" | "pre_owned" | "replica";
39
+ export interface CategoryDescriptionTemplate {
40
+ id: string;
41
+ /** What the picker shows, e.g. "Pre-owned — condition as pictured". */
42
+ label: string;
43
+ variant: CategoryTemplateVariant;
44
+ /** `{{placeholder}}` slots; HTML permitted, rendered via RichTextRenderer. */
45
+ body: string;
46
+ /**
47
+ * Slot names found in `body`, derived at write time as a convenience.
48
+ *
49
+ * 🛑 ABSENT means "re-parse", never "this body has no slots". A reader that
50
+ * treats `undefined` as an empty list will skip interpolation entirely on
51
+ * every template written before this field existed — and the failure is a
52
+ * published description containing a literal `{{series}}`, which is the most
53
+ * visible way this feature can break. Use `extractPlaceholders(body)` when
54
+ * it is absent.
55
+ */
56
+ placeholders?: string[];
57
+ /**
58
+ * An admin has published a real listing from this template.
59
+ *
60
+ * Exists because of raikages, the crawl's counter-example: 114-char bodies
61
+ * lifted from the Beyblade Wiki, one of which ships with the literal UI
62
+ * string "Add to Wishlist Add to Wishlist" welded into the product body. An
63
+ * unreviewed template is how that happens, so the flag records review rather
64
+ * than assuming it.
65
+ */
66
+ isTested?: boolean;
67
+ version?: number;
68
+ }
69
+ /** A spec row as the product page renders it. */
70
+ export interface CategorySpecification {
71
+ name: string;
72
+ value: string;
73
+ unit?: string;
74
+ }
75
+ /**
76
+ * What a category hands down to a product filed under it.
77
+ *
78
+ * Three behaviours, and the distinction matters more than the field list:
79
+ *
80
+ * WRITTEN tax, specifications, inTheBox, features — written server-side
81
+ * by `deriveTaxonomy` when the product omits them.
82
+ * FILL-BLANK seo* — fills only an empty field, never overwrites.
83
+ * ADVISORY priceGuidance — NEVER written to `price`. It renders as a hint
84
+ * beside the price input and nothing else.
85
+ */
86
+ export interface CategoryProductDefaults {
87
+ /**
88
+ * Points at a `taxCodes` row. `deriveTaxonomy` resolves it and writes
89
+ * `gstRate`/`hsnCode` onto the product; the order item then snapshots from
90
+ * the product. That layering is what stops a tax-code edit retroactively
91
+ * re-rating an invoice already issued.
92
+ */
93
+ taxCodeId?: string;
94
+ specifications?: CategorySpecification[];
95
+ /**
96
+ * Feature ids merged onto the product, never replacing what it already has.
97
+ * (The old `defaultTags` name went with `ProductDocument.tags`.)
98
+ */
99
+ defaultFeatures?: string[];
100
+ defaultCondition?: string;
101
+ defaultAuthenticity?: ProductAuthenticity;
102
+ /** "What's in the box?" — becomes `ProductDocument.features[]` prose. */
103
+ inTheBox?: string[];
104
+ priceGuidance?: CategoryPriceGuidance;
105
+ seoTitleTemplate?: string;
106
+ seoDescriptionTemplate?: string;
107
+ /** Merged with whatever the product already has. */
108
+ seoKeywords?: string[];
109
+ weightG?: number;
110
+ dimensionsCm?: {
111
+ l: number;
112
+ w: number;
113
+ h: number;
114
+ };
115
+ shippingPaidBy?: string;
116
+ fragile?: boolean;
117
+ /**
118
+ * Rendered at READ time, so fixing a wrong answer fixes every listing at
119
+ * once. The tcgindia model — five per product, three interpolating the
120
+ * name — and nobody else in the crawl of fifteen does it.
121
+ */
122
+ faqTemplates?: {
123
+ question: string;
124
+ answer: string;
125
+ }[];
126
+ }
127
+ /**
128
+ * Two price views, never blended.
129
+ *
130
+ * 🛑 `market` and `sold` are rendered SIDE BY SIDE — "Market ₹799–₹1,499 ·
131
+ * Sold here ₹850–₹1,200 (7 sales)" — and the gap between them is the most
132
+ * useful thing we can tell a seller, given 76–93% of competitor listings never
133
+ * sell at their ask. Averaging the two would launder an inference into a
134
+ * measurement.
135
+ */
136
+ export interface CategoryPriceGuidance {
137
+ currency: "INR";
138
+ /**
139
+ * Day-1 baseline from the competitor crawl: a TYPICAL MARKET LISTING PRICE.
140
+ *
141
+ * 🛑 NOT evidence of a transaction. A competitor's sold-out row proves the
142
+ * item is GONE, not that it sold at that price. Seed-owned — the rollup job
143
+ * must never write here, or a quiet night would erase the baseline.
144
+ */
145
+ market?: {
146
+ /**
147
+ * The one figure every measured band has. Required for exactly that
148
+ * reason — a band with no central value is not a band.
149
+ */
150
+ median: number;
151
+ /**
152
+ * 🛑 OPTIONAL, and that is load-bearing. The crawl recorded quartiles for
153
+ * parts, launchers, TCG and figures and only a MEDIAN AND A RANGE for Hot
154
+ * Wheels (n=7,418, ₹99–36,500). Requiring p25/p75 would have forced a
155
+ * range to be retyped as quartiles, which is a different statistic
156
+ * presented as the one the field is named after. Absent means "not
157
+ * measured", and the hint renders as "around ₹850" rather than a span.
158
+ */
159
+ p25?: number;
160
+ p75?: number;
161
+ /**
162
+ * 🛑 Also optional, for the same reason: the crawl's Beyblade-top table
163
+ * carries no per-leaf sample size at all, and the one place two sources
164
+ * disagreed (HMS: beybladeshopindia n=39 against worldhobbyshop n=2) is
165
+ * recorded precisely because an n=2 median is a coin flip presented as
166
+ * data. Absent must render as absent — never as a confident figure, and
167
+ * never as a number somebody filled in to satisfy the type.
168
+ */
169
+ sampleSize?: number;
170
+ asOf: Date;
171
+ soldOutShare?: number;
172
+ /**
173
+ * Which crawl this came from, and any caveat about it — "worldhobbyshop",
174
+ * "raikages n=68 (the only component-level pricing anywhere)",
175
+ * "toycollectorsindia 1:64 POOLED across mainline and premium".
176
+ *
177
+ * Carried so the question "where did this number come from" is answerable
178
+ * from the data rather than from a markdown file nobody has open, and so a
179
+ * source-priority decision can be applied without re-deriving the bands.
180
+ */
181
+ source?: string;
182
+ };
183
+ /**
184
+ * OUR completed sales only, including won auctions and accepted offers.
185
+ *
186
+ * Absent until there are some — **never zeroed**. A zero would render as a
187
+ * real figure meaning "things sell here for ₹0", which is worse than silence.
188
+ */
189
+ sold?: {
190
+ min: number;
191
+ median: number;
192
+ max: number;
193
+ sampleSize: number;
194
+ windowDays: number;
195
+ byPath?: {
196
+ standard?: number;
197
+ auction?: number;
198
+ offer?: number;
199
+ };
200
+ };
201
+ lastSold?: {
202
+ amount: number;
203
+ at: Date;
204
+ via?: "standard" | "auction" | "offer";
205
+ };
206
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Category-owned content — description templates and product defaults.
3
+ *
4
+ * ## Why this lives on the CATEGORY and not on a listing template
5
+ *
6
+ * `ListingTemplateDocument` is store-scoped. The ask is that the description
7
+ * lives with the *category*, and the crawl proves the model: worldhobbyshop
8
+ * reuses TWO bodies across 1,195 listings, and tcgindia generates five
9
+ * templated FAQs per product. Nobody writes 1,195 descriptions by hand — our
10
+ * sellers do, because no category→description mechanism exists anywhere in
11
+ * this codebase.
12
+ *
13
+ * ## Why it costs ZERO extra reads
14
+ *
15
+ * Both hot paths already hold the category document: `deriveTaxonomy` does
16
+ * `cats.doc(leaf).get()`, and `CategoryDetailPageView` does `getCategoryBySlug`
17
+ * plus seven parallel reads — already at Rule #6's ~3-round-trip ceiling. A
18
+ * sidecar collection would add an eighth read to the second-most-crawled page
19
+ * on the site.
20
+ *
21
+ * ## Why three fields rather than one blob
22
+ *
23
+ * Each has exactly one reader: `contentBody` → the category page;
24
+ * `descriptionTemplates` → the seller form plus the narrow template endpoint;
25
+ * `productDefaults` → `deriveTaxonomy`. A single `content` object would make
26
+ * every one of those read the other two.
27
+ */
28
+ export {};
@@ -6,6 +6,8 @@
6
6
  */
7
7
  import type { CategoryAncestor, CategoryMetrics, CategoryDisplay } from "../types";
8
8
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
9
+ import type { CategoryDescriptionTemplate, CategoryProductDefaults } from "./category-content";
10
+ export type { CategoryDescriptionTemplate, CategoryProductDefaults, CategoryPriceGuidance, CategorySpecification, CategoryTemplateVariant, } from "./category-content";
9
11
  export type { CategoryAncestor, CategoryMetrics };
10
12
  /**
11
13
  * How a bundle resolves its member products.
@@ -82,9 +84,9 @@ export interface CategoryDocument extends BaseDocument {
82
84
  name: string;
83
85
  slug: string;
84
86
  description?: string;
85
- /** Tester sandbox flag — set on shared admin-seeded test categories/brands/bundles; swept by testerSandboxCleanup. */
87
+ /** Test-data flag — set on shared admin-seeded test categories/brands/bundles; swept by testDataCleanup. */
86
88
  isTestData?: boolean;
87
- /** When isTestData, the cutoff after which testerSandboxCleanup deletes this doc. */
89
+ /** When isTestData, the cutoff after which testDataCleanup deletes this doc. */
88
90
  testDataExpiresAt?: Date;
89
91
  rootId: string;
90
92
  parentIds: string[];
@@ -135,6 +137,38 @@ export interface CategoryDocument extends BaseDocument {
135
137
  question: string;
136
138
  answer: string;
137
139
  }[];
140
+ /**
141
+ * Alternate names this category is searched by — indexed into `searchTxt`
142
+ * and rendered nowhere.
143
+ *
144
+ * B5. A named-model leaf is the case that needs it: *Lost Longinus* is sold
145
+ * in the west as *Lost Luinor L2* and its product code is *B-66*, and a
146
+ * buyer types whichever one they learned first. `name` can only hold one of
147
+ * the three.
148
+ *
149
+ * 🛑 Watch the 600-token ceiling in `buildSearchTxt`, which truncates
150
+ * SILENTLY. A category's own tokens (name, slug, description, path, every
151
+ * ancestor name) come first in the array, so an unbounded alias list would
152
+ * evict the lineage tokens that are the whole reason ancestors are indexed.
153
+ * The generator caps a model leaf at the Hasbro name plus the product code.
154
+ */
155
+ aliases?: string[];
156
+ /**
157
+ * 400–900 chars of keyword-rich HTML with sibling/parent cross-links,
158
+ * rendered via `<RichTextRenderer>`.
159
+ *
160
+ * 🛑 DISTINCT from `description`, which is a <=230-char blurb that also
161
+ * feeds `seo.description` and every card. Conflating them would put a
162
+ * 900-char body in a card and a 230-char stub on the page.
163
+ *
164
+ * 36 of worldhobbyshop's 46 categories carry one and it is their actual
165
+ * ranking mechanism; no Shopify competitor in the crawl has any.
166
+ */
167
+ contentBody?: string;
168
+ /** Condition-keyed description bodies. A SET, because WHS reuses two per category. */
169
+ descriptionTemplates?: CategoryDescriptionTemplate[];
170
+ /** What this category hands down to a product filed under it. */
171
+ productDefaults?: CategoryProductDefaults;
138
172
  /**
139
173
  * "special" — admin-curated bundle; writes reverse `partOfBundleIds`
140
174
  * pointers on member products and shows the "Bundled" badge on cards.
@@ -194,6 +228,19 @@ export interface CategoryDocument extends BaseDocument {
194
228
  isActive: boolean;
195
229
  isSearchable: boolean;
196
230
  showOnHomepage?: boolean;
231
+ /**
232
+ * Prefix-expanded search tokens, derived on every write by
233
+ * `CategoriesRepository.buildSearchTxtFor` → `buildCategorySearchTxt`.
234
+ *
235
+ * 🛑 Distinct from `isSearchable`, which is an admin visibility flag. This is
236
+ * the index that makes a category findable AT ALL: before it existed,
237
+ * `CATEGORY_SEARCH_SCAN_LIMIT` was capped at 100 by `SIEVE_DEFAULTS.maxPageSize`
238
+ * — so raising that number did nothing — and a tier-4 model like
239
+ * "Lost Longinus" was unfindable in every picker.
240
+ *
241
+ * Never fed any PII: no `createdBy` (a raw uid) and no `createdByStoreName`.
242
+ */
243
+ searchTxt?: string[];
197
244
  createdBy: string;
198
245
  /** Whether this category was created by admin or a store owner. */
199
246
  createdByType?: "admin" | "store";
@@ -204,12 +251,12 @@ export interface CategoryDocument extends BaseDocument {
204
251
  ancestors: CategoryAncestor[];
205
252
  }
206
253
  export declare const CATEGORIES_COLLECTION: "categories";
207
- export declare const CATEGORIES_INDEXED_FIELDS: readonly ["slug", "rootId", "tier", "parentIds", "isLeaf", "isFeatured", "featuredPriority", "isBrand", "categoryType", "isActive", "isSearchable", "showOnHomepage", "createdBy", "createdByType", "createdByStoreId", "createdAt"];
254
+ export declare const CATEGORIES_INDEXED_FIELDS: readonly ["slug", "rootId", "tier", "parentIds", "isLeaf", "isFeatured", "featuredPriority", "isBrand", "categoryType", "isActive", "isSearchable", "showOnHomepage", "searchTxt", "createdBy", "createdByType", "createdByStoreId", "createdAt"];
208
255
  export declare const MIN_ITEMS_FOR_FEATURED: 8;
209
256
  export declare const MAX_FEATURED_CATEGORIES: 4;
210
257
  export declare const DEFAULT_CATEGORY_DATA: Partial<CategoryDocument>;
211
- export declare const CATEGORIES_PUBLIC_FIELDS: readonly ["id", "name", "slug", "description", "rootId", "parentIds", "childrenIds", "tier", "path", "order", "position", "subtreeSize", "isLeaf", "metrics.totalProductCount", "metrics.totalAuctionCount", "metrics.totalItemCount", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "ancestors"];
212
- export declare const CATEGORIES_UPDATABLE_FIELDS: readonly ["name", "slug", "description", "order", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "isSearchable"];
258
+ export declare const CATEGORIES_PUBLIC_FIELDS: readonly ["id", "name", "slug", "description", "rootId", "parentIds", "childrenIds", "tier", "path", "order", "position", "subtreeSize", "isLeaf", "metrics.totalProductCount", "metrics.totalAuctionCount", "metrics.totalItemCount", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "ancestors", "highlights", "faqs", "contentBody", "descriptionTemplates", "productDefaults", "aliases"];
259
+ export declare const CATEGORIES_UPDATABLE_FIELDS: readonly ["name", "slug", "description", "order", "isFeatured", "featuredPriority", "isBrand", "seo", "display", "isActive", "isSearchable", "highlights", "faqs", "contentBody", "descriptionTemplates", "productDefaults", "aliases"];
213
260
  export type CategoryCreateInput = Omit<CategoryDocument, "id" | "createdAt" | "updatedAt" | "metrics" | "isLeaf" | "ancestors"> & {
214
261
  parentIds?: string[];
215
262
  parentId?: string | null;
@@ -285,6 +332,11 @@ export declare const CATEGORY_FIELDS: {
285
332
  readonly VIEW_COUNT: "viewCount";
286
333
  readonly IS_ACTIVE: "isActive";
287
334
  readonly IS_SEARCHABLE: "isSearchable";
335
+ readonly SEARCH_TXT: "searchTxt";
336
+ readonly CONTENT_BODY: "contentBody";
337
+ readonly DESCRIPTION_TEMPLATES: "descriptionTemplates";
338
+ readonly PRODUCT_DEFAULTS: "productDefaults";
339
+ readonly PRODUCT_DEFAULTS_TAX_CODE_ID: "productDefaults.taxCodeId";
288
340
  readonly CATEGORY_TYPE: "categoryType";
289
341
  readonly CREATED_BY: "createdBy";
290
342
  readonly CREATED_AT: "createdAt";
@@ -293,5 +345,22 @@ export declare const CATEGORY_FIELDS: {
293
345
  };
294
346
  /**
295
347
  * Build a category tree from a flat list of CategoryDocument objects.
348
+ *
349
+ * 🛑 ONE INDEXING PASS, then recursion — not a `.filter()` per node.
350
+ * This used to scan the whole array once per category to find its children,
351
+ * which is O(n²): 2,209 comparisons at today's 47 nodes, ~109,000 at the ~330
352
+ * this plan seeds, and **2.25 million** at 1,500. It runs inside
353
+ * `categoriesRepository.buildTree()`, i.e. on a request path. Grouping by
354
+ * parent id first makes it O(n log n) (the sorts) and the output is
355
+ * byte-identical: same roots, same child order, same depths.
356
+ *
357
+ * 🛑 The `seen` set is not defensive padding. A root is picked by
358
+ * `tier === 0`, and nothing stops a corrupt row from being tier 0 *and* a
359
+ * descendant of its own child (`parentIds` is a denormalised chain that four
360
+ * different write paths maintain). The old code would recurse forever on that,
361
+ * and a hung SSR render is worse than a failed one — it holds a function open
362
+ * for its whole timeout and reports nothing. A revisited node is dropped from
363
+ * the second position it appears in rather than throwing: a malformed edge
364
+ * should cost one tile, not the category page.
296
365
  */
297
366
  export declare function buildCategoryTree(categories: CategoryDocument[], rootId?: string): CategoryTreeNode[];
@@ -20,6 +20,7 @@ export const CATEGORIES_INDEXED_FIELDS = [
20
20
  "isActive",
21
21
  "isSearchable",
22
22
  "showOnHomepage",
23
+ "searchTxt",
23
24
  "createdBy",
24
25
  "createdByType",
25
26
  "createdByStoreId",
@@ -76,6 +77,31 @@ export const CATEGORIES_PUBLIC_FIELDS = [
76
77
  "display",
77
78
  "isActive",
78
79
  "ancestors",
80
+ /*
81
+ * B3. `highlights` and `faqs` were MISSING from both this list and the
82
+ * updatable one, which is the more interesting half: `CategoryUpdateInput`
83
+ * derives from UPDATABLE_FIELDS, so a field absent from it is not even
84
+ * TYPE-LEGAL to update — and both had shipped with no editor at all.
85
+ * Precedent-by-omission gets closed, not copied.
86
+ *
87
+ * `productDefaults` is public because its advisory half (priceGuidance,
88
+ * specifications, inTheBox) is rendered to buyers and sellers. Its
89
+ * `taxCodeId` is a reference to an admin-only row, not a secret.
90
+ */
91
+ "highlights",
92
+ "faqs",
93
+ "contentBody",
94
+ "descriptionTemplates",
95
+ "productDefaults",
96
+ /*
97
+ * B5. `aliases` is public because it is already published in a form nobody
98
+ * can withhold: every alias is tokenised into `searchTxt`, which is what the
99
+ * search endpoint matches on, so the facts are inferable from search results
100
+ * whether or not the array is in the payload. Withholding it would hide a
101
+ * useful "also known as" line from the page while changing nothing about
102
+ * what is reachable.
103
+ */
104
+ "aliases",
79
105
  ];
80
106
  export const CATEGORIES_UPDATABLE_FIELDS = [
81
107
  "name",
@@ -89,6 +115,14 @@ export const CATEGORIES_UPDATABLE_FIELDS = [
89
115
  "display",
90
116
  "isActive",
91
117
  "isSearchable",
118
+ // B3 — see the note in CATEGORIES_PUBLIC_FIELDS. Without these five,
119
+ // CategoryUpdateInput cannot express them and no editor can save them.
120
+ "highlights",
121
+ "faqs",
122
+ "contentBody",
123
+ "descriptionTemplates",
124
+ "productDefaults",
125
+ "aliases",
92
126
  ];
93
127
  export const categoryQueryHelpers = {
94
128
  bySlug: (slug) => ["slug", "==", slug],
@@ -179,6 +213,12 @@ export const CATEGORY_FIELDS = {
179
213
  VIEW_COUNT: "viewCount",
180
214
  IS_ACTIVE: "isActive",
181
215
  IS_SEARCHABLE: "isSearchable",
216
+ SEARCH_TXT: "searchTxt",
217
+ CONTENT_BODY: "contentBody",
218
+ DESCRIPTION_TEMPLATES: "descriptionTemplates",
219
+ PRODUCT_DEFAULTS: "productDefaults",
220
+ /* Dot-path, so a referrer query is one equality and needs no index. */
221
+ PRODUCT_DEFAULTS_TAX_CODE_ID: "productDefaults.taxCodeId",
182
222
  CATEGORY_TYPE: "categoryType",
183
223
  CREATED_BY: "createdBy",
184
224
  CREATED_AT: "createdAt",
@@ -187,12 +227,43 @@ export const CATEGORY_FIELDS = {
187
227
  };
188
228
  /**
189
229
  * Build a category tree from a flat list of CategoryDocument objects.
230
+ *
231
+ * 🛑 ONE INDEXING PASS, then recursion — not a `.filter()` per node.
232
+ * This used to scan the whole array once per category to find its children,
233
+ * which is O(n²): 2,209 comparisons at today's 47 nodes, ~109,000 at the ~330
234
+ * this plan seeds, and **2.25 million** at 1,500. It runs inside
235
+ * `categoriesRepository.buildTree()`, i.e. on a request path. Grouping by
236
+ * parent id first makes it O(n log n) (the sorts) and the output is
237
+ * byte-identical: same roots, same child order, same depths.
238
+ *
239
+ * 🛑 The `seen` set is not defensive padding. A root is picked by
240
+ * `tier === 0`, and nothing stops a corrupt row from being tier 0 *and* a
241
+ * descendant of its own child (`parentIds` is a denormalised chain that four
242
+ * different write paths maintain). The old code would recurse forever on that,
243
+ * and a hung SSR render is worse than a failed one — it holds a function open
244
+ * for its whole timeout and reports nothing. A revisited node is dropped from
245
+ * the second position it appears in rather than throwing: a malformed edge
246
+ * should cost one tile, not the category page.
190
247
  */
191
248
  export function buildCategoryTree(categories, rootId) {
249
+ const byParent = new Map();
250
+ for (const cat of categories) {
251
+ const parentId = cat.parentIds[cat.parentIds.length - 1];
252
+ if (!parentId)
253
+ continue;
254
+ const bucket = byParent.get(parentId);
255
+ if (bucket)
256
+ bucket.push(cat);
257
+ else
258
+ byParent.set(parentId, [cat]);
259
+ }
260
+ for (const bucket of byParent.values())
261
+ bucket.sort((a, b) => a.order - b.order);
262
+ const seen = new Set();
192
263
  function buildTree(category, depth) {
193
- const children = categories
194
- .filter((cat) => cat.parentIds[cat.parentIds.length - 1] === category.id)
195
- .sort((a, b) => a.order - b.order)
264
+ seen.add(category.id);
265
+ const children = (byParent.get(category.id) ?? [])
266
+ .filter((child) => !seen.has(child.id))
196
267
  .map((child) => buildTree(child, depth + 1));
197
268
  return { category, children, depth };
198
269
  }