@mohasinac/appkit 4.23.0 → 4.25.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 (69) hide show
  1. package/dist/_internal/server/features/bundles/actions.js +7 -0
  2. package/dist/_internal/server/features/checkout/actions.js +30 -21
  3. package/dist/_internal/server/features/checkout/bundle-expansion.d.ts +4 -6
  4. package/dist/_internal/server/features/checkout/bundle-expansion.js +19 -17
  5. package/dist/_internal/server/features/checkout/stock-restore.js +6 -5
  6. package/dist/_internal/server/features/products/group-members.d.ts +55 -0
  7. package/dist/_internal/server/features/products/group-members.js +41 -0
  8. package/dist/_internal/shared/actions/action-registry.js +41 -0
  9. package/dist/_internal/shared/checkout/line-members.d.ts +60 -0
  10. package/dist/_internal/shared/checkout/line-members.js +68 -0
  11. package/dist/_internal/shared/checkout/order-math.d.ts +23 -5
  12. package/dist/_internal/shared/checkout/order-math.js +29 -6
  13. package/dist/_internal/shared/checkout/rules/_limits.d.ts +12 -2
  14. package/dist/_internal/shared/checkout/rules/_limits.js +12 -2
  15. package/dist/_internal/shared/checkout/rules/_registry.js +8 -2
  16. package/dist/_internal/shared/features/cart/config.d.ts +2 -1
  17. package/dist/_internal/shared/features/cart/config.js +2 -1
  18. package/dist/_internal/shared/features/cart/schema.d.ts +6 -6
  19. package/dist/_internal/shared/features/notifications/action-url.d.ts +47 -0
  20. package/dist/_internal/shared/features/notifications/action-url.js +111 -0
  21. package/dist/client.d.ts +2 -0
  22. package/dist/client.js +1 -0
  23. package/dist/constants/api-endpoints.d.ts +27 -0
  24. package/dist/constants/api-endpoints.js +9 -0
  25. package/dist/constants/notification-types.d.ts +38 -10
  26. package/dist/constants/notification-types.js +39 -28
  27. package/dist/features/account/types/index.d.ts +1 -10
  28. package/dist/features/admin/actions/notification-actions.d.ts +12 -0
  29. package/dist/features/admin/actions/notification-actions.js +149 -4
  30. package/dist/features/admin/schemas/firestore.d.ts +24 -29
  31. package/dist/features/admin/schemas/firestore.js +89 -26
  32. package/dist/features/cart/actions/cart-actions.d.ts +47 -1
  33. package/dist/features/cart/actions/cart-actions.js +189 -12
  34. package/dist/features/cart/components/CartDrawer.js +2 -1
  35. package/dist/features/cart/repository/cart.repository.d.ts +33 -1
  36. package/dist/features/cart/repository/cart.repository.js +146 -2
  37. package/dist/features/cart/schemas/firestore.d.ts +132 -8
  38. package/dist/features/cart/schemas/firestore.js +22 -0
  39. package/dist/features/email/index.d.ts +1 -1
  40. package/dist/features/email/index.js +5 -1
  41. package/dist/features/email/notification-templates.d.ts +35 -0
  42. package/dist/features/email/notification-templates.js +96 -0
  43. package/dist/features/orders/schemas/index.d.ts +2 -2
  44. package/dist/features/products/components/GroupMemberPicker.d.ts +46 -0
  45. package/dist/features/products/components/GroupMemberPicker.js +108 -0
  46. package/dist/features/products/components/ShowGroupSection.js +7 -9
  47. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +30 -0
  48. package/dist/index.d.ts +4 -3
  49. package/dist/index.js +7 -1
  50. package/dist/schemas/registry.d.ts +2 -2
  51. package/dist/seed/factories/notification.factory.d.ts +2 -1
  52. package/dist/seed/notifications-seed-data.js +225 -0
  53. package/dist/seed/payouts-seed-data.js +47 -0
  54. package/dist/seed/scammers-seed-data.js +34 -0
  55. package/dist/seed/stores-seed-data.js +34 -0
  56. package/dist/seed/support-tickets-seed-data.js +34 -0
  57. package/dist/server-entry.d.ts +1 -0
  58. package/dist/server-entry.js +5 -0
  59. package/dist/server.d.ts +5 -1
  60. package/dist/server.js +10 -1
  61. package/dist/styles.css +58 -1
  62. package/dist/tailwind-utilities.css +1 -1
  63. package/dist/ui/components/QuantityStepper.d.ts +38 -0
  64. package/dist/ui/components/QuantityStepper.js +20 -0
  65. package/dist/ui/components/QuantityStepper.style.css +56 -0
  66. package/dist/ui/components/index.style.css +1 -0
  67. package/dist/ui/index.d.ts +2 -0
  68. package/dist/ui/index.js +1 -0
  69. package/package.json +1 -1
@@ -355,4 +355,229 @@ export const notificationsSeedData = [
355
355
  createdAt: daysAgo(60),
356
356
  updatedAt: daysAgo(60),
357
357
  },
358
+ // -- Coverage fixtures (16) - one per type that had none --------------------
359
+ //
360
+ // W19 gave every notification type a real email template and a resolved
361
+ // actionUrl. 12 of the 28 types were seeded; the other 16 had NO fixture at
362
+ // all, so a tester could not see 16 of those templates or verify where they
363
+ // land. Each of these exists to make one template and one resolver branch
364
+ // reachable.
365
+ //
366
+ // `actionUrl` is deliberately OMITTED on all but one: it is now resolved
367
+ // from relatedType + relatedId inside sendNotification, and hardcoding one
368
+ // here would seed data that no longer matches what the code produces -
369
+ // which is exactly how a fixture stops being a test.
370
+ {
371
+ id: "notif-yugi-order-confirmed-001",
372
+ userId: "user-yugi-muto",
373
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.ORDER_CONFIRMED,
374
+ priority: "normal",
375
+ title: "Order confirmed",
376
+ message: "Your order is confirmed and the seller is preparing it for dispatch.",
377
+ isRead: false,
378
+ relatedId: "order-2-20260819-a1b2c3",
379
+ relatedType: "order",
380
+ createdAt: daysAgo(3),
381
+ updatedAt: daysAgo(3),
382
+ },
383
+ {
384
+ id: "notif-yugi-bid-lost-001",
385
+ userId: "user-yugi-muto",
386
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.BID_LOST,
387
+ priority: "normal",
388
+ title: "This auction closed without you",
389
+ message: "Beyblade Original Dragoon Storm sold to another bidder at Rs 5,200.",
390
+ isRead: false,
391
+ relatedId: "auction-beyblade-original-dragoon-storm",
392
+ relatedType: "bid",
393
+ createdAt: daysAgo(2),
394
+ updatedAt: daysAgo(2),
395
+ },
396
+ {
397
+ // Seller-facing - the ONE fixture proving TYPE_AUDIENCE routes auction_ended to /store rather than /user.
398
+ id: "notif-tyson-auction-ended-001",
399
+ userId: "user-tyson-blader",
400
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.AUCTION_ENDED,
401
+ priority: "normal",
402
+ title: "Your auction ended with no winner",
403
+ message: "The highest bid did not meet your reserve price. The listing has been archived.",
404
+ isRead: false,
405
+ relatedId: "auction-beyblade-lord-spryzen-ended-unsold",
406
+ relatedType: "product",
407
+ createdAt: daysAgo(1),
408
+ updatedAt: daysAgo(1),
409
+ },
410
+ {
411
+ id: "notif-yugi-review-replied-001",
412
+ userId: "user-yugi-muto",
413
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.REVIEW_REPLIED,
414
+ priority: "low",
415
+ title: "The seller replied to your review",
416
+ message: "Beyblade Arena responded to your review of Beyblade X Dran Sword.",
417
+ isRead: false,
418
+ relatedId: "review-dran-sword-yugi-20260819",
419
+ relatedType: "review",
420
+ createdAt: daysAgo(5),
421
+ updatedAt: daysAgo(5),
422
+ },
423
+ {
424
+ // No relatedId on purpose - this fixture proves a promotion renders WITHOUT a CTA button rather than linking to the dashboard the reader is already on.
425
+ id: "notif-yugi-promotion-001",
426
+ userId: "user-yugi-muto",
427
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.PROMOTION,
428
+ priority: "low",
429
+ title: "25% off at Beyblade Arena",
430
+ message: "Use code ARENA25 on your next order. Ends Sunday.",
431
+ isRead: false,
432
+ createdAt: hoursAgo(20),
433
+ updatedAt: hoursAgo(20),
434
+ },
435
+ {
436
+ id: "notif-yugi-account-action-001",
437
+ userId: "user-yugi-muto",
438
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.ACCOUNT_ACTION,
439
+ priority: "high",
440
+ title: "Your account details were updated",
441
+ message: "Your delivery address was changed. If this wasn't you, contact support immediately.",
442
+ isRead: false,
443
+ relatedId: "user-yugi-muto",
444
+ relatedType: "user",
445
+ createdAt: daysAgo(6),
446
+ updatedAt: daysAgo(6),
447
+ },
448
+ {
449
+ id: "notif-yugi-offer-responded-001",
450
+ userId: "user-yugi-muto",
451
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.OFFER_RESPONDED,
452
+ priority: "high",
453
+ title: "The seller countered your offer",
454
+ message: "Beyblade Arena countered at Rs 3,800 on Metal Storm Pegasus.",
455
+ isRead: false,
456
+ relatedId: "offer-metal-storm-pegasus-yugi-20260819-countered",
457
+ relatedType: "offer",
458
+ createdAt: daysAgo(2),
459
+ updatedAt: daysAgo(2),
460
+ },
461
+ {
462
+ id: "notif-yugi-offer-expired-001",
463
+ userId: "user-yugi-muto",
464
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.OFFER_EXPIRED,
465
+ priority: "normal",
466
+ title: "Your offer expired",
467
+ message: "The seller did not respond within 48 hours. You can make a new offer.",
468
+ isRead: false,
469
+ relatedId: "offer-kaiba-x-wizard-arrow-expired",
470
+ relatedType: "offer",
471
+ createdAt: daysAgo(8),
472
+ updatedAt: daysAgo(8),
473
+ },
474
+ {
475
+ id: "notif-yugi-offer-counter-accepted-001",
476
+ userId: "user-yugi-muto",
477
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.OFFER_COUNTER_ACCEPTED,
478
+ priority: "high",
479
+ title: "Your counter-offer was accepted",
480
+ message: "Complete checkout within 24 hours to secure it at Rs 3,500.",
481
+ isRead: false,
482
+ relatedId: "offer-metal-storm-pegasus-yugi-20260819-countered",
483
+ relatedType: "offer",
484
+ createdAt: hoursAgo(6),
485
+ updatedAt: hoursAgo(6),
486
+ },
487
+ {
488
+ id: "notif-yugi-refund-initiated-001",
489
+ userId: "user-yugi-muto",
490
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.REFUND_INITIATED,
491
+ priority: "high",
492
+ title: "Refund on its way",
493
+ message: "Rs 1,499 is being returned to your original payment method. Allow 5-7 working days.",
494
+ isRead: false,
495
+ relatedId: "order-1-20260812-r3f4nd",
496
+ relatedType: "order",
497
+ createdAt: daysAgo(4),
498
+ updatedAt: daysAgo(4),
499
+ },
500
+ {
501
+ id: "notif-yugi-prize-won-001",
502
+ userId: "user-yugi-muto",
503
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.PRIZE_WON,
504
+ priority: "high",
505
+ title: "You won a prize",
506
+ message: "Your entry in the Beyblade Vault draw won slot #14. Reveal it within 7 days.",
507
+ isRead: false,
508
+ relatedId: "prizedraw-beyblade-vault-closed-pending-reveal",
509
+ relatedType: "product",
510
+ createdAt: daysAgo(1),
511
+ updatedAt: daysAgo(1),
512
+ },
513
+ {
514
+ id: "notif-kaiba-prize-reveal-expired-001",
515
+ userId: "user-seto-kaiba",
516
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.PRIZE_REVEAL_EXPIRED,
517
+ priority: "normal",
518
+ title: "Your prize reveal window closed",
519
+ message: "The 7-day reveal window passed and your entry was refunded.",
520
+ isRead: false,
521
+ relatedId: "prizedraw-beyblade-vault-closed-pending-reveal",
522
+ relatedType: "product",
523
+ createdAt: daysAgo(10),
524
+ updatedAt: daysAgo(10),
525
+ },
526
+ {
527
+ id: "notif-yugi-emi-due-soon-001",
528
+ userId: "user-yugi-muto",
529
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.EMI_INSTALLMENT_DUE_SOON,
530
+ priority: "normal",
531
+ title: "EMI installment due in 3 days",
532
+ message: "Installment 2 of 6 (Rs 1,250) is due on the 29th.",
533
+ isRead: false,
534
+ relatedId: "order-3-20260801-em1pay",
535
+ relatedType: "order",
536
+ createdAt: hoursAgo(12),
537
+ updatedAt: hoursAgo(12),
538
+ },
539
+ {
540
+ id: "notif-kaiba-emi-overdue-001",
541
+ userId: "user-seto-kaiba",
542
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.EMI_INSTALLMENT_OVERDUE,
543
+ priority: "high",
544
+ title: "EMI installment overdue",
545
+ message: "Installment 3 of 6 (Rs 1,250) was due on the 20th. Pay now to avoid a late fee.",
546
+ isRead: false,
547
+ relatedId: "order-3-20260801-em1pay",
548
+ relatedType: "order",
549
+ createdAt: daysAgo(5),
550
+ updatedAt: daysAgo(5),
551
+ },
552
+ {
553
+ id: "notif-tester-catalogue-stale-001",
554
+ userId: "user-tester-qa",
555
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.CATALOGUE_IMAGES_STALE,
556
+ priority: "normal",
557
+ title: "Refresh your catalogue photos",
558
+ message: "Photos for your vintage Dragoon are getting old - refresh them within 14 days or you won't be able to list it.",
559
+ isRead: false,
560
+ relatedId: "mycatalog-user-tester-qa-vintage-dragoon",
561
+ relatedType: "catalogueItem",
562
+ createdAt: daysAgo(2),
563
+ updatedAt: daysAgo(2),
564
+ },
565
+ {
566
+ // The ONE fixture keeping an explicit actionUrl, and it earns it: the buyer
567
+ // must land on the proof-UPLOAD page, which relatedType:"order" cannot
568
+ // express. This is the case the caller-wins rule exists for.
569
+ id: "notif-yugi-payment-review-001",
570
+ userId: "user-yugi-muto",
571
+ type: NOTIFICATION_FIELDS.TYPE_VALUES.PAYMENT_REVIEW,
572
+ priority: "high",
573
+ title: "Please re-upload your payment proof",
574
+ message: "The screenshot you uploaded was unreadable. Upload a clearer one within 2 hours.",
575
+ isRead: false,
576
+ actionUrl: "/user/orders/order-4-20260825-upiman/payment",
577
+ actionLabel: "Re-upload proof",
578
+ relatedId: "order-4-20260825-upiman",
579
+ relatedType: "order",
580
+ createdAt: hoursAgo(1),
581
+ updatedAt: hoursAgo(1),
582
+ },
358
583
  ];
@@ -17,6 +17,17 @@ const _CURRENCY = getDefaultCurrency();
17
17
  import { PAYOUT_FIELDS, DEFAULT_PLATFORM_FEE_RATE, } from "../features/payments/schemas";
18
18
  const NOW = new Date();
19
19
  const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
20
+ /**
21
+ * A payout timeline entry, for the W18 `statusHistory` fixtures below.
22
+ *
23
+ * Dates come from the file's own `daysAgo()` helper, never `Date.now()` at
24
+ * import time in an ID position — Root Cause #25. `actorUid` only, never a
25
+ * name: `withHistory` scrubs PII from real writes and a fixture that carried
26
+ * a name would be the one place the rule is broken.
27
+ */
28
+ function entry(at, actorRole, trigger, changes, extra) {
29
+ return { at, actorRole, trigger, changes, ...extra };
30
+ }
20
31
  function payoutAmounts(grossINR) {
21
32
  const grossAmount = grossINR;
22
33
  const platformFee = Math.round(grossAmount * DEFAULT_PLATFORM_FEE_RATE * 100) / 100;
@@ -32,6 +43,20 @@ export const payoutsSeedData = [
32
43
  // 1. Beyblade Arena — Jan 2026
33
44
  {
34
45
  id: "payout-beyblade-arena-jan-2026-completed",
46
+ /*
47
+ * Carries the transactionId AND the timeline entry recording it. Until
48
+ * 2026-08-26 `transactionId` was absent from
49
+ * PAYOUT_ADMIN_UPDATEABLE_FIELDS, so every real payout marked paid was
50
+ * stored without the reference the modal required. A fixture that
51
+ * omitted it would have kept agreeing with the bug.
52
+ */
53
+ transactionId: "UTR2026011500042",
54
+ statusHistory: [
55
+ entry(daysAgo(150), "admin", "adminUpdatePayout", {
56
+ status: { from: "processing", to: "paid" },
57
+ transactionId: { from: null, to: "UTR2026011500042" },
58
+ }, { actorUid: "user-admin-letitrip" }),
59
+ ],
35
60
  storeId: "store-beyblade-arena",
36
61
  sellerName: "Beyblade Arena",
37
62
  sellerEmail: "tyson@beybladearena.in",
@@ -231,6 +256,22 @@ export const payoutsSeedData = [
231
256
  processedAt: daysAgo(153),
232
257
  createdAt: daysAgo(155),
233
258
  updatedAt: daysAgo(153),
259
+ /*
260
+ * TWO failed attempts, and that is the point. `lastFailureReason` holds
261
+ * only the second one; the timeline is the only place the first survives.
262
+ * This fixture is what the "a payout that failed twice shows BOTH
263
+ * reasons" tester case reads.
264
+ */
265
+ statusHistory: [
266
+ entry(daysAgo(154), "system", "payoutBatch:dispatch", {
267
+ status: { from: "pending", to: "processing" },
268
+ }),
269
+ entry(daysAgo(154), "system", "payoutBatch:failedRetrying", { status: { from: "processing", to: "pending" }, lastFailureReason: { from: null, to: "Razorpay 400: beneficiary bank returned NEFT rejection" } }, { reason: "Razorpay 400: beneficiary bank returned NEFT rejection" }),
270
+ entry(daysAgo(153), "system", "payoutBatch:dispatch", {
271
+ status: { from: "pending", to: "processing" },
272
+ }),
273
+ entry(daysAgo(153), "system", "payoutBatch:failedFinal", { status: { from: "processing", to: "failed" }, lastFailureReason: { from: "Razorpay 400: beneficiary bank returned NEFT rejection", to: "Razorpay 400: IFSC code mismatch" } }, { reason: "Razorpay 400: IFSC code mismatch" }),
274
+ ],
234
275
  },
235
276
  // 10. Beyblade Arena — Jan 2026 — UPI limit exceeded
236
277
  {
@@ -251,5 +292,11 @@ export const payoutsSeedData = [
251
292
  processedAt: daysAgo(108),
252
293
  createdAt: daysAgo(110),
253
294
  updatedAt: daysAgo(108),
295
+ statusHistory: [
296
+ entry(daysAgo(109), "system", "payoutBatch:dispatch", {
297
+ status: { from: "pending", to: "processing" },
298
+ }),
299
+ entry(daysAgo(108), "system", "payoutBatch:failedFinal", { status: { from: "processing", to: "failed" }, lastFailureReason: { from: null, to: "Razorpay 400: UPI daily limit exceeded" } }, { reason: "Razorpay 400: UPI daily limit exceeded" }),
300
+ ],
254
301
  },
255
302
  ];
@@ -21,6 +21,17 @@ import { SCAMMER_FIELDS } from "../constants/field-names";
21
21
  import { seedExtMedia } from "./_helpers/media";
22
22
  const NOW = new Date("2026-05-10T00:00:00.000Z");
23
23
  const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
24
+ /**
25
+ * A timeline entry for the W18 `statusHistory` fixtures.
26
+ *
27
+ * 🛑 `actorUid` only. A scammer profile is built ENTIRELY out of identifying
28
+ * details, which is why `SCAMMER_HISTORY_PII_FIELDS` is the longest scrub
29
+ * list of any adopter — a fixture carrying a name here would be modelling the
30
+ * exact leak the primitive exists to prevent.
31
+ */
32
+ function entry(at, actorRole, trigger, changes, extra) {
33
+ return { at, actorRole, trigger, changes, ...extra };
34
+ }
24
35
  export const scammersSeedData = [
25
36
  // ── 1. Verified — advance payment ghost ──────────────────────────────────────
26
37
  {
@@ -120,6 +131,29 @@ export const scammersSeedData = [
120
131
  isContested: false,
121
132
  createdAt: daysAgo(15),
122
133
  updatedAt: daysAgo(8),
134
+ /*
135
+ * pending_review -> rejected, with the note that decided it.
136
+ *
137
+ * `verifiedBy`/`verifiedAt` hold only the LAST decision, so a profile
138
+ * that was verified, removed, then re-verified kept no trace of the
139
+ * middle. This is the fixture the "decision history" panel reads.
140
+ */
141
+ statusHistory: [
142
+ entry(daysAgo(15), "system", "publicScamReport", {
143
+ status: { from: null, to: "pending_review" },
144
+ }),
145
+ entry(daysAgo(8), "admin", "adminScammerPatch", {
146
+ status: { from: "pending_review", to: "rejected" },
147
+ verifiedBy: { from: null, to: "user-admin-letitrip" },
148
+ verificationNote: {
149
+ from: null,
150
+ to: "Seller provided courier proof. Likely delivery dispute, not fraud. Rejected.",
151
+ },
152
+ }, {
153
+ actorUid: "user-admin-letitrip",
154
+ reason: "Seller provided courier proof. Likely delivery dispute, not fraud. Rejected.",
155
+ }),
156
+ ],
123
157
  },
124
158
  // ── 4. Verified — advance payment ghost (same scamType as #1, different person) ──
125
159
  {
@@ -19,6 +19,17 @@ import { STORE_FIELDS } from "../constants/field-names";
19
19
  import { seedExtMedia } from "./_helpers/media";
20
20
  const NOW = new Date();
21
21
  const daysAgo = (n) => new Date(NOW.getTime() - n * 86400000);
22
+ /**
23
+ * A timeline entry for the W18 `statusHistory` fixtures.
24
+ *
25
+ * Dates come from this file's own `daysAgo()`, never `Date.now()` in an ID
26
+ * position (Root Cause #25). `actorUid` only, never a name — `withHistory`
27
+ * scrubs PII from real writes, and a fixture carrying a name would be the one
28
+ * place that rule is broken.
29
+ */
30
+ function entry(at, actorRole, trigger, changes, extra) {
31
+ return { at, actorRole, trigger, changes, ...extra };
32
+ }
22
33
  export const storesSeedData = [
23
34
  // ── Store 1: LetItRip Official (admin-owned, platform storefront) ──────────
24
35
  {
@@ -226,5 +237,28 @@ export const storesSeedData = [
226
237
  capabilities: ["suggest_brands"],
227
238
  createdAt: daysAgo(220),
228
239
  updatedAt: daysAgo(9),
240
+ /*
241
+ * active -> suspended, with the admin who did it and why. This is the
242
+ * fixture behind "a suspended store's page shows who suspended it" — the
243
+ * answer was previously only in adminAuditLog, which the store owner can
244
+ * never see.
245
+ *
246
+ * `isPublic` moves WITH `status`, which is the invariant the admin PATCH
247
+ * route was breaking until 2026-08-26 (it wrote status alone, leaving an
248
+ * approved store active and invisible).
249
+ */
250
+ statusHistory: [
251
+ entry(daysAgo(9), "admin", "adminStorePatch", {
252
+ status: { from: "active", to: "suspended" },
253
+ isPublic: { from: true, to: false },
254
+ suspensionReason: {
255
+ from: null,
256
+ to: "Three listings flagged as possible reproductions; suspended while authenticity documentation is reviewed.",
257
+ },
258
+ }, {
259
+ actorUid: "user-admin-letitrip",
260
+ reason: "Three listings flagged as possible reproductions; suspended while authenticity documentation is reviewed.",
261
+ }),
262
+ ],
229
263
  },
230
264
  ];
@@ -21,6 +21,16 @@ const BASE = Date.now();
21
21
  function daysBack(n) {
22
22
  return new Date(BASE - n * 24 * 60 * 60 * 1000);
23
23
  }
24
+ /**
25
+ * A timeline entry for the W18 `statusHistory` fixtures.
26
+ *
27
+ * `actorUid` only, never a name — `withHistory` scrubs
28
+ * SUPPORT_TICKET_PII_FIELDS (which includes `assignedToName`) from real
29
+ * writes, so a fixture carrying one would be the single place that is untrue.
30
+ */
31
+ function entry(at, actorRole, trigger, changes, extra) {
32
+ return { at, actorRole, trigger, changes, ...extra };
33
+ }
24
34
  export const supportTicketsSeedData = [
25
35
  // ── 1. Order issue — in progress ─────────────────────────────────────────
26
36
  {
@@ -100,6 +110,30 @@ export const supportTicketsSeedData = [
100
110
  msg("msg-yugi-003-s2", "user-admin-letitrip", "support", "Refund of ₹2,499 has been processed back to your original payment method. This ticket is now resolved.", 8),
101
111
  ],
102
112
  resolvedAt: daysBack(8),
113
+ /*
114
+ * open -> in_progress -> resolved, with the assignment in between.
115
+ *
116
+ * `resolvedAt` above was ALREADY in this fixture, and no code path had
117
+ * ever written it (verified 2026-08-26) — the seed described a shape the
118
+ * application could not produce. The repository stamps it on the
119
+ * transition now, so this fixture and reality finally agree.
120
+ */
121
+ statusHistory: [
122
+ entry(daysBack(12), "buyer", "createTicket", {
123
+ status: { from: null, to: "open" },
124
+ }, { actorUid: "user-yugi-muto" }),
125
+ entry(daysBack(11), "admin", "assignTicket", {
126
+ assignedTo: { from: null, to: "user-admin-letitrip" },
127
+ status: { from: "open", to: "in_progress" },
128
+ }, { actorUid: "user-admin-letitrip" }),
129
+ entry(daysBack(8), "admin", "adminUpdateTicket", {
130
+ status: { from: "in_progress", to: "resolved" },
131
+ resolvedAt: { from: null, to: "2026-08-18T00:00:00.000Z" },
132
+ }, {
133
+ actorUid: "user-admin-letitrip",
134
+ reason: "Return accepted and refund processed.",
135
+ }),
136
+ ],
103
137
  createdAt: daysBack(12),
104
138
  updatedAt: daysBack(8),
105
139
  },
@@ -67,6 +67,7 @@ export { getSearchResults, searchAction, type SearchQuery, } from "./_internal/s
67
67
  export { getCategoryForDetail, listRootCategories, listFeaturedCategories, listMenuCategories, getCategoryTree, listSitemapCategories, CATEGORIES_PAGE_SIZE, CATEGORIES_ROOT_TIER, CATEGORIES_MAX_DEPTH, CATEGORIES_SITEMAP_LIMIT, CATEGORIES_FEATURED_LIMIT, CATEGORIES_MENU_LIMIT, } from "./_internal/server/features/categories/index";
68
68
  export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, getGroupsForProduct, getGroupsWithItemsForProduct, GROUPED_LISTINGS_PAGE_SIZE, GROUPED_LISTINGS_FEATURED_LIMIT, GROUPED_LISTINGS_SITEMAP_LIMIT, type GroupedListingWithItems, type ListGroupedListingsParams, type SitemapGroupedListing, } from "./_internal/server/features/grouped/index";
69
69
  export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, previewCheckoutPricing, resolveShippingCost, formatShippingAddress, CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, type CreateCheckoutOrderInput, type VerifyAndPlaceRazorpayOrderInput, type CheckoutOrderResult, type CheckoutPaymentMethod, type CheckoutPricingPreviewInput, type CheckoutPricingPreview, } from "./_internal/server/features/checkout/index";
70
+ export { unitPriceFor, lineTotalFor, computePreOrderDepositAmount, } from "./_internal/shared/checkout/order-math";
70
71
  export { processRefundAction, type ProcessRefundInput, } from "./_internal/server/features/refunds/actions";
71
72
  export { createPaymentIntentAction, verifyPaymentSignatureAction, resolvePaymentFee, PAYMENTS_DEFAULT_PLATFORM_FEE_PERCENT, PAYMENTS_DEFAULT_GST_PERCENT, PAYMENTS_RECEIPT_PREFIX, type CreatePaymentIntentInput, type CreatePaymentIntentResult, type VerifyPaymentSignatureInput, type ResolvedPaymentFee, } from "./_internal/server/features/payments/index";
72
73
  export { getSublistingCategoryForDetail } from "./_internal/server/features/sublisting-categories/index";
@@ -97,6 +97,11 @@ export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedList
97
97
  // `.listByType("bundle", opts)` / `.findById(id)` directly.
98
98
  // S4: checkout actions
99
99
  export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, previewCheckoutPricing, resolveShippingCost, formatShippingAddress, CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, } from "./_internal/server/features/checkout/index";
100
+ // The one definition of "what does this cart line cost". Exported because the
101
+ // consumer's Razorpay create-order route needs it: that route used to hand-roll
102
+ // the rule and reproduced only the bundle branch, so it captured the LIST price
103
+ // for an accepted offer. Pure math — no Firestore, no server-only imports.
104
+ export { unitPriceFor, lineTotalFor, computePreOrderDepositAmount, } from "./_internal/shared/checkout/order-math";
100
105
  // S-SBUNI-RULES: refund action
101
106
  export { processRefundAction, } from "./_internal/server/features/refunds/actions";
102
107
  // S4: payments actions
package/dist/server.d.ts CHANGED
@@ -231,7 +231,7 @@ export { getLatestBlogPosts } from "./features/blog/server";
231
231
  export { listBlogPosts } from "./features/blog/server";
232
232
  export { updateBlogPost } from "./features/blog/server";
233
233
  export { updateBlogPostSchema } from "./features/blog/server";
234
- export { addItemToCart } from "./features/cart/server";
234
+ export { addItemToCart, assertCanAddNewItems, addGroupLineToCart, updateCartGroupMembers, GROUP_LINE_MAX_MEMBERS, type AddGroupLineToCartInput, } from "./features/cart/server";
235
235
  export { clearCart } from "./features/cart/server";
236
236
  export { getCart } from "./features/cart/server";
237
237
  export { mergeGuestCart } from "./features/cart/server";
@@ -573,3 +573,7 @@ export { toClientLotterySlot, toClientLotteryConfig } from "./_internal/server/f
573
573
  export { buildLotteryMetadata } from "./_internal/server/features/lottery/metadata";
574
574
  export { submitLotteryPullAction, flagLotteryEntryAction, reopenLotterySlotAction, createLotteryEventAction, updateLotteryEventAction, setProductLotteryModeAction, } from "./_internal/server/features/lottery/actions";
575
575
  export { lotteryEntryRepository } from "./features/lottery/repository/lottery-entry.repository";
576
+ export { renderNotificationEmail, NOTIFICATION_EMAIL_TEMPLATES, } from "./features/email/notification-templates";
577
+ export type { NotificationTemplateDef, NotificationEmailContext, } from "./features/email/notification-templates";
578
+ export { resolveNotificationActionUrl, } from "./_internal/shared/features/notifications/action-url";
579
+ export type { NotificationAudience } from "./_internal/shared/features/notifications/action-url";
package/dist/server.js CHANGED
@@ -658,7 +658,8 @@ export { updateBlogPost } from "./features/blog/server";
658
658
  export { updateBlogPostSchema } from "./features/blog/server";
659
659
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
660
660
  // addItemToCart - Helper for add item to cart.
661
- export { addItemToCart } from "./features/cart/server";
661
+ // assertCanAddNewItems - the lane gate every add-to-cart entry point must run.
662
+ export { addItemToCart, assertCanAddNewItems, addGroupLineToCart, updateCartGroupMembers, GROUP_LINE_MAX_MEMBERS, } from "./features/cart/server";
662
663
  // [SERVER-ONLY]-Server-only — uses Node.js, Next.js server internals, or third-party server SDKs (auth, email, payment, shipping).
663
664
  // clearCart - Shared export for clear cart.
664
665
  export { clearCart } from "./features/cart/server";
@@ -1444,3 +1445,11 @@ export { toClientLotterySlot, toClientLotteryConfig } from "./_internal/server/f
1444
1445
  export { buildLotteryMetadata } from "./_internal/server/features/lottery/metadata";
1445
1446
  export { submitLotteryPullAction, flagLotteryEntryAction, reopenLotterySlotAction, createLotteryEventAction, updateLotteryEventAction, setProductLotteryModeAction, } from "./_internal/server/features/lottery/actions";
1446
1447
  export { lotteryEntryRepository } from "./features/lottery/repository/lottery-entry.repository";
1448
+ /*
1449
+ * Notification email templates + the shared actionUrl resolver.
1450
+ *
1451
+ * Server-only: the templates call `renderToStaticMarkup` through a lazy
1452
+ * `require`, which must never end up reachable from a client chunk.
1453
+ */
1454
+ export { renderNotificationEmail, NOTIFICATION_EMAIL_TEMPLATES, } from "./features/email/notification-templates";
1455
+ export { resolveNotificationActionUrl, } from "./_internal/shared/features/notifications/action-url";