@mohasinac/appkit 4.43.0 → 4.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
  2. package/dist/_internal/server/features/categories/adapters.js +235 -0
  3. package/dist/_internal/server/features/checkout/actions.js +161 -22
  4. package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
  5. package/dist/_internal/server/features/orders/adapters.js +21 -0
  6. package/dist/_internal/server/features/refunds/actions.js +1 -1
  7. package/dist/_internal/server/features/seo/sitemap.js +54 -15
  8. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  9. package/dist/_internal/server/features/stores/adapters.js +6 -0
  10. package/dist/_internal/server/functions/scheduled.d.ts +4 -2
  11. package/dist/_internal/server/functions/scheduled.js +64 -6
  12. package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
  13. package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
  14. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
  15. package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
  16. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
  17. package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
  18. package/dist/_internal/server/jobs/core/onCategoryWrite.js +54 -0
  19. package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
  20. package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
  21. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
  22. package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
  23. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
  24. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
  25. package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
  26. package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
  27. package/dist/_internal/server/jobs/handlers/index.js +3 -1
  28. package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
  29. package/dist/_internal/server/jobs/handlers/messages.js +5 -2
  30. package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
  31. package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
  32. package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
  33. package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
  34. package/dist/_internal/shared/actions/action-registry.js +34 -0
  35. package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
  36. package/dist/_internal/shared/checkout/order-math.js +194 -7
  37. package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
  38. package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
  39. package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
  40. package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
  41. package/dist/_internal/shared/fees/calculator.d.ts +105 -0
  42. package/dist/_internal/shared/fees/calculator.js +143 -0
  43. package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
  44. package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
  45. package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
  46. package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
  47. package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
  48. package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
  49. package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
  50. package/dist/_internal/shared/templating/placeholders.js +73 -0
  51. package/dist/client.d.ts +2 -0
  52. package/dist/client.js +1 -0
  53. package/dist/constants/api-endpoints.d.ts +42 -0
  54. package/dist/constants/api-endpoints.js +14 -0
  55. package/dist/features/admin/actions/admin-actions.d.ts +1 -1
  56. package/dist/features/admin/actions/admin-actions.js +1 -1
  57. package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
  58. package/dist/features/admin/components/AdminProductEditorView.js +8 -1
  59. package/dist/features/admin/components/AdminProductsView.js +78 -7
  60. package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
  61. package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
  62. package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
  63. package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
  64. package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
  65. package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
  66. package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
  67. package/dist/features/auth/schemas/firestore.d.ts +0 -1
  68. package/dist/features/blog/schemas/firestore.d.ts +2 -2
  69. package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
  70. package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
  71. package/dist/features/categories/api/[id]/description-template/route.js +252 -0
  72. package/dist/features/categories/api/route.js +156 -11
  73. package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
  74. package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
  75. package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
  76. package/dist/features/categories/constants/listing-categories.d.ts +17 -0
  77. package/dist/features/categories/constants/listing-categories.js +53 -0
  78. package/dist/features/categories/hooks/useCategoryTree.js +28 -2
  79. package/dist/features/categories/repository/categories.repository.d.ts +62 -11
  80. package/dist/features/categories/repository/categories.repository.js +178 -35
  81. package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
  82. package/dist/features/categories/repository/price-index.repository.js +85 -0
  83. package/dist/features/categories/schemas/category-content.d.ts +206 -0
  84. package/dist/features/categories/schemas/category-content.js +28 -0
  85. package/dist/features/categories/schemas/firestore.d.ts +74 -5
  86. package/dist/features/categories/schemas/firestore.js +74 -3
  87. package/dist/features/categories/schemas/index.d.ts +1047 -0
  88. package/dist/features/categories/schemas/index.js +96 -0
  89. package/dist/features/categories/server.d.ts +1 -0
  90. package/dist/features/categories/server.js +7 -0
  91. package/dist/features/categories/types/index.d.ts +9 -0
  92. package/dist/features/events/schemas/firestore.d.ts +2 -2
  93. package/dist/features/faq/schemas/firestore.d.ts +2 -1
  94. package/dist/features/faq/schemas/firestore.js +20 -8
  95. package/dist/features/orders/schemas/firestore.d.ts +70 -0
  96. package/dist/features/orders/types/index.d.ts +55 -0
  97. package/dist/features/products/columns/index.d.ts +0 -1
  98. package/dist/features/products/columns/index.js +5 -1
  99. package/dist/features/products/components/ProductFilters.d.ts +0 -9
  100. package/dist/features/products/components/ProductForm.d.ts +27 -1
  101. package/dist/features/products/components/ProductForm.js +26 -12
  102. package/dist/features/products/constants/sieve.d.ts +0 -6
  103. package/dist/features/products/constants/sieve.js +35 -2
  104. package/dist/features/products/repository/products.repository.d.ts +115 -1
  105. package/dist/features/products/repository/products.repository.js +491 -39
  106. package/dist/features/products/schemas/firestore.d.ts +65 -5
  107. package/dist/features/products/schemas/firestore.js +9 -0
  108. package/dist/features/products/schemas/product-features.d.ts +140 -2
  109. package/dist/features/products/schemas/product-features.js +62 -1
  110. package/dist/features/promotions/repository/coupons.repository.js +6 -1
  111. package/dist/features/promotions/schemas/firestore.d.ts +44 -1
  112. package/dist/features/promotions/schemas/firestore.js +76 -2
  113. package/dist/features/seller/actions/seller-actions.js +17 -2
  114. package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
  115. package/dist/features/seller/components/PrintCenterView.js +3 -2
  116. package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
  117. package/dist/features/seller/components/SellerOrdersView.js +6 -1
  118. package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
  119. package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
  120. package/dist/features/seller/components/SellerProductShell.js +101 -9
  121. package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
  122. package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
  123. package/dist/features/seller/components/SellerProductsView.js +108 -3
  124. package/dist/features/seller/types/index.d.ts +17 -0
  125. package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
  126. package/dist/features/seller/utils/product-draft-mapping.js +17 -0
  127. package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
  128. package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
  129. package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
  130. package/dist/features/stores/schemas/firestore.d.ts +31 -2
  131. package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
  132. package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
  133. package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
  134. package/dist/features/tax-codes/schemas/firestore.js +82 -0
  135. package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
  136. package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
  137. package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
  138. package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
  139. package/dist/features/tester/actions/index.d.ts +0 -1
  140. package/dist/features/tester/actions/index.js +0 -1
  141. package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
  142. package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
  143. package/dist/features/tester/components/index.d.ts +0 -4
  144. package/dist/features/tester/components/index.js +0 -2
  145. package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
  146. package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
  147. package/dist/features/tester/schemas/firestore.d.ts +0 -6
  148. package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
  149. package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
  150. package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
  151. package/dist/features/tester/seed-data/authored/index.js +0 -2
  152. package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
  153. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +8 -27
  154. package/dist/index.d.ts +21 -7
  155. package/dist/index.js +53 -7
  156. package/dist/next/routing/route-map.d.ts +2 -4
  157. package/dist/next/routing/route-map.js +1 -2
  158. package/dist/repositories/index.d.ts +1 -0
  159. package/dist/repositories/index.js +1 -0
  160. package/dist/schemas/registry.d.ts +506 -0
  161. package/dist/seed/_helpers/brand-rows.d.ts +35 -0
  162. package/dist/seed/_helpers/brand-rows.js +576 -0
  163. package/dist/seed/_helpers/category-content.d.ts +39 -0
  164. package/dist/seed/_helpers/category-content.js +634 -0
  165. package/dist/seed/_helpers/category-forest.d.ts +2 -0
  166. package/dist/seed/_helpers/category-forest.js +585 -243
  167. package/dist/seed/_helpers/category-models.d.ts +5 -0
  168. package/dist/seed/_helpers/category-models.js +1364 -0
  169. package/dist/seed/_helpers/category-tree.js +17 -1
  170. package/dist/seed/_helpers/character-features.d.ts +10 -0
  171. package/dist/seed/_helpers/character-features.js +118 -0
  172. package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
  173. package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
  174. package/dist/seed/categories-seed-data.js +147 -169
  175. package/dist/seed/index.d.ts +1 -0
  176. package/dist/seed/index.js +1 -0
  177. package/dist/seed/manifest.d.ts +1 -1
  178. package/dist/seed/manifest.js +5 -0
  179. package/dist/seed/product-features-seed-data.d.ts +1 -8
  180. package/dist/seed/product-features-seed-data.js +358 -154
  181. package/dist/seed/products-art-seed-data.js +16 -4
  182. package/dist/seed/products-auctions-seed-data.js +6 -6
  183. package/dist/seed/products-classifieds-seed-data.js +17 -7
  184. package/dist/seed/products-digital-codes-seed-data.js +12 -12
  185. package/dist/seed/products-preorders-seed-data.js +8 -8
  186. package/dist/seed/products-prize-draws-seed-data.js +25 -8
  187. package/dist/seed/products-standard-seed-data.js +10 -60
  188. package/dist/seed/products-stickers-seed-data.js +6 -6
  189. package/dist/seed/site-settings-seed-data.js +24 -0
  190. package/dist/seed/store-extensions-seed-data.js +10 -4
  191. package/dist/seed/tax-codes-seed-data.d.ts +34 -0
  192. package/dist/seed/tax-codes-seed-data.js +100 -0
  193. package/dist/seed/test-data-ttl.d.ts +29 -0
  194. package/dist/seed/test-data-ttl.js +31 -0
  195. package/dist/server-entry.d.ts +1 -1
  196. package/dist/server-entry.js +6 -1
  197. package/dist/server.d.ts +2 -1
  198. package/dist/server.js +5 -2
  199. package/dist/styles.css +40 -0
  200. package/dist/ui/components/PaginatedSelect.d.ts +14 -0
  201. package/dist/ui/components/PaginatedSelect.js +0 -0
  202. package/dist/ui/components/PaginatedSelect.style.css +40 -0
  203. package/dist/utils/physical-location.d.ts +23 -0
  204. package/dist/utils/physical-location.js +52 -0
  205. package/dist/utils/search-txt-builders.d.ts +20 -0
  206. package/dist/utils/search-txt-builders.js +36 -0
  207. package/package.json +1 -1
  208. package/scripts/seed-cli.mjs +4 -1
  209. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
  210. package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
  211. package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
  212. package/dist/features/tester/actions/leaderboard-actions.js +0 -5
  213. package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
  214. package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
  215. package/dist/features/tester/components/TesterHubView.d.ts +0 -5
  216. package/dist/features/tester/components/TesterHubView.js +0 -110
  217. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
  218. package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
  219. package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
  220. package/dist/features/tester/seed-data/tester-ttl.js +0 -6
@@ -36,7 +36,23 @@
36
36
  */
37
37
  export function buildCategoryTree(roots, opts) {
38
38
  const rows = [];
39
- let position = 0;
39
+ /*
40
+ * 🛑 1-BASED, and that is not cosmetic — it must match `positionsReconcile`,
41
+ * which starts its own DFS at `let counter = 1`.
42
+ *
43
+ * This counter started at 0 until 2026-10-10. The nightly reconcile then
44
+ * found every row off by one (`current.position !== position`) and rewrote
45
+ * the entire collection on the first run after any reseed — 58 writes then,
46
+ * ~330 once the catalogue grows, each re-entering `onCategoryWrite` as a
47
+ * no-op invocation. Measured in production before the change: seed emits
48
+ * 0..57, live data reads 1..58, i.e. the reconcile had already corrected
49
+ * all of it.
50
+ *
51
+ * It also makes position 0 UNAMBIGUOUS, which `handleCategoryWrite` now
52
+ * relies on: a document arriving with `position >= 1` was numbered by this
53
+ * builder, and one with 0/absent was not.
54
+ */
55
+ let position = 1;
40
56
  function visit(node, rootId,
41
57
  /** Ancestors nearest-last, matching how `parentIds` is read elsewhere. */
42
58
  ancestors, tier, parentPath, order) {
@@ -0,0 +1,10 @@
1
+ /** A blader, and how many canonical beys the corpus attributes to them. */
2
+ export interface CharacterSeed {
3
+ /** Without the `feature-` prefix — the factory prepends it. */
4
+ slug: string;
5
+ name: string;
6
+ /** Canonical beys owned. Drives display order; not stored on the row. */
7
+ beyCount: number;
8
+ }
9
+ export declare const CHARACTER_SEEDS: CharacterSeed[];
10
+ export declare const CHARACTER_SEED_COUNT = 93;
@@ -0,0 +1,118 @@
1
+ /*
2
+ * GENERATED by scripts/gen-category-models.mjs — do not edit by hand.
3
+ *
4
+ * WHY: the clickable owner linkage. `Owner (Anime)` is 100% populated in the
5
+ * corpus but it is a SENTENCE — "Tyson Granger (Tyson's 9th and final
6
+ * bey)" — so it renders as prose and filters nothing. These are the
7
+ * normalised names as `character`-group features, which makes
8
+ * "show me every Tyson Granger bey" one click from any listing page.
9
+ *
10
+ * 🛑 A FEATURE and never a category node. A blader crosses every generation —
11
+ * Tyson owns nine beys spanning Spin Gear, Magnacore, Engine Gear and HMS —
12
+ * so a tree node would duplicate all nine under a second parent and break
13
+ * the one-chain-per-product invariant. As a feature it COMPOSES with the
14
+ * generation tree instead of competing with it: browse Beyblade Original,
15
+ * then narrow to Tyson's.
16
+ *
17
+ * @tag domain:products
18
+ * @tag layer:seed
19
+ * @tag pattern:generated
20
+ * @tag access:server-only
21
+ * @tag sideEffects:none
22
+ */
23
+ export const CHARACTER_SEEDS = [
24
+ { slug: "blader-tyson-granger", name: "Tyson Granger", beyCount: 9 },
25
+ { slug: "blader-kai-hiwatari", name: "Kai Hiwatari", beyCount: 7 },
26
+ { slug: "blader-daichi-sumeragi", name: "Daichi Sumeragi", beyCount: 6 },
27
+ { slug: "blader-max-tate", name: "Max Tate", beyCount: 6 },
28
+ { slug: "blader-ray-kon", name: "Ray Kon", beyCount: 6 },
29
+ { slug: "blader-shu-kurenai", name: "Shu Kurenai", beyCount: 6 },
30
+ { slug: "blader-gingka-hagane", name: "Gingka Hagane", beyCount: 5 },
31
+ { slug: "blader-lui-shirosagi", name: "Lui Shirosagi", beyCount: 5 },
32
+ { slug: "blader-tala", name: "Tala", beyCount: 5 },
33
+ { slug: "blader-valt-aoi", name: "Valt Aoi", beyCount: 5 },
34
+ { slug: "blader-free-de-la-hoya", name: "Free De La Hoya", beyCount: 4 },
35
+ { slug: "blader-aiger-akabane", name: "Aiger Akabane", beyCount: 3 },
36
+ { slug: "blader-dante-koryu", name: "Dante Koryu", beyCount: 3 },
37
+ { slug: "blader-jaxon-cross", name: "Jaxon Cross", beyCount: 3 },
38
+ { slug: "blader-kenta-yumiya", name: "Kenta Yumiya", beyCount: 3 },
39
+ { slug: "blader-kyoya-tategami", name: "Kyoya Tategami", beyCount: 3 },
40
+ { slug: "blader-multi-nana-iro", name: "Multi Nana-iro", beyCount: 3 },
41
+ { slug: "blader-ryuga", name: "Ryuga", beyCount: 3 },
42
+ { slug: "blader-benkei-hanawa", name: "Benkei Hanawa", beyCount: 2 },
43
+ { slug: "blader-daigo-kurogami", name: "Daigo Kurogami", beyCount: 2 },
44
+ { slug: "blader-damian-hart", name: "Damian Hart", beyCount: 2 },
45
+ { slug: "blader-dashan-wang", name: "Dashan Wang", beyCount: 2 },
46
+ { slug: "blader-delta-zakuro", name: "Delta Zakuro", beyCount: 2 },
47
+ { slug: "blader-dunga", name: "Dunga", beyCount: 2 },
48
+ { slug: "blader-eddy", name: "Eddy", beyCount: 2 },
49
+ { slug: "blader-fumiya-kindo", name: "Fumiya Kindo", beyCount: 2 },
50
+ { slug: "blader-hikaru-hizashi", name: "Hikaru Hizashi", beyCount: 2 },
51
+ { slug: "blader-hyde", name: "Hyde", beyCount: 2 },
52
+ { slug: "blader-jin-of-the-gale", name: "Jin of the Gale", beyCount: 2 },
53
+ { slug: "blader-kira-hayama", name: "Kira Hayama", beyCount: 2 },
54
+ { slug: "blader-lee", name: "Lee", beyCount: 2 },
55
+ { slug: "blader-masamune-kadoya", name: "Masamune Kadoya", beyCount: 2 },
56
+ { slug: "blader-michael-summers", name: "Michael Summers", beyCount: 2 },
57
+ { slug: "blader-miguel", name: "Miguel", beyCount: 2 },
58
+ { slug: "blader-ozuma", name: "Ozuma", beyCount: 2 },
59
+ { slug: "blader-robin-kazami", name: "Robin Kazami", beyCount: 2 },
60
+ { slug: "blader-tsubasa-otori", name: "Tsubasa Otori", beyCount: 2 },
61
+ { slug: "blader-wakiya-murasaki", name: "Wakiya Murasaki", beyCount: 2 },
62
+ { slug: "blader-xander-shakadera", name: "Xander Shakadera", beyCount: 2 },
63
+ { slug: "blader-yu-tendo", name: "Yu Tendo", beyCount: 2 },
64
+ { slug: "blader-aguma", name: "Aguma", beyCount: 1 },
65
+ { slug: "blader-akuya-onizaki", name: "Akuya Onizaki", beyCount: 1 },
66
+ { slug: "blader-anime", name: "Anime", beyCount: 1 },
67
+ { slug: "blader-anime-secondary", name: "Anime-secondary", beyCount: 1 },
68
+ { slug: "blader-blader-dj", name: "Blader DJ", beyCount: 1 },
69
+ { slug: "blader-brooklyn", name: "Brooklyn", beyCount: 1 },
70
+ { slug: "blader-chris", name: "Chris", beyCount: 1 },
71
+ { slug: "blader-claude", name: "Claude", beyCount: 1 },
72
+ { slug: "blader-clio-delon", name: "Clio Delon", beyCount: 1 },
73
+ { slug: "blader-crusher", name: "Crusher", beyCount: 1 },
74
+ { slug: "blader-cycnus", name: "Cycnus", beyCount: 1 },
75
+ { slug: "blader-dan-sodo-and-reiki-sodo", name: "Dan Sodo and Reiki Sodo", beyCount: 1 },
76
+ { slug: "blader-doji", name: "Doji", beyCount: 1 },
77
+ { slug: "blader-faust", name: "Faust", beyCount: 1 },
78
+ { slug: "blader-garland-siebald", name: "Garland Siebald", beyCount: 1 },
79
+ { slug: "blader-gary", name: "Gary", beyCount: 1 },
80
+ { slug: "blader-ginro", name: "Ginro", beyCount: 1 },
81
+ { slug: "blader-hikaru-hasama", name: "Hikaru Hasama", beyCount: 1 },
82
+ { slug: "blader-hokuto", name: "Hokuto", beyCount: 1 },
83
+ { slug: "blader-hyoma", name: "Hyoma", beyCount: 1 },
84
+ { slug: "blader-hyuga-hizashi", name: "Hyuga Hizashi", beyCount: 1 },
85
+ { slug: "blader-ian-papov", name: "Ian Papov", beyCount: 1 },
86
+ { slug: "blader-johnny-mcgregor", name: "Johnny McGregor", beyCount: 1 },
87
+ { slug: "blader-joseph-burton", name: "Joseph Burton", beyCount: 1 },
88
+ { slug: "blader-julia-fernandez", name: "Julia Fernandez", beyCount: 1 },
89
+ { slug: "blader-julian-konzern", name: "Julian Konzern", beyCount: 1 },
90
+ { slug: "blader-karla-konjiki", name: "Karla Konjiki", beyCount: 1 },
91
+ { slug: "blader-ken-midori", name: "Ken Midori", beyCount: 1 },
92
+ { slug: "blader-kenny", name: "Kenny", beyCount: 1 },
93
+ { slug: "blader-kevin", name: "Kevin", beyCount: 1 },
94
+ { slug: "blader-kite-unabara", name: "Kite Unabara", beyCount: 1 },
95
+ { slug: "blader-lain-valhalla", name: "Lain Valhalla", beyCount: 1 },
96
+ { slug: "blader-meiko-myoden", name: "Meiko Myoden", beyCount: 1 },
97
+ { slug: "blader-nile", name: "Nile", beyCount: 1 },
98
+ { slug: "blader-nine-kurosu", name: "Nine Kurosu", beyCount: 1 },
99
+ { slug: "blader-omega-shiroboshi", name: "Omega Shiroboshi", beyCount: 1 },
100
+ { slug: "blader-phi", name: "Phi", beyCount: 1 },
101
+ { slug: "blader-rago", name: "Rago", beyCount: 1 },
102
+ { slug: "blader-reiji-mizuchi", name: "Reiji Mizuchi", beyCount: 1 },
103
+ { slug: "blader-reiyu-kuwabara", name: "Reiyu Kuwabara", beyCount: 1 },
104
+ { slug: "blader-rex-jura", name: "Rex Jura", beyCount: 1 },
105
+ { slug: "blader-rick-anderson", name: "Rick Anderson", beyCount: 1 },
106
+ { slug: "blader-robert-jurgens", name: "Robert Jurgens", beyCount: 1 },
107
+ { slug: "blader-sakyo-kurayami", name: "Sakyo Kurayami", beyCount: 1 },
108
+ { slug: "blader-silas-karlisle", name: "Silas Karlisle", beyCount: 1 },
109
+ { slug: "blader-spencer", name: "Spencer", beyCount: 1 },
110
+ { slug: "blader-teru-saotome", name: "Teru Saotome", beyCount: 1 },
111
+ { slug: "blader-tobio-oike", name: "Tobio Oike", beyCount: 1 },
112
+ { slug: "blader-toby", name: "Toby", beyCount: 1 },
113
+ { slug: "blader-toguro-okunaga", name: "Toguro Okunaga", beyCount: 1 },
114
+ { slug: "blader-yuki-mizusawa", name: "Yuki Mizusawa", beyCount: 1 },
115
+ { slug: "blader-yuni-naniwa", name: "Yuni Naniwa", beyCount: 1 },
116
+ { slug: "blader-zyro-kurogane", name: "Zyro Kurogane", beyCount: 1 },
117
+ ];
118
+ export const CHARACTER_SEED_COUNT = 93;
@@ -1,3 +1,4 @@
1
+ import type { CategoryDocument } from "../../features/categories/schemas/firestore";
1
2
  import type { StoreDocument } from "../../features/stores/schemas/firestore";
2
3
  import type { ScammerDocument } from "../../features/scams/schemas/firestore";
3
4
  import type { EventDocument } from "../../features/events/schemas/firestore";
@@ -6,6 +7,15 @@ import type { ReviewDocument } from "../../features/reviews/schemas/firestore";
6
7
  import type { OrderDocument } from "../../features/orders/schemas/firestore";
7
8
  import type { CouponDocument } from "../../features/promotions/schemas/firestore";
8
9
  import type { OfferDocument } from "../../features/seller/schemas/firestore";
10
+ /**
11
+ * 🛑 Apply AFTER `buildCategoryTree`, never before.
12
+ *
13
+ * `buildCategorySearchTxt` indexes `path` and `ancestors[].name`, and both are
14
+ * DERIVED by the tree builder — a row wrapped before derivation gets tokens
15
+ * from `name`/`slug`/`description` only, and silently loses the lineage search
16
+ * that is the whole reason ancestors are indexed.
17
+ */
18
+ export declare function withCategorySearchTxt<T extends Partial<CategoryDocument>>(p: T): T;
9
19
  export declare function withStoreSearchTxt<T extends Partial<StoreDocument>>(p: T): T;
10
20
  export declare function withEventSearchTxt<T extends Partial<EventDocument>>(p: T): T;
11
21
  export declare function withBlogSearchTxt<T extends Partial<BlogPostDocument>>(p: T): T;
@@ -1,4 +1,15 @@
1
- import { buildStoreSearchTxt, buildEventSearchTxt, buildBlogSearchTxt, buildReviewSearchTxt, buildOrderSearchTxt, buildCouponSearchTxt, buildOfferSearchTxt, buildScammerSearchTxt, } from "../../utils/search-txt-builders";
1
+ import { buildCategorySearchTxt, buildStoreSearchTxt, buildEventSearchTxt, buildBlogSearchTxt, buildReviewSearchTxt, buildOrderSearchTxt, buildCouponSearchTxt, buildOfferSearchTxt, buildScammerSearchTxt, } from "../../utils/search-txt-builders";
2
+ /**
3
+ * 🛑 Apply AFTER `buildCategoryTree`, never before.
4
+ *
5
+ * `buildCategorySearchTxt` indexes `path` and `ancestors[].name`, and both are
6
+ * DERIVED by the tree builder — a row wrapped before derivation gets tokens
7
+ * from `name`/`slug`/`description` only, and silently loses the lineage search
8
+ * that is the whole reason ancestors are indexed.
9
+ */
10
+ export function withCategorySearchTxt(p) {
11
+ return { ...p, searchTxt: buildCategorySearchTxt(p) };
12
+ }
2
13
  export function withStoreSearchTxt(p) {
3
14
  return { ...p, searchTxt: buildStoreSearchTxt(p) };
4
15
  }
@@ -1,17 +1,34 @@
1
1
  /*
2
- * WHY: Provides the Beyblade-focused category hierarchy plus brand entries for the
3
- * demo seed. The site itself stays generically branded as a collectibles marketplace —
4
- * only the seeded catalog data is narrowed to Beyblade so the demo has a coherent
5
- * dataset instead of a sprawling multi-franchise catalog.
6
- * WHAT: Exports categoriesSeedData — a 4-level, TWO-root category forest
7
- * (`Spinning Tops` + `Living Collectibles`, see _helpers/category-forest.ts),
8
- * brand rows (categoryType:"brand") and pricing bundles (categoryType:"bundle").
2
+ * WHY: The real catalogue taxonomy. 🛑 This file is NO LONGER Beyblade-only —
3
+ * its header said "narrowed to Beyblade so the demo has a coherent
4
+ * dataset instead of a sprawling multi-franchise catalog" and B5 reversed
5
+ * that deliberately, on evidence: a 15-site crawl of ~25,900 live Indian
6
+ * listings showed trading cards, collectible figures and Hot Wheels are
7
+ * each a real vertical with its own measured price bands, and three of
8
+ * the five largest competitors sell more than one of them.
9
9
  *
10
- * The tree was 2 levels under a single root until 2026-08-24. That made
10
+ * The old framing is not contradicted here, it is superseded — coherence
11
+ * now comes from the TREE being one axis deep per vertical rather than
12
+ * from the catalogue being one franchise wide.
13
+ *
14
+ * WHAT: Exports categoriesSeedData — a 5-level, SIX-root forest (Spinning Tops,
15
+ * Trading Cards, Collectible Figures, Hot Wheels, Model Kits, Living
16
+ * Collectibles — see _helpers/category-forest.ts), 28 brand rows
17
+ * (categoryType:"brand", _helpers/brand-rows.ts), two sublisting rows and
18
+ * five pricing bundles (categoryType:"bundle").
19
+ *
20
+ * The tree was 2 levels under a single root until 2026-08-24 and 47 nodes
21
+ * across 2 roots until B5. The first state made
11
22
  * `categoryType:"sublisting"` — documented as "tier-4 leaf groups under a
12
23
  * parent category" — structurally unreachable, and left the live-item
13
24
  * products with no category at all.
14
25
  *
26
+ * The tier-4 layer is 216 NAMED MODEL leaves (Lost Longinus, Storm
27
+ * Pegasus, Dran Sword …), GENERATED from the canonical corpus in
28
+ * _helpers/category-models.ts rather than typed by hand — a slug is the
29
+ * document id, the public URL and a searchTxt source, so it is the one
30
+ * field that cannot be quietly corrected later.
31
+ *
15
32
  * Products tag their FULL ancestor chain (leaf -> … -> root) so a single
16
33
  * array-contains at any level returns the whole subtree. That is what lets
17
34
  * a category page match on its own id alone; expanding descendants into an
@@ -36,7 +53,12 @@
36
53
  import { CATEGORY_FIELDS } from "../constants/field-names";
37
54
  import { seedPhoto } from "./_helpers/media";
38
55
  import { buildCategoryTree } from "./_helpers/category-tree";
56
+ import { withCategorySearchTxt } from "./_helpers/search-txt-wrappers";
39
57
  import { CATEGORY_FOREST } from "./_helpers/category-forest";
58
+ import { BRAND_ROW_COUNT, buildBrandRows } from "./_helpers/brand-rows";
59
+ import { MODEL_LEAF_COUNT } from "./_helpers/category-models";
60
+ import { MARKET_BAND_LEAF_IDS } from "./_helpers/category-content";
61
+ import { productsStandardSeedData } from "./products-standard-seed-data";
40
62
  const NOW = new Date();
41
63
  const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
42
64
  const emptyMetrics = {
@@ -50,9 +72,14 @@ const emptyMetrics = {
50
72
  lastUpdated: daysAgo(1),
51
73
  };
52
74
  // ──────────────────────────────────────────────────────────────────────────────
53
- // Categories — 47 rows across 4 tiers and 2 roots, all structural fields derived.
54
- // The shape lives in _helpers/category-forest.ts; the derivation in
55
- // _helpers/category-tree.ts.
75
+ // Categories — 381 rows across 5 tiers and 6 roots, every structural field
76
+ // derived. The shape lives in _helpers/category-forest.ts, the named-model
77
+ // tier-4 leaves in the GENERATED _helpers/category-models.ts, and the
78
+ // derivation in _helpers/category-tree.ts.
79
+ //
80
+ // 🛑 Never quote this count without running `buildCategoryTree` against the
81
+ // forest — it was 47 before B5 and the comment here said so for months after
82
+ // the tree changed underneath it.
56
83
  // ──────────────────────────────────────────────────────────────────────────────
57
84
  const rawCategories = buildCategoryTree(CATEGORY_FOREST, {
58
85
  createdBy: "user-admin-letitrip",
@@ -153,157 +180,61 @@ const sublistingRows = [
153
180
  },
154
181
  ];
155
182
  // ──────────────────────────────────────────────────────────────────────────────
156
- // Brands (categoryType:"brand").
183
+ // Brands (categoryType:"brand") — 4 rows -> 28, built by a factory.
157
184
  //
158
- // ⚠️ `BrandDetailPageView` matches products on the free-text `brand` DISPLAY
159
- // NAME (`sieveFilter("brand", EQ, brandName)`), not on `brandSlug`. So a brand
160
- // row's `name` here and the `brand:` string on every product that belongs to it
161
- // must match EXACTLY — renaming a brand silently orphans its whole catalogue.
162
- // Keep `brand` and `brandSlug` in lockstep on the product side.
185
+ // The shape, the three live traps (display-name matching, icon-beats-cover,
186
+ // and why a brand with no products is worse than no brand) and the logo
187
+ // decision all live in _helpers/brand-rows.ts. Four inline rows at ~35 lines
188
+ // each was already the wrong shape for 28.
163
189
  // ──────────────────────────────────────────────────────────────────────────────
164
- const brandRows = [
165
- {
166
- id: "brand-takara-tomy",
167
- slug: "brand-takara-tomy",
168
- name: "Takara-Tomy",
169
- categoryType: CATEGORY_FIELDS.CATEGORY_TYPE_VALUES.BRAND,
170
- description: "Japanese toy company behind Beyblade, Tomica, Transformers (Japan), and Duel Masters. Known for premium quality and Japan-exclusive releases.",
171
- brandWebsite: "https://www.takaratomy.co.jp",
172
- brandCountry: "Japan",
173
- brandFounded: 2006,
174
- highlights: [
175
- "The Japanese toy company that manufactures every Beyblade generation",
176
- "Also behind Tomica, Transformers (Japan region), and Duel Masters",
177
- "Known for Japan-exclusive releases and premium tooling quality",
178
- ],
179
- faqs: [
180
- { question: "Is Takara-Tomy the same company as Hasbro's Beyblade line?", answer: "No — Takara-Tomy manufactures and sells Beyblade in Japan and most of Asia; Hasbro licenses and distributes a separate (sometimes different) product line internationally, primarily in North America." },
181
- { question: "Are Takara-Tomy imports compatible with Hasbro Beyblade parts?", answer: "Within the same generation (e.g. Burst-to-Burst), yes, in most cases — cross-generation compatibility is not guaranteed." },
182
- ],
183
- rootId: "brand-takara-tomy",
184
- parentIds: [],
185
- tier: 0,
186
- path: "brand-takara-tomy",
187
- isLeaf: true,
188
- order: 1,
189
- display: { coverImage: seedPhoto("brand-logo-takara-tomy-20260101", 800, 800), showInMenu: false, showInFooter: true },
190
- isFeatured: true,
191
- isBrand: true,
192
- isActive: true,
193
- isSearchable: true,
194
- createdBy: "user-admin-letitrip",
195
- createdAt: daysAgo(300),
196
- updatedAt: daysAgo(30),
197
- seo: { title: "Takara-Tomy | LetItRip", description: "Shop Takara-Tomy collectibles — Beyblade, Tomica, Transformers.", keywords: ["takara tomy", "takara tomy beyblade", "tomica takara"] },
198
- },
199
- {
200
- id: "brand-beyblade",
201
- slug: "brand-beyblade",
202
- name: "Beyblade",
203
- categoryType: CATEGORY_FIELDS.CATEGORY_TYPE_VALUES.BRAND,
204
- description: "Spinning top battle franchise by Takara-Tomy (Japan) and Hasbro (international). Covers the Original series, Metal Fight, Burst, and the latest Beyblade X generation.",
205
- brandWebsite: "https://beyblade.takaratomy.co.jp",
206
- brandCountry: "Japan",
207
- brandFounded: 1999,
208
- highlights: [
209
- "The franchise itself — spanning four generations since 1999",
210
- "One of the best-selling spinning-top toy lines in the world",
211
- "A competitive tournament scene alongside casual collecting",
212
- ],
213
- faqs: [
214
- { question: "What age range is Beyblade designed for?", answer: "Most sets are labeled for ages 8+ due to small parts and launcher spring mechanisms — always check the age rating on the listing before buying for a young child." },
215
- { question: "Where can I find official tournament rules?", answer: "Takara-Tomy and Hasbro both publish official battle rules for their respective regions — check the manufacturer's site linked from this page's \"Website\" field for the current rulebook." },
216
- ],
217
- rootId: "brand-beyblade",
218
- parentIds: [],
219
- tier: 0,
220
- path: "brand-beyblade",
221
- isLeaf: true,
222
- order: 2,
223
- display: { coverImage: seedPhoto("brand-logo-beyblade-20260101", 800, 800), showInMenu: false, showInFooter: true },
224
- isFeatured: true,
225
- isBrand: true,
226
- isActive: true,
227
- isSearchable: true,
228
- createdBy: "user-admin-letitrip",
229
- createdAt: daysAgo(300),
230
- updatedAt: daysAgo(30),
231
- seo: { title: "Beyblade | LetItRip", description: "Shop Beyblade — Original, X, Burst, Metal Fight tops and stadiums.", keywords: ["beyblade", "beyblade x", "beyblade burst", "spinning top battle"] },
232
- },
233
- {
234
- id: "brand-hasbro",
235
- slug: "brand-hasbro",
236
- name: "Hasbro",
237
- categoryType: CATEGORY_FIELDS.CATEGORY_TYPE_VALUES.BRAND,
238
- description: "The international Beyblade licensee — Hasbro distributes its own Beyblade product line outside Japan, with different packaging, part names and sometimes different mould tolerances from the Takara-Tomy originals.",
239
- brandWebsite: "https://shop.hasbro.com",
240
- brandCountry: "United States",
241
- brandFounded: 1923,
242
- highlights: [
243
- "The Beyblade line most collectors outside Japan grew up with",
244
- "Widely available and generally cheaper than Japanese imports",
245
- "Same-generation parts are usually cross-compatible with Takara-Tomy",
246
- ],
247
- faqs: [
248
- { question: "Is a Hasbro Beyblade worse than the Takara-Tomy version?", answer: "Not worse, but often different — Hasbro releases can use different plastics and slightly looser tolerances, which competitive players notice. For casual play they perform comparably." },
249
- { question: "Can I mix Hasbro and Takara-Tomy parts?", answer: "Within the same generation, almost always yes. Across generations, no — the launcher and locking systems differ." },
250
- ],
251
- rootId: "brand-hasbro",
252
- parentIds: [],
253
- tier: 0,
254
- path: "brand-hasbro",
255
- isLeaf: true,
256
- order: 3,
257
- display: { coverImage: seedPhoto("brand-logo-hasbro-20260101", 800, 800), showInMenu: false, showInFooter: true },
258
- isFeatured: false,
259
- isBrand: true,
260
- isActive: true,
261
- isSearchable: true,
262
- createdBy: "user-admin-letitrip",
263
- createdAt: daysAgo(300),
264
- updatedAt: daysAgo(30),
265
- seo: { title: "Hasbro | LetItRip", description: "Shop Hasbro Beyblade — the international product line.", keywords: ["hasbro", "hasbro beyblade", "beyblade burst hasbro"] },
266
- },
267
- {
268
- // The Living Collectibles root needs a brand of its own: live listings are
269
- // sold by individual keepers and breeders, not by a manufacturer. Without
270
- // one, the three live-item products carried a `brand: "Beyblade Arena"`
271
- // string (a STORE name in a brand field) that matched no brand row, so they
272
- // were invisible on every brand page.
273
- id: "brand-independent-keepers",
274
- slug: "brand-independent-keepers",
275
- name: "Independent Keepers",
276
- categoryType: CATEGORY_FIELDS.CATEGORY_TYPE_VALUES.BRAND,
277
- description: "Live animals and plants come from individual keepers, breeders and growers rather than a manufacturer. This entry groups those listings so they are reachable from brand browsing like everything else.",
278
- brandCountry: "India",
279
- highlights: [
280
- "Every seller is verified before a live listing can go public",
281
- "Provenance, age and health information disclosed per listing",
282
- ],
283
- faqs: [
284
- { question: "Why is there a \"brand\" for living things at all?", answer: "Purely so live listings behave like every other listing in browse and search. It identifies the class of seller, not a manufacturer." },
285
- ],
286
- rootId: "brand-independent-keepers",
287
- parentIds: [],
288
- tier: 0,
289
- path: "brand-independent-keepers",
290
- isLeaf: true,
291
- order: 4,
292
- display: { coverImage: seedPhoto("brand-logo-independent-keepers-20260101", 800, 800), showInMenu: false, showInFooter: false },
293
- isFeatured: false,
294
- isBrand: true,
295
- isActive: true,
296
- isSearchable: true,
297
- createdBy: "user-admin-letitrip",
298
- createdAt: daysAgo(300),
299
- updatedAt: daysAgo(30),
300
- seo: { title: "Independent Keepers | LetItRip", description: "Live animals and plants from verified independent keepers and growers.", keywords: ["independent keepers", "breeders", "live plants"] },
301
- },
302
- ];
190
+ const brandRows = buildBrandRows({
191
+ createdBy: "user-admin-letitrip",
192
+ createdAt: daysAgo(300),
193
+ updatedAt: daysAgo(30),
194
+ });
303
195
  // P-17 — 5 bundle rows (categoryType:"bundle") grouping the Beyblade-minimal
304
196
  // standard products by generation, plus one cross-generation starter pack.
305
197
  // Every bundle references real seeded product ids (products-standard-seed-data.ts),
306
198
  // all currently under store-beyblade-arena.
199
+ /*
200
+ * 🛑 `bundleCategorySlugs` is DERIVED, because it is a MIRROR and mirrors
201
+ * drift (Root Cause #42).
202
+ *
203
+ * The field is the union of the members' `categorySlugs`, and it is what lets
204
+ * a category page scope its Bundles tab at all. It was hand-written across
205
+ * all five bundles — and it drifted in B5 the moment the Beyblade X tree
206
+ * changed: two bundles still named `category-x-starters` and
207
+ * `category-x-boosters`, leaves that no longer exist, so those bundles had
208
+ * silently stopped appearing under any Beyblade X category.
209
+ *
210
+ * Worse, the stale strings were still being READ as valid seed ids by
211
+ * `audit-tester-plugin-wiring` R7 once its id scan was widened, which briefly
212
+ * made two genuinely-broken authored tester cases stop being reported. A
213
+ * hand-maintained mirror does not only go wrong — it can keep the check that
214
+ * would have caught it quiet.
215
+ *
216
+ * Deriving from the members' own chains means the union cannot disagree with
217
+ * them, and a member whose chain moves updates every bundle holding it.
218
+ */
219
+ const PRODUCT_CHAINS = new Map(productsStandardSeedData.map((p) => [String(p.id), (p.categorySlugs ?? [])]));
220
+ function bundleCategorySlugsFor(productIds) {
221
+ const out = [];
222
+ for (const id of productIds) {
223
+ const chain = PRODUCT_CHAINS.get(id);
224
+ /*
225
+ * A member missing from the standard seed is a BUG, not something to shrug
226
+ * past: the bundle would silently lose that member's categories and the
227
+ * Bundles tab would under-report. All eight current members are standard
228
+ * products; if that changes, widen the map rather than softening this.
229
+ */
230
+ if (!chain)
231
+ throw new Error(`bundle member ${id} is not in productsStandardSeedData`);
232
+ for (const c of chain)
233
+ if (!out.includes(c))
234
+ out.push(c);
235
+ }
236
+ return out;
237
+ }
307
238
  const bundleRows = [
308
239
  {
309
240
  id: "bundle-original-collectors-set",
@@ -319,7 +250,7 @@ const bundleRows = [
319
250
  productIds: ["product-beyblade-original-dranzer-s", "product-beyblade-original-driger-v", "product-beyblade-metal-storm-pegasus"],
320
251
  },
321
252
  bundleProductIds: ["product-beyblade-original-dranzer-s", "product-beyblade-original-driger-v", "product-beyblade-metal-storm-pegasus"],
322
- bundleCategorySlugs: ["category-original-hms", "category-original-tops", "category-beyblade-original", "category-spinning-tops", "category-original-plastic-gen", "category-metal-masters", "category-metal-tops", "category-beyblade-metal"],
253
+ bundleCategorySlugs: bundleCategorySlugsFor(["product-beyblade-original-dranzer-s", "product-beyblade-original-driger-v", "product-beyblade-metal-storm-pegasus"]),
323
254
  bundleOriginalTotal: 4597, // 1499 + 1799 + 1299
324
255
  bundleStockStatus: "in_stock",
325
256
  display: { coverImage: seedPhoto("bundle-original-collectors-set-20260101", 1200, 900), showInFooter: false },
@@ -354,7 +285,7 @@ const bundleRows = [
354
285
  productIds: ["product-beyblade-metal-storm-pegasus", "product-beyblade-metal-flame-sagittario", "product-beyblade-original-dranzer-s"],
355
286
  },
356
287
  bundleProductIds: ["product-beyblade-metal-storm-pegasus", "product-beyblade-metal-flame-sagittario", "product-beyblade-original-dranzer-s"],
357
- bundleCategorySlugs: ["category-metal-masters", "category-metal-tops", "category-beyblade-metal", "category-spinning-tops", "category-metal-fury", "category-original-hms", "category-original-tops", "category-beyblade-original"],
288
+ bundleCategorySlugs: bundleCategorySlugsFor(["product-beyblade-metal-storm-pegasus", "product-beyblade-metal-flame-sagittario", "product-beyblade-original-dranzer-s"]),
358
289
  bundleOriginalTotal: 3997, // 1299 + 1199 + 1499
359
290
  bundleStockStatus: "in_stock",
360
291
  display: { coverImage: seedPhoto("bundle-metal-fusion-duo-20260101", 1200, 900), showInFooter: false },
@@ -391,7 +322,7 @@ const bundleRows = [
391
322
  productIds: ["product-beyblade-burst-valkyrie", "product-beyblade-burst-regalia-genesis", "product-beyblade-metal-flame-sagittario"],
392
323
  },
393
324
  bundleProductIds: ["product-beyblade-burst-valkyrie", "product-beyblade-burst-regalia-genesis", "product-beyblade-metal-flame-sagittario"],
394
- bundleCategorySlugs: ["category-burst-superking", "category-burst-tops", "category-beyblade-burst", "category-spinning-tops", "category-burst-classic", "category-metal-fury", "category-metal-tops", "category-beyblade-metal"],
325
+ bundleCategorySlugs: bundleCategorySlugsFor(["product-beyblade-burst-valkyrie", "product-beyblade-burst-regalia-genesis", "product-beyblade-metal-flame-sagittario"]),
395
326
  bundleOriginalTotal: 3597, // 999 + 1399 + 1199
396
327
  bundleStockStatus: "in_stock",
397
328
  display: { coverImage: seedPhoto("bundle-burst-battlers-pack-20260101", 1200, 900), showInFooter: false },
@@ -426,7 +357,7 @@ const bundleRows = [
426
357
  productIds: ["product-beyblade-x-wizard-arrow", "product-beyblade-x-knife-shinobi", "product-beyblade-burst-valkyrie"],
427
358
  },
428
359
  bundleProductIds: ["product-beyblade-x-wizard-arrow", "product-beyblade-x-knife-shinobi", "product-beyblade-burst-valkyrie"],
429
- bundleCategorySlugs: ["category-x-starters", "category-x-tops", "category-beyblade-x", "category-spinning-tops", "category-x-boosters", "category-burst-superking", "category-burst-tops", "category-beyblade-burst"],
360
+ bundleCategorySlugs: bundleCategorySlugsFor(["product-beyblade-x-wizard-arrow", "product-beyblade-x-knife-shinobi", "product-beyblade-burst-valkyrie"]),
430
361
  bundleOriginalTotal: 2847, // 899 + 949 + 999
431
362
  bundleStockStatus: "in_stock",
432
363
  display: { coverImage: seedPhoto("bundle-x-series-starter-20260101", 1200, 900), showInFooter: false },
@@ -474,7 +405,7 @@ const bundleRows = [
474
405
  "product-beyblade-burst-valkyrie",
475
406
  "product-beyblade-x-wizard-arrow",
476
407
  ],
477
- bundleCategorySlugs: ["category-original-plastic-gen", "category-original-tops", "category-beyblade-original", "category-spinning-tops", "category-metal-masters", "category-metal-tops", "category-beyblade-metal", "category-burst-superking", "category-burst-tops", "category-beyblade-burst", "category-x-starters", "category-x-tops", "category-beyblade-x"],
408
+ bundleCategorySlugs: bundleCategorySlugsFor(["product-beyblade-original-driger-v", "product-beyblade-metal-storm-pegasus", "product-beyblade-burst-valkyrie", "product-beyblade-x-wizard-arrow"]),
478
409
  bundleOriginalTotal: 4996, // 1799 + 1299 + 999 + 899
479
410
  bundleStockStatus: "in_stock",
480
411
  display: { coverImage: seedPhoto("bundle-every-generation-starter-pack-20260101", 1200, 900), showInFooter: false },
@@ -496,17 +427,64 @@ const bundleRows = [
496
427
  updatedAt: daysAgo(1),
497
428
  },
498
429
  ];
499
- export const categoriesSeedData = [
430
+ /*
431
+ * One array, wrapped once.
432
+ *
433
+ * `categoriesSeedData` and `categoriesP1SeedData` were two byte-identical
434
+ * literals, so every change had to be made twice — and `searchTxt` would have
435
+ * been two derivations to keep in step, which is the exact drift
436
+ * `search-txt-wrappers.ts` exists to prevent.
437
+ *
438
+ * 🛑 `withCategorySearchTxt` is applied AFTER the spread, deliberately:
439
+ * `buildCategorySearchTxt` indexes `path` and `ancestors[].name`, both DERIVED
440
+ * by `buildCategoryTree`. Wrapping a row before derivation would index
441
+ * name/slug/description only and silently drop the lineage search — typing
442
+ * "burst" would stop reaching a tier-4 model under Beyblade Burst.
443
+ */
444
+ const allCategoryRows = [
500
445
  ...rawCategories.map((c) => ({ ancestors: [], ...c, createdByType: "admin" })),
501
446
  ...sublistingRows.map((s) => ({ ancestors: [], ...s, createdByType: "admin" })),
502
447
  ...brandRows.map((b) => ({ ancestors: [], ...b, createdByType: "admin" })),
503
448
  ...bundleRows.map((b) => ({ ancestors: [], ...b })),
504
- ];
449
+ ].map(withCategorySearchTxt);
450
+ /*
451
+ * ── Assertions, at MODULE LOAD, so a silent drop is loud ─────────────────────
452
+ *
453
+ * 🛑 These exist because of a specific failure shape this file is exposed to:
454
+ * `appkit-seed status` counts documents BY ID, so it cannot see content drift
455
+ * at all, and `load` is a `set(…, {merge:true})` that never removes a field.
456
+ * A generated map that silently emptied — `MODEL_LEAVES` keyed on a line id
457
+ * the forest renamed, say — would produce a tree that builds, typechecks,
458
+ * seeds without error, reports perfectly in sync, and is missing 216 pages.
459
+ *
460
+ * Throwing at import is the right severity: every consumer of this module
461
+ * (the seed CLI, the tester fixtures, the audits) wants to fail before writing
462
+ * rather than after.
463
+ */
464
+ const modelLeafRows = rawCategories.filter((c) => c.tier === 4).length;
465
+ if (modelLeafRows !== MODEL_LEAF_COUNT) {
466
+ throw new Error(`category seed: ${modelLeafRows} tier-4 model leaves reached the tree but ` +
467
+ `category-models.ts generated ${MODEL_LEAF_COUNT}. A line id in the ` +
468
+ `forest no longer matches a key in MODEL_LEAVES — re-run ` +
469
+ `node scripts/gen-category-models.mjs and check the line ids.`);
470
+ }
471
+ if (brandRows.length !== BRAND_ROW_COUNT) {
472
+ throw new Error(`category seed: ${brandRows.length} brand rows built, expected ${BRAND_ROW_COUNT}`);
473
+ }
474
+ {
475
+ /*
476
+ * A price band keyed on a leaf that does not exist is dead weight that READS
477
+ * AS COVERAGE — the hint renders nowhere and nothing says so. Cheap to
478
+ * check, and it is the most likely thing to rot as the tree is edited.
479
+ */
480
+ const ids = new Set(rawCategories.map((c) => c.id));
481
+ const orphans = MARKET_BAND_LEAF_IDS.filter((id) => !ids.has(id));
482
+ if (orphans.length) {
483
+ throw new Error(`category seed: ${orphans.length} market band(s) key a non-existent ` +
484
+ `leaf: ${orphans.join(", ")}`);
485
+ }
486
+ }
487
+ export const categoriesSeedData = allCategoryRows;
505
488
  // P-1 default seed: identical to categoriesSeedData (bundle rows included — P-17
506
489
  // re-added them 2026-08-15 after the earlier removal referenced below).
507
- export const categoriesP1SeedData = [
508
- ...rawCategories.map((c) => ({ ancestors: [], ...c, createdByType: "admin" })),
509
- ...sublistingRows.map((s) => ({ ancestors: [], ...s, createdByType: "admin" })),
510
- ...brandRows.map((b) => ({ ancestors: [], ...b, createdByType: "admin" })),
511
- ...bundleRows.map((b) => ({ ancestors: [], ...b })),
512
- ];
490
+ export const categoriesP1SeedData = allCategoryRows;
@@ -80,6 +80,7 @@ export { scammersSeedData } from "./scammers-seed-data";
80
80
  export { supportTicketsSeedData } from "./support-tickets-seed-data";
81
81
  export { testerChecklistSeedData, testerResponsesSeedData, } from "../features/tester/seed-data";
82
82
  export { productFeaturesSeedData } from "./product-features-seed-data";
83
+ export { taxCodesSeedData } from "./tax-codes-seed-data";
83
84
  export { offersSeedData } from "./offers-seed-data";
84
85
  export { payoutMethodsSeedData, shippingConfigsSeedData, analyticsCardsSeedData, analyticsAlertsSeedData, storeCategoriesSeedData, listingTemplatesSeedData, moderationQueueSeedData, reportsSeedData, itemRequestsSeedData, storeWhatsAppConfigSeedData, storeGoogleConfigSeedData, } from "./store-extensions-seed-data";
85
86
  export type { SeedManifest, SeedManifestEntry, SeedCollectionName } from "./manifest";
@@ -77,6 +77,7 @@ export { testerChecklistSeedData,
77
77
  // continues, which is exactly what happened on the first attempt here.
78
78
  testerResponsesSeedData, } from "../features/tester/seed-data";
79
79
  export { productFeaturesSeedData } from "./product-features-seed-data";
80
+ export { taxCodesSeedData } from "./tax-codes-seed-data";
80
81
  export { offersSeedData } from "./offers-seed-data";
81
82
  // S-STORE Extensions (Tier S-STORE) — 11 new collections
82
83
  export { payoutMethodsSeedData, shippingConfigsSeedData, analyticsCardsSeedData, analyticsAlertsSeedData, storeCategoriesSeedData, listingTemplatesSeedData, moderationQueueSeedData, reportsSeedData, itemRequestsSeedData, storeWhatsAppConfigSeedData, storeGoogleConfigSeedData, } from "./store-extensions-seed-data";
@@ -11,6 +11,6 @@ export interface SeedManifestEntry {
11
11
  type?: string;
12
12
  }
13
13
  /** Every collection name the CLI seeder (appkit/scripts/seed-cli.mjs) and this manifest cover. */
14
- export type SeedCollectionName = "users" | "addresses" | "categories" | "stores" | "products" | "orders" | "reviews" | "bids" | "coupons" | "carousels" | "carouselSlides" | "homepageSections" | "siteSettings" | "faqs" | "notifications" | "payouts" | "blogPosts" | "events" | "eventEntries" | "sessions" | "carts" | "wishlists" | "history" | "groupedListings" | "scammerProfiles" | "supportTickets" | "productFeatures" | "offers" | "couponUsage" | "claimedCoupons" | "payoutMethods" | "shippingConfigs" | "analyticsCards" | "analyticsAlerts" | "storeCategories" | "listingTemplates" | "moderationQueue" | "reports" | "itemRequests" | "storeWhatsAppConfig" | "storeGoogleConfig" | "roleOverrides" | "customRoles" | "adminNotifications" | "lotteryEntries" | "shipments" | "shipmentLots" | "shipmentItems" | "catalogueItems" | "testerChecklistItems";
14
+ export type SeedCollectionName = "users" | "addresses" | "categories" | "stores" | "products" | "orders" | "reviews" | "bids" | "coupons" | "carousels" | "carouselSlides" | "homepageSections" | "siteSettings" | "faqs" | "notifications" | "payouts" | "blogPosts" | "events" | "eventEntries" | "sessions" | "carts" | "wishlists" | "history" | "groupedListings" | "scammerProfiles" | "supportTickets" | "productFeatures" | "taxCodes" | "offers" | "couponUsage" | "claimedCoupons" | "payoutMethods" | "shippingConfigs" | "analyticsCards" | "analyticsAlerts" | "storeCategories" | "listingTemplates" | "moderationQueue" | "reports" | "itemRequests" | "storeWhatsAppConfig" | "storeGoogleConfig" | "roleOverrides" | "customRoles" | "adminNotifications" | "lotteryEntries" | "shipments" | "shipmentLots" | "shipmentItems" | "catalogueItems" | "testerChecklistItems";
15
15
  export type SeedManifest = Record<SeedCollectionName, SeedManifestEntry[]>;
16
16
  export declare const SEED_MANIFEST: SeedManifest;