@mohasinac/appkit 3.0.6 → 3.1.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 (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
@@ -0,0 +1,607 @@
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, mockBatch } = makeMockDb();
4
+ const { mockCacheManager } = vi.hoisted(() => ({
5
+ mockCacheManager: {
6
+ get: vi.fn().mockReturnValue(null),
7
+ set: vi.fn(),
8
+ delete: vi.fn(),
9
+ },
10
+ }));
11
+ vi.mock("../../../../providers/db-firebase/admin", () => ({
12
+ getAdminDb: () => db,
13
+ }));
14
+ vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
15
+ const actual = await importOriginal();
16
+ return {
17
+ ...actual,
18
+ prepareForFirestore: (d) => d,
19
+ };
20
+ });
21
+ vi.mock("../../../../contracts/field-ops", () => ({
22
+ serverTimestamp: () => new Date(),
23
+ increment: (n) => n,
24
+ arrayUnion: (...args) => args,
25
+ arrayRemove: (...args) => args,
26
+ deleteField: () => null,
27
+ registerFieldOps: vi.fn(),
28
+ }));
29
+ vi.mock("../../../../core", () => ({
30
+ cacheManager: mockCacheManager,
31
+ }));
32
+ vi.mock("../../../../errors/normalize", () => ({ normalizeError: vi.fn() }));
33
+ vi.mock("../../../../monitoring", () => ({
34
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
35
+ }));
36
+ // buildSearchTokens returns an array of tokens — mock as passthrough
37
+ vi.mock("../../../../utils", () => ({
38
+ generateUniqueId: vi.fn(async (gen, exists) => {
39
+ const candidate = gen(0);
40
+ const taken = await exists(candidate);
41
+ return taken ? gen(1) : candidate;
42
+ }),
43
+ slugify: (s) => s.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""),
44
+ buildSearchTokens: (...args) => args.flat().filter(Boolean),
45
+ tokenizeQuery: (q) => q.trim() ? q.trim().toLowerCase().split(/\s+/) : [],
46
+ generateBarcodeId: (id) => Promise.resolve(`barcode-${id}`),
47
+ }));
48
+ import { ProductRepository } from "../products.repository";
49
+ const repo = new ProductRepository();
50
+ function makeProductDoc(overrides = {}) {
51
+ return {
52
+ id: "product-charizard-psa9",
53
+ slug: "product-charizard-psa9",
54
+ title: "Charizard PSA 9",
55
+ description: "1st Edition base set Charizard PSA 9",
56
+ brand: "Pokemon",
57
+ brandSlug: "brand-pokemon",
58
+ storeId: "store-pokemon-palace",
59
+ categorySlug: "category-trading-cards",
60
+ categorySlugs: ["category-trading-cards", "category-pokemon"],
61
+ listingType: "standard",
62
+ status: "published",
63
+ price: 50000,
64
+ currency: "INR",
65
+ stockQuantity: 1,
66
+ availableQuantity: 1,
67
+ currentBid: 0,
68
+ bidCount: 0,
69
+ viewCount: 0,
70
+ searchTokens: ["charizard", "psa9"],
71
+ barcodeId: "barcode-product-charizard-psa9",
72
+ images: [],
73
+ tags: [],
74
+ features: [],
75
+ createdAt: new Date(),
76
+ updatedAt: new Date(),
77
+ ...overrides,
78
+ };
79
+ }
80
+ beforeEach(() => {
81
+ vi.clearAllMocks();
82
+ mockCacheManager.get.mockReturnValue(null);
83
+ db.collection.mockReturnValue(mockCollection);
84
+ mockCollection.doc.mockReturnValue(mockDocRef);
85
+ mockCollection.where.mockReturnValue(mockQuery);
86
+ mockCollection.get = vi.fn().mockResolvedValue(makeQuerySnap([]));
87
+ mockQuery.where.mockReturnValue(mockQuery);
88
+ mockQuery.orderBy.mockReturnValue(mockQuery);
89
+ mockQuery.limit.mockReturnValue(mockQuery);
90
+ mockQuery.offset = vi.fn().mockReturnValue(mockQuery);
91
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
92
+ mockQuery.count.mockReturnValue({
93
+ get: vi.fn().mockResolvedValue({ data: () => ({ count: 0 }) }),
94
+ });
95
+ mockDocRef.get.mockResolvedValue(makeSnap(makeProductDoc(), "product-charizard-psa9"));
96
+ mockDocRef.set.mockResolvedValue(undefined);
97
+ mockDocRef.update.mockResolvedValue(undefined);
98
+ mockDocRef.delete.mockResolvedValue(undefined);
99
+ db.batch.mockReturnValue(mockBatch);
100
+ db.getAll = vi.fn().mockResolvedValue([]);
101
+ });
102
+ // ---------------------------------------------------------------------------
103
+ // findById — caching
104
+ // ---------------------------------------------------------------------------
105
+ describe("ProductRepository.findById — caching", () => {
106
+ it("returns cached product without hitting Firestore on warm cache", async () => {
107
+ const cached = makeProductDoc();
108
+ mockCacheManager.get.mockReturnValue(cached);
109
+ const result = await repo.findById("product-charizard-psa9");
110
+ expect(result?.id).toBe("product-charizard-psa9");
111
+ expect(mockDocRef.get).not.toHaveBeenCalled();
112
+ });
113
+ it("fetches from Firestore on cache miss and caches result", async () => {
114
+ mockCacheManager.get.mockReturnValue(null);
115
+ const product = makeProductDoc();
116
+ mockDocRef.get.mockResolvedValue(makeSnap(product, "product-charizard-psa9"));
117
+ await repo.findById("product-charizard-psa9");
118
+ expect(mockDocRef.get).toHaveBeenCalledOnce();
119
+ expect(mockCacheManager.set).toHaveBeenCalledWith(expect.stringContaining("product-charizard-psa9"), expect.any(Object), expect.objectContaining({ ttl: 30000 }));
120
+ });
121
+ it("returns null when product does not exist (Firestore miss)", async () => {
122
+ mockCacheManager.get.mockReturnValue(null);
123
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
124
+ const result = await repo.findById("nonexistent");
125
+ expect(result).toBeNull();
126
+ });
127
+ it("does NOT cache null results (product not found)", async () => {
128
+ mockCacheManager.get.mockReturnValue(null);
129
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
130
+ await repo.findById("nonexistent");
131
+ expect(mockCacheManager.set).not.toHaveBeenCalled();
132
+ });
133
+ });
134
+ // ---------------------------------------------------------------------------
135
+ // delete — cache invalidation
136
+ // ---------------------------------------------------------------------------
137
+ describe("ProductRepository.delete — cache invalidation", () => {
138
+ it("invalidates cache entry before deleting", async () => {
139
+ await repo.delete("product-charizard-psa9");
140
+ expect(mockCacheManager.delete).toHaveBeenCalledWith(expect.stringContaining("product-charizard-psa9"));
141
+ // delete happens before or alongside cache invalidation
142
+ expect(mockDocRef.delete).toHaveBeenCalledOnce();
143
+ });
144
+ it("deletes the Firestore document", async () => {
145
+ await repo.delete("product-charizard-psa9");
146
+ expect(mockDocRef.delete).toHaveBeenCalledOnce();
147
+ });
148
+ });
149
+ // ---------------------------------------------------------------------------
150
+ // create
151
+ // ---------------------------------------------------------------------------
152
+ describe("ProductRepository.create", () => {
153
+ beforeEach(() => {
154
+ // After doc.set, findById is called to re-read — return the stored product
155
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
156
+ });
157
+ it("uses title to generate the slug when no explicit slug provided", async () => {
158
+ await repo.create({
159
+ title: "Hot Wheels Redline Vintage",
160
+ storeId: "store-diecast-depot",
161
+ price: 5000,
162
+ currency: "INR",
163
+ stockQuantity: 3,
164
+ listingType: "standard",
165
+ });
166
+ // generateUniqueId calls gen(0) = slugify("Hot Wheels Redline Vintage")
167
+ expect(mockCollection.doc).toHaveBeenCalledWith("hot-wheels-redline-vintage");
168
+ });
169
+ it("uses explicit slug when provided", async () => {
170
+ await repo.create({
171
+ title: "Anything",
172
+ slug: "product-custom-slug",
173
+ storeId: "store-1",
174
+ price: 1000,
175
+ currency: "INR",
176
+ stockQuantity: 1,
177
+ listingType: "standard",
178
+ });
179
+ expect(mockCollection.doc).toHaveBeenCalledWith("product-custom-slug");
180
+ });
181
+ it("builds and stores searchTokens", async () => {
182
+ await repo.create({
183
+ title: "Charizard",
184
+ storeId: "store-1",
185
+ price: 1000,
186
+ currency: "INR",
187
+ stockQuantity: 1,
188
+ listingType: "standard",
189
+ });
190
+ const setArg = mockDocRef.set.mock.calls[0][0];
191
+ expect(Array.isArray(setArg.searchTokens)).toBe(true);
192
+ });
193
+ it("sets availableQuantity from stockQuantity", async () => {
194
+ await repo.create({
195
+ title: "Test Product",
196
+ storeId: "store-1",
197
+ price: 1000,
198
+ currency: "INR",
199
+ stockQuantity: 5,
200
+ listingType: "standard",
201
+ });
202
+ const setArg = mockDocRef.set.mock.calls[0][0];
203
+ expect(setArg.availableQuantity).toBe(5);
204
+ });
205
+ it("generates barcodeId when not provided", async () => {
206
+ await repo.create({
207
+ title: "New Product",
208
+ storeId: "store-1",
209
+ price: 1000,
210
+ currency: "INR",
211
+ stockQuantity: 1,
212
+ listingType: "standard",
213
+ });
214
+ const setArg = mockDocRef.set.mock.calls[0][0];
215
+ expect(setArg.barcodeId).toBeDefined();
216
+ expect(typeof setArg.barcodeId).toBe("string");
217
+ });
218
+ it("uses provided barcodeId when given", async () => {
219
+ await repo.create({
220
+ title: "New Product",
221
+ barcodeId: "custom-barcode-123",
222
+ storeId: "store-1",
223
+ price: 1000,
224
+ currency: "INR",
225
+ stockQuantity: 1,
226
+ listingType: "standard",
227
+ });
228
+ const setArg = mockDocRef.set.mock.calls[0][0];
229
+ expect(setArg.barcodeId).toBe("custom-barcode-123");
230
+ });
231
+ it("caches the newly created product", async () => {
232
+ await repo.create({
233
+ title: "Cache Me",
234
+ storeId: "store-1",
235
+ price: 1000,
236
+ currency: "INR",
237
+ stockQuantity: 1,
238
+ listingType: "standard",
239
+ });
240
+ expect(mockCacheManager.set).toHaveBeenCalled();
241
+ });
242
+ it("sets createdAt and updatedAt timestamps", async () => {
243
+ await repo.create({
244
+ title: "New Product",
245
+ storeId: "store-1",
246
+ price: 1000,
247
+ currency: "INR",
248
+ stockQuantity: 1,
249
+ listingType: "standard",
250
+ });
251
+ const setArg = mockDocRef.set.mock.calls[0][0];
252
+ expect(setArg.createdAt).toBeInstanceOf(Date);
253
+ expect(setArg.updatedAt).toBeInstanceOf(Date);
254
+ });
255
+ });
256
+ // ---------------------------------------------------------------------------
257
+ // updateBid
258
+ // ---------------------------------------------------------------------------
259
+ describe("ProductRepository.updateBid", () => {
260
+ it("updates currentBid field", async () => {
261
+ const product = makeProductDoc();
262
+ mockDocRef.get.mockResolvedValue(makeSnap(product, product.id));
263
+ await repo.updateBid("product-charizard-psa9", 75000, 5);
264
+ const updateArg = mockDocRef.update.mock.calls[0][0];
265
+ expect(updateArg.currentBid).toBe(75000);
266
+ });
267
+ it("updates bidCount field", async () => {
268
+ const product = makeProductDoc();
269
+ mockDocRef.get.mockResolvedValue(makeSnap(product, product.id));
270
+ await repo.updateBid("product-charizard-psa9", 75000, 5);
271
+ const updateArg = mockDocRef.update.mock.calls[0][0];
272
+ expect(updateArg.bidCount).toBe(5);
273
+ });
274
+ });
275
+ // ---------------------------------------------------------------------------
276
+ // startGroup
277
+ // ---------------------------------------------------------------------------
278
+ describe("ProductRepository.startGroup", () => {
279
+ it("sets groupId, isGroupParent: true, and groupChildSlugs: []", async () => {
280
+ await repo.startGroup("product-charizard-psa9", "group-pokemon-starter-bundle");
281
+ expect(mockDocRef.update).toHaveBeenCalledWith({
282
+ groupId: "group-pokemon-starter-bundle",
283
+ isGroupParent: true,
284
+ groupChildSlugs: [],
285
+ });
286
+ });
287
+ it("calls doc(productId) to target the correct product", async () => {
288
+ await repo.startGroup("product-abc", "group-xyz");
289
+ expect(mockCollection.doc).toHaveBeenCalledWith("product-abc");
290
+ });
291
+ });
292
+ // ---------------------------------------------------------------------------
293
+ // dissolveGroup
294
+ // ---------------------------------------------------------------------------
295
+ describe("ProductRepository.dissolveGroup", () => {
296
+ it("clears groupId and group fields from all members in a batch", async () => {
297
+ const members = [
298
+ makeProductDoc({ id: "product-1", groupId: "group-1" }),
299
+ makeProductDoc({ id: "product-2", groupId: "group-1" }),
300
+ ];
301
+ mockQuery.get.mockResolvedValue(makeQuerySnap(members.map((m) => ({ id: m.id, data: m }))));
302
+ await repo.dissolveGroup("group-1");
303
+ expect(mockBatch.update).toHaveBeenCalledTimes(2);
304
+ const calls = mockBatch.update.mock.calls;
305
+ expect(calls.every((c) => c[1].groupId === null)).toBe(true);
306
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
307
+ });
308
+ it("does not call batch.update when group has no members", async () => {
309
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
310
+ await repo.dissolveGroup("group-empty");
311
+ expect(mockBatch.update).not.toHaveBeenCalled();
312
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
313
+ });
314
+ it("clears all group fields (isGroupParent, groupParentSlug, groupChildSlugs, groupTitle)", async () => {
315
+ const parent = makeProductDoc({ id: "p-1", groupId: "group-1", isGroupParent: true });
316
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "p-1", data: parent }]));
317
+ await repo.dissolveGroup("group-1");
318
+ const updateArg = mockBatch.update.mock.calls[0][1];
319
+ expect(updateArg.isGroupParent).toBeNull();
320
+ expect(updateArg.groupParentSlug).toBeNull();
321
+ expect(updateArg.groupChildSlugs).toBeNull();
322
+ expect(updateArg.groupTitle).toBeNull();
323
+ });
324
+ });
325
+ // ---------------------------------------------------------------------------
326
+ // linkChildToGroup
327
+ // ---------------------------------------------------------------------------
328
+ describe("ProductRepository.linkChildToGroup", () => {
329
+ it("batch-updates child with groupId, groupParentSlug", async () => {
330
+ const parent = makeProductDoc({
331
+ id: "product-parent",
332
+ groupId: "group-1",
333
+ groupChildSlugs: [],
334
+ slug: "product-parent",
335
+ });
336
+ const child = makeProductDoc({ id: "product-child", slug: "product-child" });
337
+ await repo.linkChildToGroup(parent, child);
338
+ const calls = mockBatch.update.mock.calls;
339
+ const childUpdate = calls.find((c) => c[1].groupId === "group-1");
340
+ expect(childUpdate).toBeDefined();
341
+ expect(childUpdate[1].groupParentSlug).toBe("product-parent");
342
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
343
+ });
344
+ it("appends child id to parent's groupChildSlugs", async () => {
345
+ const parent = makeProductDoc({
346
+ id: "product-parent",
347
+ groupId: "group-1",
348
+ groupChildSlugs: ["product-existing"],
349
+ slug: "product-parent",
350
+ });
351
+ const child = makeProductDoc({ id: "product-new-child", slug: "product-new-child" });
352
+ await repo.linkChildToGroup(parent, child);
353
+ const calls = mockBatch.update.mock.calls;
354
+ const parentUpdate = calls.find((c) => Array.isArray(c[1].groupChildSlugs));
355
+ expect(parentUpdate[1].groupChildSlugs).toContain("product-new-child");
356
+ expect(parentUpdate[1].groupChildSlugs).toContain("product-existing");
357
+ });
358
+ });
359
+ // ---------------------------------------------------------------------------
360
+ // unlinkChildFromGroup
361
+ // ---------------------------------------------------------------------------
362
+ describe("ProductRepository.unlinkChildFromGroup", () => {
363
+ it("clears child's groupId and groupParentSlug", async () => {
364
+ const parent = makeProductDoc({ id: "p-parent", groupChildSlugs: ["p-child"], slug: "p-parent" });
365
+ const child = makeProductDoc({ id: "p-child", groupId: "group-1", groupParentSlug: "p-parent" });
366
+ await repo.unlinkChildFromGroup(parent, child);
367
+ const calls = mockBatch.update.mock.calls;
368
+ const childUpdate = calls.find((c) => c[1].groupId === null);
369
+ expect(childUpdate).toBeDefined();
370
+ expect(childUpdate[1].groupParentSlug).toBeNull();
371
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
372
+ });
373
+ it("removes child from parent's groupChildSlugs", async () => {
374
+ const parent = makeProductDoc({
375
+ id: "p-parent",
376
+ groupChildSlugs: ["p-child", "p-other"],
377
+ slug: "p-parent",
378
+ });
379
+ const child = makeProductDoc({ id: "p-child", slug: "p-child" });
380
+ await repo.unlinkChildFromGroup(parent, child);
381
+ const calls = mockBatch.update.mock.calls;
382
+ const parentUpdate = calls.find((c) => Array.isArray(c[1].groupChildSlugs));
383
+ expect(parentUpdate[1].groupChildSlugs).not.toContain("p-child");
384
+ expect(parentUpdate[1].groupChildSlugs).toContain("p-other");
385
+ });
386
+ });
387
+ // ---------------------------------------------------------------------------
388
+ // getExpiredAuctions
389
+ // ---------------------------------------------------------------------------
390
+ describe("ProductRepository.getExpiredAuctions", () => {
391
+ it("queries listingType==auction, auctionEndDate < now, status==published", async () => {
392
+ const now = new Date();
393
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
394
+ await repo.getExpiredAuctions(now);
395
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("listingType"), "==", "auction");
396
+ expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("auctionEndDate"), "<", now);
397
+ expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("status"), "==", "published");
398
+ });
399
+ it("limits to 500 documents", async () => {
400
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
401
+ await repo.getExpiredAuctions(new Date());
402
+ expect(mockQuery.limit).toHaveBeenCalledWith(500);
403
+ });
404
+ it("returns { id, ref, data } tuples", async () => {
405
+ const auction = makeProductDoc({ id: "auction-expired", listingType: "auction" });
406
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "auction-expired", data: auction }]));
407
+ const result = await repo.getExpiredAuctions(new Date());
408
+ expect(result).toHaveLength(1);
409
+ expect(result[0].id).toBe("auction-expired");
410
+ expect(result[0].data).toBeDefined();
411
+ expect(result[0].ref).toBeDefined();
412
+ });
413
+ it("returns empty array when no expired auctions", async () => {
414
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
415
+ const result = await repo.getExpiredAuctions(new Date());
416
+ expect(result).toEqual([]);
417
+ });
418
+ });
419
+ // ---------------------------------------------------------------------------
420
+ // updateStatusInBatch
421
+ // ---------------------------------------------------------------------------
422
+ describe("ProductRepository.updateStatusInBatch", () => {
423
+ it("stages status update in the caller's batch without committing", () => {
424
+ const callerBatch = { update: vi.fn(), commit: vi.fn() };
425
+ repo.updateStatusInBatch(callerBatch, "product-1", "draft");
426
+ expect(callerBatch.update).toHaveBeenCalledOnce();
427
+ expect(callerBatch.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ status: "draft" }));
428
+ expect(callerBatch.commit).not.toHaveBeenCalled();
429
+ });
430
+ it("sets updatedAt timestamp in the staged update", () => {
431
+ const callerBatch = { update: vi.fn(), commit: vi.fn() };
432
+ repo.updateStatusInBatch(callerBatch, "product-1", "suspended");
433
+ expect(callerBatch.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ updatedAt: expect.any(Date) }));
434
+ });
435
+ });
436
+ // ---------------------------------------------------------------------------
437
+ // incrementBidCountInBatch
438
+ // ---------------------------------------------------------------------------
439
+ describe("ProductRepository.incrementBidCountInBatch", () => {
440
+ it("stages bid count update without committing", () => {
441
+ const callerBatch = { update: vi.fn(), commit: vi.fn() };
442
+ repo.incrementBidCountInBatch(callerBatch, "auction-1", 5000);
443
+ expect(callerBatch.update).toHaveBeenCalledOnce();
444
+ expect(callerBatch.commit).not.toHaveBeenCalled();
445
+ });
446
+ it("sets currentBid to the provided amount", () => {
447
+ const callerBatch = { update: vi.fn(), commit: vi.fn() };
448
+ repo.incrementBidCountInBatch(callerBatch, "auction-1", 7500);
449
+ expect(callerBatch.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ currentBid: 7500 }));
450
+ });
451
+ it("sets bidsHaveStarted: true on first bid", () => {
452
+ const callerBatch = { update: vi.fn(), commit: vi.fn() };
453
+ repo.incrementBidCountInBatch(callerBatch, "auction-1", 1000);
454
+ expect(callerBatch.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ bidsHaveStarted: true }));
455
+ });
456
+ it("includes leadingBidderId when provided", () => {
457
+ const callerBatch = { update: vi.fn(), commit: vi.fn() };
458
+ repo.incrementBidCountInBatch(callerBatch, "auction-1", 5000, "user-ravi");
459
+ expect(callerBatch.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ leadingBidderId: "user-ravi" }));
460
+ });
461
+ it("does NOT include leadingBidderId when not provided", () => {
462
+ const callerBatch = { update: vi.fn(), commit: vi.fn() };
463
+ repo.incrementBidCountInBatch(callerBatch, "auction-1", 5000);
464
+ const updateArg = callerBatch.update.mock.calls[0][1];
465
+ expect(updateArg).not.toHaveProperty("leadingBidderId");
466
+ });
467
+ });
468
+ // ---------------------------------------------------------------------------
469
+ // FILTER_ALIASES — pure function tests
470
+ // ---------------------------------------------------------------------------
471
+ describe("ProductRepository.FILTER_ALIASES — listingType", () => {
472
+ const aliases = ProductRepository.FILTER_ALIASES;
473
+ it("canonical 'auction' → listingType==auction", () => {
474
+ expect(aliases.listingType("auction", "==")).toContain("auction");
475
+ });
476
+ it("legacy 'preorder' → normalises to pre-order", () => {
477
+ const result = aliases.listingType("preorder", "==");
478
+ expect(result).toContain("pre-order");
479
+ });
480
+ it("legacy 'product' → normalises to standard", () => {
481
+ const result = aliases.listingType("product", "==");
482
+ expect(result).toContain("standard");
483
+ });
484
+ it("'standard' → listingType==standard", () => {
485
+ const result = aliases.listingType("standard", "==");
486
+ expect(result).toContain("standard");
487
+ });
488
+ it("unknown value → returns empty string (dropped)", () => {
489
+ expect(aliases.listingType("unknown-type", "==")).toBe("");
490
+ });
491
+ it("!= operator produces listingType!=<value>", () => {
492
+ const result = aliases.listingType("auction", "!=");
493
+ expect(result).toContain("!=");
494
+ expect(result).toContain("auction");
495
+ });
496
+ it("unsupported operator → returns empty string", () => {
497
+ expect(aliases.listingType("auction", ">")).toBe("");
498
+ });
499
+ });
500
+ describe("ProductRepository.FILTER_ALIASES — scope", () => {
501
+ const aliases = ProductRepository.FILTER_ALIASES;
502
+ it("scope==publicProducts → status==published,listingType==standard", () => {
503
+ const result = aliases.scope("publicProducts", "==");
504
+ expect(result).toContain("status");
505
+ expect(result).toContain("published");
506
+ expect(result).toContain("standard");
507
+ });
508
+ it("scope==publicAuctions → status==published,listingType==auction", () => {
509
+ const result = aliases.scope("publicAuctions", "==");
510
+ expect(result).toContain("published");
511
+ expect(result).toContain("auction");
512
+ });
513
+ it("scope==published → status==published only", () => {
514
+ const result = aliases.scope("published", "==");
515
+ expect(result).toContain("published");
516
+ expect(result).not.toContain("listingType");
517
+ });
518
+ it("scope with != operator → returns empty string", () => {
519
+ expect(aliases.scope("published", "!=")).toBe("");
520
+ });
521
+ it("unknown scope value → returns empty string", () => {
522
+ expect(aliases.scope("someUnknownScope", "==")).toBe("");
523
+ });
524
+ });
525
+ describe("ProductRepository.FILTER_ALIASES — other aliases", () => {
526
+ const aliases = ProductRepository.FILTER_ALIASES;
527
+ it("promoted==true → status==published,isPromoted==true", () => {
528
+ const result = aliases.promoted("true", "==");
529
+ expect(result).toContain("published");
530
+ expect(result).toContain("isPromoted");
531
+ });
532
+ it("promoted==false → returns empty string (not supported)", () => {
533
+ expect(aliases.promoted("false", "==")).toBe("");
534
+ });
535
+ it("featuredPublic==true → status==published,isFeatured==true", () => {
536
+ const result = aliases.featuredPublic("true", "==");
537
+ expect(result).toContain("published");
538
+ expect(result).toContain("featured");
539
+ });
540
+ it("category==slug → expands to categorySlugs@=slug (array-contains)", () => {
541
+ const result = aliases.category("category-trading-cards", "==");
542
+ expect(result).toContain("categorySlugs");
543
+ expect(result).toContain("category-trading-cards");
544
+ expect(result).toContain("@=");
545
+ });
546
+ });
547
+ // ---------------------------------------------------------------------------
548
+ // findByCategory
549
+ // ---------------------------------------------------------------------------
550
+ describe("ProductRepository.findByCategory", () => {
551
+ it("queries categorySlugs array-contains the category slug", async () => {
552
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
553
+ await repo.findByCategory("category-trading-cards");
554
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("categorySlugs"), "array-contains", "category-trading-cards");
555
+ });
556
+ });
557
+ // ---------------------------------------------------------------------------
558
+ // incrementViewCount
559
+ // ---------------------------------------------------------------------------
560
+ describe("ProductRepository.incrementViewCount", () => {
561
+ it("increments viewCount field (fire-and-forget)", async () => {
562
+ await repo.incrementViewCount("product-1");
563
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ viewCount: 1 }));
564
+ });
565
+ it("does not throw when Firestore update fails", async () => {
566
+ mockDocRef.update.mockRejectedValue(new Error("Firestore error"));
567
+ await expect(repo.incrementViewCount("product-1")).resolves.toBeUndefined();
568
+ });
569
+ });
570
+ // ---------------------------------------------------------------------------
571
+ // deleteByStore
572
+ // ---------------------------------------------------------------------------
573
+ describe("ProductRepository.deleteByStore", () => {
574
+ it("returns 0 when no products exist for the store", async () => {
575
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
576
+ const count = await repo.deleteByStore("store-empty");
577
+ expect(count).toBe(0);
578
+ });
579
+ it("batch-deletes all products for the store and returns count", async () => {
580
+ const products = [
581
+ makeProductDoc({ id: "p-1", storeId: "store-1" }),
582
+ makeProductDoc({ id: "p-2", storeId: "store-1" }),
583
+ ];
584
+ mockQuery.get.mockResolvedValue(makeQuerySnap(products.map((p) => ({ id: p.id, data: p }))));
585
+ const count = await repo.deleteByStore("store-1");
586
+ expect(count).toBe(2);
587
+ expect(mockBatch.delete).toHaveBeenCalledTimes(2);
588
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
589
+ });
590
+ });
591
+ // ---------------------------------------------------------------------------
592
+ // getStaleDraftRefs
593
+ // ---------------------------------------------------------------------------
594
+ describe("ProductRepository.getStaleDraftRefs", () => {
595
+ it("queries status==draft and updatedAt < cutoff", async () => {
596
+ const cutoff = new Date("2026-01-01");
597
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
598
+ await repo.getStaleDraftRefs(cutoff);
599
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("status"), "==", "draft");
600
+ expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("updatedAt"), "<", cutoff);
601
+ });
602
+ it("limits to 200 documents", async () => {
603
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
604
+ await repo.getStaleDraftRefs(new Date());
605
+ expect(mockQuery.limit).toHaveBeenCalledWith(200);
606
+ });
607
+ });
@@ -4,6 +4,7 @@
4
4
  import { type GenerateProductIdInput, type GenerateAuctionIdInput, type GeneratePreOrderIdInput } from "../../../utils/id-generators";
5
5
  import type { ProductStatus, ListingType } from "../types";
6
6
  import type { BaseDocument } from "../../../_internal/shared/types/base-document";
7
+ import type { LotteryConfig } from "../../lottery/types";
7
8
  export interface ProductVideoField {
8
9
  url: string;
9
10
  thumbnailUrl: string;
@@ -142,7 +143,7 @@ export interface ProductDocument extends BaseDocument {
142
143
  features?: string[];
143
144
  shippingInfo?: string;
144
145
  returnPolicy?: string;
145
- condition?: "new" | "used" | "refurbished" | "broken" | "graded";
146
+ condition?: "new" | "like_new" | "good" | "fair" | "poor" | "used" | "refurbished" | "broken" | "graded";
146
147
  insurance?: boolean;
147
148
  insuranceCost?: number;
148
149
  shippingPaidBy?: "seller" | "buyer";
@@ -236,6 +237,17 @@ export interface ProductDocument extends BaseDocument {
236
237
  prizeRevealDeadlineDays?: number;
237
238
  /** Public proof-of-fairness file (commit-reveal scheme). */
238
239
  prizeGithubFileUrl?: string;
240
+ /**
241
+ * Draw mode for prize-draw listings.
242
+ * "reveal" = classic buyer-reveal flow (default, backward-compat).
243
+ * "lottery" = user self-pull with weighted random slot assignment.
244
+ */
245
+ prizeDrawMode?: "reveal" | "lottery";
246
+ /**
247
+ * Full lottery config. Only set when prizeDrawMode === "lottery".
248
+ * Includes server-only price/weight fields — stripped by adapter before any client response.
249
+ */
250
+ lotteryConfig?: LotteryConfig;
239
251
  /**
240
252
  * Optional per-item shipping constraints. When absent the store's
241
253
  * shippingConfig applies unchanged.
@@ -286,6 +298,8 @@ export interface PrizeDrawItem {
286
298
  };
287
299
  condition: string;
288
300
  estimatedValue?: number;
301
+ /** Per-slot price in paise for lottery mode. Lower price = higher weight = higher chance. */
302
+ prizeSlotPrice?: number;
289
303
  isWon: boolean;
290
304
  }
291
305
  /** Runtime-accessible product status values — use instead of bare string literals. */