@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
@@ -0,0 +1,290 @@
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 { mockGetFirestoreCount } = vi.hoisted(() => ({
5
+ mockGetFirestoreCount: vi.fn().mockResolvedValue(0),
6
+ }));
7
+ vi.mock("../../../../providers/db-firebase/admin", () => ({
8
+ getAdminDb: () => db,
9
+ }));
10
+ vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
11
+ const actual = await importOriginal();
12
+ return {
13
+ ...actual,
14
+ prepareForFirestore: (d) => d,
15
+ getFirestoreCount: mockGetFirestoreCount,
16
+ };
17
+ });
18
+ vi.mock("../../../../errors/normalize", () => ({
19
+ normalizeError: vi.fn(),
20
+ }));
21
+ vi.mock("../../../../security", () => ({
22
+ encryptPiiFields: (d) => d,
23
+ decryptPiiFields: (d) => d,
24
+ BID_PII_FIELDS: [],
25
+ }));
26
+ import { BidRepository } from "../bid.repository";
27
+ const repo = new BidRepository();
28
+ function makeBidDoc(overrides = {}) {
29
+ return {
30
+ id: "bid-1",
31
+ productId: "auction-x",
32
+ bidderId: "user-1",
33
+ userId: "user-1",
34
+ userName: "Ravi",
35
+ userEmail: "ravi@test.com",
36
+ bidAmount: 5000,
37
+ status: "active",
38
+ isWinning: false,
39
+ bidDate: new Date(),
40
+ createdAt: new Date(),
41
+ updatedAt: new Date(),
42
+ ...overrides,
43
+ };
44
+ }
45
+ function makeFakeRef(id = "bid-1") {
46
+ return { id };
47
+ }
48
+ beforeEach(() => {
49
+ vi.clearAllMocks();
50
+ db.collection.mockReturnValue(mockCollection);
51
+ mockCollection.doc.mockReturnValue(mockDocRef);
52
+ mockCollection.where.mockReturnValue(mockQuery);
53
+ mockQuery.where.mockReturnValue(mockQuery);
54
+ mockQuery.orderBy.mockReturnValue(mockQuery);
55
+ mockQuery.limit.mockReturnValue(mockQuery);
56
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
57
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
58
+ mockDocRef.set.mockResolvedValue(undefined);
59
+ db.batch.mockReturnValue(mockBatch);
60
+ mockGetFirestoreCount.mockResolvedValue(0);
61
+ });
62
+ // ---------------------------------------------------------------------------
63
+ // markWon / markLost / markOutbid / markWinning (batch helpers)
64
+ // ---------------------------------------------------------------------------
65
+ describe("BidRepository.markWon", () => {
66
+ it("stages status=won, isWinning=true in caller batch", () => {
67
+ const ref = makeFakeRef();
68
+ repo.markWon(mockBatch, ref);
69
+ expect(mockBatch.update).toHaveBeenCalledWith(ref, expect.objectContaining({ status: "won", isWinning: true }));
70
+ expect(mockBatch.commit).not.toHaveBeenCalled();
71
+ });
72
+ });
73
+ describe("BidRepository.markLost", () => {
74
+ it("stages status=lost, isWinning=false in caller batch", () => {
75
+ const ref = makeFakeRef();
76
+ repo.markLost(mockBatch, ref);
77
+ expect(mockBatch.update).toHaveBeenCalledWith(ref, expect.objectContaining({ status: "lost", isWinning: false }));
78
+ });
79
+ });
80
+ describe("BidRepository.markOutbid", () => {
81
+ it("stages status=outbid, isWinning=false in caller batch", () => {
82
+ const ref = makeFakeRef();
83
+ repo.markOutbid(mockBatch, ref);
84
+ expect(mockBatch.update).toHaveBeenCalledWith(ref, expect.objectContaining({ status: "outbid", isWinning: false }));
85
+ });
86
+ });
87
+ describe("BidRepository.markWinning", () => {
88
+ it("stages isWinning=true (does not change status) in caller batch", () => {
89
+ const ref = makeFakeRef();
90
+ repo.markWinning(mockBatch, ref);
91
+ expect(mockBatch.update).toHaveBeenCalledWith(ref, expect.objectContaining({ isWinning: true }));
92
+ });
93
+ });
94
+ // ---------------------------------------------------------------------------
95
+ // getWinningBid
96
+ // ---------------------------------------------------------------------------
97
+ describe("BidRepository.getWinningBid", () => {
98
+ it("returns {ref, data} for the winning active bid", async () => {
99
+ const bid = makeBidDoc({ isWinning: true, status: "active" });
100
+ const snap = makeSnap(bid, bid.id);
101
+ mockQuery.get.mockResolvedValue({ ...makeQuerySnap([{ id: bid.id, data: bid }]), docs: [snap], empty: false });
102
+ const result = await repo.getWinningBid("auction-x");
103
+ expect(result).not.toBeNull();
104
+ expect(result.data).toBeDefined();
105
+ });
106
+ it("returns null when no winning bid", async () => {
107
+ mockQuery.get.mockResolvedValue({ ...makeQuerySnap([]), empty: true, docs: [] });
108
+ const result = await repo.getWinningBid("auction-x");
109
+ expect(result).toBeNull();
110
+ });
111
+ it("queries by isWinning=true AND status=active", async () => {
112
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true });
113
+ await repo.getWinningBid("auction-x");
114
+ expect(mockQuery.where).toHaveBeenCalledWith("isWinning", "==", true);
115
+ expect(mockQuery.where).toHaveBeenCalledWith("status", "==", "active");
116
+ });
117
+ });
118
+ // ---------------------------------------------------------------------------
119
+ // findWinningBid
120
+ // ---------------------------------------------------------------------------
121
+ describe("BidRepository.findWinningBid", () => {
122
+ it("returns BidDocument for winning bid", async () => {
123
+ const bid = makeBidDoc({ isWinning: true });
124
+ const snap = makeSnap(bid, bid.id);
125
+ mockQuery.get.mockResolvedValue({ docs: [snap], empty: false });
126
+ const result = await repo.findWinningBid("auction-x");
127
+ expect(result).not.toBeNull();
128
+ });
129
+ it("returns null when no winning bid", async () => {
130
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true });
131
+ const result = await repo.findWinningBid("auction-x");
132
+ expect(result).toBeNull();
133
+ });
134
+ it("queries by isWinning=true (no status filter unlike getWinningBid)", async () => {
135
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true });
136
+ await repo.findWinningBid("auction-x");
137
+ expect(mockQuery.where).toHaveBeenCalledWith("isWinning", "==", true);
138
+ // findWinningBid does NOT filter by status — that's the distinction from getWinningBid
139
+ const statusFilterCalls = mockQuery.where.mock.calls.filter((call) => call[0] === "status");
140
+ expect(statusFilterCalls).toHaveLength(0);
141
+ });
142
+ });
143
+ // ---------------------------------------------------------------------------
144
+ // findHighestBid
145
+ // ---------------------------------------------------------------------------
146
+ describe("BidRepository.findHighestBid", () => {
147
+ it("returns highest bidAmount from active bids", async () => {
148
+ const bid = makeBidDoc({ bidAmount: 8000, status: "active" });
149
+ const snap = makeSnap(bid, bid.id);
150
+ mockQuery.get.mockResolvedValue({ docs: [snap], empty: false });
151
+ const result = await repo.findHighestBid("auction-x");
152
+ expect(result).toBe(8000);
153
+ });
154
+ it("returns 0 when no bids exist", async () => {
155
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true });
156
+ const result = await repo.findHighestBid("auction-x");
157
+ expect(result).toBe(0);
158
+ });
159
+ it("queries by status=active, ordered by bidAmount desc", async () => {
160
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true });
161
+ await repo.findHighestBid("auction-x");
162
+ expect(mockQuery.where).toHaveBeenCalledWith("status", "==", "active");
163
+ expect(mockQuery.orderBy).toHaveBeenCalledWith("bidAmount", "desc");
164
+ });
165
+ });
166
+ // ---------------------------------------------------------------------------
167
+ // setWinningBid
168
+ // ---------------------------------------------------------------------------
169
+ describe("BidRepository.setWinningBid", () => {
170
+ it("sets all other bids for product to outbid", async () => {
171
+ const bid1 = makeBidDoc({ id: "bid-1", isWinning: false });
172
+ const bid2 = makeBidDoc({ id: "bid-2", isWinning: false });
173
+ const snap1 = makeSnap(bid1, "bid-1");
174
+ const snap2 = makeSnap(bid2, "bid-2");
175
+ mockQuery.get.mockResolvedValue({ docs: [snap1, snap2], empty: false });
176
+ await repo.setWinningBid("bid-1", "auction-x");
177
+ expect(mockBatch.update).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ status: "outbid" }));
178
+ });
179
+ it("sets the winning bid to status=active, isWinning=true", async () => {
180
+ const bid = makeBidDoc({ id: "bid-winner" });
181
+ const snap = makeSnap(bid, "bid-winner");
182
+ mockQuery.get.mockResolvedValue({ docs: [snap], empty: false });
183
+ await repo.setWinningBid("bid-winner", "auction-x");
184
+ // The winning bid ref is updated separately with isWinning=true
185
+ const updateCalls = mockBatch.update.mock.calls;
186
+ const winningCall = updateCalls.find((c) => c[1].isWinning === true);
187
+ expect(winningCall).toBeDefined();
188
+ expect(winningCall[1].status).toBe("active");
189
+ });
190
+ it("commits the batch", async () => {
191
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true });
192
+ await repo.setWinningBid("bid-1", "auction-x");
193
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
194
+ });
195
+ });
196
+ // ---------------------------------------------------------------------------
197
+ // endAuction
198
+ // ---------------------------------------------------------------------------
199
+ describe("BidRepository.endAuction", () => {
200
+ it("isWinning bid → status=won", async () => {
201
+ const winBid = makeBidDoc({ id: "bid-win", isWinning: true });
202
+ const snap = makeSnap(winBid, "bid-win");
203
+ mockQuery.get.mockResolvedValue({ docs: [snap], empty: false });
204
+ await repo.endAuction("auction-x");
205
+ expect(mockBatch.update).toHaveBeenCalledWith(snap.ref, expect.objectContaining({ status: "won" }));
206
+ });
207
+ it("non-winning bids → status=lost", async () => {
208
+ const loseBid = makeBidDoc({ id: "bid-lose", isWinning: false });
209
+ const snap = makeSnap(loseBid, "bid-lose");
210
+ mockQuery.get.mockResolvedValue({ docs: [snap], empty: false });
211
+ await repo.endAuction("auction-x");
212
+ expect(mockBatch.update).toHaveBeenCalledWith(snap.ref, expect.objectContaining({ status: "lost" }));
213
+ });
214
+ it("commits the batch", async () => {
215
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true });
216
+ await repo.endAuction("auction-x");
217
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
218
+ });
219
+ });
220
+ // ---------------------------------------------------------------------------
221
+ // cancelProductBids
222
+ // ---------------------------------------------------------------------------
223
+ describe("BidRepository.cancelProductBids", () => {
224
+ it("sets all bids to status=cancelled", async () => {
225
+ const bid = makeBidDoc({ id: "bid-1" });
226
+ const snap = makeSnap(bid, "bid-1");
227
+ mockQuery.get.mockResolvedValue({ docs: [snap], empty: false });
228
+ await repo.cancelProductBids("auction-x");
229
+ expect(mockBatch.update).toHaveBeenCalledWith(snap.ref, expect.objectContaining({ status: "cancelled", isWinning: false }));
230
+ });
231
+ it("commits the batch", async () => {
232
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true });
233
+ await repo.cancelProductBids("auction-x");
234
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
235
+ });
236
+ });
237
+ // ---------------------------------------------------------------------------
238
+ // countByProduct / countByUser
239
+ // ---------------------------------------------------------------------------
240
+ describe("BidRepository.countByProduct", () => {
241
+ it("returns count from getFirestoreCount", async () => {
242
+ mockGetFirestoreCount.mockResolvedValue(7);
243
+ const count = await repo.countByProduct("auction-x");
244
+ expect(count).toBe(7);
245
+ });
246
+ it("queries by productId", async () => {
247
+ await repo.countByProduct("auction-x");
248
+ expect(mockCollection.where).toHaveBeenCalledWith("productId", "==", "auction-x");
249
+ });
250
+ });
251
+ describe("BidRepository.countByUser", () => {
252
+ it("returns count from getFirestoreCount", async () => {
253
+ mockGetFirestoreCount.mockResolvedValue(3);
254
+ const count = await repo.countByUser("user-1");
255
+ expect(count).toBe(3);
256
+ });
257
+ it("queries by userId", async () => {
258
+ await repo.countByUser("user-1");
259
+ expect(mockCollection.where).toHaveBeenCalledWith("userId", "==", "user-1");
260
+ });
261
+ });
262
+ // ---------------------------------------------------------------------------
263
+ // findOneByProductAndUser
264
+ // ---------------------------------------------------------------------------
265
+ describe("BidRepository.findOneByProductAndUser", () => {
266
+ it("returns matching bid", async () => {
267
+ const bid = makeBidDoc({ productId: "auction-x", userId: "user-1", status: "active" });
268
+ const snap = makeSnap(bid, bid.id);
269
+ mockQuery.get.mockResolvedValue({ docs: [snap], empty: false });
270
+ const result = await repo.findOneByProductAndUser("auction-x", "user-1");
271
+ expect(result).not.toBeNull();
272
+ });
273
+ it("returns null when not found", async () => {
274
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true });
275
+ const result = await repo.findOneByProductAndUser("auction-x", "user-2");
276
+ expect(result).toBeNull();
277
+ });
278
+ it("defaults status to 'active'", async () => {
279
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true });
280
+ await repo.findOneByProductAndUser("auction-x", "user-1");
281
+ expect(mockQuery.where).toHaveBeenCalledWith("status", "==", "active");
282
+ });
283
+ it("queries by all three conditions: productId, userId, status", async () => {
284
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true });
285
+ await repo.findOneByProductAndUser("auction-x", "user-1", "outbid");
286
+ expect(mockQuery.where).toHaveBeenCalledWith("productId", "==", "auction-x");
287
+ expect(mockQuery.where).toHaveBeenCalledWith("userId", "==", "user-1");
288
+ expect(mockQuery.where).toHaveBeenCalledWith("status", "==", "outbid");
289
+ });
290
+ });
@@ -17,6 +17,6 @@
17
17
  * export { authMeGET as GET } from "@mohasinac/feat-auth";
18
18
  * ```
19
19
  */
20
- export declare const authMeGET: (request: Request, context: {
20
+ export declare const authMeGET: (request: Request, context?: {
21
21
  params: Promise<Record<string, string>>;
22
- }) => Promise<Response>;
22
+ } | undefined) => Promise<Response>;
@@ -0,0 +1,310 @@
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 { ...actual, prepareForFirestore: (d) => d };
10
+ });
11
+ vi.mock("../../../../contracts/field-ops", () => ({
12
+ serverTimestamp: () => new Date(),
13
+ increment: (n) => n,
14
+ arrayUnion: (...args) => args,
15
+ arrayRemove: (...args) => args,
16
+ deleteField: () => null,
17
+ registerFieldOps: vi.fn(),
18
+ }));
19
+ vi.mock("../../../../errors/normalize", () => ({ normalizeError: vi.fn() }));
20
+ vi.mock("../../../../monitoring", () => ({
21
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
22
+ }));
23
+ vi.mock("../../token-helpers", () => ({
24
+ generateSessionId: vi.fn(() => "mock-session-id-abc123"),
25
+ generateToken: vi.fn(() => "mock-token-xyz"),
26
+ generateEmailVerificationToken: vi.fn(() => "mock-email-token"),
27
+ generatePasswordResetToken: vi.fn(() => "mock-pw-reset-token"),
28
+ }));
29
+ import { SessionRepository } from "../session.repository";
30
+ import { SESSION_EXPIRATION_MS } from "../../schemas";
31
+ const repo = new SessionRepository();
32
+ function makeSessionDoc(overrides = {}) {
33
+ const now = new Date();
34
+ return {
35
+ id: "mock-session-id-abc123",
36
+ userId: "user-ravi",
37
+ deviceInfo: { browser: "Chrome", os: "Windows", device: "desktop", ip: "*.*.*.1" },
38
+ location: { country: "IN" },
39
+ isActive: true,
40
+ createdAt: now,
41
+ lastActivity: now,
42
+ expiresAt: new Date(now.getTime() + SESSION_EXPIRATION_MS),
43
+ ...overrides,
44
+ };
45
+ }
46
+ beforeEach(() => {
47
+ vi.clearAllMocks();
48
+ db.collection.mockReturnValue(mockCollection);
49
+ mockCollection.doc.mockReturnValue(mockDocRef);
50
+ mockCollection.where.mockReturnValue(mockQuery);
51
+ mockCollection.orderBy = vi.fn().mockReturnValue(mockQuery);
52
+ mockCollection.get = vi.fn().mockResolvedValue(makeQuerySnap([]));
53
+ mockQuery.where.mockReturnValue(mockQuery);
54
+ mockQuery.orderBy.mockReturnValue(mockQuery);
55
+ mockQuery.limit.mockReturnValue(mockQuery);
56
+ mockQuery.offset = vi.fn().mockReturnValue(mockQuery);
57
+ mockQuery.count = vi.fn().mockReturnValue({
58
+ get: vi.fn().mockResolvedValue({ data: () => ({ count: 0 }) }),
59
+ });
60
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
61
+ mockDocRef.get.mockResolvedValue(makeSnap(makeSessionDoc(), "mock-session-id-abc123"));
62
+ mockDocRef.set.mockResolvedValue(undefined);
63
+ mockDocRef.update.mockResolvedValue(undefined);
64
+ mockDocRef.delete.mockResolvedValue(undefined);
65
+ });
66
+ // ---------------------------------------------------------------------------
67
+ // createSession
68
+ // ---------------------------------------------------------------------------
69
+ describe("SessionRepository.createSession", () => {
70
+ it("sets isActive: true on new session", async () => {
71
+ await repo.createSession("user-ravi", {
72
+ deviceInfo: { browser: "Chrome", os: "Windows", device: "desktop", ip: "*.*.*.1" },
73
+ location: { country: "IN" },
74
+ });
75
+ const setArg = mockDocRef.set.mock.calls[0][0];
76
+ expect(setArg.isActive).toBe(true);
77
+ });
78
+ it("sets expiresAt = now + SESSION_EXPIRATION_MS", async () => {
79
+ const before = Date.now();
80
+ await repo.createSession("user-ravi", {
81
+ deviceInfo: { browser: "Chrome", os: "Windows", device: "desktop", ip: "*.*.*.1" },
82
+ location: { country: "IN" },
83
+ });
84
+ const setArg = mockDocRef.set.mock.calls[0][0];
85
+ const expiresAt = setArg.expiresAt.getTime();
86
+ const expectedExpiry = before + SESSION_EXPIRATION_MS;
87
+ expect(expiresAt).toBeGreaterThanOrEqual(expectedExpiry - 100);
88
+ expect(expiresAt).toBeLessThanOrEqual(expectedExpiry + 1000);
89
+ });
90
+ it("stores deviceInfo in the session document", async () => {
91
+ await repo.createSession("user-ravi", {
92
+ deviceInfo: { browser: "Safari", os: "iOS", device: "mobile", ip: "*.*.*.2" },
93
+ location: { country: "IN" },
94
+ });
95
+ const setArg = mockDocRef.set.mock.calls[0][0];
96
+ expect(setArg.deviceInfo.browser).toBe("Safari");
97
+ });
98
+ it("uses generated sessionId as document ID", async () => {
99
+ await repo.createSession("user-ravi", {
100
+ deviceInfo: { browser: "Chrome", os: "Windows", device: "desktop", ip: "*.*.*.1" },
101
+ location: { country: "IN" },
102
+ });
103
+ expect(mockCollection.doc).toHaveBeenCalledWith("mock-session-id-abc123");
104
+ });
105
+ it("stores the userId in the session", async () => {
106
+ await repo.createSession("user-ravi", {
107
+ deviceInfo: { browser: "Chrome", os: "Windows", device: "desktop", ip: "*.*.*.1" },
108
+ location: { country: "IN" },
109
+ });
110
+ const setArg = mockDocRef.set.mock.calls[0][0];
111
+ expect(setArg.userId).toBe("user-ravi");
112
+ });
113
+ });
114
+ // ---------------------------------------------------------------------------
115
+ // updateActivity
116
+ // ---------------------------------------------------------------------------
117
+ describe("SessionRepository.updateActivity", () => {
118
+ it("updates lastActivity to current time", async () => {
119
+ const session = makeSessionDoc();
120
+ mockDocRef.get.mockResolvedValue(makeSnap(session, "mock-session-id-abc123"));
121
+ await repo.updateActivity("mock-session-id-abc123");
122
+ const updateArg = mockDocRef.update.mock.calls[0][0];
123
+ expect(updateArg.lastActivity).toBeInstanceOf(Date);
124
+ });
125
+ it("does NOT throw on NOT_FOUND error (session may have been revoked)", async () => {
126
+ const notFoundErr = Object.assign(new Error("NOT_FOUND"), { code: 5 });
127
+ mockDocRef.update.mockRejectedValue(notFoundErr);
128
+ // update calls super.update which calls docRef.update
129
+ await expect(repo.updateActivity("revoked-session")).resolves.toBeUndefined();
130
+ });
131
+ it("does NOT throw on 'No document to update' error", async () => {
132
+ const err = new Error("No document to update");
133
+ mockDocRef.update.mockRejectedValue(err);
134
+ await expect(repo.updateActivity("deleted-session")).resolves.toBeUndefined();
135
+ });
136
+ it("DOES re-throw errors that are not NOT_FOUND", async () => {
137
+ const permErr = Object.assign(new Error("Permission denied"), { code: 7 });
138
+ mockDocRef.update.mockRejectedValue(permErr);
139
+ // BaseRepository.update() wraps in DatabaseError, so the thrown error has DATABASE_ERROR message
140
+ await expect(repo.updateActivity("session-id")).rejects.toThrow("Failed to update document: session-id");
141
+ });
142
+ });
143
+ // ---------------------------------------------------------------------------
144
+ // revokeSession
145
+ // ---------------------------------------------------------------------------
146
+ describe("SessionRepository.revokeSession", () => {
147
+ it("sets isActive: false on the session", async () => {
148
+ const session = makeSessionDoc();
149
+ mockDocRef.get.mockResolvedValue(makeSnap(session, "mock-session-id-abc123"));
150
+ await repo.revokeSession("mock-session-id-abc123", "admin");
151
+ const updateArg = mockDocRef.update.mock.calls[0][0];
152
+ expect(updateArg.isActive).toBe(false);
153
+ });
154
+ it("stores revokedAt timestamp", async () => {
155
+ const session = makeSessionDoc();
156
+ mockDocRef.get.mockResolvedValue(makeSnap(session, "mock-session-id-abc123"));
157
+ await repo.revokeSession("mock-session-id-abc123", "admin");
158
+ const updateArg = mockDocRef.update.mock.calls[0][0];
159
+ expect(updateArg.revokedAt).toBeInstanceOf(Date);
160
+ });
161
+ it("stores revokedBy actor", async () => {
162
+ const session = makeSessionDoc();
163
+ mockDocRef.get.mockResolvedValue(makeSnap(session, "mock-session-id-abc123"));
164
+ await repo.revokeSession("mock-session-id-abc123", "user-admin");
165
+ const updateArg = mockDocRef.update.mock.calls[0][0];
166
+ expect(updateArg.revokedBy).toBe("user-admin");
167
+ });
168
+ });
169
+ // ---------------------------------------------------------------------------
170
+ // revokeAllUserSessions
171
+ // ---------------------------------------------------------------------------
172
+ describe("SessionRepository.revokeAllUserSessions", () => {
173
+ it("revokes all active sessions for the user", async () => {
174
+ const sessions = [makeSessionDoc({ id: "s-1" }), makeSessionDoc({ id: "s-2" })];
175
+ // findActiveByUser uses a multi-where + orderBy + get chain
176
+ mockQuery.get.mockResolvedValue(makeQuerySnap(sessions.map((s) => ({ id: s.id, data: s }))));
177
+ mockDocRef.get.mockResolvedValue(makeSnap(makeSessionDoc(), "s-1"));
178
+ const count = await repo.revokeAllUserSessions("user-ravi", "admin");
179
+ expect(count).toBe(2);
180
+ expect(mockDocRef.update).toHaveBeenCalledTimes(2);
181
+ });
182
+ it("returns 0 when user has no active sessions", async () => {
183
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
184
+ const count = await repo.revokeAllUserSessions("user-no-sessions", "admin");
185
+ expect(count).toBe(0);
186
+ expect(mockDocRef.update).not.toHaveBeenCalled();
187
+ });
188
+ });
189
+ // ---------------------------------------------------------------------------
190
+ // findActiveByUser
191
+ // ---------------------------------------------------------------------------
192
+ describe("SessionRepository.findActiveByUser", () => {
193
+ it("queries userId, isActive==true, and expiresAt > now", async () => {
194
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
195
+ await repo.findActiveByUser("user-ravi");
196
+ expect(mockCollection.where).toHaveBeenCalledWith("userId", "==", "user-ravi");
197
+ expect(mockQuery.where).toHaveBeenCalledWith("isActive", "==", true);
198
+ expect(mockQuery.where).toHaveBeenCalledWith("expiresAt", ">", expect.any(Date));
199
+ });
200
+ it("returns only active sessions for the given user", async () => {
201
+ const session = makeSessionDoc({ isActive: true });
202
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: session.id, data: session }]));
203
+ const results = await repo.findActiveByUser("user-ravi");
204
+ expect(results).toHaveLength(1);
205
+ expect(results[0].userId).toBe("user-ravi");
206
+ });
207
+ it("orders by expiresAt desc", async () => {
208
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
209
+ await repo.findActiveByUser("user-ravi");
210
+ expect(mockQuery.orderBy).toHaveBeenCalledWith("expiresAt", "desc");
211
+ });
212
+ });
213
+ // ---------------------------------------------------------------------------
214
+ // cleanupExpiredSessions
215
+ // ---------------------------------------------------------------------------
216
+ describe("SessionRepository.cleanupExpiredSessions", () => {
217
+ it("queries sessions where expiresAt <= now", async () => {
218
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
219
+ await repo.cleanupExpiredSessions();
220
+ expect(mockCollection.where).toHaveBeenCalledWith("expiresAt", "<=", expect.any(Date));
221
+ });
222
+ it("deletes each expired session and returns count", async () => {
223
+ const expired = [
224
+ makeSessionDoc({ id: "s-expired-1", isActive: false }),
225
+ makeSessionDoc({ id: "s-expired-2", isActive: false }),
226
+ ];
227
+ mockQuery.get.mockResolvedValue(makeQuerySnap(expired.map((s) => ({ id: s.id, data: s }))));
228
+ const count = await repo.cleanupExpiredSessions();
229
+ expect(count).toBe(2);
230
+ expect(mockDocRef.delete).toHaveBeenCalledTimes(2);
231
+ });
232
+ it("returns 0 and deletes nothing when no expired sessions", async () => {
233
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
234
+ const count = await repo.cleanupExpiredSessions();
235
+ expect(count).toBe(0);
236
+ expect(mockDocRef.delete).not.toHaveBeenCalled();
237
+ });
238
+ it("limits to 500 documents per cleanup run", async () => {
239
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
240
+ await repo.cleanupExpiredSessions();
241
+ expect(mockQuery.limit).toHaveBeenCalledWith(500);
242
+ });
243
+ });
244
+ // ---------------------------------------------------------------------------
245
+ // countActiveByUser
246
+ // ---------------------------------------------------------------------------
247
+ describe("SessionRepository.countActiveByUser", () => {
248
+ it("returns count of active sessions for user", async () => {
249
+ const sessions = [makeSessionDoc(), makeSessionDoc({ id: "s-2" })];
250
+ mockQuery.get.mockResolvedValue(makeQuerySnap(sessions.map((s) => ({ id: s.id, data: s }))));
251
+ const count = await repo.countActiveByUser("user-ravi");
252
+ expect(count).toBe(2);
253
+ });
254
+ it("returns 0 when user has no active sessions", async () => {
255
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
256
+ const count = await repo.countActiveByUser("user-no-sessions");
257
+ expect(count).toBe(0);
258
+ });
259
+ });
260
+ // ---------------------------------------------------------------------------
261
+ // findActiveSession
262
+ // ---------------------------------------------------------------------------
263
+ describe("SessionRepository.findActiveSession", () => {
264
+ it("returns session when it is active and not expired", async () => {
265
+ const session = makeSessionDoc({ isActive: true, expiresAt: new Date(Date.now() + 60000) });
266
+ mockDocRef.get.mockResolvedValue(makeSnap(session, "mock-session-id-abc123"));
267
+ const result = await repo.findActiveSession("mock-session-id-abc123");
268
+ expect(result).not.toBeNull();
269
+ expect(result?.userId).toBe("user-ravi");
270
+ });
271
+ it("returns null when session is isActive: false", async () => {
272
+ const session = makeSessionDoc({ isActive: false });
273
+ mockDocRef.get.mockResolvedValue(makeSnap(session, "s-revoked"));
274
+ const result = await repo.findActiveSession("s-revoked");
275
+ expect(result).toBeNull();
276
+ });
277
+ it("returns null when session is expired (expiresAt in past)", async () => {
278
+ const session = makeSessionDoc({ isActive: true, expiresAt: new Date(Date.now() - 1000) });
279
+ mockDocRef.get.mockResolvedValue(makeSnap(session, "s-expired"));
280
+ const result = await repo.findActiveSession("s-expired");
281
+ expect(result).toBeNull();
282
+ });
283
+ it("returns null when session does not exist", async () => {
284
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
285
+ const result = await repo.findActiveSession("nonexistent");
286
+ expect(result).toBeNull();
287
+ });
288
+ });
289
+ // ---------------------------------------------------------------------------
290
+ // getExpiredRefs
291
+ // ---------------------------------------------------------------------------
292
+ describe("SessionRepository.getExpiredRefs", () => {
293
+ it("queries sessions where expiresAt < now", async () => {
294
+ const now = new Date();
295
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
296
+ await repo.getExpiredRefs(now);
297
+ expect(mockCollection.where).toHaveBeenCalledWith("expiresAt", "<", now);
298
+ });
299
+ it("limits to 500 documents", async () => {
300
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
301
+ await repo.getExpiredRefs(new Date());
302
+ expect(mockQuery.limit).toHaveBeenCalledWith(500);
303
+ });
304
+ it("returns document refs from the matching sessions", async () => {
305
+ const session = makeSessionDoc({ id: "s-expired" });
306
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "s-expired", data: session }]));
307
+ const refs = await repo.getExpiredRefs(new Date());
308
+ expect(refs).toHaveLength(1);
309
+ });
310
+ });