@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
@@ -1,162 +1,366 @@
1
- /**
2
- * Product Features Seed Data
1
+ /*
2
+ * WHY: `productFeatures` held 10 platform badge flags while `ProductDocument.tags`
3
+ * held free text with no vocabulary at all — which is how `attack-type` and
4
+ * `attack type` coexist. B5 retires `tags` and makes this collection the ONE
5
+ * vocabulary, so it has to carry everything a facet needs: release type,
6
+ * authenticity, battle type, spin, card language, card format, grade, chase
7
+ * status, figure scale. Those are exactly the axes the crawl measured and
8
+ * that §4.4 refuses to put in the tree, because a product has one category
9
+ * chain and any number of features.
10
+ * WHAT: 171 platform-scope rows, grouped by `FeatureGroup`, built by a factory —
11
+ * 76 hand-authored from the crawl's measured vocabulary plus 95 GENERATED
12
+ * blader/character rows. id === slug, prefix `feature-`.
3
13
  *
4
- * 10 platform-scope feature flags available to every store. Store-scope custom
5
- * features are not seeded — sellers create their own via /store/[slug]/features.
14
+ * EXPORTS:
15
+ * productFeaturesSeedData — ProductFeatureDocument[]
16
+ * PRODUCT_FEATURE_COUNT — asserted downstream so a silent drop is loud
6
17
  *
7
- * id === slug, prefix: feature-
18
+ * ## 🛑 Three things the factory exists to prevent
19
+ *
20
+ * 1. **`searchTxt` is built from the LABEL and the aliases, never the slug.**
21
+ * `buildProductSearchTxt` used to index `features` raw, so the id
22
+ * `feature-free-shipping` tokenised to free/shipping and searching the
23
+ * LABEL "Free Shipping" worked only by coincidence — while a label like
24
+ * "New in Box" whose slug is `feature-nib` was unfindable entirely.
25
+ *
26
+ * 2. **Aliases are BUDGETED.** They fold into a 600-token array that truncates
27
+ * SILENTLY, so each row caps at a handful. The misspelling corpus from
28
+ * bladekingbeys is deliberately NOT dumped in here wholesale — a
29
+ * counterfeiter's trademark-evasion spellings (`Pegasis`, `Jupitar`,
30
+ * `Fireblase`) are what real buyers mistype, so they belong on the MODEL
31
+ * leaf that owns the name, not on a generic feature.
32
+ *
33
+ * 3. **`group` is not `category`.** `category` groups by the SURFACE that shows
34
+ * the badge; `group` groups by what the feature MEANS, and drives one facet
35
+ * section each. "Sealed" on a TCG booster box and "sealed" on a bey are the
36
+ * same word about different things.
37
+ *
38
+ * @tag domain:products
39
+ * @tag layer:seed
40
+ * @tag pattern:shared-literal
41
+ * @tag access:server-only
42
+ * @tag consumers:seed/index.ts,seed/runner.ts
43
+ * @tag sideEffects:none
8
44
  */
45
+ import { buildSearchTxt } from "../utils/search-txt";
46
+ import { CHARACTER_SEEDS, CHARACTER_SEED_COUNT } from "./_helpers/character-features";
9
47
  const NOW = new Date("2026-05-11T00:00:00.000Z");
10
48
  const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
11
- export const productFeaturesSeedData = [
12
- {
13
- id: "feature-free-shipping",
14
- slug: "feature-free-shipping",
15
- label: "Free Shipping",
16
- description: "Shipping is included in the listed price.",
17
- icon: "truck",
18
- iconColor: "--appkit-color-primary",
19
- category: "shipping",
20
- scope: "platform",
21
- productTypes: ["all"],
22
- isActive: true,
23
- displayOrder: 10,
24
- createdAt: daysAgo(30),
25
- updatedAt: daysAgo(30),
26
- },
27
- {
28
- id: "feature-verified-seller",
29
- slug: "feature-verified-seller",
30
- label: "Verified Seller",
31
- description: "Sold by a LetItRip verified seller — identity and address confirmed.",
32
- icon: "badge-check",
33
- iconColor: "--appkit-color-primary",
34
- category: "seller",
35
- scope: "platform",
36
- productTypes: ["all"],
37
- isActive: true,
38
- displayOrder: 20,
39
- createdAt: daysAgo(30),
40
- updatedAt: daysAgo(30),
41
- },
42
- {
43
- id: "feature-accept-returns",
44
- slug: "feature-accept-returns",
45
- label: "Returns Accepted",
46
- description: "Eligible for return within the platform return window.",
47
- icon: "refresh-ccw",
48
- iconColor: "--appkit-color-secondary",
49
- category: "platform",
50
- scope: "platform",
51
- productTypes: ["all"],
52
- isActive: true,
53
- displayOrder: 30,
54
- createdAt: daysAgo(30),
55
- updatedAt: daysAgo(30),
56
- },
57
- {
58
- id: "feature-condition-new",
59
- slug: "feature-condition-new",
60
- label: "Brand New",
61
- description: "Factory-sealed or unopened, in original packaging.",
62
- icon: "sparkles",
63
- iconColor: "--appkit-color-primary",
64
- category: "condition",
65
- scope: "platform",
66
- productTypes: ["all"],
67
- isActive: true,
68
- displayOrder: 40,
69
- createdAt: daysAgo(30),
70
- updatedAt: daysAgo(30),
71
- },
72
- {
73
- id: "feature-condition-used",
74
- slug: "feature-condition-used",
75
- label: "Pre-owned",
76
- description: "Used item — see listing description for condition details.",
77
- icon: "package",
78
- iconColor: "--appkit-color-secondary",
79
- category: "condition",
80
- scope: "platform",
81
- productTypes: ["all"],
82
- isActive: true,
83
- displayOrder: 50,
84
- createdAt: daysAgo(30),
85
- updatedAt: daysAgo(30),
86
- },
87
- {
88
- id: "feature-featured",
89
- slug: "feature-featured",
90
- label: "Featured",
91
- description: "Hand-picked by the LetItRip editorial team.",
92
- icon: "star",
93
- iconColor: "--appkit-color-primary",
94
- category: "platform",
95
- scope: "platform",
96
- productTypes: ["all"],
97
- isActive: true,
98
- displayOrder: 60,
99
- createdAt: daysAgo(30),
100
- updatedAt: daysAgo(30),
101
- },
102
- {
103
- id: "feature-promoted",
104
- slug: "feature-promoted",
105
- label: "Promoted",
106
- description: "Boosted placement — seller has promoted this listing.",
107
- icon: "megaphone",
108
- iconColor: "--appkit-color-secondary",
109
- category: "platform",
110
- scope: "platform",
111
- productTypes: ["all"],
112
- isActive: true,
113
- displayOrder: 70,
114
- createdAt: daysAgo(30),
115
- updatedAt: daysAgo(30),
116
- },
117
- {
118
- id: "feature-auction-winner-badge",
119
- slug: "feature-auction-winner-badge",
120
- label: "Winner Badge",
121
- description: "Winners of this auction receive a special badge on their profile.",
122
- icon: "trophy",
123
- iconColor: "--appkit-color-primary",
124
- category: "auction",
125
- scope: "platform",
126
- productTypes: ["auction"],
127
- isActive: true,
128
- displayOrder: 80,
129
- createdAt: daysAgo(30),
130
- updatedAt: daysAgo(30),
131
- },
132
- {
133
- id: "feature-shipping-paid-by-seller",
134
- slug: "feature-shipping-paid-by-seller",
135
- label: "Seller Pays Shipping",
136
- description: "Seller covers all shipping costs on this auction — winner pays only the bid.",
137
- icon: "package-check",
138
- iconColor: "--appkit-color-primary",
139
- category: "shipping",
140
- scope: "platform",
141
- productTypes: ["auction"],
142
- isActive: true,
143
- displayOrder: 90,
144
- createdAt: daysAgo(30),
145
- updatedAt: daysAgo(30),
146
- },
147
- {
148
- id: "feature-preorder-confirmed",
149
- slug: "feature-preorder-confirmed",
150
- label: "Pre-order Confirmed",
151
- description: "Production and delivery dates are confirmed by the manufacturer.",
152
- icon: "calendar-check",
153
- iconColor: "--appkit-color-primary",
154
- category: "preorder",
49
+ const STAMP = daysAgo(30);
50
+ const TONE = {
51
+ primary: "--appkit-color-primary",
52
+ secondary: "--appkit-color-secondary",
53
+ neutral: undefined,
54
+ };
55
+ function featureRow(input, order) {
56
+ const id = `feature-${input.slug}`;
57
+ return {
58
+ id,
59
+ slug: id,
60
+ label: input.label,
61
+ description: input.description,
62
+ icon: input.icon,
63
+ ...(TONE[input.tone ?? "neutral"] ? { iconColor: TONE[input.tone ?? "neutral"] } : {}),
64
+ category: input.category,
65
+ group: input.group,
155
66
  scope: "platform",
156
- productTypes: ["preorder"],
67
+ productTypes: input.types ?? ["all"],
68
+ ...(input.aliases?.length ? { aliases: input.aliases } : {}),
69
+ /*
70
+ * 🛑 LABEL + aliases. Never the slug — see note 1. A row whose label and
71
+ * slug diverge ("New in Box" / `feature-nib`) is precisely the case a
72
+ * slug-derived index cannot find.
73
+ */
74
+ searchTxt: buildSearchTxt([input.label, ...(input.aliases ?? [])]),
157
75
  isActive: true,
158
- displayOrder: 100,
159
- createdAt: daysAgo(30),
160
- updatedAt: daysAgo(30),
161
- },
76
+ displayOrder: order,
77
+ createdAt: STAMP,
78
+ updatedAt: STAMP,
79
+ };
80
+ }
81
+ // ─────────────────────────────────────────────────────────────────────────────
82
+ // Platform badges — the ORIGINAL TEN, now grouped
83
+ //
84
+ // 🛑 Their ids are unchanged on purpose. `group` was optional purely so these
85
+ // compiled before B5; leaving them ungrouped would have put them in no facet
86
+ // section, which is indistinguishable from not existing.
87
+ //
88
+ // 🛑 `feature-condition-used` is relabelled "Pre-owned" with "Used" as an
89
+ // ALIAS rather than being joined by a second `feature-pre-owned` row. The two
90
+ // words describe one state — the category description templates treat them
91
+ // identically, by design — and a second row for a synonym is how the two
92
+ // vocabularies this merge exists to collapse started in the first place.
93
+ // ─────────────────────────────────────────────────────────────────────────────
94
+ const PLATFORM = [
95
+ { slug: "free-shipping", label: "Free Shipping", description: "Shipping is included in the listed price.", icon: "truck", group: "commercial", category: "shipping", tone: "primary", aliases: ["Free delivery"] },
96
+ { slug: "verified-seller", label: "Verified Seller", description: "Sold by a LetItRip verified seller — identity and address confirmed.", icon: "badge-check", group: "commercial", category: "seller", tone: "primary" },
97
+ { slug: "accept-returns", label: "Returns Accepted", description: "Eligible for return within the platform return window.", icon: "refresh-ccw", group: "commercial", category: "platform", tone: "secondary", aliases: ["Returnable"] },
98
+ { slug: "condition-new", label: "Brand New", description: "Factory-sealed or unopened, in original packaging.", icon: "sparkles", group: "condition", category: "condition", tone: "primary", aliases: ["New", "Unused"] },
99
+ /*
100
+ * 🛑 "Preowned" unhyphenated is a real alias, not a typo. `buildSearchTxt`
101
+ * splits on punctuation, so the label "Pre-owned" indexes as `pre` + `owned`
102
+ * and a buyer who types "preowned" as one word matches NEITHER. "Pre owned"
103
+ * with a space would have been useless here — it tokenises identically to
104
+ * the label it was meant to supplement.
105
+ */
106
+ { slug: "condition-used", label: "Pre-owned", description: "Previously owned and sold as pictured. Condition is graded on the listing.", icon: "history", group: "condition", category: "condition", aliases: ["Used", "Second-hand", "Preowned"] },
107
+ { slug: "featured", label: "Featured", description: "Highlighted by the platform across browse and homepage surfaces.", icon: "star", group: "commercial", category: "platform", tone: "primary" },
108
+ { slug: "promoted", label: "Promoted", description: "A paid promotion placement by the seller.", icon: "megaphone", group: "commercial", category: "platform", tone: "secondary" },
109
+ { slug: "auction-winner-badge", label: "Auction Win", description: "Acquired by winning an auction on LetItRip.", icon: "gavel", group: "commercial", category: "auction", types: ["auction"] },
110
+ { slug: "shipping-paid-by-seller", label: "Seller Pays Shipping", description: "The seller covers shipping on this listing.", icon: "package", group: "commercial", category: "shipping", tone: "primary", aliases: ["Seller paid shipping"] },
111
+ { slug: "preorder-confirmed", label: "Pre-order Confirmed", description: "Stock is allocated and the release date is confirmed by the seller.", icon: "calendar-check", group: "release", category: "preorder", types: ["preorder"] },
112
+ ];
113
+ // ─────────────────────────────────────────────────────────────────────────────
114
+ // condition — the packaging and wear vocabulary, from the crawl
115
+ //
116
+ // `without-box` is worth its own row for a measured reason: hobson's 57
117
+ // "Without Box" listings have a median of ₹599 against ₹1,699 for the same
118
+ // items boxed. A ~3× spread is a facet, not a prose detail.
119
+ // ─────────────────────────────────────────────────────────────────────────────
120
+ const CONDITION = [
121
+ { slug: "nib", label: "New in Box", description: "Unopened in its original box, with the box intact.", icon: "box", group: "condition", category: "condition", tone: "primary", aliases: ["NIB", "Boxed", "Sealed box", "MIB"] },
122
+ { slug: "nip", label: "New in Packet", description: "New on its original card or in its original polybag, unopened.", icon: "package-2", group: "condition", category: "condition", tone: "primary", aliases: ["NIP", "Carded", "On card", "Blister"] },
123
+ { slug: "sealed", label: "Sealed", description: "Factory wrap intact and never opened.", icon: "shield-check", group: "condition", category: "condition", tone: "primary", aliases: ["Factory sealed", "Shrink wrapped"] },
124
+ { slug: "mint", label: "Mint", description: "No visible wear — indistinguishable from new on inspection.", icon: "gem", group: "condition", category: "condition", tone: "primary", aliases: ["Mint condition", "As new"] },
125
+ { slug: "loose", label: "Loose", description: "Out of its packaging, complete and functional.", icon: "puzzle", group: "condition", category: "condition", aliases: ["Unboxed", "Open"] },
126
+ { slug: "no-box", label: "Without Box", description: "The item only — no original box or card. Priced well below a boxed example.", icon: "box-select", group: "condition", category: "condition", aliases: ["No box", "Box missing", "Item only"] },
127
+ { slug: "incomplete", label: "Incomplete", description: "One or more original parts are missing. What is included is listed and photographed.", icon: "circle-slash", group: "condition", category: "condition", aliases: ["Missing parts", "Partial"] },
128
+ { slug: "damaged", label: "Damaged", description: "Has a chip, crack, stripped tooth or similar structural fault, disclosed and photographed.", icon: "alert-triangle", group: "condition", category: "condition", aliases: ["Broken", "Faulty", "Cracked"] },
129
+ ];
130
+ // ─────────────────────────────────────────────────────────────────────────────
131
+ // authenticity — mirrors `ProductDocument.authenticity`
132
+ //
133
+ // 🛑 There is NO "fake" row, and that is a product decision rather than an
134
+ // omission. A lead-bearing counterfeit is not a listing state we offer; it is
135
+ // a scam report. The duplication with the enum field is the one place it is
136
+ // justified: a feature can be left off and nothing notices, and a mislabelled
137
+ // reproduction is a refund while a mislabelled counterfeit is a child handling
138
+ // leaded metal.
139
+ //
140
+ // 🛑 "Midfake" is an ALIAS on `reproduction`, not a row. It is the word the
141
+ // community actually searches — worldhobbyshop ships a top-level nav category
142
+ // literally called "MidFake Beyblades" — but it names the same state.
143
+ // ─────────────────────────────────────────────────────────────────────────────
144
+ const AUTHENTICITY = [
145
+ { slug: "original", label: "Original", description: "Licensed manufacture — Takara, Takara Tomy, Hasbro or a regional licensee.", icon: "shield-check", group: "authenticity", category: "condition", tone: "primary", aliases: ["Genuine", "Authentic", "Licensed"] },
146
+ { slug: "reproduction", label: "Reproduction", description: "An unlicensed copy of an original design, carrying no manufacturer's marque. Disclosed, never described as original.", icon: "copy", group: "authenticity", category: "condition", tone: "secondary", aliases: ["Midfake", "Repro", "1st copy", "Aftermarket"] },
147
+ { slug: "unverified", label: "Authenticity Unverified", description: "The seller has not confirmed whether this is an original or a reproduction.", icon: "help-circle", group: "authenticity", category: "condition", aliases: ["Unconfirmed", "Unknown authenticity"] },
148
+ ];
149
+ // ─────────────────────────────────────────────────────────────────────────────
150
+ // release — measured as a REAL price multiplier, not a label
151
+ //
152
+ // beybladeshopindia, n=419: Limited Releases median ₹2,499 against Regular's
153
+ // ₹1,399 — 1.8×. Early Releases run 0.6×. That is why release type has to be a
154
+ // facet the price hint can account for.
155
+ // ─────────────────────────────────────────────────────────────────────────────
156
+ const RELEASE = [
157
+ { slug: "regular-release", label: "Regular Release", description: "A standard retail release, generally available.", icon: "circle", group: "release", category: "platform", aliases: ["Standard release"] },
158
+ { slug: "limited-release", label: "Limited Release", description: "A limited production run. Measured at ~1.8× a regular release's median price.", icon: "timer", group: "release", category: "platform", tone: "secondary", aliases: ["Limited edition", "Limited press", "Ltd"] },
159
+ { slug: "early-release", label: "Early Release", description: "Released ahead of general availability, often at an event.", icon: "fast-forward", group: "release", category: "platform", aliases: ["Pre-release", "Early"] },
160
+ { slug: "random-booster", label: "Random Booster", description: "Sold in a sealed random assortment — the specific contents were not chosen.", icon: "dices", group: "release", category: "platform", aliases: ["RB", "Blind box", "Random"] },
161
+ { slug: "rlc", label: "Red Line Club", description: "A members-only Hot Wheels release, produced in small numbers and sold directly by Mattel.", icon: "crown", group: "release", category: "platform", tone: "secondary", aliases: ["RLC", "Red Line"] },
162
+ { slug: "remake", label: "Remake", description: "A modern reissue of an earlier design on current hardware.", icon: "rotate-ccw", group: "release", category: "platform", aliases: ["Reissue", "Reprint", "X-Over"] },
163
+ { slug: "promo", label: "Promotional", description: "Given away or sold at an event, a campaign or a magazine tie-in rather than at retail.", icon: "gift", group: "release", category: "platform", aliases: ["Promotional", "Event exclusive", "Prize"] },
164
+ { slug: "store-exclusive", label: "Store Exclusive", description: "Sold only through one retailer or region.", icon: "store", group: "release", category: "platform", aliases: ["Retailer exclusive", "Regional exclusive"] },
165
+ ];
166
+ // ─────────────────────────────────────────────────────────────────────────────
167
+ // type and spin — the Beyblade axes
168
+ //
169
+ // 🛑 FOUR type values and THREE spin values, and no more. The local corpus has
170
+ // 100% Type coverage in ~90 SPELLINGS and 97% Spin in 54 — a feature slug like
171
+ // `feature-attack-hammer-variant-heavy-slow-smash` is a vocabulary that has
172
+ // already failed. The parenthetical qualifier stays product prose; only the
173
+ // leading enum word becomes a feature.
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+ const BATTLE_TYPE = [
176
+ { slug: "type-attack", label: "Attack Type", description: "Built to strike hard and end a battle quickly, at the cost of stamina.", icon: "swords", group: "type", category: "platform", tone: "secondary", aliases: ["Attack", "Attack-type"] },
177
+ { slug: "type-defense", label: "Defense Type", description: "Built to absorb hits and stay in the stadium.", icon: "shield", group: "type", category: "platform", aliases: ["Defense", "Defence", "Defense-type"] },
178
+ { slug: "type-stamina", label: "Stamina Type", description: "Built to outlast — the last top spinning wins.", icon: "battery-full", group: "type", category: "platform", aliases: ["Stamina", "Endurance", "Stamina-type"] },
179
+ { slug: "type-balance", label: "Balance Type", description: "A compromise build, competent at attack, defense and stamina without excelling.", icon: "scale", group: "type", category: "platform", aliases: ["Balance", "Balance-type"] },
180
+ ];
181
+ const SPIN = [
182
+ { slug: "right-spin", label: "Right Spin", description: "Spins clockwise — the standard direction for most releases.", icon: "rotate-cw", group: "spin", category: "platform", aliases: ["Right", "Clockwise", "Right-spin"] },
183
+ { slug: "left-spin", label: "Left Spin", description: "Spins anticlockwise. Needs a left-capable or LR launcher, and disrupts a right-spin opponent on contact.", icon: "rotate-ccw", group: "spin", category: "platform", tone: "secondary", aliases: ["Left", "Anticlockwise", "Left-spin"] },
184
+ { slug: "dual-spin", label: "Dual Spin", description: "Can be launched in either direction — the HMS standard, and rare elsewhere.", icon: "repeat", group: "spin", category: "platform", aliases: ["Dual", "Both directions", "Dual-spin"] },
185
+ ];
186
+ // ─────────────────────────────────────────────────────────────────────────────
187
+ // commercial, sourcing
188
+ // ─────────────────────────────────────────────────────────────────────────────
189
+ const COMMERCIAL = [
190
+ { slug: "clearance", label: "Clearance", description: "Discounted to clear. Often paired with a reproduction or damaged disclosure.", icon: "tag", group: "commercial", category: "platform", tone: "secondary", aliases: ["Sale", "Clearance sale"] },
191
+ { slug: "on-sale", label: "On Sale", description: "Currently below the seller's usual price.", icon: "percent", group: "commercial", category: "platform", tone: "secondary", aliases: ["Discounted", "Reduced"] },
192
+ { slug: "latest-release", label: "Latest Release", description: "From the most recent wave or set.", icon: "zap", group: "commercial", category: "platform", tone: "primary", aliases: ["New release", "Newest"] },
193
+ { slug: "best-seller", label: "Best Seller", description: "Among the most-sold listings in its category here.", icon: "trending-up", group: "commercial", category: "platform", tone: "primary", aliases: ["Popular", "Top seller"] },
194
+ { slug: "lot", label: "Lot", description: "Several items sold together as one listing.", icon: "layers", group: "commercial", category: "platform", aliases: ["Bundle lot", "Job lot", "Multi-pack"] },
195
+ ];
196
+ const SOURCING = [
197
+ { slug: "japan-import", label: "Japan Import", description: "A Japanese-market release, imported. Packaging and part names are the Takara Tomy originals.", icon: "plane", group: "sourcing", category: "shipping", tone: "secondary", aliases: ["JP import", "Japanese release", "TT release"] },
198
+ { slug: "imported", label: "Imported", description: "Imported from outside India. Customs duty was paid on entry.", icon: "globe", group: "sourcing", category: "shipping", aliases: ["Import", "Overseas"] },
199
+ { slug: "domestic", label: "Indian Release", description: "Sold through the Indian market — no import duty in the price.", icon: "home", group: "sourcing", category: "shipping", aliases: ["India release", "Local", "Funskool"] },
162
200
  ];
201
+ // ─────────────────────────────────────────────────────────────────────────────
202
+ // Trading cards — language and format
203
+ //
204
+ // 🛑 Language is a FACET and not prose, because a Japanese print and an
205
+ // English print of one card are different products with different print runs,
206
+ // different pull rates and often very different prices. Measured at tcgindia:
207
+ // japanese 159, english 14, korean 12, chinese 2.
208
+ //
209
+ // 🛑 Format is a facet because the bands differ ~20× — a single is ₹350–500
210
+ // and a booster box ₹7,000–14,500 — and shoppers never cross-shop them.
211
+ // ─────────────────────────────────────────────────────────────────────────────
212
+ const TCG_LANG = [
213
+ { slug: "lang-japanese", label: "Japanese", description: "A Japanese-language print. Smaller print runs and generally better-regarded card stock.", icon: "languages", group: "tcg-lang", category: "platform", tone: "secondary", aliases: ["JP", "Japanese print"] },
214
+ { slug: "lang-english", label: "English", description: "An English-language print — the largest market and the most liquid resale.", icon: "languages", group: "tcg-lang", category: "platform", aliases: ["EN", "English print"] },
215
+ { slug: "lang-korean", label: "Korean", description: "A Korean-language print.", icon: "languages", group: "tcg-lang", category: "platform", aliases: ["KR"] },
216
+ { slug: "lang-chinese", label: "Chinese", description: "A Simplified or Traditional Chinese print.", icon: "languages", group: "tcg-lang", category: "platform", aliases: ["CN", "Simplified Chinese"] },
217
+ ];
218
+ const TCG_FORMAT = [
219
+ { slug: "single-card", label: "Single Card", description: "One individual card, graded on condition and rarity.", icon: "credit-card", group: "tcg-format", category: "platform", aliases: ["Single", "Singles"] },
220
+ { slug: "booster-pack", label: "Booster Pack", description: "One sealed pack. Contents are random by design.", icon: "package", group: "tcg-format", category: "platform", aliases: ["Pack", "Booster"] },
221
+ { slug: "booster-box", label: "Booster Box", description: "A sealed box of packs — the best per-pack price of any sealed format.", icon: "boxes", group: "tcg-format", category: "platform", tone: "primary", aliases: ["Box", "Sealed box", "Display box"] },
222
+ { slug: "etb", label: "Elite Trainer Box", description: "Fewer packs than a booster box, plus sleeves, dice and a deck box. Better value if you want the accessories.", icon: "briefcase", group: "tcg-format", category: "platform", aliases: ["ETB", "Elite Trainer"] },
223
+ { slug: "blister", label: "Blister Pack", description: "A small carded pack, usually with a promo card.", icon: "package-2", group: "tcg-format", category: "platform", aliases: ["Blister", "Three-pack blister"] },
224
+ { slug: "card-bundle", label: "Bundle", description: "A small sealed pack count at the lowest entry price of the sealed formats.", icon: "layers", group: "tcg-format", category: "platform", aliases: ["Pack bundle", "Booster bundle"] },
225
+ { slug: "collection-box", label: "Collection Box", description: "A themed sealed box with promo cards and often a figure or a pin.", icon: "archive", group: "tcg-format", category: "platform", aliases: ["Collection", "Premium collection", "Box set"] },
226
+ { slug: "premium-set", label: "Premium Set", description: "A high-tier sealed set, usually with an exclusive card treatment.", icon: "gem", group: "tcg-format", category: "platform", tone: "secondary", aliases: ["Premium", "UPC", "Ultra premium"] },
227
+ { slug: "starter-deck", label: "Starter Deck", description: "A ready-to-play preconstructed deck.", icon: "list-ordered", group: "tcg-format", category: "platform", aliases: ["Deck", "Structure deck", "Preconstructed"] },
228
+ ];
229
+ // ─────────────────────────────────────────────────────────────────────────────
230
+ // grade — pairs with the `sublisting` + `itemCode` axis
231
+ //
232
+ // A graded slab is not a condition claim, it is a third party's certification,
233
+ // which is exactly why a PSA 10 of a ₹500 raw card can be worth twenty times
234
+ // that. hobbykart's 15 slabs run ₹4,600–11,000 against ₹350–500 raw.
235
+ // ─────────────────────────────────────────────────────────────────────────────
236
+ const GRADE = [
237
+ { slug: "grade-psa-10", label: "PSA 10", description: "Graded Gem Mint by Professional Sports Authenticator — their top grade.", icon: "award", group: "grade", category: "condition", tone: "primary", aliases: ["PSA10", "Gem Mint", "PSA Gem Mint 10"] },
238
+ { slug: "grade-psa-9", label: "PSA 9", description: "Graded Mint by PSA — one step below Gem Mint, at a materially lower price.", icon: "award", group: "grade", category: "condition", aliases: ["PSA9", "PSA Mint 9"] },
239
+ { slug: "grade-bgs", label: "BGS Graded", description: "Graded by Beckett Grading Services, with four subgrades on the label.", icon: "award", group: "grade", category: "condition", aliases: ["Beckett", "BGS", "Black label"] },
240
+ { slug: "grade-cgc", label: "CGC Graded", description: "Graded by Certified Guaranty Company.", icon: "award", group: "grade", category: "condition", aliases: ["CGC"] },
241
+ { slug: "grade-raw", label: "Raw", description: "Ungraded and unslabbed. Condition is the seller's own assessment.", icon: "file", group: "grade", category: "condition", aliases: ["Ungraded", "Unslabbed", "Not graded"] },
242
+ ];
243
+ // ─────────────────────────────────────────────────────────────────────────────
244
+ // chase — Hot Wheels variants
245
+ //
246
+ // 🛑 `indian-card` is here because it is a genuinely LOCAL signal no global
247
+ // taxonomy would surface: toycollectorsindia tags 40 products
248
+ // `Hotwheelsindiancard` / `hotwheelsindian` / `Hotwheels indian`, because the
249
+ // Indian-market blister card is printed differently from the US one and
250
+ // collectors here price the two differently.
251
+ // ─────────────────────────────────────────────────────────────────────────────
252
+ const CHASE = [
253
+ { slug: "treasure-hunt", label: "Treasure Hunt", description: "A chase casting within a mainline wave, marked with the flame-circle logo.", icon: "search", group: "chase", category: "platform", tone: "secondary", aliases: ["TH", "Treasure Hunt", "Regular TH"] },
254
+ { slug: "super-treasure-hunt", label: "Super Treasure Hunt", description: "The premium chase: Spectraflame paint, Real Riders rubber tyres and the TH logo. Usually one per case.", icon: "flame", group: "chase", category: "platform", tone: "primary", aliases: ["STH", "Super TH", "Super Treasure"] },
255
+ { slug: "real-riders", label: "Real Riders", description: "Rubber tyres rather than plastic — a Premium and Super Treasure Hunt marker.", icon: "circle-dot", group: "chase", category: "platform", aliases: ["Rubber tyres", "Rubber tires", "RR"] },
256
+ { slug: "indian-card", label: "Indian Card", description: "On the Indian-market blister card, which is printed differently from the US release and priced differently here.", icon: "map-pin", group: "chase", category: "platform", tone: "secondary", aliases: ["India card", "Indian blister", "IND card"] },
257
+ ];
258
+ // ─────────────────────────────────────────────────────────────────────────────
259
+ // figure — scale, finish, packaging
260
+ // ─────────────────────────────────────────────────────────────────────────────
261
+ const FIGURE = [
262
+ { slug: "scale-1-12", label: "1/12 Scale", description: "Around 15 cm — the articulated-figure standard.", icon: "ruler", group: "figure", category: "platform", aliases: ["1:12", "6 inch", "15cm"] },
263
+ { slug: "scale-1-7", label: "1/7 Scale", description: "Around 25 cm — the most common scale-statue size.", icon: "ruler", group: "figure", category: "platform", aliases: ["1:7", "25cm"] },
264
+ { slug: "scale-1-6", label: "1/6 Scale", description: "Around 30 cm — large display statues and the premium tier.", icon: "ruler", group: "figure", category: "platform", tone: "secondary", aliases: ["1:6", "12 inch", "30cm"] },
265
+ { slug: "prize-figure", label: "Prize Figure", description: "Produced for Japanese arcade prize machines rather than retail. Usually unarticulated, and genuinely cheaper.", icon: "joystick", group: "figure", category: "platform", aliases: ["Arcade prize", "Banpresto prize", "Crane figure"] },
266
+ { slug: "articulated", label: "Articulated", description: "Poseable joints rather than a fixed sculpt.", icon: "move", group: "figure", category: "platform", aliases: ["Poseable", "Action figure", "Jointed"] },
267
+ { slug: "statue", label: "Statue", description: "A fixed-pose sculpt with no articulation.", icon: "image", group: "figure", category: "platform", aliases: ["Fixed pose", "Scale statue"] },
268
+ { slug: "pvc", label: "PVC", description: "Moulded PVC — the standard material for anime scale figures.", icon: "droplet", group: "figure", category: "platform", aliases: ["Polyvinyl", "PVC figure"] },
269
+ { slug: "vintage-figure", label: "Vintage", description: "From a production run that has long ended — pre-2005 for most lines.", icon: "clock", group: "figure", category: "platform", tone: "secondary", aliases: ["Retro", "Old stock", "Classic"] },
270
+ ];
271
+ // ─────────────────────────────────────────────────────────────────────────────
272
+ // tournament — legality, as DATA
273
+ //
274
+ // The Beyblade Wiki's infoboxes carry Burst{Standard,GT,Limited,Classic}
275
+ // legality flags per release, so this is a fact to record rather than a claim
276
+ // to assert per listing.
277
+ // ─────────────────────────────────────────────────────────────────────────────
278
+ const TOURNAMENT = [
279
+ { slug: "wbo-legal", label: "WBO Legal", description: "Permitted in World Beyblade Organization standard formats — the rules most Indian events run.", icon: "check-circle", group: "tournament", category: "platform", tone: "primary", aliases: ["WBO", "Tournament legal", "Standard format"] },
280
+ { slug: "limited-format", label: "Limited Format", description: "Restricted or banned in limited formats, usually for being too strong.", icon: "ban", group: "tournament", category: "platform", aliases: ["Limited", "Restricted", "Banned"] },
281
+ ];
282
+ // ─────────────────────────────────────────────────────────────────────────────
283
+ // character — the clickable owner linkage, GENERATED from the corpus
284
+ //
285
+ // 95 bladers across 183 of the 216 canonical beys, from a field that is 100%
286
+ // populated but written as prose: "Tyson Granger (Tyson's 9th and final bey)".
287
+ // The normalisation is in scripts/parse-bey-corpus.mjs and the rows in
288
+ // _helpers/character-features.ts.
289
+ //
290
+ // 🛑 Generated rather than hand-listed, and these 95 are the reason the
291
+ // hand-authored blocks above stop at 76: a name typed by hand is a name that
292
+ // can be typed two ways, and this group is where that would bite hardest —
293
+ // `Tyson Granger` and `Tyson Granger (Tyson's 4th bey` were BOTH produced by
294
+ // an earlier normaliser, and a facet offering both is worse than no facet.
295
+ //
296
+ // Each row is also what a model leaf's `productDefaults.defaultFeatures`
297
+ // points at, so a seller who files a Dragoon G gets the blader without
298
+ // choosing it. That is what turns an owner from prose into a facet.
299
+ // ─────────────────────────────────────────────────────────────────────────────
300
+ const CHARACTERS = CHARACTER_SEEDS.map((c) => ({
301
+ slug: c.slug,
302
+ label: c.name,
303
+ description: c.beyCount > 1
304
+ ? `${c.name}'s Beyblades — ${c.beyCount} models in the catalogue, across every generation they appear in.`
305
+ : `${c.name}'s Beyblade.`,
306
+ icon: "user",
307
+ group: "character",
308
+ category: "platform",
309
+ /*
310
+ * The FIRST NAME as an alias, because that is what people actually type —
311
+ * "tyson", not "tyson granger" — and `buildSearchTxt` indexes edge n-grams
312
+ * per WORD, so the full label already matches a "tyson" prefix. The alias
313
+ * earns its place on the handful of bladers known by a single name in the
314
+ * dub ("Tala", "Ozuma", "Lee"), where there is nothing to abbreviate and the
315
+ * alias is skipped.
316
+ */
317
+ aliases: c.name.includes(" ") ? [c.name.split(" ")[0]] : undefined,
318
+ }));
319
+ const ALL_FEATURE_INPUTS = [
320
+ ...PLATFORM,
321
+ ...CONDITION,
322
+ ...AUTHENTICITY,
323
+ ...RELEASE,
324
+ ...BATTLE_TYPE,
325
+ ...SPIN,
326
+ ...COMMERCIAL,
327
+ ...SOURCING,
328
+ ...TCG_LANG,
329
+ ...TCG_FORMAT,
330
+ ...GRADE,
331
+ ...CHASE,
332
+ ...FIGURE,
333
+ ...TOURNAMENT,
334
+ ...CHARACTERS,
335
+ ];
336
+ /*
337
+ * 🛑 A duplicate slug is FATAL. The id derives from it, so two rows at one id
338
+ * write the first and SILENTLY overwrite it — and `appkit-seed status` cannot
339
+ * see it, because it counts ids and the id exists either way. With 68 rows
340
+ * authored across fourteen blocks, a collision is a realistic mistake rather
341
+ * than a theoretical one; `pre-owned` vs `condition-used` was exactly that
342
+ * near-miss, resolved as an alias.
343
+ */
344
+ {
345
+ const seen = new Set();
346
+ for (const f of ALL_FEATURE_INPUTS) {
347
+ if (seen.has(f.slug))
348
+ throw new Error(`duplicate feature slug: feature-${f.slug}`);
349
+ seen.add(f.slug);
350
+ }
351
+ }
352
+ export const productFeaturesSeedData = ALL_FEATURE_INPUTS.map((f, i) => featureRow(f, (i + 1) * 10));
353
+ /** Asserted downstream so a silently dropped block is loud. */
354
+ /*
355
+ * 🛑 Asserted at MODULE LOAD, because the character block is GENERATED and a
356
+ * generated import that silently empties is the failure this cannot otherwise
357
+ * see: 95 facet rows would vanish, every model leaf's `defaultFeatures` would
358
+ * point at ids that do not exist, and the only symptom would be a "Blader &
359
+ * character" section that renders nothing — which reads as "no bladers in
360
+ * this category" rather than as a broken build.
361
+ */
362
+ if (CHARACTERS.length !== CHARACTER_SEED_COUNT) {
363
+ throw new Error(`product features: ${CHARACTERS.length} character rows built, expected ` +
364
+ `${CHARACTER_SEED_COUNT}. Re-run node scripts/gen-category-models.mjs.`);
365
+ }
366
+ export const PRODUCT_FEATURE_COUNT = ALL_FEATURE_INPUTS.length;
@@ -27,8 +27,20 @@ const _rawProductsArtSeedData = [
27
27
  slug: "art-dranzer-phoenix-poster",
28
28
  title: "Dranzer Phoenix Rising — Fan Art Poster",
29
29
  description: "A vibrant fan-art poster of Dranzer erupting in phoenix flame, hand-illustrated and giclée-printed on matte photo paper.",
30
- categorySlugs: ["category-stadiums-standard", "category-gear-stadiums", "category-battle-gear", "category-spinning-tops"],
31
- categoryNames: ["Standard Stadiums", "Stadiums", "Battle Gear", "Spinning Tops"],
30
+ /*
31
+ * 🛑 Re-filed in B5, and the old chain was wrong twice over: it pointed at
32
+ * `category-stadiums-standard` -> `category-gear-stadiums` ->
33
+ * `category-battle-gear`, all three of which were renamed when stadiums
34
+ * moved to per-generation nodes — AND a fan-art poster of Dranzer was
35
+ * never a stadium to begin with.
36
+ *
37
+ * The named-model leaf is where it belongs, and that is the whole point of
38
+ * having them: a Dranzer poster, a Dranzer sticker sheet and the Dranzer
39
+ * bey itself are siblings under one leaf, so "related items" is literally
40
+ * one `array-contains` rather than a heuristic.
41
+ */
42
+ categorySlugs: ["category-dranzer-f", "category-original-spin-gear", "category-original-tops", "category-beyblade-original", "category-spinning-tops"],
43
+ categoryNames: ["Dranzer F", "Spin Gear System", "Original Tops", "Beyblade Original", "Spinning Tops"],
32
44
  brandSlug: "brand-beyblade",
33
45
  brand: "Beyblade",
34
46
  price: 599,
@@ -111,8 +123,8 @@ const _rawProductsArtSeedData = [
111
123
  slug: "art-beyblade-x-lineup-canvas",
112
124
  title: "Beyblade X Full Lineup — Canvas Print",
113
125
  description: "A gallery-style canvas print featuring the full current Beyblade X roster in battle formation. Stretched and ready to hang.",
114
- categorySlugs: ["category-x-starters", "category-x-tops", "category-beyblade-x", "category-spinning-tops"],
115
- categoryNames: ["Starter Sets", "Beyblade X Tops", "Beyblade X", "Spinning Tops"],
126
+ categorySlugs: ["category-x-basic", "category-x-tops", "category-beyblade-x", "category-spinning-tops"],
127
+ categoryNames: ["Basic Line", "Beyblade X Tops", "Beyblade X", "Spinning Tops"],
116
128
  brandSlug: "brand-takara-tomy",
117
129
  brand: "Takara-Tomy",
118
130
  price: 2499,
@@ -138,8 +138,8 @@ const _rawProductsAuctionsSeedData = [
138
138
  slug: "auction-beyblade-x-shark-edge",
139
139
  title: "Beyblade X BX-04 Shark Edge (Buy It Now Available)",
140
140
  description: "Shark Edge 3-60GF straight from a fresh case pull. Grab it at the Buy It Now price before the clock runs, or start the bidding.",
141
- categorySlugs: ["category-x-boosters", "category-x-tops", "category-beyblade-x", "category-spinning-tops"],
142
- categoryNames: ["Boosters", "Beyblade X Tops", "Beyblade X", "Spinning Tops"],
141
+ categorySlugs: ["category-x-basic", "category-x-tops", "category-beyblade-x", "category-spinning-tops"],
142
+ categoryNames: ["Basic Line", "Beyblade X Tops", "Beyblade X", "Spinning Tops"],
143
143
  brandSlug: "brand-takara-tomy",
144
144
  brand: "Takara-Tomy",
145
145
  startingBid: 799,
@@ -252,8 +252,8 @@ const _rawProductsAuctionsSeedData = [
252
252
  slug: "auction-beyblade-metal-diablo-nemesis",
253
253
  title: "Metal Fight Beyblade BB-122 Diablo Nemesis (Ended — Sold)",
254
254
  description: "Diablo Nemesis XF, the final boss top of Metal Fight Beyblade Zero-G. Auction closed with a winning bidder.",
255
- categorySlugs: ["category-metal-fury", "category-metal-tops", "category-beyblade-metal", "category-spinning-tops"],
256
- categoryNames: ["Metal Fury", "Metal Fight Tops", "Beyblade Metal Fight", "Spinning Tops"],
255
+ categorySlugs: ["category-metal-fury-4d", "category-metal-tops", "category-beyblade-metal", "category-spinning-tops"],
256
+ categoryNames: ["Metal Fury (4D)", "Metal Fight Tops", "Beyblade Metal Fight", "Spinning Tops"],
257
257
  brandSlug: "brand-takara-tomy",
258
258
  brand: "Takara-Tomy",
259
259
  startingBid: 3499,
@@ -367,8 +367,8 @@ const _rawProductsAuctionsSeedData = [
367
367
  slug: "auction-beyblade-x-wizard-fafnir",
368
368
  title: "Beyblade X BX-06 Wizard Fafnir (Long-Running Auction)",
369
369
  description: "Wizard Fafnir 4-60F, one week left on the clock — a rare Beyblade X chase top with active bidding.",
370
- categorySlugs: ["category-x-starters", "category-x-tops", "category-beyblade-x", "category-spinning-tops"],
371
- categoryNames: ["Starter Sets", "Beyblade X Tops", "Beyblade X", "Spinning Tops"],
370
+ categorySlugs: ["category-x-basic", "category-x-tops", "category-beyblade-x", "category-spinning-tops"],
371
+ categoryNames: ["Basic Line", "Beyblade X Tops", "Beyblade X", "Spinning Tops"],
372
372
  brandSlug: "brand-takara-tomy",
373
373
  brand: "Takara-Tomy",
374
374
  startingBid: 999,