@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,458 @@
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
+ 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("../../../../security", () => ({
15
+ encryptPiiFields: (d) => d,
16
+ decryptPiiFields: (d) => d,
17
+ OFFER_PII_FIELDS: ["buyerName", "buyerEmail"],
18
+ }));
19
+ vi.mock("../../../../monitoring", () => ({
20
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
21
+ }));
22
+ vi.mock("../../../../errors/normalize", () => ({
23
+ normalizeError: vi.fn(),
24
+ }));
25
+ import { OfferRepository } from "../offer.repository";
26
+ const repo = new OfferRepository();
27
+ function makeOfferDoc(overrides = {}) {
28
+ return {
29
+ id: "offer-product-charizard-user-ravi-20260627-abc123",
30
+ productId: "product-charizard-psa9",
31
+ storeId: "store-pokemon-palace",
32
+ buyerUid: "user-ravi",
33
+ buyerName: "Ravi Kumar",
34
+ buyerEmail: "ravi@test.com",
35
+ offerAmount: 45000, // paise
36
+ status: "pending",
37
+ expiresAt: new Date(Date.now() + 48 * 60 * 60 * 1000),
38
+ createdAt: new Date(),
39
+ updatedAt: new Date(),
40
+ ...overrides,
41
+ };
42
+ }
43
+ beforeEach(() => {
44
+ vi.clearAllMocks();
45
+ db.collection.mockReturnValue(mockCollection);
46
+ mockCollection.doc.mockReturnValue(mockDocRef);
47
+ mockCollection.where.mockReturnValue(mockQuery);
48
+ mockQuery.where.mockReturnValue(mockQuery);
49
+ mockQuery.orderBy.mockReturnValue(mockQuery);
50
+ mockQuery.limit.mockReturnValue(mockQuery);
51
+ mockQuery.select = vi.fn().mockReturnValue(mockQuery);
52
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
53
+ const offerDoc = makeOfferDoc();
54
+ mockDocRef.get.mockResolvedValue(makeSnap(offerDoc, offerDoc.id));
55
+ mockDocRef.set.mockResolvedValue(undefined);
56
+ mockDocRef.update.mockResolvedValue(undefined);
57
+ db.batch.mockReturnValue(mockBatch);
58
+ });
59
+ // ---------------------------------------------------------------------------
60
+ // create
61
+ // ---------------------------------------------------------------------------
62
+ describe("OfferRepository.create", () => {
63
+ it("sets status: pending on creation", async () => {
64
+ await repo.create({
65
+ productId: "product-charizard-psa9",
66
+ storeId: "store-pokemon-palace",
67
+ buyerUid: "user-ravi",
68
+ buyerName: "Ravi Kumar",
69
+ buyerEmail: "ravi@test.com",
70
+ offerAmount: 45000,
71
+ });
72
+ const setArg = mockDocRef.set.mock.calls[0][0];
73
+ expect(setArg.status).toBe("pending");
74
+ });
75
+ it("sets expiresAt to approximately 48 hours from now", async () => {
76
+ const before = Date.now();
77
+ await repo.create({
78
+ productId: "product-1",
79
+ storeId: "store-1",
80
+ buyerUid: "user-1",
81
+ buyerName: "Buyer",
82
+ buyerEmail: "b@test.com",
83
+ offerAmount: 10000,
84
+ });
85
+ const after = Date.now();
86
+ const setArg = mockDocRef.set.mock.calls[0][0];
87
+ const expiresAt = setArg.expiresAt.getTime();
88
+ const expectedMs = 48 * 60 * 60 * 1000;
89
+ expect(expiresAt).toBeGreaterThanOrEqual(before + expectedMs - 100);
90
+ expect(expiresAt).toBeLessThanOrEqual(after + expectedMs + 100);
91
+ });
92
+ it("persists to Firestore via doc.set", async () => {
93
+ await repo.create({
94
+ productId: "p-1",
95
+ storeId: "s-1",
96
+ buyerUid: "u-1",
97
+ buyerName: "Buyer",
98
+ buyerEmail: "b@test.com",
99
+ offerAmount: 20000,
100
+ });
101
+ expect(mockDocRef.set).toHaveBeenCalledOnce();
102
+ });
103
+ it("returns plaintext document (not encrypted) to caller", async () => {
104
+ const result = await repo.create({
105
+ productId: "p-1",
106
+ storeId: "s-1",
107
+ buyerUid: "u-1",
108
+ buyerName: "Ravi Kumar",
109
+ buyerEmail: "ravi@test.com",
110
+ offerAmount: 10000,
111
+ });
112
+ // Passthrough mock means plaintext === encrypted; verify field is accessible
113
+ expect(result.buyerName).toBe("Ravi Kumar");
114
+ });
115
+ it("sets createdAt and updatedAt timestamps", async () => {
116
+ const result = await repo.create({
117
+ productId: "p-1",
118
+ storeId: "s-1",
119
+ buyerUid: "u-1",
120
+ buyerName: "Buyer",
121
+ buyerEmail: "b@test.com",
122
+ offerAmount: 5000,
123
+ });
124
+ expect(result.createdAt).toBeInstanceOf(Date);
125
+ expect(result.updatedAt).toBeInstanceOf(Date);
126
+ });
127
+ });
128
+ // ---------------------------------------------------------------------------
129
+ // hasActiveOffer
130
+ // ---------------------------------------------------------------------------
131
+ describe("OfferRepository.hasActiveOffer", () => {
132
+ it("returns true when a pending offer exists for buyer + product", async () => {
133
+ const offer = makeOfferDoc({ status: "pending" });
134
+ mockQuery.get.mockResolvedValue({
135
+ empty: false,
136
+ size: 1,
137
+ docs: [makeSnap(offer, offer.id)],
138
+ });
139
+ const result = await repo.hasActiveOffer("user-ravi", "product-charizard-psa9");
140
+ expect(result).toBe(true);
141
+ });
142
+ it("returns true when a countered offer exists for buyer + product", async () => {
143
+ const offer = makeOfferDoc({ status: "countered" });
144
+ mockQuery.get.mockResolvedValue({
145
+ empty: false,
146
+ size: 1,
147
+ docs: [makeSnap(offer, offer.id)],
148
+ });
149
+ const result = await repo.hasActiveOffer("user-ravi", "product-charizard-psa9");
150
+ expect(result).toBe(true);
151
+ });
152
+ it("returns false when only expired/declined offers exist", async () => {
153
+ mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
154
+ const result = await repo.hasActiveOffer("user-ravi", "product-1");
155
+ expect(result).toBe(false);
156
+ });
157
+ it("returns false when no offers exist at all", async () => {
158
+ mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
159
+ const result = await repo.hasActiveOffer("user-nobody", "product-1");
160
+ expect(result).toBe(false);
161
+ });
162
+ it("queries by buyerUid, productId, and status in [pending, countered]", async () => {
163
+ mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
164
+ await repo.hasActiveOffer("user-xyz", "product-abc");
165
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("buyerUid"), "==", "user-xyz");
166
+ expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("productId"), "==", "product-abc");
167
+ expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("status"), "in", ["pending", "countered"]);
168
+ });
169
+ it("uses select() stub read for minimal Firestore cost", async () => {
170
+ mockQuery.get.mockResolvedValue({ empty: false, size: 1, docs: [] });
171
+ await repo.hasActiveOffer("user-ravi", "product-1");
172
+ expect(mockQuery.select).toHaveBeenCalled();
173
+ });
174
+ it("uses limit(1) to avoid scanning all offers", async () => {
175
+ mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
176
+ await repo.hasActiveOffer("user-ravi", "product-1");
177
+ expect(mockQuery.limit).toHaveBeenCalledWith(1);
178
+ });
179
+ });
180
+ // ---------------------------------------------------------------------------
181
+ // countByBuyerAndProduct
182
+ // ---------------------------------------------------------------------------
183
+ describe("OfferRepository.countByBuyerAndProduct", () => {
184
+ it("returns 0 when no offers match", async () => {
185
+ mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
186
+ const count = await repo.countByBuyerAndProduct("user-ravi", "product-1", new Date(Date.now() - 7 * 24 * 60 * 60 * 1000));
187
+ expect(count).toBe(0);
188
+ });
189
+ it("returns correct count when offers exist", async () => {
190
+ mockQuery.get.mockResolvedValue({ empty: false, size: 3, docs: [] });
191
+ const count = await repo.countByBuyerAndProduct("user-ravi", "product-1", new Date());
192
+ expect(count).toBe(3);
193
+ });
194
+ it("queries by buyerUid, productId, and createdAt >= since", async () => {
195
+ mockQuery.get.mockResolvedValue({ empty: false, size: 2, docs: [] });
196
+ const since = new Date(Date.now() - 86400000);
197
+ await repo.countByBuyerAndProduct("user-abc", "product-xyz", since);
198
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("buyerUid"), "==", "user-abc");
199
+ expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("productId"), "==", "product-xyz");
200
+ expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("createdAt"), ">=", since);
201
+ });
202
+ it("uses select() stub read for minimal Firestore cost", async () => {
203
+ mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
204
+ await repo.countByBuyerAndProduct("user-1", "product-1", new Date());
205
+ expect(mockQuery.select).toHaveBeenCalled();
206
+ });
207
+ it("returns snap.size (not docs.length)", async () => {
208
+ // size may differ from docs.length in real Firestore (SELECT stub reads)
209
+ mockQuery.get.mockResolvedValue({ empty: false, size: 5, docs: [] });
210
+ const count = await repo.countByBuyerAndProduct("user-1", "product-1", new Date());
211
+ expect(count).toBe(5);
212
+ });
213
+ });
214
+ // ---------------------------------------------------------------------------
215
+ // findExpiredActive
216
+ // ---------------------------------------------------------------------------
217
+ describe("OfferRepository.findExpiredActive", () => {
218
+ it("returns offers with status in [pending, countered] and expiresAt <= now", async () => {
219
+ const now = new Date();
220
+ const expiredOffer = makeOfferDoc({
221
+ status: "pending",
222
+ expiresAt: new Date(now.getTime() - 3600000),
223
+ });
224
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: expiredOffer.id, data: expiredOffer }]));
225
+ const results = await repo.findExpiredActive(now);
226
+ expect(results).toHaveLength(1);
227
+ });
228
+ it("queries status in [pending, countered]", async () => {
229
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
230
+ await repo.findExpiredActive(new Date());
231
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("status"), "in", ["pending", "countered"]);
232
+ });
233
+ it("queries expiresAt <= now", async () => {
234
+ const now = new Date();
235
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
236
+ await repo.findExpiredActive(now);
237
+ expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("expiresAt"), "<=", now);
238
+ });
239
+ it("applies limit(500) to avoid unbounded scans", async () => {
240
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
241
+ await repo.findExpiredActive(new Date());
242
+ expect(mockQuery.limit).toHaveBeenCalledWith(500);
243
+ });
244
+ it("returns empty array when no expired active offers", async () => {
245
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
246
+ const results = await repo.findExpiredActive(new Date());
247
+ expect(results).toHaveLength(0);
248
+ });
249
+ });
250
+ // ---------------------------------------------------------------------------
251
+ // expireMany
252
+ // ---------------------------------------------------------------------------
253
+ describe("OfferRepository.expireMany", () => {
254
+ it("batch-updates each offer ID to status: expired", async () => {
255
+ await repo.expireMany(["offer-1", "offer-2", "offer-3"]);
256
+ expect(mockBatch.update).toHaveBeenCalledTimes(3);
257
+ const calls = mockBatch.update.mock.calls;
258
+ expect(calls.every((c) => c[1].status === "expired")).toBe(true);
259
+ });
260
+ it("sets updatedAt on every expired offer", async () => {
261
+ await repo.expireMany(["offer-1", "offer-2"]);
262
+ const calls = mockBatch.update.mock.calls;
263
+ expect(calls.every((c) => c[1].updatedAt instanceof Date)).toBe(true);
264
+ });
265
+ it("commits the batch exactly once", async () => {
266
+ await repo.expireMany(["offer-a", "offer-b"]);
267
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
268
+ });
269
+ it("empty ID list → batch.commit called, no updates staged", async () => {
270
+ await repo.expireMany([]);
271
+ expect(mockBatch.update).not.toHaveBeenCalled();
272
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
273
+ });
274
+ it("single offer → one batch.update call", async () => {
275
+ await repo.expireMany(["offer-only"]);
276
+ expect(mockBatch.update).toHaveBeenCalledTimes(1);
277
+ });
278
+ });
279
+ // ---------------------------------------------------------------------------
280
+ // accept
281
+ // ---------------------------------------------------------------------------
282
+ describe("OfferRepository.accept", () => {
283
+ it("sets status: accepted", async () => {
284
+ await repo.accept("offer-1", 40000);
285
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "accepted" }));
286
+ });
287
+ it("stores lockedPrice on accept", async () => {
288
+ await repo.accept("offer-1", 40000);
289
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ lockedPrice: 40000 }));
290
+ });
291
+ it("sets acceptedAt timestamp", async () => {
292
+ await repo.accept("offer-1", 40000);
293
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ acceptedAt: expect.any(Date) }));
294
+ });
295
+ it("sets respondedAt timestamp", async () => {
296
+ await repo.accept("offer-1", 40000);
297
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ respondedAt: expect.any(Date) }));
298
+ });
299
+ it("stores sellerNote when provided", async () => {
300
+ await repo.accept("offer-1", 40000, "Deal accepted!");
301
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ sellerNote: "Deal accepted!" }));
302
+ });
303
+ it("sets updatedAt timestamp", async () => {
304
+ await repo.accept("offer-1", 40000);
305
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
306
+ });
307
+ });
308
+ // ---------------------------------------------------------------------------
309
+ // decline
310
+ // ---------------------------------------------------------------------------
311
+ describe("OfferRepository.decline", () => {
312
+ it("sets status: declined", async () => {
313
+ await repo.decline("offer-1");
314
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "declined" }));
315
+ });
316
+ it("sets respondedAt timestamp", async () => {
317
+ await repo.decline("offer-1");
318
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ respondedAt: expect.any(Date) }));
319
+ });
320
+ it("stores sellerNote when provided", async () => {
321
+ await repo.decline("offer-1", "Not interested");
322
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ sellerNote: "Not interested" }));
323
+ });
324
+ it("does NOT set lockedPrice on decline", async () => {
325
+ await repo.decline("offer-1");
326
+ const updateArg = mockDocRef.update.mock.calls[0][0];
327
+ expect(Object.keys(updateArg)).not.toContain("lockedPrice");
328
+ });
329
+ });
330
+ // ---------------------------------------------------------------------------
331
+ // counter
332
+ // ---------------------------------------------------------------------------
333
+ describe("OfferRepository.counter", () => {
334
+ it("sets status: countered", async () => {
335
+ await repo.counter("offer-1", 38000);
336
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "countered" }));
337
+ });
338
+ it("stores counterAmount", async () => {
339
+ await repo.counter("offer-1", 38000);
340
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ counterAmount: 38000 }));
341
+ });
342
+ it("sets respondedAt timestamp", async () => {
343
+ await repo.counter("offer-1", 38000);
344
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ respondedAt: expect.any(Date) }));
345
+ });
346
+ it("stores sellerNote when provided", async () => {
347
+ await repo.counter("offer-1", 38000, "Best I can do");
348
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ sellerNote: "Best I can do" }));
349
+ });
350
+ });
351
+ // ---------------------------------------------------------------------------
352
+ // acceptCounter
353
+ // ---------------------------------------------------------------------------
354
+ describe("OfferRepository.acceptCounter", () => {
355
+ it("sets lockedPrice = counterAmount from existing offer", async () => {
356
+ const offerDoc = makeOfferDoc({ status: "countered", counterAmount: 38000 });
357
+ mockDocRef.get.mockResolvedValue(makeSnap(offerDoc, "offer-1"));
358
+ await repo.acceptCounter("offer-1");
359
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ lockedPrice: 38000 }));
360
+ });
361
+ it("sets status: accepted", async () => {
362
+ const offerDoc = makeOfferDoc({ status: "countered", counterAmount: 38000 });
363
+ mockDocRef.get.mockResolvedValue(makeSnap(offerDoc, "offer-1"));
364
+ await repo.acceptCounter("offer-1");
365
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "accepted" }));
366
+ });
367
+ it("sets acceptedAt and respondedAt", async () => {
368
+ const offerDoc = makeOfferDoc({ status: "countered", counterAmount: 38000 });
369
+ mockDocRef.get.mockResolvedValue(makeSnap(offerDoc, "offer-1"));
370
+ await repo.acceptCounter("offer-1");
371
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({
372
+ acceptedAt: expect.any(Date),
373
+ respondedAt: expect.any(Date),
374
+ }));
375
+ });
376
+ it("throws when offer not found", async () => {
377
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
378
+ await expect(repo.acceptCounter("offer-ghost")).rejects.toThrow(/offer.*not found|counter.*not found/i);
379
+ });
380
+ it("throws when offer has no counterAmount", async () => {
381
+ const offerDoc = makeOfferDoc({ status: "pending", counterAmount: undefined });
382
+ mockDocRef.get.mockResolvedValue(makeSnap(offerDoc, "offer-1"));
383
+ await expect(repo.acceptCounter("offer-1")).rejects.toThrow();
384
+ });
385
+ });
386
+ // ---------------------------------------------------------------------------
387
+ // withdraw
388
+ // ---------------------------------------------------------------------------
389
+ describe("OfferRepository.withdraw", () => {
390
+ it("sets status: withdrawn", async () => {
391
+ await repo.withdraw("offer-1");
392
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "withdrawn" }));
393
+ });
394
+ it("sets respondedAt timestamp", async () => {
395
+ await repo.withdraw("offer-1");
396
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ respondedAt: expect.any(Date) }));
397
+ });
398
+ it("sets updatedAt timestamp", async () => {
399
+ await repo.withdraw("offer-1");
400
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
401
+ });
402
+ it("does NOT set lockedPrice on withdraw", async () => {
403
+ await repo.withdraw("offer-1");
404
+ const updateArg = mockDocRef.update.mock.calls[0][0];
405
+ expect(Object.keys(updateArg)).not.toContain("lockedPrice");
406
+ });
407
+ });
408
+ // ---------------------------------------------------------------------------
409
+ // findPendingByStore
410
+ // ---------------------------------------------------------------------------
411
+ describe("OfferRepository.findPendingByStore", () => {
412
+ it("returns only pending offers for the given storeId", async () => {
413
+ const offer = makeOfferDoc({ status: "pending", storeId: "store-pokemon-palace" });
414
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: offer.id, data: offer }]));
415
+ const results = await repo.findPendingByStore("store-pokemon-palace");
416
+ expect(results).toHaveLength(1);
417
+ });
418
+ it("queries by storeId and status == pending", async () => {
419
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
420
+ await repo.findPendingByStore("store-xyz");
421
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("storeId"), "==", "store-xyz");
422
+ expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("status"), "==", "pending");
423
+ });
424
+ it("orders by createdAt ascending (oldest first for seller to act on)", async () => {
425
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
426
+ await repo.findPendingByStore("store-1");
427
+ expect(mockQuery.orderBy).toHaveBeenCalledWith(expect.stringContaining("createdAt"), "asc");
428
+ });
429
+ it("returns empty array when no pending offers", async () => {
430
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
431
+ const results = await repo.findPendingByStore("store-empty");
432
+ expect(results).toHaveLength(0);
433
+ });
434
+ });
435
+ // ---------------------------------------------------------------------------
436
+ // findExpired
437
+ // ---------------------------------------------------------------------------
438
+ describe("OfferRepository.findExpired", () => {
439
+ it("returns offers with status in [pending, countered] and expiresAt <= now", async () => {
440
+ const expired = makeOfferDoc({
441
+ status: "pending",
442
+ expiresAt: new Date(Date.now() - 1000),
443
+ });
444
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: expired.id, data: expired }]));
445
+ const results = await repo.findExpired();
446
+ expect(results).toHaveLength(1);
447
+ });
448
+ it("queries status in [pending, countered]", async () => {
449
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
450
+ await repo.findExpired();
451
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("status"), "in", ["pending", "countered"]);
452
+ });
453
+ it("queries expiresAt <= current time", async () => {
454
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
455
+ await repo.findExpired();
456
+ expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("expiresAt"), "<=", expect.any(Date));
457
+ });
458
+ });