@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,211 @@
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", () => ({
5
+ getAdminDb: () => db,
6
+ prepareForFirestore: (data) => data,
7
+ BaseRepository: class {
8
+ get db() { return db; }
9
+ constructor(col) { this.collection = col; }
10
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
+ getCollection() { return db.collection(this.collection); }
12
+ mapDoc(snap) {
13
+ return { id: snap.id, ...snap.data() };
14
+ }
15
+ async findById(id) {
16
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
17
+ const snap = await db.collection(this.collection).doc(id).get();
18
+ return snap.exists ? { id: snap.id, ...snap.data() } : null;
19
+ }
20
+ async findBy(field, value) {
21
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
22
+ const snap = await db.collection(this.collection).where(field, "==", value).get();
23
+ return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
24
+ }
25
+ async update(id, data) {
26
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
27
+ await db.collection(this.collection).doc(id).update(data);
28
+ return { id, ...data };
29
+ }
30
+ },
31
+ }));
32
+ vi.mock("../../../../monitoring", () => ({
33
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
34
+ }));
35
+ vi.mock("../../../../errors/normalize", () => ({
36
+ normalizeError: vi.fn(),
37
+ }));
38
+ vi.mock("../../../../security", () => ({
39
+ ORDER_PII_FIELDS: ["shippingAddress.phone", "shippingAddress.fullName"],
40
+ encryptPiiFields: (data) => data,
41
+ decryptPiiFields: (data) => data,
42
+ }));
43
+ vi.mock("../../../../contracts/field-ops", () => ({
44
+ serverTimestamp: () => new Date(),
45
+ }));
46
+ vi.mock("../../schemas", async () => ({
47
+ ORDER_COLLECTION: "orders",
48
+ OrderStatusValues: {
49
+ PENDING: "pending",
50
+ CONFIRMED: "confirmed",
51
+ PROCESSING: "processing",
52
+ SHIPPED: "shipped",
53
+ DELIVERED: "delivered",
54
+ CANCELLED: "cancelled",
55
+ REFUNDED: "refunded",
56
+ },
57
+ createOrderId: () => "order-1-20260101-abc123",
58
+ }));
59
+ import { OrderRepository } from "../orders.repository";
60
+ const repo = new OrderRepository();
61
+ beforeEach(() => {
62
+ vi.clearAllMocks();
63
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
64
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
65
+ mockDocRef.update.mockResolvedValue(undefined);
66
+ mockDocRef.delete.mockResolvedValue(undefined);
67
+ mockCollection.doc.mockReturnValue(mockDocRef);
68
+ db.collection.mockReturnValue(mockCollection);
69
+ db.batch.mockReturnValue(mockBatch);
70
+ });
71
+ describe("OrderRepository.cancelOrder", () => {
72
+ it("sets status: cancelled", async () => {
73
+ mockDocRef.update.mockResolvedValue(undefined);
74
+ await repo.cancelOrder("order-1", "user changed mind");
75
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "cancelled" }));
76
+ });
77
+ it("stores cancellation reason", async () => {
78
+ await repo.cancelOrder("order-1", "payment failed");
79
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ cancellationReason: "payment failed" }));
80
+ });
81
+ });
82
+ describe("OrderRepository.updateStatus", () => {
83
+ it("updates status and updatedAt", async () => {
84
+ await repo.updateStatus("order-1", "shipped");
85
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "shipped", updatedAt: expect.any(Date) }));
86
+ });
87
+ });
88
+ describe("OrderRepository.hasUserPurchased", () => {
89
+ it("returns true when confirmed order exists for user + product", async () => {
90
+ const docs = makeQuerySnap([
91
+ { id: "o1", data: { userId: "user-1", productId: "product-x", status: "confirmed" } },
92
+ ]);
93
+ mockQuery.get.mockResolvedValue(docs);
94
+ const result = await repo.hasUserPurchased("user-1", "product-x");
95
+ expect(result).toBe(true);
96
+ });
97
+ it("returns true for DELIVERED status", async () => {
98
+ const docs = makeQuerySnap([
99
+ { id: "o1", data: { userId: "user-1", productId: "product-x", status: "delivered" } },
100
+ ]);
101
+ mockQuery.get.mockResolvedValue(docs);
102
+ const result = await repo.hasUserPurchased("user-1", "product-x");
103
+ expect(result).toBe(true);
104
+ });
105
+ it("returns false for CANCELLED status", async () => {
106
+ const docs = makeQuerySnap([
107
+ { id: "o1", data: { userId: "user-1", productId: "product-x", status: "cancelled" } },
108
+ ]);
109
+ mockQuery.get.mockResolvedValue(docs);
110
+ const result = await repo.hasUserPurchased("user-1", "product-x");
111
+ expect(result).toBe(false);
112
+ });
113
+ it("returns false when no orders exist", async () => {
114
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
115
+ const result = await repo.hasUserPurchased("user-1", "product-x");
116
+ expect(result).toBe(false);
117
+ });
118
+ });
119
+ describe("OrderRepository.countByUserAndProduct", () => {
120
+ it("returns query size (active statuses)", async () => {
121
+ mockQuery.get.mockResolvedValue({ ...makeQuerySnap([
122
+ { id: "o1", data: { status: "pending" } },
123
+ { id: "o2", data: { status: "delivered" } },
124
+ ]), size: 2 });
125
+ const count = await repo.countByUserAndProduct("user-1", "product-x");
126
+ expect(count).toBe(2);
127
+ });
128
+ });
129
+ describe("OrderRepository.getTimedOutPending", () => {
130
+ it("filters by status=pending, paymentStatus=pending, createdAt < cutoff", async () => {
131
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
132
+ await repo.getTimedOutPending(24);
133
+ expect(mockQuery.where).toHaveBeenCalledWith("status", "==", "pending");
134
+ expect(mockQuery.where).toHaveBeenCalledWith("paymentStatus", "==", "pending");
135
+ expect(mockQuery.where).toHaveBeenCalledWith("createdAt", "<", expect.any(Date));
136
+ });
137
+ it("returns docs with id, ref, and data", async () => {
138
+ const docs = makeQuerySnap([
139
+ { id: "o-timeout", data: { status: "pending", paymentStatus: "pending" } },
140
+ ]);
141
+ // Attach .ref to each doc
142
+ docs.docs[0].ref = { id: "o-timeout" };
143
+ mockQuery.get.mockResolvedValue(docs);
144
+ const result = await repo.getTimedOutPending(24);
145
+ expect(result[0].id).toBe("o-timeout");
146
+ expect(result[0].data).toBeDefined();
147
+ });
148
+ });
149
+ describe("OrderRepository.cancelInBatch", () => {
150
+ it("stages status: cancelled in caller batch, does NOT commit", () => {
151
+ const ref = { id: "order-1" };
152
+ repo.cancelInBatch(mockBatch, ref);
153
+ expect(mockBatch.update).toHaveBeenCalledWith(ref, expect.objectContaining({ status: "cancelled", cancellationReason: "payment_timeout" }));
154
+ expect(mockBatch.commit).not.toHaveBeenCalled();
155
+ });
156
+ });
157
+ describe("OrderRepository.markPayoutRequested", () => {
158
+ it("stages payoutStatus: requested in caller batch, does NOT commit", () => {
159
+ const ref = { id: "order-1" };
160
+ repo.markPayoutRequested(mockBatch, ref, "payout-123");
161
+ expect(mockBatch.update).toHaveBeenCalledWith(ref, expect.objectContaining({ payoutStatus: "requested", payoutId: "payout-123" }));
162
+ expect(mockBatch.commit).not.toHaveBeenCalled();
163
+ });
164
+ });
165
+ describe("OrderRepository.postRefundEvent", () => {
166
+ it("appends refund event to refunds array, sets contestable: false", async () => {
167
+ mockDocRef.get.mockResolvedValue(makeSnap({
168
+ status: "delivered",
169
+ refunds: [],
170
+ totalPrice: 10000,
171
+ }));
172
+ const event = { type: "partial", amountInPaise: 5000, reason: "damaged", refundedBy: "admin-1", refundedAt: new Date() };
173
+ await repo.postRefundEvent("order-1", event);
174
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ contestable: false, refunds: [event] }));
175
+ });
176
+ it("becomeRefunded=true → sets status: refunded", async () => {
177
+ mockDocRef.get.mockResolvedValue(makeSnap({ status: "delivered", refunds: [], totalPrice: 10000 }));
178
+ const event = { type: "full", amountInPaise: 10000, reason: "return", refundedBy: "admin-1", refundedAt: new Date() };
179
+ await repo.postRefundEvent("order-1", event, true);
180
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "refunded" }));
181
+ });
182
+ it("order not found → throws NotFoundError", async () => {
183
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
184
+ const { NotFoundError } = await import("../../../../errors");
185
+ await expect(repo.postRefundEvent("nonexistent", {})).rejects.toBeInstanceOf(NotFoundError);
186
+ });
187
+ });
188
+ describe("OrderRepository.createFromAuction", () => {
189
+ it("stages order creation in caller batch with correct fields", () => {
190
+ mockCollection.doc.mockReturnValue({ ...mockDocRef, id: "new-order-id" });
191
+ repo.createFromAuction(mockBatch, {
192
+ productId: "auction-test",
193
+ productTitle: "Test Auction",
194
+ userId: "user-1",
195
+ userName: "Ravi Kumar",
196
+ userEmail: "ravi@example.com",
197
+ storeId: "store-1",
198
+ amount: 10000,
199
+ currency: "INR",
200
+ auctionProductId: "auction-test",
201
+ });
202
+ expect(mockBatch.create).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({
203
+ productId: "auction-test",
204
+ userId: "user-1",
205
+ storeId: "store-1",
206
+ totalPrice: 10000,
207
+ status: "confirmed",
208
+ }));
209
+ expect(mockBatch.commit).not.toHaveBeenCalled();
210
+ });
211
+ });
@@ -0,0 +1,292 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { makeMockDb, makeSnap } from "../../../../../tests/helpers/mock-firestore";
3
+ const { db, mockDocRef, mockCollection, mockQuery, mockTxn } = 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
+ addPiiIndices: (d) => d,
18
+ encryptPayoutBankAccount: (d) => d,
19
+ decryptPayoutBankAccount: (d) => d,
20
+ PAYOUT_PII_FIELDS: ["sellerName", "sellerEmail"],
21
+ PAYOUT_PII_INDEX_MAP: {},
22
+ }));
23
+ vi.mock("../../../../errors/normalize", () => ({
24
+ normalizeError: vi.fn(),
25
+ }));
26
+ import { PayoutRepository } from "../payout.repository";
27
+ const repo = new PayoutRepository();
28
+ function makePayoutDoc(overrides = {}) {
29
+ return {
30
+ id: "payout-misty-20260101-a1b2c3",
31
+ storeId: "store-pokemon-palace",
32
+ sellerName: "Misty Singh",
33
+ sellerEmail: "misty@pokemon.com",
34
+ amount: 50000, // paise
35
+ netAmount: 50000,
36
+ status: "pending",
37
+ orderIds: ["order-1-20260101-abc", "order-2-20260101-def"],
38
+ requestedAt: new Date(),
39
+ createdAt: new Date(),
40
+ updatedAt: new Date(),
41
+ refundDeductions: [],
42
+ ...overrides,
43
+ };
44
+ }
45
+ beforeEach(() => {
46
+ vi.clearAllMocks();
47
+ db.collection.mockReturnValue(mockCollection);
48
+ mockCollection.doc.mockReturnValue(mockDocRef);
49
+ mockCollection.where.mockReturnValue(mockQuery);
50
+ mockQuery.where.mockReturnValue(mockQuery);
51
+ mockQuery.orderBy.mockReturnValue(mockQuery);
52
+ mockQuery.limit.mockReturnValue(mockQuery);
53
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true, size: 0 });
54
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
55
+ mockDocRef.set.mockResolvedValue(undefined);
56
+ mockDocRef.update.mockResolvedValue(undefined);
57
+ db.runTransaction.mockImplementation(async (fn) => fn(mockTxn));
58
+ mockTxn.get.mockResolvedValue(makeSnap(null));
59
+ mockTxn.update.mockReturnValue(undefined);
60
+ });
61
+ // ---------------------------------------------------------------------------
62
+ // create
63
+ // ---------------------------------------------------------------------------
64
+ describe("PayoutRepository.create", () => {
65
+ it("creates payout with status: pending", async () => {
66
+ const result = await repo.create({
67
+ storeId: "store-1",
68
+ sellerName: "Arun",
69
+ sellerEmail: "arun@test.com",
70
+ amount: 10000,
71
+ orderIds: [],
72
+ paymentMethod: "upi",
73
+ });
74
+ expect(result.status).toBe("pending");
75
+ });
76
+ it("persists the payout to Firestore", async () => {
77
+ await repo.create({
78
+ storeId: "store-1",
79
+ sellerName: "Arun",
80
+ sellerEmail: "arun@test.com",
81
+ amount: 10000,
82
+ orderIds: [],
83
+ paymentMethod: "upi",
84
+ });
85
+ expect(mockDocRef.set).toHaveBeenCalledOnce();
86
+ });
87
+ it("sets requestedAt timestamp", async () => {
88
+ const result = await repo.create({
89
+ storeId: "store-1",
90
+ sellerName: "Arun",
91
+ sellerEmail: "arun@test.com",
92
+ amount: 10000,
93
+ orderIds: [],
94
+ paymentMethod: "upi",
95
+ });
96
+ expect(result.requestedAt).toBeInstanceOf(Date);
97
+ });
98
+ });
99
+ // ---------------------------------------------------------------------------
100
+ // markProcessing
101
+ // ---------------------------------------------------------------------------
102
+ describe("PayoutRepository.markProcessing", () => {
103
+ it("sets status: processing on the provided ref", async () => {
104
+ await repo.markProcessing(mockDocRef);
105
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "processing" }));
106
+ });
107
+ it("sets processedAt timestamp", async () => {
108
+ await repo.markProcessing(mockDocRef);
109
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ processedAt: expect.any(Date) }));
110
+ });
111
+ });
112
+ // ---------------------------------------------------------------------------
113
+ // recordSuccess
114
+ // ---------------------------------------------------------------------------
115
+ describe("PayoutRepository.recordSuccess", () => {
116
+ it("stores razorpayPayoutId on the payout", async () => {
117
+ await repo.recordSuccess(mockDocRef, "razorpay-payout-123", "processed");
118
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ razorpayPayoutId: "razorpay-payout-123" }));
119
+ });
120
+ it("stores razorpayStatus on the payout", async () => {
121
+ await repo.recordSuccess(mockDocRef, "payout-abc", "processed");
122
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ razorpayStatus: "processed" }));
123
+ });
124
+ it("sets updatedAt timestamp", async () => {
125
+ await repo.recordSuccess(mockDocRef, "payout-abc", "processed");
126
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
127
+ });
128
+ });
129
+ // ---------------------------------------------------------------------------
130
+ // recordFailure
131
+ // ---------------------------------------------------------------------------
132
+ describe("PayoutRepository.recordFailure", () => {
133
+ it("isFinal=true → sets status: failed", async () => {
134
+ await repo.recordFailure(mockDocRef, 3, "Insufficient funds", true);
135
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "failed" }));
136
+ });
137
+ it("isFinal=false → keeps status: pending (retry)", async () => {
138
+ await repo.recordFailure(mockDocRef, 1, "Timeout", false);
139
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "pending" }));
140
+ });
141
+ it("stores failureCount", async () => {
142
+ await repo.recordFailure(mockDocRef, 2, "Network error", false);
143
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ failureCount: 2 }));
144
+ });
145
+ it("stores lastFailureReason", async () => {
146
+ await repo.recordFailure(mockDocRef, 1, "Account blocked", false);
147
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ lastFailureReason: "Account blocked" }));
148
+ });
149
+ });
150
+ // ---------------------------------------------------------------------------
151
+ // applyRefundDeduction
152
+ // ---------------------------------------------------------------------------
153
+ describe("PayoutRepository.applyRefundDeduction", () => {
154
+ it("throws when payout not found in transaction", async () => {
155
+ mockTxn.get.mockResolvedValue(makeSnap(null));
156
+ await expect(repo.applyRefundDeduction("payout-ghost", {
157
+ orderId: "order-1",
158
+ deductedAmount: 100,
159
+ })).rejects.toThrow(/not found/i);
160
+ });
161
+ it("throws when payout status is not pending", async () => {
162
+ const payout = makePayoutDoc({ status: "completed" });
163
+ mockTxn.get.mockResolvedValue({ exists: true, id: payout.id, data: () => payout });
164
+ await expect(repo.applyRefundDeduction("payout-1", {
165
+ orderId: "order-1",
166
+ deductedAmount: 100,
167
+ })).rejects.toThrow(/status/i);
168
+ });
169
+ it("computes netAmount = amount - totalDeducted", async () => {
170
+ const payout = makePayoutDoc({ amount: 50000, refundDeductions: [] });
171
+ mockTxn.get.mockResolvedValue({ exists: true, id: payout.id, data: () => payout });
172
+ const result = await repo.applyRefundDeduction("payout-1", {
173
+ orderId: "order-1",
174
+ deductedAmount: 10000,
175
+ });
176
+ expect(result.netAmount).toBe(40000);
177
+ });
178
+ it("netAmount is floored at 0 when deduction exceeds amount", async () => {
179
+ const payout = makePayoutDoc({ amount: 5000, refundDeductions: [] });
180
+ mockTxn.get.mockResolvedValue({ exists: true, id: payout.id, data: () => payout });
181
+ const result = await repo.applyRefundDeduction("payout-1", {
182
+ orderId: "order-1",
183
+ deductedAmount: 10000, // more than amount
184
+ });
185
+ expect(result.netAmount).toBe(0);
186
+ });
187
+ it("accumulates multiple deductions correctly", async () => {
188
+ const existing = [{ orderId: "order-old", deductedAmount: 5000, appliedAt: new Date() }];
189
+ const payout = makePayoutDoc({ amount: 50000, refundDeductions: existing });
190
+ mockTxn.get.mockResolvedValue({ exists: true, id: payout.id, data: () => payout });
191
+ const result = await repo.applyRefundDeduction("payout-1", {
192
+ orderId: "order-new",
193
+ deductedAmount: 10000,
194
+ });
195
+ // total deducted = 5000 + 10000 = 15000, net = 50000 - 15000 = 35000
196
+ expect(result.netAmount).toBe(35000);
197
+ });
198
+ it("sets appliedAt on the deduction entry", async () => {
199
+ const payout = makePayoutDoc({ amount: 50000, refundDeductions: [] });
200
+ mockTxn.get.mockResolvedValue({ exists: true, id: payout.id, data: () => payout });
201
+ const result = await repo.applyRefundDeduction("payout-1", {
202
+ orderId: "order-1",
203
+ deductedAmount: 100,
204
+ });
205
+ const lastDeduction = result.refundDeductions[result.refundDeductions.length - 1];
206
+ expect(lastDeduction.appliedAt).toBeInstanceOf(Date);
207
+ });
208
+ it("uses a Firestore transaction for atomic read-modify-write", async () => {
209
+ const payout = makePayoutDoc({ amount: 50000, refundDeductions: [] });
210
+ mockTxn.get.mockResolvedValue({ exists: true, id: payout.id, data: () => payout });
211
+ await repo.applyRefundDeduction("payout-1", {
212
+ orderId: "order-1",
213
+ deductedAmount: 100,
214
+ });
215
+ expect(db.runTransaction).toHaveBeenCalledOnce();
216
+ expect(mockTxn.get).toHaveBeenCalledOnce();
217
+ expect(mockTxn.update).toHaveBeenCalledOnce();
218
+ });
219
+ });
220
+ // ---------------------------------------------------------------------------
221
+ // findPendingByStore
222
+ // ---------------------------------------------------------------------------
223
+ describe("PayoutRepository.findPendingByStore", () => {
224
+ it("returns null when no pending payout found", async () => {
225
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true, size: 0 });
226
+ const result = await repo.findPendingByStore("store-1");
227
+ expect(result).toBeNull();
228
+ });
229
+ it("queries storeId AND status=pending", async () => {
230
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true, size: 0 });
231
+ await repo.findPendingByStore("store-abc");
232
+ expect(mockCollection.where).toHaveBeenCalledWith("storeId", "==", "store-abc");
233
+ expect(mockQuery.where).toHaveBeenCalledWith("status", "==", "pending");
234
+ });
235
+ it("applies limit of 1 (most recent only)", async () => {
236
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true, size: 0 });
237
+ await repo.findPendingByStore("store-abc");
238
+ expect(mockQuery.limit).toHaveBeenCalledWith(1);
239
+ });
240
+ it("returns the payout when found", async () => {
241
+ const payout = makePayoutDoc({ status: "pending" });
242
+ mockQuery.get.mockResolvedValue({
243
+ docs: [{ id: payout.id, data: () => payout }],
244
+ empty: false,
245
+ size: 1,
246
+ });
247
+ const result = await repo.findPendingByStore("store-pokemon-palace");
248
+ expect(result).not.toBeNull();
249
+ });
250
+ });
251
+ // ---------------------------------------------------------------------------
252
+ // getPaidOutOrderIds
253
+ // ---------------------------------------------------------------------------
254
+ describe("PayoutRepository.getPaidOutOrderIds", () => {
255
+ it("returns empty Set when no matching payouts", async () => {
256
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true, size: 0 });
257
+ const ids = await repo.getPaidOutOrderIds("store-1");
258
+ expect(ids.size).toBe(0);
259
+ });
260
+ it("collects all orderIds from matching payout documents", async () => {
261
+ const payout1 = makePayoutDoc({ orderIds: ["order-A", "order-B"] });
262
+ const payout2 = makePayoutDoc({ id: "payout-2", orderIds: ["order-C"] });
263
+ mockQuery.get.mockResolvedValue({
264
+ docs: [
265
+ { id: payout1.id, data: () => payout1 },
266
+ { id: payout2.id, data: () => payout2 },
267
+ ],
268
+ empty: false,
269
+ size: 2,
270
+ });
271
+ const ids = await repo.getPaidOutOrderIds("store-pokemon-palace");
272
+ expect(ids.has("order-A")).toBe(true);
273
+ expect(ids.has("order-B")).toBe(true);
274
+ expect(ids.has("order-C")).toBe(true);
275
+ });
276
+ it("queries storeId AND status in [pending, processing, completed]", async () => {
277
+ mockQuery.get.mockResolvedValue({ docs: [], empty: true, size: 0 });
278
+ await repo.getPaidOutOrderIds("store-1");
279
+ expect(mockCollection.where).toHaveBeenCalledWith("storeId", "==", "store-1");
280
+ expect(mockQuery.where).toHaveBeenCalledWith("status", "in", expect.arrayContaining(["pending", "processing", "completed"]));
281
+ });
282
+ it("handles payout with no orderIds field gracefully", async () => {
283
+ const payout = { ...makePayoutDoc(), orderIds: undefined };
284
+ mockQuery.get.mockResolvedValue({
285
+ docs: [{ id: payout.id, data: () => payout }],
286
+ empty: false,
287
+ size: 1,
288
+ });
289
+ const ids = await repo.getPaidOutOrderIds("store-1");
290
+ expect(ids.size).toBe(0);
291
+ });
292
+ });
@@ -16,14 +16,14 @@ type RouteContext = {
16
16
  }>;
17
17
  };
18
18
  export declare function GET(_request: Request, context: RouteContext): Promise<NextResponse>;
19
- export declare const PATCH: (request: Request, context: {
19
+ export declare const PATCH: (request: Request, context?: {
20
20
  params: Promise<{
21
21
  id: string;
22
22
  }>;
23
- }) => Promise<Response>;
24
- export declare const DELETE: (request: Request, context: {
23
+ } | undefined) => Promise<Response>;
24
+ export declare const DELETE: (request: Request, context?: {
25
25
  params: Promise<{
26
26
  id: string;
27
27
  }>;
28
- }) => Promise<Response>;
28
+ } | undefined) => Promise<Response>;
29
29
  export { GET as productItemGET, PATCH as productItemPATCH, DELETE as productItemDELETE, };
@@ -14,6 +14,6 @@
14
14
  */
15
15
  import { NextResponse } from "next/server.js";
16
16
  export declare function GET(request: Request): Promise<NextResponse>;
17
- export declare const POST: (request: Request, context: {
17
+ export declare const POST: (request: Request, context?: {
18
18
  params: Promise<Record<string, string>>;
19
- }) => Promise<Response>;
19
+ } | undefined) => Promise<Response>;