@mohasinac/appkit 3.0.6 → 3.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 (186) hide show
  1. package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +42 -0
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +86 -0
  3. package/dist/_internal/client/features/lottery/LotteryAdminSlotView.d.ts +14 -0
  4. package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +46 -0
  5. package/dist/_internal/client/features/lottery/LotteryDetailView.d.ts +28 -0
  6. package/dist/_internal/client/features/lottery/LotteryDetailView.js +17 -0
  7. package/dist/_internal/client/features/lottery/LotteryEntriesView.d.ts +23 -0
  8. package/dist/_internal/client/features/lottery/LotteryEntriesView.js +58 -0
  9. package/dist/_internal/client/features/lottery/LotteryListView.d.ts +21 -0
  10. package/dist/_internal/client/features/lottery/LotteryListView.js +25 -0
  11. package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +19 -0
  12. package/dist/_internal/client/features/lottery/LotteryPullForm.js +65 -0
  13. package/dist/_internal/client/features/lottery/LotterySlotGrid.d.ts +13 -0
  14. package/dist/_internal/client/features/lottery/LotterySlotGrid.js +21 -0
  15. package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.d.ts +28 -0
  16. package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.js +18 -0
  17. package/dist/_internal/server/features/auctions/__tests__/service.test.d.ts +1 -0
  18. package/dist/_internal/server/features/auctions/__tests__/service.test.js +140 -0
  19. package/dist/_internal/server/features/checkout/__tests__/actions.test.d.ts +1 -0
  20. package/dist/_internal/server/features/checkout/__tests__/actions.test.js +600 -0
  21. package/dist/_internal/server/features/lottery/actions.d.ts +43 -0
  22. package/dist/_internal/server/features/lottery/actions.js +243 -0
  23. package/dist/_internal/server/features/lottery/adapters.d.ts +9 -0
  24. package/dist/_internal/server/features/lottery/adapters.js +21 -0
  25. package/dist/_internal/server/features/lottery/data.d.ts +20 -0
  26. package/dist/_internal/server/features/lottery/data.js +41 -0
  27. package/dist/_internal/server/features/lottery/metadata.d.ts +6 -0
  28. package/dist/_internal/server/features/lottery/metadata.js +31 -0
  29. package/dist/_internal/server/features/refunds/__tests__/actions.test.d.ts +1 -0
  30. package/dist/_internal/server/features/refunds/__tests__/actions.test.js +263 -0
  31. package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.d.ts +1 -0
  32. package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.js +249 -0
  33. package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.d.ts +1 -0
  34. package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.js +201 -0
  35. package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.d.ts +1 -0
  36. package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.js +169 -0
  37. package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.d.ts +1 -0
  38. package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.js +148 -0
  39. package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.d.ts +1 -0
  40. package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.js +210 -0
  41. package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.d.ts +1 -0
  42. package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.js +190 -0
  43. package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.d.ts +1 -0
  44. package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.js +161 -0
  45. package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.d.ts +1 -0
  46. package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.js +302 -0
  47. package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.d.ts +1 -0
  48. package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.js +252 -0
  49. package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.d.ts +1 -0
  50. package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.js +129 -0
  51. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  52. package/dist/_internal/shared/actions/action-registry.js +46 -0
  53. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  54. package/dist/client.d.ts +9 -0
  55. package/dist/client.js +9 -0
  56. package/dist/constants/field-names.d.ts +5 -1
  57. package/dist/constants/field-names.js +5 -1
  58. package/dist/features/addresses/repository/__tests__/addresses.repository.test.d.ts +1 -0
  59. package/dist/features/addresses/repository/__tests__/addresses.repository.test.js +386 -0
  60. package/dist/features/admin/api/bids/route.d.ts +2 -2
  61. package/dist/features/admin/api/coupons/route.d.ts +2 -2
  62. package/dist/features/admin/api/products/route.d.ts +2 -2
  63. package/dist/features/admin/api/reviews/route.d.ts +2 -2
  64. package/dist/features/admin/repository/__tests__/notification.repository.test.d.ts +1 -0
  65. package/dist/features/admin/repository/__tests__/notification.repository.test.js +356 -0
  66. package/dist/features/admin/repository/__tests__/site-settings.repository.test.d.ts +1 -0
  67. package/dist/features/admin/repository/__tests__/site-settings.repository.test.js +298 -0
  68. package/dist/features/auctions/repository/__tests__/bid.repository.test.d.ts +1 -0
  69. package/dist/features/auctions/repository/__tests__/bid.repository.test.js +290 -0
  70. package/dist/features/auth/api/route.d.ts +2 -2
  71. package/dist/features/auth/repository/__tests__/session.repository.test.d.ts +1 -0
  72. package/dist/features/auth/repository/__tests__/session.repository.test.js +310 -0
  73. package/dist/features/auth/repository/__tests__/session.test.d.ts +1 -0
  74. package/dist/features/auth/repository/__tests__/session.test.js +146 -0
  75. package/dist/features/auth/repository/__tests__/sms-counter.repository.test.d.ts +1 -0
  76. package/dist/features/auth/repository/__tests__/sms-counter.repository.test.js +171 -0
  77. package/dist/features/auth/repository/__tests__/sms-counter.test.d.ts +1 -0
  78. package/dist/features/auth/repository/__tests__/sms-counter.test.js +88 -0
  79. package/dist/features/auth/repository/__tests__/token.repository.test.d.ts +1 -0
  80. package/dist/features/auth/repository/__tests__/token.repository.test.js +260 -0
  81. package/dist/features/auth/repository/__tests__/token.test.d.ts +1 -0
  82. package/dist/features/auth/repository/__tests__/token.test.js +140 -0
  83. package/dist/features/auth/repository/__tests__/user.repository.test.d.ts +1 -0
  84. package/dist/features/auth/repository/__tests__/user.repository.test.js +352 -0
  85. package/dist/features/auth/repository/__tests__/user.test.d.ts +1 -0
  86. package/dist/features/auth/repository/__tests__/user.test.js +226 -0
  87. package/dist/features/auth/repository/session.repository.js +10 -1
  88. package/dist/features/cart/repository/__tests__/cart.repository.test.d.ts +1 -0
  89. package/dist/features/cart/repository/__tests__/cart.repository.test.js +371 -0
  90. package/dist/features/cart/repository/cart.repository.js +2 -2
  91. package/dist/features/categories/api/[id]/route.d.ts +4 -4
  92. package/dist/features/categories/api/route.d.ts +2 -2
  93. package/dist/features/categories/repository/__tests__/categories.repository.test.d.ts +1 -0
  94. package/dist/features/categories/repository/__tests__/categories.repository.test.js +813 -0
  95. package/dist/features/events/components/EventCard.js +1 -0
  96. package/dist/features/events/repository/__tests__/event.repository.test.d.ts +1 -0
  97. package/dist/features/events/repository/__tests__/event.repository.test.js +299 -0
  98. package/dist/features/events/schemas/firestore.d.ts +3 -0
  99. package/dist/features/events/schemas/index.d.ts +834 -14
  100. package/dist/features/events/schemas/index.js +80 -1
  101. package/dist/features/events/types/index.d.ts +4 -1
  102. package/dist/features/faq/repository/__tests__/faqs.repository.test.d.ts +1 -0
  103. package/dist/features/faq/repository/__tests__/faqs.repository.test.js +311 -0
  104. package/dist/features/history/repository/__tests__/user-history.repository.test.d.ts +1 -0
  105. package/dist/features/history/repository/__tests__/user-history.repository.test.js +180 -0
  106. package/dist/features/homepage/api/[id]/route.d.ts +4 -4
  107. package/dist/features/homepage/api/carousel/[id]/route.d.ts +4 -4
  108. package/dist/features/homepage/api/carousel/route.d.ts +2 -2
  109. package/dist/features/homepage/api/route.d.ts +2 -2
  110. package/dist/features/homepage/repository/__tests__/carousel.repository.test.d.ts +1 -0
  111. package/dist/features/homepage/repository/__tests__/carousel.repository.test.js +182 -0
  112. package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.d.ts +1 -0
  113. package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.js +185 -0
  114. package/dist/features/lottery/repository/lottery-entry.repository.d.ts +33 -0
  115. package/dist/features/lottery/repository/lottery-entry.repository.js +130 -0
  116. package/dist/features/lottery/schemas/firestore.d.ts +49 -0
  117. package/dist/features/lottery/schemas/firestore.js +25 -0
  118. package/dist/features/lottery/schemas/zod.d.ts +113 -0
  119. package/dist/features/lottery/schemas/zod.js +39 -0
  120. package/dist/features/lottery/types/index.d.ts +67 -0
  121. package/dist/features/lottery/types/index.js +55 -0
  122. package/dist/features/messages/repository/__tests__/conversations.repository.test.d.ts +1 -0
  123. package/dist/features/messages/repository/__tests__/conversations.repository.test.js +257 -0
  124. package/dist/features/orders/repository/__tests__/orders.repository.test.d.ts +1 -0
  125. package/dist/features/orders/repository/__tests__/orders.repository.test.js +211 -0
  126. package/dist/features/payments/repository/__tests__/payout.repository.test.d.ts +1 -0
  127. package/dist/features/payments/repository/__tests__/payout.repository.test.js +292 -0
  128. package/dist/features/products/api/[id]/route.d.ts +4 -4
  129. package/dist/features/products/api/route.d.ts +2 -2
  130. package/dist/features/products/repository/__tests__/product-features.repository.test.d.ts +1 -0
  131. package/dist/features/products/repository/__tests__/product-features.repository.test.js +329 -0
  132. package/dist/features/products/repository/__tests__/products.repository.test.d.ts +1 -0
  133. package/dist/features/products/repository/__tests__/products.repository.test.js +607 -0
  134. package/dist/features/products/schemas/firestore.d.ts +15 -1
  135. package/dist/features/products/schemas/index.d.ts +12 -12
  136. package/dist/features/products/schemas/index.js +4 -2
  137. package/dist/features/products/types/index.d.ts +1 -1
  138. package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.d.ts +1 -0
  139. package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.js +219 -0
  140. package/dist/features/promotions/repository/__tests__/coupons.repository.test.d.ts +1 -0
  141. package/dist/features/promotions/repository/__tests__/coupons.repository.test.js +480 -0
  142. package/dist/features/reviews/api/[id]/route.d.ts +4 -4
  143. package/dist/features/reviews/api/route.d.ts +2 -2
  144. package/dist/features/reviews/repository/__tests__/reviews.repository.test.d.ts +1 -0
  145. package/dist/features/reviews/repository/__tests__/reviews.repository.test.js +327 -0
  146. package/dist/features/scams/repository/__tests__/scammer.repository.test.d.ts +1 -0
  147. package/dist/features/scams/repository/__tests__/scammer.repository.test.js +385 -0
  148. package/dist/features/search/repository/__tests__/search.repository.test.d.ts +1 -0
  149. package/dist/features/search/repository/__tests__/search.repository.test.js +369 -0
  150. package/dist/features/seller/api/coupons/route.d.ts +2 -2
  151. package/dist/features/seller/api/offers/route.d.ts +2 -2
  152. package/dist/features/seller/api/products/route.d.ts +2 -2
  153. package/dist/features/seller/api/store/route.d.ts +2 -2
  154. package/dist/features/seller/repository/__tests__/offer.repository.test.d.ts +1 -0
  155. package/dist/features/seller/repository/__tests__/offer.repository.test.js +458 -0
  156. package/dist/features/stores/repository/__tests__/store.repository.test.d.ts +1 -0
  157. package/dist/features/stores/repository/__tests__/store.repository.test.js +261 -0
  158. package/dist/features/support/repository/__tests__/support.repository.test.d.ts +1 -0
  159. package/dist/features/support/repository/__tests__/support.repository.test.js +312 -0
  160. package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.d.ts +1 -0
  161. package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.js +139 -0
  162. package/dist/index.d.ts +14 -0
  163. package/dist/index.js +17 -0
  164. package/dist/next/api/routeHandler.d.ts +1 -1
  165. package/dist/next/routing/route-map.d.ts +13 -0
  166. package/dist/next/routing/route-map.js +6 -0
  167. package/dist/repositories/index.d.ts +1 -0
  168. package/dist/repositories/index.js +2 -0
  169. package/dist/security/index.d.ts +1 -1
  170. package/dist/security/index.js +1 -1
  171. package/dist/security/pii-schemas.d.ts +2 -0
  172. package/dist/security/pii-schemas.js +6 -0
  173. package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
  174. package/dist/seed/events-seed-data.js +85 -2
  175. package/dist/seed/grouped-listings-seed-data.d.ts +3 -1
  176. package/dist/seed/index.d.ts +1 -0
  177. package/dist/seed/index.js +1 -0
  178. package/dist/seed/lottery-entries-seed-data.d.ts +2 -0
  179. package/dist/seed/lottery-entries-seed-data.js +108 -0
  180. package/dist/seed/manifest.js +1 -0
  181. package/dist/seed/products-preorders-seed-data.d.ts +3 -1
  182. package/dist/server.d.ts +6 -0
  183. package/dist/server.js +6 -0
  184. package/dist/styles.css +1 -1
  185. package/dist/tailwind-utilities.css +1 -1
  186. package/package.json +1 -1
@@ -92,8 +92,8 @@ export declare const productItemSchema: z.ZodObject<{
92
92
  categorySlug: z.ZodOptional<z.ZodString>;
93
93
  sellerAvatar: z.ZodOptional<z.ZodString>;
94
94
  status: z.ZodEnum<["draft", "published", "in_review", "archived"]>;
95
- condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>;
96
- listingType: z.ZodOptional<z.ZodEnum<["standard", "auction", "pre-order", "prize-draw", "bundle"]>>;
95
+ condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken", "graded"]>>;
96
+ listingType: z.ZodOptional<z.ZodEnum<["standard", "auction", "pre-order", "prize-draw", "classified", "digital-code", "live"]>>;
97
97
  maxPerUser: z.ZodOptional<z.ZodNumber>;
98
98
  partOfBundleIds: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
99
99
  partOfBundleTitles: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
@@ -296,7 +296,7 @@ export declare const productItemSchema: z.ZodObject<{
296
296
  storeId?: string | undefined;
297
297
  category?: string | undefined;
298
298
  featured?: boolean | undefined;
299
- listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | undefined;
299
+ listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "classified" | "digital-code" | "live" | undefined;
300
300
  isPromoted?: boolean | undefined;
301
301
  createdAt?: string | undefined;
302
302
  description?: string | undefined;
@@ -316,7 +316,7 @@ export declare const productItemSchema: z.ZodObject<{
316
316
  features?: string[] | undefined;
317
317
  shippingInfo?: string | undefined;
318
318
  returnPolicy?: string | undefined;
319
- condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
319
+ condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | "graded" | undefined;
320
320
  insurance?: boolean | undefined;
321
321
  insuranceCost?: number | undefined;
322
322
  shippingPaidBy?: "seller" | "buyer" | undefined;
@@ -425,7 +425,7 @@ export declare const productItemSchema: z.ZodObject<{
425
425
  storeId?: string | undefined;
426
426
  category?: string | undefined;
427
427
  featured?: boolean | undefined;
428
- listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | undefined;
428
+ listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "classified" | "digital-code" | "live" | undefined;
429
429
  isPromoted?: boolean | undefined;
430
430
  createdAt?: string | undefined;
431
431
  description?: string | undefined;
@@ -445,7 +445,7 @@ export declare const productItemSchema: z.ZodObject<{
445
445
  features?: string[] | undefined;
446
446
  shippingInfo?: string | undefined;
447
447
  returnPolicy?: string | undefined;
448
- condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
448
+ condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | "graded" | undefined;
449
449
  insurance?: boolean | undefined;
450
450
  insuranceCost?: number | undefined;
451
451
  shippingPaidBy?: "seller" | "buyer" | undefined;
@@ -547,11 +547,11 @@ export declare const productListParamsSchema: z.ZodObject<{
547
547
  q: z.ZodOptional<z.ZodString>;
548
548
  category: z.ZodOptional<z.ZodString>;
549
549
  status: z.ZodOptional<z.ZodEnum<["draft", "published", "in_review", "archived"]>>;
550
- condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken"]>>;
550
+ condition: z.ZodOptional<z.ZodEnum<["new", "like_new", "good", "fair", "poor", "used", "refurbished", "broken", "graded"]>>;
551
551
  minPrice: z.ZodOptional<z.ZodNumber>;
552
552
  maxPrice: z.ZodOptional<z.ZodNumber>;
553
553
  inStock: z.ZodOptional<z.ZodBoolean>;
554
- listingType: z.ZodOptional<z.ZodEnum<["standard", "auction", "pre-order", "prize-draw", "bundle"]>>;
554
+ listingType: z.ZodOptional<z.ZodEnum<["standard", "auction", "pre-order", "prize-draw", "classified", "digital-code", "live"]>>;
555
555
  storeId: z.ZodOptional<z.ZodString>;
556
556
  sort: z.ZodOptional<z.ZodString>;
557
557
  page: z.ZodOptional<z.ZodNumber>;
@@ -563,8 +563,8 @@ export declare const productListParamsSchema: z.ZodObject<{
563
563
  storeId?: string | undefined;
564
564
  category?: string | undefined;
565
565
  featured?: boolean | undefined;
566
- listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | undefined;
567
- condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
566
+ listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "classified" | "digital-code" | "live" | undefined;
567
+ condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | "graded" | undefined;
568
568
  page?: number | undefined;
569
569
  q?: string | undefined;
570
570
  perPage?: number | undefined;
@@ -577,8 +577,8 @@ export declare const productListParamsSchema: z.ZodObject<{
577
577
  storeId?: string | undefined;
578
578
  category?: string | undefined;
579
579
  featured?: boolean | undefined;
580
- listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "bundle" | undefined;
581
- condition?: "new" | "used" | "refurbished" | "broken" | "like_new" | "good" | "fair" | "poor" | undefined;
580
+ listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "classified" | "digital-code" | "live" | undefined;
581
+ condition?: "new" | "used" | "refurbished" | "like_new" | "good" | "fair" | "poor" | "broken" | "graded" | undefined;
582
582
  page?: number | undefined;
583
583
  q?: string | undefined;
584
584
  perPage?: number | undefined;
@@ -64,11 +64,12 @@ export const productItemSchema = z.object({
64
64
  "used",
65
65
  "refurbished",
66
66
  "broken",
67
+ "graded",
67
68
  ])
68
69
  .optional(),
69
70
  // SB1-G Phase 4 — canonical discriminator (legacy `isAuction`/`isPreOrder` booleans removed).
70
71
  listingType: z
71
- .enum(["standard", "auction", "pre-order", "prize-draw", "bundle"])
72
+ .enum(["standard", "auction", "pre-order", "prize-draw", "classified", "digital-code", "live"])
72
73
  .optional(),
73
74
  // SB1-C — prize-draw + bundle fields. All optional.
74
75
  maxPerUser: z.number().int().positive().optional(),
@@ -205,13 +206,14 @@ export const productListParamsSchema = z.object({
205
206
  "used",
206
207
  "refurbished",
207
208
  "broken",
209
+ "graded",
208
210
  ])
209
211
  .optional(),
210
212
  minPrice: z.coerce.number().optional(),
211
213
  maxPrice: z.coerce.number().optional(),
212
214
  inStock: z.coerce.boolean().optional(),
213
215
  listingType: z
214
- .enum(["standard", "auction", "pre-order", "prize-draw", "bundle"])
216
+ .enum(["standard", "auction", "pre-order", "prize-draw", "classified", "digital-code", "live"])
215
217
  .optional(),
216
218
  storeId: z.string().optional(),
217
219
  sort: z.string().optional(),
@@ -2,7 +2,7 @@ import type { MediaField } from "../../media/types/index";
2
2
  import type { CustomField, CustomSection } from "../schemas/firestore";
3
3
  export type { CustomField, CustomSection } from "../schemas/firestore";
4
4
  export type ProductStatus = "draft" | "published" | "in_review" | "archived";
5
- export type ProductCondition = "new" | "like_new" | "good" | "fair" | "poor" | "used" | "refurbished" | "broken";
5
+ export type ProductCondition = "new" | "like_new" | "good" | "fair" | "poor" | "used" | "refurbished" | "broken" | "graded";
6
6
  /**
7
7
  * Canonical listing-kind discriminator (SB1-G Phase 4).
8
8
  *
@@ -0,0 +1,219 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { makeMockDb, makeSnap, makeQuerySnap } from "../../../../../tests/helpers/mock-firestore";
3
+ const { db, mockDocRef, mockCollection, mockQuery } = makeMockDb();
4
+ vi.mock("../../../../providers/db-firebase/admin", () => ({
5
+ getAdminDb: () => db,
6
+ }));
7
+ vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
8
+ const actual = await importOriginal();
9
+ return {
10
+ ...actual,
11
+ prepareForFirestore: (d) => d,
12
+ };
13
+ });
14
+ vi.mock("../../../../errors/normalize", () => ({
15
+ normalizeError: vi.fn(),
16
+ }));
17
+ import { ClaimedCouponsRepository } from "../claimed-coupons.repository";
18
+ import { createClaimedCouponId } from "../../schemas";
19
+ const repo = new ClaimedCouponsRepository();
20
+ function makeClaimInput(overrides = {}) {
21
+ return {
22
+ userId: "user-yugi",
23
+ couponId: "coupon-welcome10",
24
+ couponCode: "welcome10",
25
+ source: "manual",
26
+ couponSnapshot: {
27
+ name: "10% Off Coupon",
28
+ type: "percentage",
29
+ scope: "admin",
30
+ discount: { value: 10, maxDiscount: 0, minPurchase: 0 },
31
+ restrictions: { firstTimeUserOnly: false, combineWithSellerCoupons: true },
32
+ },
33
+ expiresAt: null,
34
+ ...overrides,
35
+ };
36
+ }
37
+ function makeClaimDoc(userId = "user-yugi", code = "WELCOME10") {
38
+ return {
39
+ id: createClaimedCouponId(userId, code),
40
+ userId,
41
+ couponId: "coupon-welcome10",
42
+ couponCode: code,
43
+ source: "manual",
44
+ status: "active",
45
+ expiresAt: null,
46
+ claimedAt: new Date(),
47
+ updatedAt: new Date(),
48
+ couponSnapshot: {},
49
+ };
50
+ }
51
+ beforeEach(() => {
52
+ vi.clearAllMocks();
53
+ db.collection.mockReturnValue(mockCollection);
54
+ mockCollection.doc.mockReturnValue(mockDocRef);
55
+ mockCollection.where.mockReturnValue(mockQuery);
56
+ mockQuery.where.mockReturnValue(mockQuery);
57
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
58
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
59
+ mockDocRef.set.mockResolvedValue(undefined);
60
+ mockDocRef.update.mockResolvedValue(undefined);
61
+ });
62
+ // ---------------------------------------------------------------------------
63
+ // findByUserAndCode
64
+ // ---------------------------------------------------------------------------
65
+ describe("ClaimedCouponsRepository.findByUserAndCode", () => {
66
+ it("existing claim → returns document", async () => {
67
+ const doc = makeClaimDoc();
68
+ mockDocRef.get.mockResolvedValue(makeSnap(doc, doc.id));
69
+ const result = await repo.findByUserAndCode("user-yugi", "WELCOME10");
70
+ expect(result).not.toBeNull();
71
+ expect(result.couponCode).toBe("WELCOME10");
72
+ });
73
+ it("no claim → returns null", async () => {
74
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
75
+ const result = await repo.findByUserAndCode("user-yugi", "NOTFOUND");
76
+ expect(result).toBeNull();
77
+ });
78
+ it("uses deterministic ID based on userId + couponCode", async () => {
79
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
80
+ await repo.findByUserAndCode("user-yugi", "WELCOME10");
81
+ const expectedId = createClaimedCouponId("user-yugi", "WELCOME10");
82
+ expect(mockCollection.doc).toHaveBeenCalledWith(expectedId);
83
+ });
84
+ });
85
+ // ---------------------------------------------------------------------------
86
+ // claim
87
+ // ---------------------------------------------------------------------------
88
+ describe("ClaimedCouponsRepository.claim", () => {
89
+ it("first call → creates record with status: active", async () => {
90
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
91
+ const result = await repo.claim(makeClaimInput());
92
+ expect(result.status).toBe("active");
93
+ expect(mockDocRef.set).toHaveBeenCalledOnce();
94
+ });
95
+ it("second call for same user + code → returns existing record (idempotent)", async () => {
96
+ const existingDoc = makeClaimDoc();
97
+ mockDocRef.get.mockResolvedValue(makeSnap(existingDoc, existingDoc.id));
98
+ const result = await repo.claim(makeClaimInput());
99
+ expect(mockDocRef.set).not.toHaveBeenCalled();
100
+ expect(result.id).toBe(existingDoc.id);
101
+ });
102
+ it("couponCode normalized to uppercase on create", async () => {
103
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
104
+ const result = await repo.claim(makeClaimInput({ couponCode: "welcome10" }));
105
+ expect(result.couponCode).toBe("WELCOME10");
106
+ });
107
+ it("expiresAt stored as provided (null case)", async () => {
108
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
109
+ const result = await repo.claim(makeClaimInput({ expiresAt: null }));
110
+ expect(result.expiresAt).toBeNull();
111
+ });
112
+ it("expiresAt stored as provided (date case)", async () => {
113
+ const exp = new Date(Date.now() + 86400000);
114
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
115
+ const result = await repo.claim(makeClaimInput({ expiresAt: exp }));
116
+ expect(result.expiresAt).toEqual(exp);
117
+ });
118
+ it("uses createClaimedCouponId for the document ID", async () => {
119
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
120
+ await repo.claim(makeClaimInput({ userId: "user-yugi", couponCode: "SAVE20" }));
121
+ const expectedId = createClaimedCouponId("user-yugi", "SAVE20");
122
+ expect(mockCollection.doc).toHaveBeenCalledWith(expectedId);
123
+ });
124
+ });
125
+ // ---------------------------------------------------------------------------
126
+ // listForUser
127
+ // ---------------------------------------------------------------------------
128
+ describe("ClaimedCouponsRepository.listForUser", () => {
129
+ it("queries by userId field", async () => {
130
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
131
+ await repo.listForUser("user-yugi");
132
+ expect(mockCollection.where).toHaveBeenCalledWith("userId", "==", "user-yugi");
133
+ });
134
+ it("valid coupon (not expired) → returned without mutation", async () => {
135
+ const futureDate = new Date(Date.now() + 86400000);
136
+ const doc = { ...makeClaimDoc(), status: "active", expiresAt: futureDate };
137
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: doc.id, data: doc }]));
138
+ const results = await repo.listForUser("user-yugi");
139
+ expect(results[0].status).toBe("active");
140
+ expect(mockDocRef.update).not.toHaveBeenCalled();
141
+ });
142
+ it("active coupon with no expiresAt → not lazily expired", async () => {
143
+ const doc = { ...makeClaimDoc(), status: "active", expiresAt: null };
144
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: doc.id, data: doc }]));
145
+ const results = await repo.listForUser("user-yugi");
146
+ expect(results[0].status).toBe("active");
147
+ expect(mockDocRef.update).not.toHaveBeenCalled();
148
+ });
149
+ it("expired coupon found on read → lazy-flips status to expired in returned item", async () => {
150
+ const pastDate = new Date(Date.now() - 86400000);
151
+ const doc = { ...makeClaimDoc(), status: "active", expiresAt: pastDate };
152
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: doc.id, data: doc }]));
153
+ const results = await repo.listForUser("user-yugi");
154
+ expect(results[0].status).toBe("expired");
155
+ });
156
+ it("expired coupon → best-effort write-back update called (fire-and-forget)", async () => {
157
+ const pastDate = new Date(Date.now() - 86400000);
158
+ const doc = { ...makeClaimDoc(), status: "active", expiresAt: pastDate };
159
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: doc.id, data: doc }]));
160
+ await repo.listForUser("user-yugi");
161
+ // Allow the fire-and-forget to complete
162
+ await new Promise((r) => setTimeout(r, 10));
163
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "expired" }));
164
+ });
165
+ it("already-expired coupon → no write-back (only active transitions)", async () => {
166
+ const pastDate = new Date(Date.now() - 86400000);
167
+ const doc = { ...makeClaimDoc(), status: "expired", expiresAt: pastDate };
168
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: doc.id, data: doc }]));
169
+ await repo.listForUser("user-yugi");
170
+ await new Promise((r) => setTimeout(r, 10));
171
+ expect(mockDocRef.update).not.toHaveBeenCalled();
172
+ });
173
+ it("empty list → returns []", async () => {
174
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
175
+ const results = await repo.listForUser("user-nobody");
176
+ expect(results).toHaveLength(0);
177
+ });
178
+ });
179
+ // ---------------------------------------------------------------------------
180
+ // markUsed
181
+ // ---------------------------------------------------------------------------
182
+ describe("ClaimedCouponsRepository.markUsed", () => {
183
+ it("sets status: used on the claim document", async () => {
184
+ await repo.markUsed("user-yugi", "WELCOME10", "order-1-20260101-abc");
185
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "used" }));
186
+ });
187
+ it("stores usedOrderId on the claim", async () => {
188
+ await repo.markUsed("user-yugi", "WELCOME10", "order-1-20260101-abc");
189
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ usedOrderId: "order-1-20260101-abc" }));
190
+ });
191
+ it("sets usedAt timestamp", async () => {
192
+ await repo.markUsed("user-yugi", "WELCOME10", "order-x");
193
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ usedAt: expect.any(Date) }));
194
+ });
195
+ it("uses the correct document ID (userId + code)", async () => {
196
+ await repo.markUsed("user-yugi", "WELCOME10", "order-x");
197
+ const expectedId = createClaimedCouponId("user-yugi", "WELCOME10");
198
+ expect(mockCollection.doc).toHaveBeenCalledWith(expectedId);
199
+ });
200
+ });
201
+ // ---------------------------------------------------------------------------
202
+ // softRemove
203
+ // ---------------------------------------------------------------------------
204
+ describe("ClaimedCouponsRepository.softRemove", () => {
205
+ it("sets status: expired (audit trail preserved, no delete)", async () => {
206
+ await repo.softRemove("user-yugi", "WELCOME10");
207
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "expired" }));
208
+ expect(mockDocRef.delete).not.toHaveBeenCalled();
209
+ });
210
+ it("does NOT call delete on the document", async () => {
211
+ await repo.softRemove("user-yugi", "SAVE20");
212
+ expect(mockDocRef.delete).not.toHaveBeenCalled();
213
+ });
214
+ it("uses the correct document ID (userId + code)", async () => {
215
+ await repo.softRemove("user-yugi", "SAVE20");
216
+ const expectedId = createClaimedCouponId("user-yugi", "SAVE20");
217
+ expect(mockCollection.doc).toHaveBeenCalledWith(expectedId);
218
+ });
219
+ });