@mohasinac/appkit 4.46.0 → 4.48.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 (175) hide show
  1. package/dist/.build-stamp.json +4 -4
  2. package/dist/_internal/server/features/checkout/expand-order-items.js +3 -0
  3. package/dist/_internal/server/features/messages/actions.d.ts +68 -0
  4. package/dist/_internal/server/features/messages/actions.js +249 -0
  5. package/dist/_internal/server/features/parts/buildable.d.ts +47 -0
  6. package/dist/_internal/server/features/parts/buildable.js +159 -0
  7. package/dist/_internal/server/features/products/list-public.d.ts +17 -0
  8. package/dist/_internal/server/features/products/list-public.js +92 -5
  9. package/dist/_internal/server/features/products/og.d.ts +7 -0
  10. package/dist/_internal/server/features/products/og.js +38 -0
  11. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  12. package/dist/_internal/server/features/site-settings/adapters.js +6 -0
  13. package/dist/_internal/server/jobs/core/cleanupRtdbEvents.js +28 -0
  14. package/dist/_internal/server/jobs/core/notificationPrune.js +87 -3
  15. package/dist/_internal/server/jobs/core/priceIndexRollup.js +36 -1
  16. package/dist/_internal/shared/actions/action-registry.js +8 -0
  17. package/dist/_internal/shared/features/notifications/action-url.js +8 -0
  18. package/dist/_internal/shared/features/notifications/email-eligibility.js +3 -0
  19. package/dist/_internal/shared/features/site-settings/types.d.ts +4 -0
  20. package/dist/client.d.ts +10 -0
  21. package/dist/client.js +8 -0
  22. package/dist/constants/api-endpoints.d.ts +48 -0
  23. package/dist/constants/api-endpoints.js +30 -0
  24. package/dist/constants/notification-types.d.ts +1 -1
  25. package/dist/features/about/components/GstExplainerView.d.ts +28 -0
  26. package/dist/features/about/components/GstExplainerView.js +59 -0
  27. package/dist/features/about/components/GuidesHubView.d.ts +28 -0
  28. package/dist/features/about/components/GuidesHubView.js +58 -0
  29. package/dist/features/about/components/PublicProfileView.js +5 -0
  30. package/dist/features/about/components/SpotAFakeView.d.ts +29 -0
  31. package/dist/features/about/components/SpotAFakeView.js +49 -0
  32. package/dist/features/admin/actions/notification-actions.js +4 -0
  33. package/dist/features/admin/schemas/firestore.d.ts +13 -3
  34. package/dist/features/admin/schemas/firestore.js +8 -0
  35. package/dist/features/blog/repository/blog.repository.js +21 -7
  36. package/dist/features/categories/api/route.js +41 -1
  37. package/dist/features/categories/components/BreadcrumbTrail.d.ts +58 -13
  38. package/dist/features/categories/components/BreadcrumbTrail.js +63 -26
  39. package/dist/features/categories/components/CategoryDetailPageView.js +92 -38
  40. package/dist/features/categories/components/ContextualTrail.d.ts +62 -0
  41. package/dist/features/categories/components/ContextualTrail.js +81 -0
  42. package/dist/features/categories/components/LastCategoryBeacon.d.ts +18 -0
  43. package/dist/features/categories/components/LastCategoryBeacon.js +33 -0
  44. package/dist/features/categories/repository/categories.repository.d.ts +21 -0
  45. package/dist/features/categories/repository/categories.repository.js +36 -3
  46. package/dist/features/categories/repository/price-index.repository.d.ts +25 -0
  47. package/dist/features/email/notification-templates.js +4 -0
  48. package/dist/features/events/repository/events.repository.js +10 -3
  49. package/dist/features/faq/constants/read-caps.d.ts +29 -0
  50. package/dist/features/faq/constants/read-caps.js +29 -0
  51. package/dist/features/faq/repository/faqs.repository.d.ts +0 -2
  52. package/dist/features/faq/repository/faqs.repository.js +19 -16
  53. package/dist/features/homepage/components/AdSlot.js +10 -4
  54. package/dist/features/homepage/hooks/useActiveAd.d.ts +18 -1
  55. package/dist/features/homepage/hooks/useActiveAd.js +42 -2
  56. package/dist/features/homepage/lib/section-renderer.js +12 -2
  57. package/dist/features/layout/AppLayoutShell.d.ts +4 -1
  58. package/dist/features/layout/AppLayoutShell.js +2 -2
  59. package/dist/features/layout/TitleBarLayout.d.ts +4 -1
  60. package/dist/features/layout/TitleBarLayout.js +8 -4
  61. package/dist/features/media/AvatarUpload.js +8 -1
  62. package/dist/features/media/MediaAvatar.js +1 -1
  63. package/dist/features/media/MediaImage.js +3 -1
  64. package/dist/features/media/MediaLightbox.js +4 -1
  65. package/dist/features/media/MediaPickerModal.js +6 -1
  66. package/dist/features/media/MediaVideo.js +3 -1
  67. package/dist/features/messages/components/ConversationThreadView.d.ts +3 -0
  68. package/dist/features/messages/components/ConversationThreadView.js +75 -0
  69. package/dist/features/messages/components/ConversationsInboxView.d.ts +1 -0
  70. package/dist/features/messages/components/ConversationsInboxView.js +39 -0
  71. package/dist/features/messages/components/MessageSellerButton.d.ts +17 -0
  72. package/dist/features/messages/components/MessageSellerButton.js +33 -0
  73. package/dist/features/messages/hooks/useConversationChannel.d.ts +24 -0
  74. package/dist/features/messages/hooks/useConversationChannel.js +89 -0
  75. package/dist/features/messages/hooks/useMessages.d.ts +347 -0
  76. package/dist/features/messages/hooks/useMessages.js +113 -0
  77. package/dist/features/messages/repository/conversations.repository.d.ts +44 -0
  78. package/dist/features/messages/repository/conversations.repository.js +173 -0
  79. package/dist/features/messages/repository/messages.repository.d.ts +42 -0
  80. package/dist/features/messages/repository/messages.repository.js +142 -0
  81. package/dist/features/messages/schemas/firestore.d.ts +99 -0
  82. package/dist/features/messages/schemas/firestore.js +46 -0
  83. package/dist/features/orders/repository/orders.repository.js +21 -7
  84. package/dist/features/orders/schemas/firestore.d.ts +11 -0
  85. package/dist/features/products/components/CityCollectiblesView.d.ts +31 -0
  86. package/dist/features/products/components/CityCollectiblesView.js +82 -0
  87. package/dist/features/products/components/MarketPriceAnchor.d.ts +45 -0
  88. package/dist/features/products/components/MarketPriceAnchor.js +62 -0
  89. package/dist/features/products/components/ProductDetailPageView.js +71 -5
  90. package/dist/features/products/constants/meetup-cities.d.ts +47 -0
  91. package/dist/features/products/constants/meetup-cities.js +49 -0
  92. package/dist/features/products/repository/category-assignment.d.ts +82 -11
  93. package/dist/features/products/repository/category-assignment.js +93 -12
  94. package/dist/features/products/repository/products.repository.d.ts +12 -1
  95. package/dist/features/products/repository/products.repository.js +66 -34
  96. package/dist/features/products/schemas/firestore.d.ts +24 -2
  97. package/dist/features/products/schemas/firestore.js +13 -1
  98. package/dist/features/products/schemas/index.d.ts +2 -2
  99. package/dist/features/promotions/repository/coupons.repository.js +11 -4
  100. package/dist/features/reviews/repository/reviews.repository.js +11 -4
  101. package/dist/features/scams/repository/scammer.repository.js +21 -7
  102. package/dist/features/seller/components/CategoryInlineSelect.d.ts +17 -1
  103. package/dist/features/seller/components/CategoryInlineSelect.js +14 -3
  104. package/dist/features/seller/components/SellerProductShell.d.ts +27 -2
  105. package/dist/features/seller/components/SellerProductShell.js +32 -3
  106. package/dist/features/seller/repository/offer.repository.js +21 -6
  107. package/dist/features/seller/schemas/product-form.d.ts +3 -3
  108. package/dist/features/seller/schemas/product-form.js +1 -1
  109. package/dist/features/server-errors/repository/server-errors.repository.js +11 -0
  110. package/dist/features/server-errors/schemas/firestore.d.ts +17 -0
  111. package/dist/features/server-errors/schemas/firestore.js +2 -0
  112. package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +17 -1
  113. package/dist/features/store-extensions/repository/store-extensions.repositories.js +31 -0
  114. package/dist/features/store-extensions/schemas/firestore.d.ts +1 -1
  115. package/dist/features/store-extensions/schemas/firestore.js +4 -0
  116. package/dist/features/stores/repository/store.repository.js +11 -3
  117. package/dist/features/tester/seed-data/authored/community-support__messages.d.ts +2 -0
  118. package/dist/features/tester/seed-data/authored/community-support__messages.js +200 -0
  119. package/dist/features/tester/seed-data/authored/index.js +2 -0
  120. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +64 -1
  121. package/dist/index.d.ts +8 -2
  122. package/dist/index.js +1 -1
  123. package/dist/next/routing/route-map.d.ts +14 -0
  124. package/dist/next/routing/route-map.js +26 -0
  125. package/dist/providers/db-firebase/rtdb-paths.d.ts +8 -0
  126. package/dist/providers/db-firebase/rtdb-paths.js +8 -0
  127. package/dist/repositories/index.d.ts +1 -0
  128. package/dist/repositories/index.js +4 -0
  129. package/dist/schemas/registry.d.ts +3 -3
  130. package/dist/security/index.d.ts +1 -1
  131. package/dist/security/index.js +1 -1
  132. package/dist/security/pii-schemas.d.ts +17 -0
  133. package/dist/security/pii-schemas.js +15 -7
  134. package/dist/seed/_helpers/category-content-generated.d.ts +24 -0
  135. package/dist/seed/_helpers/category-content-generated.js +1853 -0
  136. package/dist/seed/_helpers/category-models.d.ts +1 -1
  137. package/dist/seed/_helpers/category-models.js +25 -1
  138. package/dist/seed/_helpers/category-names.d.ts +54 -0
  139. package/dist/seed/_helpers/category-names.js +118 -0
  140. package/dist/seed/_helpers/character-features.d.ts +1 -1
  141. package/dist/seed/_helpers/character-features.js +5 -4
  142. package/dist/seed/_helpers/final-sale-fixtures.js +1 -1
  143. package/dist/seed/categories-seed-data.js +55 -1
  144. package/dist/seed/grouped-listings-seed-data.js +2 -2
  145. package/dist/seed/products-art-seed-data.js +2 -1
  146. package/dist/seed/products-auctions-seed-data.js +42 -22
  147. package/dist/seed/products-classifieds-seed-data.js +21 -3
  148. package/dist/seed/products-digital-codes-seed-data.js +2 -1
  149. package/dist/seed/products-live-items-seed-data.js +2 -1
  150. package/dist/seed/products-preorders-seed-data.js +26 -11
  151. package/dist/seed/products-prize-draws-seed-data.js +8 -1
  152. package/dist/seed/products-standard-seed-data.js +26 -25
  153. package/dist/seed/products-stickers-seed-data.js +12 -11
  154. package/dist/server.d.ts +8 -0
  155. package/dist/server.js +15 -0
  156. package/dist/styles.css +113 -1
  157. package/dist/tailwind-utilities.css +1 -1
  158. package/dist/tokens/themes/sunset.js +17 -0
  159. package/dist/tokens/tokens.css +5 -0
  160. package/dist/ui/components/Breadcrumb.d.ts +8 -1
  161. package/dist/ui/components/Breadcrumb.js +2 -2
  162. package/dist/ui/components/Breadcrumb.style.css +37 -0
  163. package/dist/ui/components/Details.d.ts +11 -1
  164. package/dist/ui/components/Details.js +2 -2
  165. package/dist/ui/components/ImageMosaic.d.ts +43 -0
  166. package/dist/ui/components/ImageMosaic.js +46 -0
  167. package/dist/ui/components/ImageMosaic.style.css +69 -0
  168. package/dist/ui/components/SplitTile.d.ts +34 -0
  169. package/dist/ui/components/SplitTile.js +28 -0
  170. package/dist/ui/components/index.style.css +1 -0
  171. package/dist/ui/index.d.ts +4 -0
  172. package/dist/ui/index.js +4 -0
  173. package/dist/utils/search-txt-query.d.ts +77 -0
  174. package/dist/utils/search-txt-query.js +50 -28
  175. package/package.json +1 -1
@@ -2,4 +2,4 @@ import type { CategoryTreeNode } from "./category-tree";
2
2
  /** Named-model leaves, keyed by the tier-3 line they belong to. */
3
3
  export declare const MODEL_LEAVES: Record<string, CategoryTreeNode[]>;
4
4
  /** Total named-model leaves — asserted by the seed so a silent drop is loud. */
5
- export declare const MODEL_LEAF_COUNT = 216;
5
+ export declare const MODEL_LEAF_COUNT = 220;
@@ -142,6 +142,12 @@ export const MODEL_LEAVES = {
142
142
  description: "Luinor L2 — an Attack-type, left-spin Burst Classic release, product code E1056. Release note: Lost Lúinor L2 Nine Spiral. Complete tops, loose parts, sticker sheets and pre-owned examples of this model are listed here.",
143
143
  extra: { aliases: ["E1056", "Lost Lúinor L2", "Lost Longinus"], productDefaults: { defaultFeatures: ["feature-blader-lui-shirosagi", "feature-type-attack", "feature-left-spin"] } },
144
144
  },
145
+ {
146
+ id: "category-spryzen-s2-0q",
147
+ name: "Spryzen S2 0Q",
148
+ description: "Spryzen S2 0Q — a Balance-type, dual-spin Burst Classic release, product code E1048, released by Hasbro. Complete tops, loose parts, sticker sheets and pre-owned examples of this model are listed here.",
149
+ extra: { aliases: ["E1048", "Spryzen S2 0Q Layer: Spryzen S2", "Disc: 0", "Driver: Q — later evolves to Spryzen Requiem S3 0Cross"], productDefaults: { defaultFeatures: ["feature-blader-shu-kurenai", "feature-type-balance", "feature-dual-spin"] } },
150
+ },
145
151
  {
146
152
  id: "category-storm-spriggan-7glaive",
147
153
  name: "Storm Spriggan 7Glaive",
@@ -228,6 +234,12 @@ export const MODEL_LEAVES = {
228
234
  description: "Vanish Fafnir Tapered Kick-3 — a Stamina-type, left-spin Dynamite Battle release, product code B-180-class, released by Takara Tomy, ~2021. Complete tops, loose parts, sticker sheets and pre-owned examples of this model are listed here.",
229
235
  extra: { aliases: ["B-180-class"], productDefaults: { defaultFeatures: ["feature-blader-fumiya-kindo", "feature-type-stamina", "feature-left-spin"] } },
230
236
  },
237
+ {
238
+ id: "category-wizard-fafnir-ratchet-rise",
239
+ name: "Wizard Fafnir Ratchet Rise",
240
+ description: "Wizard Fafnir Ratchet Rise — a Stamina-type, left-spin Dynamite Battle release, product code B-139, released by Takara Tomy, S3-S4 transitional. Complete tops, loose parts, sticker sheets and pre-owned examples of this model are listed here.",
241
+ extra: { aliases: ["B-139"], productDefaults: { defaultFeatures: ["feature-blader-fubuki-sumiye", "feature-type-stamina", "feature-left-spin"] } },
242
+ },
231
243
  ],
232
244
  "category-burst-god": [
233
245
  {
@@ -386,6 +398,12 @@ export const MODEL_LEAVES = {
386
398
  description: "Cosmic Pegasis F:D — an Attack-type, right-spin Metal Fury (4D) release, product code BB-128, released by Takara Tomy. Complete tops, loose parts, sticker sheets and pre-owned examples of this model are listed here.",
387
399
  extra: { aliases: ["BB-128"], productDefaults: { defaultFeatures: ["feature-blader-gingka-hagane", "feature-type-attack", "feature-right-spin"] } },
388
400
  },
401
+ {
402
+ id: "category-diablo-nemesis-x-d",
403
+ name: "Diablo Nemesis X:D",
404
+ description: "Diablo Nemesis X:D — a Balance-type, right-spin Metal Fury (4D) release, product code BB-122. Complete tops, loose parts, sticker sheets and pre-owned examples of this model are listed here.",
405
+ extra: { aliases: ["BB-122"], productDefaults: { defaultFeatures: ["feature-blader-rago", "feature-type-balance", "feature-right-spin"] } },
406
+ },
389
407
  {
390
408
  id: "category-fang-leone-130w2d",
391
409
  name: "Fang Leone 130W2D",
@@ -1146,6 +1164,12 @@ export const MODEL_LEAVES = {
1146
1164
  description: "Dranzer F — a Balance-type, right-spin Spin Gear System release, product code A-27, released by Takara Tomy. Complete tops, loose parts, sticker sheets and pre-owned examples of this model are listed here.",
1147
1165
  extra: { aliases: ["A-27", "Dranzer Flame"], productDefaults: { defaultFeatures: ["feature-blader-kai-hiwatari", "feature-type-balance", "feature-right-spin"] } },
1148
1166
  },
1167
+ {
1168
+ id: "category-dranzer-s",
1169
+ name: "Dranzer S",
1170
+ description: "Dranzer S — a Balance-type, right-spin Spin Gear System release, product code A-2, released by Takara. Complete tops, loose parts, sticker sheets and pre-owned examples of this model are listed here.",
1171
+ extra: { aliases: ["A-2", "Dranzer Spiral"], productDefaults: { defaultFeatures: ["feature-blader-kai-hiwatari", "feature-type-balance", "feature-right-spin"] } },
1172
+ },
1149
1173
  {
1150
1174
  id: "category-driger-f",
1151
1175
  name: "Driger F",
@@ -1361,4 +1385,4 @@ export const MODEL_LEAVES = {
1361
1385
  ],
1362
1386
  };
1363
1387
  /** Total named-model leaves — asserted by the seed so a silent drop is loud. */
1364
- export const MODEL_LEAF_COUNT = 216;
1388
+ export const MODEL_LEAF_COUNT = 220;
@@ -0,0 +1,54 @@
1
+ import type { ProductDocument } from "../../features/products/schemas";
2
+ export declare function deriveCategoryNames(slugs: readonly string[] | undefined): string[] | undefined;
3
+ /**
4
+ * Derive the full ancestor closure from the head of an existing chain.
5
+ *
6
+ * ## 🛑 The drift this ends — measured, 6 of 1,039 live products
7
+ *
8
+ * `categorySlugs` is the ancestor closure and it was hand-written per record
9
+ * too, so a chain could SKIP A TIER and nothing noticed:
10
+ *
11
+ * product-beyblade-original-driger-v
12
+ * got driger-v > original-tops > beyblade-original > spinning-tops
13
+ * expected driger-v > original-magnacore > original-tops > …
14
+ *
15
+ * The category page matches `array-contains-any [its own id]` against this
16
+ * array (CLAUDE.md: "a category page matches on its own id alone"), so a
17
+ * missing intermediate means the product is **absent from that node's page**
18
+ * while appearing correctly on every other node in its chain. Six products were
19
+ * invisible on their own system sub-line.
20
+ *
21
+ * `countersReconcile`'s rollup invariant cannot see this: it computes the counts
22
+ * FROM these closures, so a skipped tier is simply a tier with a lower count,
23
+ * and `own + Σ(children.total) === total` still holds exactly. The only way to
24
+ * find it is to re-derive the chain and compare.
25
+ *
26
+ * ## 🛑 A SECOND ASSIGNMENT IS LEFT ALONE, NOT COLLAPSED
27
+ *
28
+ * This derives from `slugs[0]` — the head, which is what `readAssignments`
29
+ * treats as the assignment. A product filed under two roots would carry two
30
+ * concatenated closures, and deriving from the head alone would silently DELETE
31
+ * the second one, turning a drift-fix into data loss.
32
+ *
33
+ * So the rule is: overwrite only when the existing array is a SUBSET of the
34
+ * derived closure. That is precisely the drift case (ids missing from the
35
+ * middle). An id outside the derived closure means either a second assignment
36
+ * or something this helper cannot safely interpret, and both must survive to be
37
+ * reported rather than be quietly rewritten.
38
+ *
39
+ * Measured when this was written: 0 of 71 seeded products carry `categoryIds`
40
+ * and 0 carry a multi-root closure — so the subset rule fires on all six real
41
+ * cases and guards a case that does not exist yet.
42
+ */
43
+ export declare function deriveCategoryClosure(slugs: readonly string[] | undefined): string[] | undefined;
44
+ /**
45
+ * Pipeline step: replace whatever `categorySlugs` and `categoryNames` a record
46
+ * carries with the derivation. Deliberately an OVERWRITE rather than a
47
+ * fill-if-absent — a hand-written value that disagrees IS the defect, so
48
+ * preferring it would preserve every one of them.
49
+ *
50
+ * Order matters: the closure is derived first, then the names from the derived
51
+ * closure. Deriving names from the stale chain and then fixing the chain would
52
+ * leave the two mirrors disagreeing again, one tier apart.
53
+ */
54
+ export declare function withDerivedCategoryNames(p: Partial<ProductDocument>): Partial<ProductDocument>;
@@ -0,0 +1,118 @@
1
+ import { CATEGORY_FOREST } from "./category-forest.js";
2
+ let nameById = null;
3
+ /** id -> its ancestors, NEAREST FIRST (parent, grandparent, …, root). */
4
+ let ancestorsById = null;
5
+ function build() {
6
+ const n = new Map();
7
+ const a = new Map();
8
+ const walk = (nodes, trail) => {
9
+ for (const node of nodes ?? []) {
10
+ if (typeof node.id === "string") {
11
+ n.set(node.id, node.name);
12
+ a.set(node.id, [...trail]);
13
+ walk(node.children, [node.id, ...trail]);
14
+ }
15
+ else {
16
+ // A node with no id contributes no ancestry, but its children still do.
17
+ walk(node.children, trail);
18
+ }
19
+ }
20
+ };
21
+ walk(CATEGORY_FOREST, []);
22
+ nameById = n;
23
+ ancestorsById = a;
24
+ }
25
+ function names() {
26
+ if (!nameById)
27
+ build();
28
+ return nameById;
29
+ }
30
+ function ancestors() {
31
+ if (!ancestorsById)
32
+ build();
33
+ return ancestorsById;
34
+ }
35
+ export function deriveCategoryNames(slugs) {
36
+ if (!Array.isArray(slugs) || slugs.length === 0)
37
+ return undefined;
38
+ const byId = names();
39
+ return slugs.map((id) => byId.get(id) ?? id);
40
+ }
41
+ /**
42
+ * Derive the full ancestor closure from the head of an existing chain.
43
+ *
44
+ * ## 🛑 The drift this ends — measured, 6 of 1,039 live products
45
+ *
46
+ * `categorySlugs` is the ancestor closure and it was hand-written per record
47
+ * too, so a chain could SKIP A TIER and nothing noticed:
48
+ *
49
+ * product-beyblade-original-driger-v
50
+ * got driger-v > original-tops > beyblade-original > spinning-tops
51
+ * expected driger-v > original-magnacore > original-tops > …
52
+ *
53
+ * The category page matches `array-contains-any [its own id]` against this
54
+ * array (CLAUDE.md: "a category page matches on its own id alone"), so a
55
+ * missing intermediate means the product is **absent from that node's page**
56
+ * while appearing correctly on every other node in its chain. Six products were
57
+ * invisible on their own system sub-line.
58
+ *
59
+ * `countersReconcile`'s rollup invariant cannot see this: it computes the counts
60
+ * FROM these closures, so a skipped tier is simply a tier with a lower count,
61
+ * and `own + Σ(children.total) === total` still holds exactly. The only way to
62
+ * find it is to re-derive the chain and compare.
63
+ *
64
+ * ## 🛑 A SECOND ASSIGNMENT IS LEFT ALONE, NOT COLLAPSED
65
+ *
66
+ * This derives from `slugs[0]` — the head, which is what `readAssignments`
67
+ * treats as the assignment. A product filed under two roots would carry two
68
+ * concatenated closures, and deriving from the head alone would silently DELETE
69
+ * the second one, turning a drift-fix into data loss.
70
+ *
71
+ * So the rule is: overwrite only when the existing array is a SUBSET of the
72
+ * derived closure. That is precisely the drift case (ids missing from the
73
+ * middle). An id outside the derived closure means either a second assignment
74
+ * or something this helper cannot safely interpret, and both must survive to be
75
+ * reported rather than be quietly rewritten.
76
+ *
77
+ * Measured when this was written: 0 of 71 seeded products carry `categoryIds`
78
+ * and 0 carry a multi-root closure — so the subset rule fires on all six real
79
+ * cases and guards a case that does not exist yet.
80
+ */
81
+ export function deriveCategoryClosure(slugs) {
82
+ if (!Array.isArray(slugs) || slugs.length === 0)
83
+ return undefined;
84
+ const head = slugs[0];
85
+ const chain = ancestors().get(head);
86
+ // An unknown head keeps its hand-written chain: inventing one from a node the
87
+ // forest does not have would be worse than leaving a visibly wrong value for
88
+ // the dangling-id sweep to find.
89
+ if (!chain)
90
+ return undefined;
91
+ const derived = [head, ...chain];
92
+ const outside = slugs.filter((s) => !derived.includes(s));
93
+ if (outside.length > 0)
94
+ return undefined;
95
+ return derived;
96
+ }
97
+ /**
98
+ * Pipeline step: replace whatever `categorySlugs` and `categoryNames` a record
99
+ * carries with the derivation. Deliberately an OVERWRITE rather than a
100
+ * fill-if-absent — a hand-written value that disagrees IS the defect, so
101
+ * preferring it would preserve every one of them.
102
+ *
103
+ * Order matters: the closure is derived first, then the names from the derived
104
+ * closure. Deriving names from the stale chain and then fixing the chain would
105
+ * leave the two mirrors disagreeing again, one tier apart.
106
+ */
107
+ export function withDerivedCategoryNames(p) {
108
+ const closure = deriveCategoryClosure(p.categorySlugs);
109
+ const slugs = closure ?? p.categorySlugs;
110
+ const names = deriveCategoryNames(slugs);
111
+ if (!closure && !names)
112
+ return p;
113
+ return {
114
+ ...p,
115
+ ...(closure ? { categorySlugs: closure } : {}),
116
+ ...(names ? { categoryNames: names } : {}),
117
+ };
118
+ }
@@ -7,4 +7,4 @@ export interface CharacterSeed {
7
7
  beyCount: number;
8
8
  }
9
9
  export declare const CHARACTER_SEEDS: CharacterSeed[];
10
- export declare const CHARACTER_SEED_COUNT = 93;
10
+ export declare const CHARACTER_SEED_COUNT = 94;
@@ -22,11 +22,11 @@
22
22
  */
23
23
  export const CHARACTER_SEEDS = [
24
24
  { slug: "blader-tyson-granger", name: "Tyson Granger", beyCount: 9 },
25
- { slug: "blader-kai-hiwatari", name: "Kai Hiwatari", beyCount: 7 },
25
+ { slug: "blader-kai-hiwatari", name: "Kai Hiwatari", beyCount: 8 },
26
+ { slug: "blader-shu-kurenai", name: "Shu Kurenai", beyCount: 7 },
26
27
  { slug: "blader-daichi-sumeragi", name: "Daichi Sumeragi", beyCount: 6 },
27
28
  { slug: "blader-max-tate", name: "Max Tate", beyCount: 6 },
28
29
  { slug: "blader-ray-kon", name: "Ray Kon", beyCount: 6 },
29
- { slug: "blader-shu-kurenai", name: "Shu Kurenai", beyCount: 6 },
30
30
  { slug: "blader-gingka-hagane", name: "Gingka Hagane", beyCount: 5 },
31
31
  { slug: "blader-lui-shirosagi", name: "Lui Shirosagi", beyCount: 5 },
32
32
  { slug: "blader-tala", name: "Tala", beyCount: 5 },
@@ -56,6 +56,7 @@ export const CHARACTER_SEEDS = [
56
56
  { slug: "blader-michael-summers", name: "Michael Summers", beyCount: 2 },
57
57
  { slug: "blader-miguel", name: "Miguel", beyCount: 2 },
58
58
  { slug: "blader-ozuma", name: "Ozuma", beyCount: 2 },
59
+ { slug: "blader-rago", name: "Rago", beyCount: 2 },
59
60
  { slug: "blader-robin-kazami", name: "Robin Kazami", beyCount: 2 },
60
61
  { slug: "blader-tsubasa-otori", name: "Tsubasa Otori", beyCount: 2 },
61
62
  { slug: "blader-wakiya-murasaki", name: "Wakiya Murasaki", beyCount: 2 },
@@ -75,6 +76,7 @@ export const CHARACTER_SEEDS = [
75
76
  { slug: "blader-dan-sodo-and-reiki-sodo", name: "Dan Sodo and Reiki Sodo", beyCount: 1 },
76
77
  { slug: "blader-doji", name: "Doji", beyCount: 1 },
77
78
  { slug: "blader-faust", name: "Faust", beyCount: 1 },
79
+ { slug: "blader-fubuki-sumiye", name: "Fubuki Sumiye", beyCount: 1 },
78
80
  { slug: "blader-garland-siebald", name: "Garland Siebald", beyCount: 1 },
79
81
  { slug: "blader-gary", name: "Gary", beyCount: 1 },
80
82
  { slug: "blader-ginro", name: "Ginro", beyCount: 1 },
@@ -98,7 +100,6 @@ export const CHARACTER_SEEDS = [
98
100
  { slug: "blader-nine-kurosu", name: "Nine Kurosu", beyCount: 1 },
99
101
  { slug: "blader-omega-shiroboshi", name: "Omega Shiroboshi", beyCount: 1 },
100
102
  { slug: "blader-phi", name: "Phi", beyCount: 1 },
101
- { slug: "blader-rago", name: "Rago", beyCount: 1 },
102
103
  { slug: "blader-reiji-mizuchi", name: "Reiji Mizuchi", beyCount: 1 },
103
104
  { slug: "blader-reiyu-kuwabara", name: "Reiyu Kuwabara", beyCount: 1 },
104
105
  { slug: "blader-rex-jura", name: "Rex Jura", beyCount: 1 },
@@ -115,4 +116,4 @@ export const CHARACTER_SEEDS = [
115
116
  { slug: "blader-yuni-naniwa", name: "Yuni Naniwa", beyCount: 1 },
116
117
  { slug: "blader-zyro-kurogane", name: "Zyro Kurogane", beyCount: 1 },
117
118
  ];
118
- export const CHARACTER_SEED_COUNT = 93;
119
+ export const CHARACTER_SEED_COUNT = 94;
@@ -36,7 +36,7 @@ export const RETURNABLE_FIXTURE_IDS = new Set([
36
36
  // art
37
37
  "art-valkyrie-holographic-print",
38
38
  // stickers
39
- "stickers-beyblade-x-holographic-pack",
39
+ "sticker-beyblade-x-holographic-pack",
40
40
  ]);
41
41
  /**
42
42
  * Set `finalSale: false` on the opted-out fixtures and leave it ABSENT on
@@ -59,6 +59,7 @@ import { BRAND_ROW_COUNT, buildBrandRows } from "./_helpers/brand-rows.js";
59
59
  import { MODEL_LEAF_COUNT } from "./_helpers/category-models.js";
60
60
  import { PART_LEAF_COUNT } from "./_helpers/category-parts.js";
61
61
  import { MARKET_BAND_LEAF_IDS } from "./_helpers/category-content.js";
62
+ import { GENERATED_CATEGORY_CONTENT, GENERATED_CATEGORY_CONTENT_COUNT, } from "./_helpers/category-content-generated.js";
62
63
  import { productsStandardSeedData } from "./products-standard-seed-data.js";
63
64
  const NOW = new Date();
64
65
  const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
@@ -442,12 +443,42 @@ const bundleRows = [
442
443
  * name/slug/description only and silently drop the lineage search — typing
443
444
  * "burst" would stop reaching a tier-4 model under Beyblade Burst.
444
445
  */
446
+ /**
447
+ * I7 — attach the GENERATED body to any row that has one and does not already
448
+ * carry a hand-authored one.
449
+ *
450
+ * 🛑 The generator ran and its 745 bodies were imported by NOBODY. The corpus
451
+ * sat in the repo as 128 KB of dead weight: not seeded, so not in Firestore,
452
+ * so unreadable by the page that renders it. The generator being written is
453
+ * not the same as its output being wired, and the second half is invisible —
454
+ * a category page with no body renders perfectly well, it just ranks on
455
+ * nothing.
456
+ *
457
+ * 🛑 A HAND-AUTHORED body always wins. The four generation roots and several
458
+ * tier-1 nodes carry prose written against the crawl research; the generator
459
+ * composes from facts and is deliberately the weaker writer at those tiers
460
+ * (its own header says the part leaves are its strongest tier). Overwriting
461
+ * authored copy with composed copy would be a silent downgrade of the exact
462
+ * pages this is for.
463
+ *
464
+ * 🛑 And nothing is INVENTED here: a row absent from the map keeps no body at
465
+ * all, because `GENERATED_CATEGORY_CONTENT` omits a node it has nothing
466
+ * factual to say about rather than emitting an empty one.
467
+ */
468
+ function withGeneratedContent(row) {
469
+ if (typeof row.contentBody === "string" && row.contentBody.trim().length > 0)
470
+ return row;
471
+ const generated = row.id ? GENERATED_CATEGORY_CONTENT[row.id] : undefined;
472
+ return generated ? { ...row, contentBody: generated } : row;
473
+ }
445
474
  const allCategoryRows = [
446
475
  ...rawCategories.map((c) => ({ ancestors: [], ...c, createdByType: "admin" })),
447
476
  ...sublistingRows.map((s) => ({ ancestors: [], ...s, createdByType: "admin" })),
448
477
  ...brandRows.map((b) => ({ ancestors: [], ...b, createdByType: "admin" })),
449
478
  ...bundleRows.map((b) => ({ ancestors: [], ...b })),
450
- ].map(withCategorySearchTxt);
479
+ ]
480
+ .map(withGeneratedContent)
481
+ .map(withCategorySearchTxt);
451
482
  /*
452
483
  * ── Assertions, at MODULE LOAD, so a silent drop is loud ─────────────────────
453
484
  *
@@ -487,6 +518,29 @@ const allCategoryRows = [
487
518
  * Counting rows is not counting documents, and this is the one check that can
488
519
  * tell the difference BEFORE anything is written.
489
520
  */
521
+ /*
522
+ * 🛑 THE GENERATED BODIES ACTUALLY LANDED ON ROWS.
523
+ *
524
+ * I7's corpus is a `Record<id, html>` keyed on category ids, and a key that no
525
+ * longer matches a row is invisible everywhere: the map still exports 745
526
+ * entries, the seed still loads 1,173 rows, `appkit-seed status` still reports
527
+ * in sync, and every one of those category pages renders without a body. That
528
+ * is the same failure shape as the 20 ids that overwrote each other below —
529
+ * counting the SOURCE is not counting what reached a row.
530
+ *
531
+ * Tolerance rather than exact equality, for one real reason: a hand-authored
532
+ * body legitimately wins over a generated one, so the number of rows carrying
533
+ * a GENERATED body is `COUNT` minus however many nodes have been written by
534
+ * hand since. A floor of 90% catches a key-space break (which drops it to ~0)
535
+ * without failing on authored prose being added.
536
+ */
537
+ const generatedApplied = allCategoryRows.filter((c) => typeof c.id === "string" && GENERATED_CATEGORY_CONTENT[c.id] === c.contentBody).length;
538
+ if (generatedApplied < Math.floor(GENERATED_CATEGORY_CONTENT_COUNT * 0.9)) {
539
+ throw new Error(`category seed: only ${generatedApplied} of ${GENERATED_CATEGORY_CONTENT_COUNT} generated ` +
540
+ `content bodies matched a row id. The corpus is keyed on category ids, so this means ` +
541
+ `the key space has drifted — re-run scripts/gen-category-content.mjs against the ` +
542
+ `current forest. Nothing would error at load time: the pages would simply have no body.`);
543
+ }
490
544
  const allIds = allCategoryRows.map((c) => c.id).filter((x) => typeof x === "string");
491
545
  const duplicateIds = [...new Set(allIds.filter((id, i) => allIds.indexOf(id) !== i))];
492
546
  if (duplicateIds.length > 0) {
@@ -226,8 +226,8 @@ export const groupedListingsSeedData = [
226
226
  productIds: [
227
227
  "art-dranzer-phoenix-poster",
228
228
  "art-valkyrie-holographic-print",
229
- "stickers-beyblade-original-classic-sheet",
230
- "stickers-beyblade-x-holographic-pack",
229
+ "sticker-beyblade-original-classic-sheet",
230
+ "sticker-beyblade-x-holographic-pack",
231
231
  ],
232
232
  coverImage: seedPhoto("group-beyblade-fan-art-and-stickers-20260819", 1200, 900),
233
233
  groupTheme: "related",
@@ -21,6 +21,7 @@ import { PRODUCT_FIELDS } from "../constants/field-names.js";
21
21
  import { buildSearchTxt } from "../utils/search-txt.js";
22
22
  import { seedPhoto } from "./_helpers/media.js";
23
23
  import { withFinalSale } from "./_helpers/final-sale-fixtures.js";
24
+ import { withDerivedCategoryNames } from "./_helpers/category-names.js";
24
25
  const _rawProductsArtSeedData = [
25
26
  {
26
27
  id: "art-dranzer-phoenix-poster",
@@ -296,4 +297,4 @@ const _rawProductsArtSeedData = [
296
297
  * so the final-sale opt-outs are applied the same way everywhere rather than
297
298
  * being hand-written into two records that reviewers would have to find.
298
299
  */
299
- export const productsArtSeedData = _rawProductsArtSeedData.map(withFinalSale);
300
+ export const productsArtSeedData = _rawProductsArtSeedData.map(withFinalSale).map(withDerivedCategoryNames);
@@ -26,6 +26,7 @@ import { PRODUCT_FIELDS } from "../constants/field-names.js";
26
26
  import { buildSearchTxt } from "../utils/search-txt.js";
27
27
  import { seedPhoto } from "./_helpers/media.js";
28
28
  import { withFinalSale } from "./_helpers/final-sale-fixtures.js";
29
+ import { withDerivedCategoryNames } from "./_helpers/category-names.js";
29
30
  function withTokens(p) {
30
31
  return {
31
32
  tags: [],
@@ -53,7 +54,7 @@ const _rawProductsAuctionsSeedData = [
53
54
  slug: "auction-beyblade-original-dragoon-storm",
54
55
  title: "Beyblade Original — Dragoon Storm (Rare Sealed)",
55
56
  description: "Sealed, unopened Dragoon Storm from the original 1999-2003 series. A grail piece for original-generation collectors.",
56
- categorySlugs: ["category-original-plastic-gen", "category-original-tops", "category-beyblade-original", "category-spinning-tops"],
57
+ categorySlugs: ["category-dragoon-storm", "category-original-tops", "category-beyblade-original", "category-spinning-tops"],
57
58
  categoryNames: ["Plastic Generation", "Original Tops", "Beyblade Original", "Spinning Tops"],
58
59
  brandSlug: "brand-beyblade",
59
60
  brand: "Beyblade",
@@ -97,7 +98,7 @@ const _rawProductsAuctionsSeedData = [
97
98
  slug: "auction-beyblade-metal-lightning-l-drago",
98
99
  title: "Metal Fight Beyblade BB-99 Lightning L-Drago",
99
100
  description: "Lightning L-Drago 100HF/S, a highly sought-after Metal Fury era top in excellent condition.",
100
- categorySlugs: ["category-metal-masters", "category-metal-tops", "category-beyblade-metal", "category-spinning-tops"],
101
+ categorySlugs: ["category-lightning-l-drago-100hf", "category-metal-fusion", "category-metal-tops", "category-beyblade-metal", "category-spinning-tops"],
101
102
  categoryNames: ["Metal Masters", "Metal Fight Tops", "Beyblade Metal Fight", "Spinning Tops"],
102
103
  brandSlug: "brand-takara-tomy",
103
104
  brand: "Takara-Tomy",
@@ -138,7 +139,7 @@ const _rawProductsAuctionsSeedData = [
138
139
  slug: "auction-beyblade-x-shark-edge",
139
140
  title: "Beyblade X BX-04 Shark Edge (Buy It Now Available)",
140
141
  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-basic", "category-x-tops", "category-beyblade-x", "category-spinning-tops"],
142
+ categorySlugs: ["category-sharkedge-3-60lf", "category-x-tops", "category-beyblade-x", "category-spinning-tops"],
142
143
  categoryNames: ["Basic Line", "Beyblade X Tops", "Beyblade X", "Spinning Tops"],
143
144
  brandSlug: "brand-takara-tomy",
144
145
  brand: "Takara-Tomy",
@@ -179,7 +180,7 @@ const _rawProductsAuctionsSeedData = [
179
180
  slug: "auction-beyblade-burst-xcalius-passed-bin",
180
181
  title: "Beyblade Burst Xcalius X2 (bidding passed Buy It Now)",
181
182
  description: "Xcalius X2 attack type. The Buy It Now price has already been overtaken by live bidding, so only bidding remains.",
182
- categorySlugs: ["category-burst-cho-z", "category-burst-tops", "category-beyblade-burst", "category-spinning-tops"],
183
+ categorySlugs: ["category-xcalius-x2-1iron", "category-burst-classic", "category-burst-tops", "category-beyblade-burst", "category-spinning-tops"],
183
184
  categoryNames: ["Cho-Z", "Burst Tops", "Beyblade Burst", "Spinning Tops"],
184
185
  brandSlug: "brand-takara-tomy",
185
186
  brand: "Takara-Tomy",
@@ -215,7 +216,13 @@ const _rawProductsAuctionsSeedData = [
215
216
  slug: "auction-beyblade-original-seaborg",
216
217
  title: "Beyblade Original — Seaborg 2000 (Reserve Auction)",
217
218
  description: "Rare Seaborg 2000 defense-type top from the original series. Reserve price set by the seller — current bids haven't met it yet.",
218
- categorySlugs: ["category-original-hms", "category-original-tops", "category-beyblade-original", "category-spinning-tops"],
219
+ /*
220
+ * Was filed at `category-original-hms` — the Heavy Metal System line, a
221
+ * DIFFERENT generation from the one its own copy names ("from the original
222
+ * series"). Seaborg 2000 is the Spin Gear System release the canonical
223
+ * corpus carries as "Seaborg 2", which is its leaf.
224
+ */
225
+ categorySlugs: ["category-seaborg-2", "category-original-spin-gear", "category-original-tops", "category-beyblade-original", "category-spinning-tops"],
219
226
  categoryNames: ["Heavy Metal System", "Original Tops", "Beyblade Original", "Spinning Tops"],
220
227
  brandSlug: "brand-beyblade",
221
228
  brand: "Beyblade",
@@ -252,7 +259,7 @@ const _rawProductsAuctionsSeedData = [
252
259
  slug: "auction-beyblade-metal-diablo-nemesis",
253
260
  title: "Metal Fight Beyblade BB-122 Diablo Nemesis (Ended — Sold)",
254
261
  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-4d", "category-metal-tops", "category-beyblade-metal", "category-spinning-tops"],
262
+ categorySlugs: ["category-diablo-nemesis-x-d", "category-metal-fury-4d", "category-metal-tops", "category-beyblade-metal", "category-spinning-tops"],
256
263
  categoryNames: ["Metal Fury (4D)", "Metal Fight Tops", "Beyblade Metal Fight", "Spinning Tops"],
257
264
  brandSlug: "brand-takara-tomy",
258
265
  brand: "Takara-Tomy",
@@ -294,7 +301,7 @@ const _rawProductsAuctionsSeedData = [
294
301
  slug: "auction-beyblade-burst-spriggan-requiem-bought-out",
295
302
  title: "Beyblade Burst B-128 Spriggan Requiem (Ended — Bought Out)",
296
303
  description: "Spriggan Requiem, the dual-spin legend of Burst Super Z. Bidding had reached ₹3,100 when a buyer took the ₹4,999 Buy Now price and ended the auction early.",
297
- categorySlugs: ["category-burst-tops", "category-beyblade-burst", "category-spinning-tops"],
304
+ categorySlugs: ["category-spriggan-requiem-0-zeta", "category-burst-cho-z", "category-burst-tops", "category-beyblade-burst", "category-spinning-tops"],
298
305
  categoryNames: ["Burst Tops", "Beyblade Burst", "Spinning Tops"],
299
306
  brandSlug: "brand-takara-tomy",
300
307
  brand: "Takara-Tomy",
@@ -331,7 +338,7 @@ const _rawProductsAuctionsSeedData = [
331
338
  slug: "auction-beyblade-burst-cho-z-achilles",
332
339
  title: "Beyblade Burst B-100 Cho-Z Achilles",
333
340
  description: "Cho-Z Achilles.Ow.Zn', a Super Zeta stamina-type top from the Cho-Z generation, in excellent spinning condition.",
334
- categorySlugs: ["category-burst-superking", "category-burst-tops", "category-beyblade-burst", "category-spinning-tops"],
341
+ categorySlugs: ["category-cho-z-achilles-00-dimension", "category-burst-cho-z", "category-burst-tops", "category-beyblade-burst", "category-spinning-tops"],
335
342
  categoryNames: ["Superking", "Burst Tops", "Beyblade Burst", "Spinning Tops"],
336
343
  brandSlug: "brand-beyblade",
337
344
  brand: "Beyblade",
@@ -365,9 +372,9 @@ const _rawProductsAuctionsSeedData = [
365
372
  {
366
373
  id: "auction-beyblade-x-wizard-fafnir",
367
374
  slug: "auction-beyblade-x-wizard-fafnir",
368
- title: "Beyblade X BX-06 Wizard Fafnir (Long-Running Auction)",
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-basic", "category-x-tops", "category-beyblade-x", "category-spinning-tops"],
375
+ title: "Beyblade X UX-03 WizardRod 5-70DB (Long-Running Auction)",
376
+ description: "WizardRod 5-70DB from the Unique Line, one week left on the clock — a rare Beyblade X chase top with active bidding.",
377
+ categorySlugs: ["category-wizardrod-5-70db", "category-x-unique-ux", "category-x-tops", "category-beyblade-x", "category-spinning-tops"],
371
378
  categoryNames: ["Basic Line", "Beyblade X Tops", "Beyblade X", "Spinning Tops"],
372
379
  brandSlug: "brand-takara-tomy",
373
380
  brand: "Takara-Tomy",
@@ -398,15 +405,24 @@ const _rawProductsAuctionsSeedData = [
398
405
  updatedAt: new Date("2026-08-19"),
399
406
  },
400
407
  ];
401
- // `.map(withTokens)` is applied to the WHOLE array at the end, not just to the
402
- // spread above. It used to wrap only the spread, so the ended-unsold fixture
403
- // appended after it shipped with no searchTxt — the one row that proves the
404
- // auction availability branch works, invisible to search.
408
+ // 🛑 EVERY wrapper is applied to the WHOLE array at the end, never to the
409
+ // spread. It used to wrap only the spread, so the ended-unsold fixture appended
410
+ // after it shipped with no searchTxt — the one row that proves the auction
411
+ // availability branch works, invisible to search.
412
+ //
413
+ // `listingType` was the SAME BUG, one field over, and survived that fix: it
414
+ // stayed inside the spread while `withTokens` was lifted out, so the appended
415
+ // fixture reached Firestore with no `listingType` at all. Measured 2026-10-14 —
416
+ // 9 of the 10 seeded auctions carried it and this one did not. A missing
417
+ // `listingType` is invisible to inspection (`normalizeListingType` defaults it
418
+ // to "standard" in memory) and fatal to every query, because a Firestore
419
+ // equality cannot match an absent field: `where("listingType","==","auction")`
420
+ // skipped it, so the fixture never appeared on /auctions and the "Ended" tab it
421
+ // exists to prove had nothing to show. Root Cause #101.
422
+ //
423
+ // Anything added to this pipeline goes AFTER the `]`, applied to all rows.
405
424
  export const productsAuctionsSeedData = [
406
- ..._rawProductsAuctionsSeedData.map((a) => ({
407
- ...a,
408
- listingType: PRODUCT_FIELDS.LISTING_TYPE_VALUES.AUCTION,
409
- })),
425
+ ..._rawProductsAuctionsSeedData,
410
426
  {
411
427
  // Ended with NO winner — reserve never met, zero bids.
412
428
  //
@@ -417,9 +433,9 @@ export const productsAuctionsSeedData = [
417
433
  // the base check and exercises nothing type-specific.
418
434
  id: "auction-beyblade-burst-lord-spryzen-ended-unsold",
419
435
  slug: "auction-beyblade-burst-lord-spryzen-ended-unsold",
420
- title: "Beyblade Burst B-174 Lord Spryzen (Ended — Reserve Not Met)",
436
+ title: "Beyblade Burst Storm Spriggan Knuckle Unite (Ended — Reserve Not Met)",
421
437
  description: "Auction closed without meeting its reserve, so no winner was declared. Relisting soon.",
422
- categorySlugs: ["category-burst-classic", "category-burst-tops", "category-beyblade-burst", "category-spinning-tops"],
438
+ categorySlugs: ["category-storm-spriggan-knuckle-unite", "category-burst-classic", "category-burst-tops", "category-beyblade-burst", "category-spinning-tops"],
423
439
  categoryNames: ["Burst Classic", "Burst Tops", "Beyblade Burst", "Spinning Tops"],
424
440
  brandSlug: "brand-takara-tomy",
425
441
  brand: "Takara-Tomy",
@@ -443,4 +459,8 @@ export const productsAuctionsSeedData = [
443
459
  createdAt: new Date("2026-08-10"),
444
460
  updatedAt: new Date("2026-08-24"),
445
461
  },
446
- ].map(withTokens).map(withFinalSale);
462
+ ]
463
+ .map((a) => ({ ...a, listingType: PRODUCT_FIELDS.LISTING_TYPE_VALUES.AUCTION }))
464
+ .map(withTokens)
465
+ .map(withFinalSale)
466
+ .map(withDerivedCategoryNames);