@mohasinac/appkit 4.0.3 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/dist/_internal/client/features/layout/useSidebarSearch.d.ts +19 -0
  2. package/dist/_internal/client/features/layout/useSidebarSearch.js +21 -0
  3. package/dist/_internal/server/features/auth/permissions.d.ts +2 -0
  4. package/dist/_internal/server/features/auth/permissions.js +4 -2
  5. package/dist/_internal/server/features/seo/sitemap.js +3 -2
  6. package/dist/client.d.ts +1 -1
  7. package/dist/client.js +1 -1
  8. package/dist/features/about/components/FAQPageView.d.ts +4 -1
  9. package/dist/features/about/components/FAQPageView.js +17 -7
  10. package/dist/features/account/components/UserSidebar.js +15 -9
  11. package/dist/features/admin/components/AdminSidebar.js +15 -9
  12. package/dist/features/admin/components/AdminTesterChecklistItemEditorView.js +4 -1
  13. package/dist/features/admin/components/AdminUserEditorView.d.ts +3 -1
  14. package/dist/features/admin/components/AdminUserEditorView.js +10 -2
  15. package/dist/features/admin/components/AdminUsersView.js +1 -1
  16. package/dist/features/auth/role-predicates.d.ts +14 -0
  17. package/dist/features/auth/role-predicates.js +1 -0
  18. package/dist/features/auth/schemas/firestore.d.ts +2 -0
  19. package/dist/features/auth/schemas/firestore.js +1 -0
  20. package/dist/features/events/actions/event-actions.js +15 -8
  21. package/dist/features/events/repository/events.repository.d.ts +2 -0
  22. package/dist/features/events/repository/events.repository.js +9 -0
  23. package/dist/features/events/schemas/firestore.d.ts +1 -0
  24. package/dist/features/events/schemas/firestore.js +1 -0
  25. package/dist/features/faq/components/FAQSearchableList.d.ts +11 -0
  26. package/dist/features/faq/components/FAQSearchableList.js +21 -0
  27. package/dist/features/homepage/components/BeforeAfterCard.js +2 -1
  28. package/dist/features/homepage/components/BrandsSection.js +2 -1
  29. package/dist/features/homepage/components/CharacterHotspot.js +2 -1
  30. package/dist/features/homepage/components/CharacterHotspotForm.js +4 -3
  31. package/dist/features/homepage/components/HeroBanner.js +2 -1
  32. package/dist/features/homepage/components/PromoGrid.js +2 -1
  33. package/dist/features/homepage/components/ShopByCategorySection.js +2 -1
  34. package/dist/features/seller/components/SellerSidebar.js +15 -9
  35. package/dist/features/tester/actions/checklist-item-actions.d.ts +6 -0
  36. package/dist/features/tester/actions/checklist-item-actions.js +2 -0
  37. package/dist/features/tester/components/TesterHubView.js +5 -4
  38. package/dist/features/tester/schemas/firestore.d.ts +3 -1
  39. package/dist/features/tester/schemas/firestore.js +2 -0
  40. package/dist/features/tester/seed-data/bids-tester-seed-data.d.ts +2 -0
  41. package/dist/features/tester/seed-data/bids-tester-seed-data.js +37 -0
  42. package/dist/features/tester/seed-data/coupons-tester-seed-data.d.ts +2 -0
  43. package/dist/features/tester/seed-data/coupons-tester-seed-data.js +76 -0
  44. package/dist/features/tester/seed-data/events-tester-seed-data.js +50 -2
  45. package/dist/features/tester/seed-data/index.d.ts +3 -0
  46. package/dist/features/tester/seed-data/index.js +3 -0
  47. package/dist/features/tester/seed-data/orders-tester-seed-data.d.ts +2 -0
  48. package/dist/features/tester/seed-data/orders-tester-seed-data.js +265 -0
  49. package/dist/features/tester/seed-data/products-tester-seed-data.js +213 -8
  50. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +421 -9
  51. package/dist/index.d.ts +1 -1
  52. package/dist/index.js +1 -1
  53. package/dist/next/api/routeHandler.js +22 -4
  54. package/dist/react/contexts/SessionContext.d.ts +2 -0
  55. package/dist/react/contexts/SessionContext.js +2 -0
  56. package/dist/seed/index.d.ts +1 -1
  57. package/dist/seed/index.js +1 -1
  58. package/dist/seed/manifest.js +4 -4
  59. package/dist/seed/orders-seed-data.js +4 -0
  60. package/dist/seed/products-classifieds-seed-data.js +48 -5
  61. package/dist/seed/products-digital-codes-seed-data.js +48 -5
  62. package/dist/seed/products-live-items-seed-data.js +6 -1
  63. package/dist/seed/products-prize-draws-seed-data.js +58 -5
  64. package/dist/styles.css +1 -1
  65. package/dist/tailwind-utilities.css +1 -1
  66. package/package.json +1 -1
  67. package/scripts/seed-cli.mjs +10 -4
@@ -30,9 +30,10 @@ import { normalizeError } from "../../errors/normalize";
30
30
  */
31
31
  import { NextResponse } from "next/server.js";
32
32
  import { getProviders } from "../../contracts";
33
- import { isAdminUser } from "../../features/auth/role-predicates";
33
+ import { isEffectiveAdminUser } from "../../features/auth/role-predicates";
34
34
  import { mapToHttpError, HTTP_ERROR_CODES } from "../../errors/error-mapping";
35
35
  import { serverErrorsRepository } from "../../features/server-errors/repository/server-errors.repository";
36
+ import { userRepository } from "../../repositories";
36
37
  import { SCHEMAS } from "../../schemas/registry";
37
38
  function buildPublicCacheControl(policy) {
38
39
  const maxAge = policy?.maxAge ?? 30;
@@ -138,6 +139,20 @@ function errorJson(status, code, message, requestId, issues) {
138
139
  headers: { "X-Request-Id": requestId, "Cache-Control": "no-store" },
139
140
  });
140
141
  }
142
+ /**
143
+ * A tester explicitly flagged for admin testing (isTester && canTestAdmin) gets the
144
+ * same access as "admin" on routes that allow it. isTester/canTestAdmin never appear
145
+ * in the session-cookie JWT claims (only `role` does), so this requires one live
146
+ * Firestore read — paid only on the role-mismatch rejection path, never on the common
147
+ * case (matching role, or a real admin whose JWT role already says "admin").
148
+ */
149
+ async function isTesterEligibleForAdminRoute(user, effectiveRoles) {
150
+ if (!user || !effectiveRoles.includes("admin"))
151
+ return false;
152
+ const profile = await userRepository.findById(user.uid);
153
+ return Boolean(profile?.isTester &&
154
+ profile?.canTestAdmin);
155
+ }
141
156
  export function createRouteHandler(options) {
142
157
  if (options.bodyOptional && options.bodyRequired) {
143
158
  throw new Error("createRouteHandler: bodyOptional and bodyRequired are mutually exclusive");
@@ -173,13 +188,16 @@ export function createRouteHandler(options) {
173
188
  : options.permission
174
189
  ? ["admin", "employee"]
175
190
  : [];
176
- if (effectiveRoles.length > 0) {
177
- if (!user || !effectiveRoles.includes(user.role ?? "")) {
191
+ if (effectiveRoles.length > 0 && (!user || !effectiveRoles.includes(user.role ?? ""))) {
192
+ const testerEligible = await isTesterEligibleForAdminRoute(user, effectiveRoles);
193
+ if (!testerEligible || !user) {
178
194
  return errorJson(403, HTTP_ERROR_CODES.FORBIDDEN, "Insufficient permissions", requestId);
179
195
  }
196
+ user.isTester = true;
197
+ user.canTestAdmin = true;
180
198
  }
181
199
  // -- Permission check (employee fine-grained) --------------------------
182
- if (options.permission && user && !isAdminUser(user)) {
200
+ if (options.permission && user && !isEffectiveAdminUser(user)) {
183
201
  const { rbac } = getProviders();
184
202
  if (!rbac) {
185
203
  return errorJson(503, HTTP_ERROR_CODES.UNAVAILABLE, "RBAC provider not configured", requestId);
@@ -31,6 +31,8 @@ export interface SessionUser {
31
31
  storeId?: string;
32
32
  /** Tester program flag — orthogonal to `role`. Unlocks the Tester Hub nav item/page. */
33
33
  isTester?: boolean;
34
+ /** Orthogonal to isTester — unlocks admin-only checklist items and real /admin/** RBAC access. */
35
+ canTestAdmin?: boolean;
34
36
  publicProfile?: FirestoreDocument;
35
37
  stats?: FirestoreDocument;
36
38
  metadata?: FirestoreDocument;
@@ -64,6 +64,7 @@ function buildSessionUser(authUser, serverData) {
64
64
  phoneVerified: serverData.phoneVerified,
65
65
  storeId: serverData.storeId,
66
66
  isTester: serverData.isTester,
67
+ canTestAdmin: serverData.canTestAdmin,
67
68
  avatarMetadata: serverData.avatarMetadata?.url
68
69
  ? {
69
70
  url: serverData.avatarMetadata.url,
@@ -134,6 +135,7 @@ export function SessionProvider({ children, initialUser, endpoints: endpointOver
134
135
  phoneVerified: data.phoneVerified,
135
136
  storeId: data.storeId,
136
137
  isTester: data.isTester,
138
+ canTestAdmin: data.canTestAdmin,
137
139
  avatarMetadata: data.avatarMetadata?.url
138
140
  ? {
139
141
  url: data.avatarMetadata.url,
@@ -79,7 +79,7 @@ export { conversationsSeedData } from "./conversations-seed-data";
79
79
  export { groupedListingsSeedData } from "./grouped-listings-seed-data";
80
80
  export { scammersSeedData } from "./scammers-seed-data";
81
81
  export { supportTicketsSeedData } from "./support-tickets-seed-data";
82
- export { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, } from "../features/tester/seed-data";
82
+ export { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, ordersTesterSeedData, } from "../features/tester/seed-data";
83
83
  export { productFeaturesSeedData } from "./product-features-seed-data";
84
84
  export { offersSeedData } from "./offers-seed-data";
85
85
  export { payoutMethodsSeedData, shippingConfigsSeedData, analyticsCardsSeedData, analyticsAlertsSeedData, storeCategoriesSeedData, listingTemplatesSeedData, moderationQueueSeedData, reportsSeedData, itemRequestsSeedData, storeWhatsAppConfigSeedData, storeGoogleConfigSeedData, } from "./store-extensions-seed-data";
@@ -70,7 +70,7 @@ export { scammersSeedData } from "./scammers-seed-data";
70
70
  export { supportTicketsSeedData } from "./support-tickets-seed-data";
71
71
  // Tester sandbox seed fixtures live in ../features/tester/seed-data (isolated from this
72
72
  // folder on purpose) — re-exported here so every other seed consumer keeps one import surface.
73
- export { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, } from "../features/tester/seed-data";
73
+ export { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, ordersTesterSeedData, } from "../features/tester/seed-data";
74
74
  export { productFeaturesSeedData } from "./product-features-seed-data";
75
75
  export { offersSeedData } from "./offers-seed-data";
76
76
  // S-STORE Extensions (Tier S-STORE) — 11 new collections
@@ -51,7 +51,7 @@ import { offersSeedData } from "./offers-seed-data";
51
51
  import { couponUsageSeedData } from "./coupon-usage-seed-data";
52
52
  import { claimedCouponsSeedData } from "./claimed-coupons-seed-data";
53
53
  // Tester sandbox seed fixtures live in ../features/tester/seed-data (isolated on purpose).
54
- import { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, } from "../features/tester/seed-data";
54
+ import { testerChecklistSeedData, storesTesterSeedData, categoriesTesterSeedData, productsTesterSeedData, blogTesterSeedData, eventsTesterSeedData, couponsTesterSeedData, bidsTesterSeedData, ordersTesterSeedData, } from "../features/tester/seed-data";
55
55
  function asArr(items) {
56
56
  return items ?? [];
57
57
  }
@@ -104,10 +104,10 @@ export const SEED_MANIFEST = {
104
104
  ? "bundle"
105
105
  : LISTING_TYPE_TO_MANIFEST_TAG[p.listingType ?? "standard"],
106
106
  })), "title"),
107
- orders: pick(asArr(ordersSeedData)),
107
+ orders: pick([...asArr(ordersSeedData), ...asArr(ordersTesterSeedData)]),
108
108
  reviews: pick(asArr(reviewsSeedData), "title"),
109
- bids: pick(asArr(bidsSeedData)),
110
- coupons: pick(asArr(couponsSeedData).map((c) => ({
109
+ bids: pick([...asArr(bidsSeedData), ...asArr(bidsTesterSeedData)]),
110
+ coupons: pick([...asArr(couponsSeedData), ...asArr(couponsTesterSeedData)].map((c) => ({
111
111
  ...c,
112
112
  name: c.code ?? c.id,
113
113
  }))),
@@ -239,21 +239,25 @@ const productPool = [
239
239
  ];
240
240
  const statuses = [
241
241
  "pending",
242
+ "confirmed",
242
243
  "processing",
243
244
  "shipped",
244
245
  "delivered",
245
246
  "cancelled",
246
247
  "refunded",
247
248
  "return_requested",
249
+ "returned",
248
250
  ];
249
251
  const paymentStatuses = {
250
252
  pending: "pending",
253
+ confirmed: "paid",
251
254
  processing: "processing",
252
255
  shipped: "paid",
253
256
  delivered: "paid",
254
257
  cancelled: "refunded",
255
258
  refunded: "refunded",
256
259
  return_requested: "paid",
260
+ returned: "refunded",
257
261
  };
258
262
  const buyerStoreMatrix = [
259
263
  { userId: "user-yugi-muto", storeId: "store-kaiba-corp-cards", addressId: "addr-yugi-home" },
@@ -1,10 +1,12 @@
1
1
  /*
2
- * WHY: Classified listings are out of scope for the minimal Beyblade-focused demo catalog.
3
- * WHAT: Exports an empty array — kept as a stub so seed/index.ts and the seed API route's
4
- * SEED_DATA_MAP.products concatenation keep working without a code change elsewhere.
2
+ * WHY: Real (non-tester-sandbox) classified listing so testers and shoppers can exercise
3
+ * the classified contact-seller flow (no checkout, chat/phone only) against permanent
4
+ * catalog content. Previously an empty stub "out of scope for the minimal
5
+ * Beyblade-focused demo catalog" — now populated with one seller-run local listing.
6
+ * WHAT: Exports 1 classified listing on Beyblade Arena — local meetup, negotiable.
5
7
  *
6
8
  * EXPORTS:
7
- * productsClassifiedsSeedData — empty array
9
+ * productsClassifiedsSeedData — Array of 1 Partial<ProductDocument> with listingType:"classified"
8
10
  *
9
11
  * @tag domain:products,classifieds
10
12
  * @tag layer:seed
@@ -13,4 +15,45 @@
13
15
  * @tag consumers:seed/index.ts,seed/runner.ts
14
16
  * @tag sideEffects:none
15
17
  */
16
- export const productsClassifiedsSeedData = [];
18
+ import { PRODUCT_FIELDS } from "../constants/field-names";
19
+ import { buildSearchTokens } from "../utils/search-tokens";
20
+ import { seedExtMedia } from "./_helpers/media";
21
+ export const productsClassifiedsSeedData = [
22
+ {
23
+ id: "classified-beyblade-stadium-set",
24
+ slug: "classified-beyblade-stadium-set",
25
+ title: "Used Beyblade Stadium Set — Local Pickup Only",
26
+ description: "Well-loved Beyblade stadium with 6 tops and 2 launchers, sold as-is. Meet up locally or arrange your own shipping — price is negotiable.",
27
+ categorySlugs: ["category-spinning-tops"],
28
+ categoryNames: ["Spinning Tops"],
29
+ brandSlug: "brand-beyblade",
30
+ brand: "Beyblade",
31
+ price: 1200,
32
+ currency: "INR",
33
+ stockQuantity: 1,
34
+ availableQuantity: 1,
35
+ isSold: false,
36
+ mainImage: seedExtMedia("https://picsum.photos/seed/classified-image-beyblade-stadium-set-1-20260101/900/900"),
37
+ images: [seedExtMedia("https://picsum.photos/seed/classified-image-beyblade-stadium-set-1-20260101/900/900")],
38
+ status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
39
+ condition: PRODUCT_FIELDS.CONDITION_VALUES.USED,
40
+ listingType: "classified",
41
+ storeId: "store-beyblade-arena",
42
+ storeName: "Beyblade Arena",
43
+ classified: {
44
+ meetupArea: { city: "Mumbai", locality: "Andheri West", pincode: "400058" },
45
+ contactMethod: "both",
46
+ acceptsShipping: false,
47
+ negotiable: true,
48
+ },
49
+ customFields: [],
50
+ customSections: [],
51
+ featured: false,
52
+ isPromoted: false,
53
+ isOnSale: false,
54
+ tags: ["classified", "local-pickup"],
55
+ createdAt: new Date("2026-05-05"),
56
+ updatedAt: new Date("2026-05-05"),
57
+ searchTokens: buildSearchTokens("Used Beyblade Stadium Set — Local Pickup Only", "Well-loved Beyblade stadium with 6 tops and 2 launchers.", "Beyblade", "brand-beyblade", ["Spinning Tops"], ["classified", "local-pickup"]),
58
+ },
59
+ ];
@@ -1,10 +1,12 @@
1
1
  /*
2
- * WHY: Digital-code listings are out of scope for the minimal Beyblade-focused demo catalog.
3
- * WHAT: Exports an empty array kept as a stub so seed/index.ts and the seed API route's
4
- * SEED_DATA_MAP.products concatenation keep working without a code change elsewhere.
2
+ * WHY: Real (non-tester-sandbox) digital-code listing so testers and shoppers can exercise
3
+ * the auto-claim digital delivery flow against permanent catalog content. Previously an
4
+ * empty stub "out of scope for the minimal Beyblade-focused demo catalog" — now
5
+ * populated with one companion-app unlock code, on-theme for the spinning-tops catalog.
6
+ * WHAT: Exports 1 digital-code listing on Beyblade Arena — auto-claim delivery.
5
7
  *
6
8
  * EXPORTS:
7
- * productsDigitalCodesSeedData — empty array
9
+ * productsDigitalCodesSeedData — Array of 1 Partial<ProductDocument> with listingType:"digital-code"
8
10
  *
9
11
  * @tag domain:products,digital-codes
10
12
  * @tag layer:seed
@@ -13,4 +15,45 @@
13
15
  * @tag consumers:seed/index.ts,seed/runner.ts
14
16
  * @tag sideEffects:none
15
17
  */
16
- export const productsDigitalCodesSeedData = [];
18
+ import { PRODUCT_FIELDS } from "../constants/field-names";
19
+ import { buildSearchTokens } from "../utils/search-tokens";
20
+ import { seedExtMedia } from "./_helpers/media";
21
+ export const productsDigitalCodesSeedData = [
22
+ {
23
+ id: "digitalcode-beyblade-x-app-unlock",
24
+ slug: "digitalcode-beyblade-x-app-unlock",
25
+ title: "Beyblade X Companion App — Premium Unlock Code",
26
+ description: "Digital unlock code for the Beyblade X companion app's premium tournament tracker and battle-log features. Delivered instantly after payment.",
27
+ categorySlugs: ["category-spinning-tops"],
28
+ categoryNames: ["Spinning Tops"],
29
+ brandSlug: "brand-takara-tomy",
30
+ brand: "Takara-Tomy",
31
+ price: 199,
32
+ currency: "INR",
33
+ stockQuantity: 25,
34
+ availableQuantity: 25,
35
+ isSold: false,
36
+ mainImage: seedExtMedia("https://picsum.photos/seed/digitalcode-image-beyblade-x-app-unlock-1-20260101/900/900"),
37
+ images: [seedExtMedia("https://picsum.photos/seed/digitalcode-image-beyblade-x-app-unlock-1-20260101/900/900")],
38
+ status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
39
+ condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
40
+ listingType: "digital-code",
41
+ storeId: "store-beyblade-arena",
42
+ storeName: "Beyblade Arena",
43
+ digitalCode: {
44
+ codeDeliveryMethod: "auto-claim",
45
+ codePoolSize: 25,
46
+ codesAvailable: 25,
47
+ redemptionInstructions: "Open the Beyblade X companion app, go to Settings → Redeem Code, and enter your code to unlock premium features.",
48
+ },
49
+ customFields: [],
50
+ customSections: [],
51
+ featured: false,
52
+ isPromoted: false,
53
+ isOnSale: false,
54
+ tags: ["digital-code", "app-unlock"],
55
+ createdAt: new Date("2026-05-05"),
56
+ updatedAt: new Date("2026-05-05"),
57
+ searchTokens: buildSearchTokens("Beyblade X Companion App — Premium Unlock Code", "Digital unlock code for the Beyblade X companion app.", "Takara-Tomy", "brand-takara-tomy", ["Spinning Tops"], ["digital-code", "app-unlock"]),
58
+ },
59
+ ];
@@ -1,5 +1,10 @@
1
1
  /*
2
- * WHY: Live-item listings are out of scope for the minimal Beyblade-focused demo catalog.
2
+ * WHY: `listingType: "live"` is schema-specific to animals/plants (ProductLiveItemMeta:
3
+ * species, ageMonths, sex, jurisdictionAllowed, vendor verification, CITES permit) —
4
+ * genuinely inapplicable to this catalog's spinning-tops/collectibles theme, so left
5
+ * deliberately empty in the real seed rather than forcing a nonsensical example.
6
+ * Testers can still exercise the live-item buyer flow against the disposable
7
+ * tester-sandbox fixture in appkit/src/features/tester/seed-data/products-tester-seed-data.ts.
3
8
  * WHAT: Exports an empty array — kept as a stub so seed/index.ts and the seed API route's
4
9
  * SEED_DATA_MAP.products concatenation keep working without a code change elsewhere.
5
10
  *
@@ -1,10 +1,14 @@
1
1
  /*
2
- * WHY: Prize-draw listings are out of scope for the minimal Beyblade-focused demo catalog.
3
- * WHAT: Exports an empty array kept as a stub so seed/index.ts and the seed API route's
4
- * SEED_DATA_MAP.products concatenation keep working without a code change elsewhere.
2
+ * WHY: Real (non-tester-sandbox) prize-draw listing so testers and shoppers can exercise
3
+ * the prize-draw buy reveal flow against permanent catalog content, not just the
4
+ * disposable 7-day tester sandbox item. Previously an empty stub "out of scope for
5
+ * the minimal Beyblade-focused demo catalog" — now populated with one platform-run
6
+ * mystery-box draw, on-theme for the spinning-tops catalog.
7
+ * WHAT: Exports 1 prize-draw listing (classic reveal mode, 4 prizes) run by the platform's
8
+ * own store, letitrip-official.
5
9
  *
6
10
  * EXPORTS:
7
- * productsPrizeDrawsSeedData — empty array
11
+ * productsPrizeDrawsSeedData — Array of 1 Partial<ProductDocument> with listingType:"prize-draw"
8
12
  *
9
13
  * @tag domain:products,prize-draws
10
14
  * @tag layer:seed
@@ -13,4 +17,53 @@
13
17
  * @tag consumers:seed/index.ts,seed/runner.ts
14
18
  * @tag sideEffects:none
15
19
  */
16
- export const productsPrizeDrawsSeedData = [];
20
+ import { PRODUCT_FIELDS } from "../constants/field-names";
21
+ import { buildSearchTokens } from "../utils/search-tokens";
22
+ import { seedExtMedia } from "./_helpers/media";
23
+ export const productsPrizeDrawsSeedData = [
24
+ {
25
+ id: "prizedraw-beyblade-mystery-box",
26
+ slug: "prizedraw-beyblade-mystery-box",
27
+ title: "Beyblade Mystery Box — Prize Draw",
28
+ description: "Buy an entry for ₹99 and reveal your prize instantly — win anything from a rare limited-edition Beyblade down to a spare launcher grip. 4 prizes available, revealed on a first-come basis.",
29
+ categorySlugs: ["category-spinning-tops"],
30
+ categoryNames: ["Spinning Tops"],
31
+ brandSlug: "brand-beyblade",
32
+ brand: "Beyblade",
33
+ price: 99,
34
+ currency: "INR",
35
+ stockQuantity: 4,
36
+ availableQuantity: 4,
37
+ isSold: false,
38
+ mainImage: seedExtMedia("https://picsum.photos/seed/prizedraw-image-beyblade-mystery-box-1-20260101/900/900"),
39
+ images: [seedExtMedia("https://picsum.photos/seed/prizedraw-image-beyblade-mystery-box-1-20260101/900/900")],
40
+ status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
41
+ condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
42
+ listingType: "prize-draw",
43
+ storeId: "store-letitrip-official",
44
+ storeName: "LetItRip Official",
45
+ prizeDrawMode: "reveal",
46
+ pricePerEntry: 99,
47
+ prizeMaxEntries: 4,
48
+ prizeCurrentEntries: 0,
49
+ prizeRevealWindowStart: new Date("2026-05-01"),
50
+ prizeRevealWindowEnd: new Date("2026-12-31"),
51
+ prizeRevealStatus: "open",
52
+ prizeRevealDeadlineDays: 7,
53
+ prizeDrawItems: [
54
+ { itemNumber: 1, title: "Limited-Edition Gold Dranzer S", description: "Rare gold-finish collector's edition.", images: [seedExtMedia("https://picsum.photos/seed/prizedraw-item-beyblade-1-20260101/600/600")], condition: "new", estimatedValue: 3499, isWon: false },
55
+ { itemNumber: 2, title: "Beyblade X Starter Set", description: "Full starter set with stadium.", images: [seedExtMedia("https://picsum.photos/seed/prizedraw-item-beyblade-2-20260101/600/600")], condition: "new", estimatedValue: 1999, isWon: false },
56
+ { itemNumber: 3, title: "Spare Parts Bundle", description: "Assorted spare tips, rings, and drivers.", images: [seedExtMedia("https://picsum.photos/seed/prizedraw-item-beyblade-3-20260101/600/600")], condition: "new", estimatedValue: 399, isWon: false },
57
+ { itemNumber: 4, title: "Spare Launcher Grip", description: "Standard replacement launcher grip.", images: [seedExtMedia("https://picsum.photos/seed/prizedraw-item-beyblade-4-20260101/600/600")], condition: "new", estimatedValue: 149, isWon: false },
58
+ ],
59
+ customFields: [],
60
+ customSections: [],
61
+ featured: false,
62
+ isPromoted: true,
63
+ isOnSale: false,
64
+ tags: ["prize-draw", "mystery-box"],
65
+ createdAt: new Date("2026-05-01"),
66
+ updatedAt: new Date("2026-05-01"),
67
+ searchTokens: buildSearchTokens("Beyblade Mystery Box — Prize Draw", "Buy an entry and reveal your prize instantly.", "Beyblade", "brand-beyblade", ["Spinning Tops"], ["prize-draw", "mystery-box"]),
68
+ },
69
+ ];