@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,385 @@
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("../../../../contracts/field-ops", () => ({
15
+ serverTimestamp: () => new Date(),
16
+ increment: (n) => n,
17
+ arrayUnion: (...args) => args,
18
+ arrayRemove: (...args) => args,
19
+ deleteField: () => null,
20
+ registerFieldOps: vi.fn(),
21
+ }));
22
+ vi.mock("../../../../errors/normalize", () => ({ normalizeError: vi.fn() }));
23
+ vi.mock("../../../../monitoring", () => ({
24
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
25
+ }));
26
+ import { ScammerRepository } from "../scammer.repository";
27
+ const repo = new ScammerRepository();
28
+ function makeScammerDoc(overrides = {}) {
29
+ return {
30
+ id: "scammer-fake-seller-lk4e7",
31
+ status: "verified",
32
+ displayNames: ["Fake Seller"],
33
+ phones: ["9876543210"],
34
+ upiIds: ["fakeseller@paytm"],
35
+ emails: ["fake@example.com"],
36
+ scamType: "fake_seller",
37
+ scamPlatform: "instagram",
38
+ reportedBy: "user-ravi",
39
+ views: 0,
40
+ incidentCount: 0,
41
+ commentCount: 0,
42
+ contestCount: 0,
43
+ isContested: false,
44
+ seoSlug: "scammer-fake-seller-lk4e7",
45
+ relatedScammerIds: [],
46
+ mergedFromIds: [],
47
+ tags: [],
48
+ createdAt: new Date(),
49
+ updatedAt: new Date(),
50
+ ...overrides,
51
+ };
52
+ }
53
+ // Subcollection mock
54
+ const mockSubCollection = {
55
+ where: vi.fn(),
56
+ orderBy: vi.fn(),
57
+ limit: vi.fn(),
58
+ get: vi.fn().mockResolvedValue(makeQuerySnap([])),
59
+ };
60
+ beforeEach(() => {
61
+ vi.clearAllMocks();
62
+ db.collection.mockReturnValue(mockCollection);
63
+ mockCollection.doc.mockReturnValue(mockDocRef);
64
+ mockCollection.where.mockReturnValue(mockQuery);
65
+ mockQuery.where.mockReturnValue(mockQuery);
66
+ mockQuery.orderBy.mockReturnValue(mockQuery);
67
+ mockQuery.limit.mockReturnValue(mockQuery);
68
+ mockQuery.offset = vi.fn().mockReturnValue(mockQuery);
69
+ mockQuery.count.mockReturnValue({
70
+ get: vi.fn().mockResolvedValue({ data: () => ({ count: 0 }) }),
71
+ });
72
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
73
+ mockDocRef.get.mockResolvedValue(makeSnap(makeScammerDoc(), "scammer-1"));
74
+ mockDocRef.set.mockResolvedValue(undefined);
75
+ mockDocRef.update.mockResolvedValue(undefined);
76
+ mockDocRef.delete.mockResolvedValue(undefined);
77
+ // Subcollection chain
78
+ mockSubCollection.where.mockReturnValue(mockSubCollection);
79
+ mockSubCollection.orderBy.mockReturnValue(mockSubCollection);
80
+ mockSubCollection.limit.mockReturnValue(mockSubCollection);
81
+ mockSubCollection.get.mockResolvedValue(makeQuerySnap([]));
82
+ mockDocRef.collection = vi.fn().mockReturnValue(mockSubCollection);
83
+ });
84
+ // ---------------------------------------------------------------------------
85
+ // findByContactField
86
+ // ---------------------------------------------------------------------------
87
+ describe("ScammerRepository.findByContactField", () => {
88
+ it("queries phones array-contains the given value", async () => {
89
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
90
+ await repo.findByContactField("phones", "9876543210");
91
+ expect(mockCollection.where).toHaveBeenCalledWith("phones", "array-contains", "9876543210");
92
+ });
93
+ it("queries upiIds array-contains the given value", async () => {
94
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
95
+ await repo.findByContactField("upiIds", "fakeseller@paytm");
96
+ expect(mockCollection.where).toHaveBeenCalledWith("upiIds", "array-contains", "fakeseller@paytm");
97
+ });
98
+ it("queries emails array-contains the given value", async () => {
99
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
100
+ await repo.findByContactField("emails", "fake@example.com");
101
+ expect(mockCollection.where).toHaveBeenCalledWith("emails", "array-contains", "fake@example.com");
102
+ });
103
+ it("queries displayNames array-contains the given value", async () => {
104
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
105
+ await repo.findByContactField("displayNames", "Fake Seller");
106
+ expect(mockCollection.where).toHaveBeenCalledWith("displayNames", "array-contains", "Fake Seller");
107
+ });
108
+ it("limits to 5 results", async () => {
109
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
110
+ await repo.findByContactField("phones", "123");
111
+ expect(mockQuery.limit).toHaveBeenCalledWith(5);
112
+ });
113
+ it("returns empty array when no matches", async () => {
114
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
115
+ const result = await repo.findByContactField("phones", "no-match");
116
+ expect(result).toEqual([]);
117
+ });
118
+ it("returns matched documents", async () => {
119
+ const scammer = makeScammerDoc({ phones: ["9876543210"] });
120
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: scammer.id, data: scammer }]));
121
+ const result = await repo.findByContactField("phones", "9876543210");
122
+ expect(result).toHaveLength(1);
123
+ });
124
+ });
125
+ // ---------------------------------------------------------------------------
126
+ // create
127
+ // ---------------------------------------------------------------------------
128
+ describe("ScammerRepository.create", () => {
129
+ it("generates an ID prefixed with 'scammer-'", async () => {
130
+ await repo.create({
131
+ displayNames: ["John Faker"],
132
+ phones: ["9999999999"],
133
+ upiIds: [],
134
+ emails: [],
135
+ scamType: "fake_seller",
136
+ scamPlatform: "whatsapp",
137
+ reportedBy: "user-ravi",
138
+ description: "Fraud",
139
+ });
140
+ const docId = mockCollection.doc.mock.calls[0][0];
141
+ expect(docId).toMatch(/^scammer-/);
142
+ });
143
+ it("slugifies displayNames[0] into the ID", async () => {
144
+ await repo.create({
145
+ displayNames: ["John Faker"],
146
+ phones: [],
147
+ upiIds: [],
148
+ emails: [],
149
+ scamType: "fake_seller",
150
+ scamPlatform: "whatsapp",
151
+ reportedBy: "user-ravi",
152
+ description: "Fraud",
153
+ });
154
+ const docId = mockCollection.doc.mock.calls[0][0];
155
+ expect(docId).toContain("john-faker");
156
+ });
157
+ it("generates fallback ID when displayNames is empty", async () => {
158
+ await repo.create({
159
+ displayNames: [],
160
+ phones: ["9999999999"],
161
+ upiIds: [],
162
+ emails: [],
163
+ scamType: "fake_seller",
164
+ scamPlatform: "instagram",
165
+ reportedBy: "user-ravi",
166
+ description: "Fraud",
167
+ });
168
+ const docId = mockCollection.doc.mock.calls[0][0];
169
+ expect(docId).toMatch(/^scammer-/);
170
+ });
171
+ it("sets status: pending_review on new scammer profile", async () => {
172
+ await repo.create({
173
+ displayNames: ["Faker"],
174
+ phones: [],
175
+ upiIds: [],
176
+ emails: [],
177
+ scamType: "fake_seller",
178
+ scamPlatform: "instagram",
179
+ reportedBy: "user-ravi",
180
+ description: "Fraud",
181
+ });
182
+ const setArg = mockDocRef.set.mock.calls[0][0];
183
+ expect(setArg.status).toBe("pending_review");
184
+ });
185
+ it("initialises counters to zero", async () => {
186
+ await repo.create({
187
+ displayNames: ["Faker"],
188
+ phones: [],
189
+ upiIds: [],
190
+ emails: [],
191
+ scamType: "fake_seller",
192
+ scamPlatform: "instagram",
193
+ reportedBy: "user-ravi",
194
+ description: "Fraud",
195
+ });
196
+ const setArg = mockDocRef.set.mock.calls[0][0];
197
+ expect(setArg.views).toBe(0);
198
+ expect(setArg.incidentCount).toBe(0);
199
+ expect(setArg.commentCount).toBe(0);
200
+ expect(setArg.contestCount).toBe(0);
201
+ });
202
+ it("sets isContested: false", async () => {
203
+ await repo.create({
204
+ displayNames: ["Faker"],
205
+ phones: [],
206
+ upiIds: [],
207
+ emails: [],
208
+ scamType: "fake_seller",
209
+ scamPlatform: "instagram",
210
+ reportedBy: "user-ravi",
211
+ description: "Fraud",
212
+ });
213
+ const setArg = mockDocRef.set.mock.calls[0][0];
214
+ expect(setArg.isContested).toBe(false);
215
+ });
216
+ it("sets seoSlug equal to the generated ID", async () => {
217
+ await repo.create({
218
+ displayNames: ["Faker"],
219
+ phones: [],
220
+ upiIds: [],
221
+ emails: [],
222
+ scamType: "fake_seller",
223
+ scamPlatform: "instagram",
224
+ reportedBy: "user-ravi",
225
+ description: "Fraud",
226
+ });
227
+ const docId = mockCollection.doc.mock.calls[0][0];
228
+ const setArg = mockDocRef.set.mock.calls[0][0];
229
+ expect(setArg.seoSlug).toBe(docId);
230
+ });
231
+ it("initialises empty arrays for relatedScammerIds, mergedFromIds, tags", async () => {
232
+ await repo.create({
233
+ displayNames: ["Faker"],
234
+ phones: [],
235
+ upiIds: [],
236
+ emails: [],
237
+ scamType: "fake_seller",
238
+ scamPlatform: "instagram",
239
+ reportedBy: "user-ravi",
240
+ description: "Fraud",
241
+ });
242
+ const setArg = mockDocRef.set.mock.calls[0][0];
243
+ expect(setArg.relatedScammerIds).toEqual([]);
244
+ expect(setArg.mergedFromIds).toEqual([]);
245
+ expect(setArg.tags).toEqual([]);
246
+ });
247
+ });
248
+ // ---------------------------------------------------------------------------
249
+ // adminUpdate
250
+ // ---------------------------------------------------------------------------
251
+ describe("ScammerRepository.adminUpdate", () => {
252
+ it("calls doc(id).update with provided fields", async () => {
253
+ await repo.adminUpdate("scammer-1", { status: "verified" });
254
+ expect(mockCollection.doc).toHaveBeenCalledWith("scammer-1");
255
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "verified" }));
256
+ });
257
+ it("always sets updatedAt on update", async () => {
258
+ await repo.adminUpdate("scammer-1", { status: "dismissed" });
259
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
260
+ });
261
+ it("supports partial updates (only provided fields changed)", async () => {
262
+ await repo.adminUpdate("scammer-1", { isContested: true });
263
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ isContested: true }));
264
+ });
265
+ });
266
+ // ---------------------------------------------------------------------------
267
+ // incrementViews
268
+ // ---------------------------------------------------------------------------
269
+ describe("ScammerRepository.incrementViews", () => {
270
+ it("calls doc(id).update with views increment (fire-and-forget)", async () => {
271
+ await repo.incrementViews("scammer-1");
272
+ expect(mockCollection.doc).toHaveBeenCalledWith("scammer-1");
273
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ views: 1 }));
274
+ });
275
+ it("does not throw when Firestore update fails (graceful fallback)", async () => {
276
+ mockDocRef.update.mockRejectedValue(new Error("Firestore error"));
277
+ await expect(repo.incrementViews("scammer-1")).resolves.toBeUndefined();
278
+ });
279
+ });
280
+ // ---------------------------------------------------------------------------
281
+ // findBySeoSlug
282
+ // ---------------------------------------------------------------------------
283
+ describe("ScammerRepository.findBySeoSlug", () => {
284
+ it("queries by seoSlug field with == operator", async () => {
285
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
286
+ await repo.findBySeoSlug("scammer-fake-seller-lk4e7");
287
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("seoSlug"), "==", "scammer-fake-seller-lk4e7");
288
+ });
289
+ it("limits to 1 result", async () => {
290
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
291
+ await repo.findBySeoSlug("scammer-abc");
292
+ expect(mockQuery.limit).toHaveBeenCalledWith(1);
293
+ });
294
+ it("returns null when no match found", async () => {
295
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
296
+ const result = await repo.findBySeoSlug("nonexistent-slug");
297
+ expect(result).toBeNull();
298
+ });
299
+ it("returns matched document", async () => {
300
+ const scammer = makeScammerDoc({ seoSlug: "scammer-fake-seller-lk4e7" });
301
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: scammer.id, data: scammer }]));
302
+ const result = await repo.findBySeoSlug("scammer-fake-seller-lk4e7");
303
+ expect(result).not.toBeNull();
304
+ expect(result?.seoSlug).toBe("scammer-fake-seller-lk4e7");
305
+ });
306
+ });
307
+ // ---------------------------------------------------------------------------
308
+ // findManyById
309
+ // ---------------------------------------------------------------------------
310
+ describe("ScammerRepository.findManyById", () => {
311
+ it("returns empty array immediately when ids is empty", async () => {
312
+ const result = await repo.findManyById([]);
313
+ expect(result).toEqual([]);
314
+ expect(mockDocRef.get).not.toHaveBeenCalled();
315
+ });
316
+ it("returns only verified scammers", async () => {
317
+ const verified = makeScammerDoc({ id: "s-1", status: "verified" });
318
+ const pending = makeScammerDoc({ id: "s-2", status: "pending_review" });
319
+ mockDocRef.get
320
+ .mockResolvedValueOnce(makeSnap(verified, "s-1"))
321
+ .mockResolvedValueOnce(makeSnap(pending, "s-2"));
322
+ const result = await repo.findManyById(["s-1", "s-2"]);
323
+ expect(result).toHaveLength(1);
324
+ expect(result[0].status).toBe("verified");
325
+ });
326
+ it("does not include null results (non-existent IDs)", async () => {
327
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
328
+ const result = await repo.findManyById(["nonexistent"]);
329
+ expect(result).toEqual([]);
330
+ });
331
+ it("caps at 5 IDs (slices excess)", async () => {
332
+ const ids = ["a", "b", "c", "d", "e", "f", "g"];
333
+ const verified = makeScammerDoc({ status: "verified" });
334
+ mockDocRef.get.mockResolvedValue(makeSnap(verified, "a"));
335
+ await repo.findManyById(ids);
336
+ expect(mockDocRef.get).toHaveBeenCalledTimes(5);
337
+ });
338
+ });
339
+ // ---------------------------------------------------------------------------
340
+ // listVerified
341
+ // ---------------------------------------------------------------------------
342
+ describe("ScammerRepository.listVerified", () => {
343
+ it("applies status==verified as the base query filter", async () => {
344
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
345
+ mockQuery.count = vi.fn().mockReturnValue({ get: vi.fn().mockResolvedValue({ data: () => ({ count: 0 }) }) });
346
+ await repo.listVerified({ page: 1, pageSize: 20, filters: "" });
347
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("status"), "==", "verified");
348
+ });
349
+ });
350
+ // ---------------------------------------------------------------------------
351
+ // listPublicIncidents
352
+ // ---------------------------------------------------------------------------
353
+ describe("ScammerRepository.listPublicIncidents", () => {
354
+ it("queries subcollection for verified incidents", async () => {
355
+ await repo.listPublicIncidents("scammer-1");
356
+ expect(mockSubCollection.where).toHaveBeenCalledWith(expect.stringContaining("status"), "==", "verified");
357
+ });
358
+ it("limits to 20 incidents", async () => {
359
+ await repo.listPublicIncidents("scammer-1");
360
+ expect(mockSubCollection.limit).toHaveBeenCalledWith(20);
361
+ });
362
+ it("returns empty array when subcollection query fails", async () => {
363
+ mockSubCollection.get.mockRejectedValue(new Error("subcollection error"));
364
+ const result = await repo.listPublicIncidents("scammer-1");
365
+ expect(result).toEqual([]);
366
+ });
367
+ });
368
+ // ---------------------------------------------------------------------------
369
+ // listPublicComments
370
+ // ---------------------------------------------------------------------------
371
+ describe("ScammerRepository.listPublicComments", () => {
372
+ it("filters by isHidden == false", async () => {
373
+ await repo.listPublicComments("scammer-1");
374
+ expect(mockSubCollection.where).toHaveBeenCalledWith("isHidden", "==", false);
375
+ });
376
+ it("limits to 30 comments", async () => {
377
+ await repo.listPublicComments("scammer-1");
378
+ expect(mockSubCollection.limit).toHaveBeenCalledWith(30);
379
+ });
380
+ it("returns empty array when query fails", async () => {
381
+ mockSubCollection.get.mockRejectedValue(new Error("error"));
382
+ const result = await repo.listPublicComments("scammer-1");
383
+ expect(result).toEqual([]);
384
+ });
385
+ });