@mohasinac/appkit 3.0.6 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +42 -0
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +86 -0
- package/dist/_internal/client/features/lottery/LotteryAdminSlotView.d.ts +14 -0
- package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +46 -0
- package/dist/_internal/client/features/lottery/LotteryDetailView.d.ts +28 -0
- package/dist/_internal/client/features/lottery/LotteryDetailView.js +17 -0
- package/dist/_internal/client/features/lottery/LotteryEntriesView.d.ts +23 -0
- package/dist/_internal/client/features/lottery/LotteryEntriesView.js +58 -0
- package/dist/_internal/client/features/lottery/LotteryListView.d.ts +21 -0
- package/dist/_internal/client/features/lottery/LotteryListView.js +25 -0
- package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +19 -0
- package/dist/_internal/client/features/lottery/LotteryPullForm.js +65 -0
- package/dist/_internal/client/features/lottery/LotterySlotGrid.d.ts +13 -0
- package/dist/_internal/client/features/lottery/LotterySlotGrid.js +21 -0
- package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.d.ts +28 -0
- package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.js +18 -0
- package/dist/_internal/server/features/auctions/__tests__/service.test.d.ts +1 -0
- package/dist/_internal/server/features/auctions/__tests__/service.test.js +140 -0
- package/dist/_internal/server/features/checkout/__tests__/actions.test.d.ts +1 -0
- package/dist/_internal/server/features/checkout/__tests__/actions.test.js +600 -0
- package/dist/_internal/server/features/lottery/actions.d.ts +43 -0
- package/dist/_internal/server/features/lottery/actions.js +243 -0
- package/dist/_internal/server/features/lottery/adapters.d.ts +9 -0
- package/dist/_internal/server/features/lottery/adapters.js +21 -0
- package/dist/_internal/server/features/lottery/data.d.ts +20 -0
- package/dist/_internal/server/features/lottery/data.js +41 -0
- package/dist/_internal/server/features/lottery/metadata.d.ts +6 -0
- package/dist/_internal/server/features/lottery/metadata.js +31 -0
- package/dist/_internal/server/features/refunds/__tests__/actions.test.d.ts +1 -0
- package/dist/_internal/server/features/refunds/__tests__/actions.test.js +263 -0
- package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.js +249 -0
- package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.js +201 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.js +169 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.js +148 -0
- package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.js +210 -0
- package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.js +190 -0
- package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.js +161 -0
- package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.js +302 -0
- package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.js +252 -0
- package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.js +129 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +46 -0
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/client.d.ts +9 -0
- package/dist/client.js +9 -0
- package/dist/constants/field-names.d.ts +5 -1
- package/dist/constants/field-names.js +5 -1
- package/dist/features/addresses/repository/__tests__/addresses.repository.test.d.ts +1 -0
- package/dist/features/addresses/repository/__tests__/addresses.repository.test.js +386 -0
- package/dist/features/admin/api/bids/route.d.ts +2 -2
- package/dist/features/admin/api/coupons/route.d.ts +2 -2
- package/dist/features/admin/api/products/route.d.ts +2 -2
- package/dist/features/admin/api/reviews/route.d.ts +2 -2
- package/dist/features/admin/repository/__tests__/notification.repository.test.d.ts +1 -0
- package/dist/features/admin/repository/__tests__/notification.repository.test.js +356 -0
- package/dist/features/admin/repository/__tests__/site-settings.repository.test.d.ts +1 -0
- package/dist/features/admin/repository/__tests__/site-settings.repository.test.js +298 -0
- package/dist/features/auctions/repository/__tests__/bid.repository.test.d.ts +1 -0
- package/dist/features/auctions/repository/__tests__/bid.repository.test.js +290 -0
- package/dist/features/auth/api/route.d.ts +2 -2
- package/dist/features/auth/repository/__tests__/session.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/session.repository.test.js +310 -0
- package/dist/features/auth/repository/__tests__/session.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/session.test.js +146 -0
- package/dist/features/auth/repository/__tests__/sms-counter.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/sms-counter.repository.test.js +171 -0
- package/dist/features/auth/repository/__tests__/sms-counter.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/sms-counter.test.js +88 -0
- package/dist/features/auth/repository/__tests__/token.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/token.repository.test.js +260 -0
- package/dist/features/auth/repository/__tests__/token.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/token.test.js +140 -0
- package/dist/features/auth/repository/__tests__/user.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/user.repository.test.js +352 -0
- package/dist/features/auth/repository/__tests__/user.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/user.test.js +226 -0
- package/dist/features/auth/repository/session.repository.js +10 -1
- package/dist/features/cart/repository/__tests__/cart.repository.test.d.ts +1 -0
- package/dist/features/cart/repository/__tests__/cart.repository.test.js +371 -0
- package/dist/features/cart/repository/cart.repository.js +2 -2
- package/dist/features/categories/api/[id]/route.d.ts +4 -4
- package/dist/features/categories/api/route.d.ts +2 -2
- package/dist/features/categories/repository/__tests__/categories.repository.test.d.ts +1 -0
- package/dist/features/categories/repository/__tests__/categories.repository.test.js +813 -0
- package/dist/features/events/components/EventCard.js +1 -0
- package/dist/features/events/repository/__tests__/event.repository.test.d.ts +1 -0
- package/dist/features/events/repository/__tests__/event.repository.test.js +299 -0
- package/dist/features/events/schemas/firestore.d.ts +3 -0
- package/dist/features/events/schemas/index.d.ts +834 -14
- package/dist/features/events/schemas/index.js +80 -1
- package/dist/features/events/types/index.d.ts +4 -1
- package/dist/features/faq/repository/__tests__/faqs.repository.test.d.ts +1 -0
- package/dist/features/faq/repository/__tests__/faqs.repository.test.js +311 -0
- package/dist/features/history/repository/__tests__/user-history.repository.test.d.ts +1 -0
- package/dist/features/history/repository/__tests__/user-history.repository.test.js +180 -0
- package/dist/features/homepage/api/[id]/route.d.ts +4 -4
- package/dist/features/homepage/api/carousel/[id]/route.d.ts +4 -4
- package/dist/features/homepage/api/carousel/route.d.ts +2 -2
- package/dist/features/homepage/api/route.d.ts +2 -2
- package/dist/features/homepage/repository/__tests__/carousel.repository.test.d.ts +1 -0
- package/dist/features/homepage/repository/__tests__/carousel.repository.test.js +182 -0
- package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.d.ts +1 -0
- package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.js +185 -0
- package/dist/features/lottery/repository/lottery-entry.repository.d.ts +33 -0
- package/dist/features/lottery/repository/lottery-entry.repository.js +130 -0
- package/dist/features/lottery/schemas/firestore.d.ts +49 -0
- package/dist/features/lottery/schemas/firestore.js +25 -0
- package/dist/features/lottery/schemas/zod.d.ts +113 -0
- package/dist/features/lottery/schemas/zod.js +39 -0
- package/dist/features/lottery/types/index.d.ts +67 -0
- package/dist/features/lottery/types/index.js +55 -0
- package/dist/features/messages/repository/__tests__/conversations.repository.test.d.ts +1 -0
- package/dist/features/messages/repository/__tests__/conversations.repository.test.js +257 -0
- package/dist/features/orders/repository/__tests__/orders.repository.test.d.ts +1 -0
- package/dist/features/orders/repository/__tests__/orders.repository.test.js +211 -0
- package/dist/features/payments/repository/__tests__/payout.repository.test.d.ts +1 -0
- package/dist/features/payments/repository/__tests__/payout.repository.test.js +292 -0
- package/dist/features/products/api/[id]/route.d.ts +4 -4
- package/dist/features/products/api/route.d.ts +2 -2
- package/dist/features/products/repository/__tests__/product-features.repository.test.d.ts +1 -0
- package/dist/features/products/repository/__tests__/product-features.repository.test.js +329 -0
- package/dist/features/products/repository/__tests__/products.repository.test.d.ts +1 -0
- package/dist/features/products/repository/__tests__/products.repository.test.js +607 -0
- package/dist/features/products/schemas/firestore.d.ts +15 -1
- package/dist/features/products/schemas/index.d.ts +12 -12
- package/dist/features/products/schemas/index.js +4 -2
- package/dist/features/products/types/index.d.ts +1 -1
- package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.d.ts +1 -0
- package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.js +219 -0
- package/dist/features/promotions/repository/__tests__/coupons.repository.test.d.ts +1 -0
- package/dist/features/promotions/repository/__tests__/coupons.repository.test.js +480 -0
- package/dist/features/reviews/api/[id]/route.d.ts +4 -4
- package/dist/features/reviews/api/route.d.ts +2 -2
- package/dist/features/reviews/repository/__tests__/reviews.repository.test.d.ts +1 -0
- package/dist/features/reviews/repository/__tests__/reviews.repository.test.js +327 -0
- package/dist/features/scams/repository/__tests__/scammer.repository.test.d.ts +1 -0
- package/dist/features/scams/repository/__tests__/scammer.repository.test.js +385 -0
- package/dist/features/search/repository/__tests__/search.repository.test.d.ts +1 -0
- package/dist/features/search/repository/__tests__/search.repository.test.js +369 -0
- package/dist/features/seller/api/coupons/route.d.ts +2 -2
- package/dist/features/seller/api/offers/route.d.ts +2 -2
- package/dist/features/seller/api/products/route.d.ts +2 -2
- package/dist/features/seller/api/store/route.d.ts +2 -2
- package/dist/features/seller/repository/__tests__/offer.repository.test.d.ts +1 -0
- package/dist/features/seller/repository/__tests__/offer.repository.test.js +458 -0
- package/dist/features/stores/repository/__tests__/store.repository.test.d.ts +1 -0
- package/dist/features/stores/repository/__tests__/store.repository.test.js +261 -0
- package/dist/features/support/repository/__tests__/support.repository.test.d.ts +1 -0
- package/dist/features/support/repository/__tests__/support.repository.test.js +312 -0
- package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.d.ts +1 -0
- package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.js +139 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +17 -0
- package/dist/next/api/routeHandler.d.ts +1 -1
- package/dist/next/routing/route-map.d.ts +13 -0
- package/dist/next/routing/route-map.js +6 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +2 -0
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-schemas.d.ts +2 -0
- package/dist/security/pii-schemas.js +6 -0
- package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
- package/dist/seed/events-seed-data.js +85 -2
- package/dist/seed/grouped-listings-seed-data.d.ts +3 -1
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/lottery-entries-seed-data.d.ts +2 -0
- package/dist/seed/lottery-entries-seed-data.js +108 -0
- package/dist/seed/manifest.js +1 -0
- package/dist/seed/products-preorders-seed-data.d.ts +3 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.js +6 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
vi.mock("../../../../../errors/normalize", () => ({
|
|
3
|
+
normalizeError: vi.fn(),
|
|
4
|
+
}));
|
|
5
|
+
import { handleUserBanChange } from "../onUserBanChange";
|
|
6
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
7
|
+
function makeBatch() {
|
|
8
|
+
return {
|
|
9
|
+
set: vi.fn(),
|
|
10
|
+
update: vi.fn(),
|
|
11
|
+
delete: vi.fn(),
|
|
12
|
+
commit: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function makeCtx(batch = makeBatch()) {
|
|
16
|
+
const mockDoc = vi.fn().mockReturnValue({});
|
|
17
|
+
const banHistoryCollection = { doc: mockDoc };
|
|
18
|
+
const userDoc = { collection: vi.fn().mockReturnValue(banHistoryCollection) };
|
|
19
|
+
const usersCollection = { doc: vi.fn().mockReturnValue(userDoc) };
|
|
20
|
+
return {
|
|
21
|
+
db: {
|
|
22
|
+
collection: vi.fn().mockReturnValue(usersCollection),
|
|
23
|
+
batch: vi.fn().mockReturnValue(batch),
|
|
24
|
+
},
|
|
25
|
+
batch,
|
|
26
|
+
now: new Date(),
|
|
27
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
beforeEach(() => {
|
|
31
|
+
vi.clearAllMocks();
|
|
32
|
+
});
|
|
33
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
34
|
+
describe("handleUserBanChange — early exit", () => {
|
|
35
|
+
it("returns immediately when after is null", async () => {
|
|
36
|
+
const ctx = makeCtx();
|
|
37
|
+
await handleUserBanChange({ uid: "user-1", before: null, after: null }, ctx);
|
|
38
|
+
expect(ctx.batch.commit).not.toHaveBeenCalled();
|
|
39
|
+
});
|
|
40
|
+
it("returns without writing when no ban changes detected", async () => {
|
|
41
|
+
// before = active, after = active (same state, no ban)
|
|
42
|
+
const ctx = makeCtx();
|
|
43
|
+
await handleUserBanChange({
|
|
44
|
+
uid: "user-1",
|
|
45
|
+
before: { isDisabled: false, softBans: [] },
|
|
46
|
+
after: { isDisabled: false, softBans: [] },
|
|
47
|
+
}, ctx);
|
|
48
|
+
expect(ctx.batch.commit).not.toHaveBeenCalled();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
describe("handleUserBanChange — hard ban applied", () => {
|
|
52
|
+
it("writes a hard_ban entry when isDisabled transitions false→true", async () => {
|
|
53
|
+
const batch = makeBatch();
|
|
54
|
+
const ctx = makeCtx(batch);
|
|
55
|
+
await handleUserBanChange({
|
|
56
|
+
uid: "user-1",
|
|
57
|
+
before: { isDisabled: false },
|
|
58
|
+
after: { isDisabled: true, hardBanReason: "Violating ToS", hardBannedBy: "admin-1" },
|
|
59
|
+
}, ctx);
|
|
60
|
+
expect(batch.set).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ type: "hard_ban", reason: "Violating ToS" }));
|
|
61
|
+
expect(batch.commit).toHaveBeenCalled();
|
|
62
|
+
});
|
|
63
|
+
it("stores hardBannedBy as performedBy in the history entry", async () => {
|
|
64
|
+
const batch = makeBatch();
|
|
65
|
+
const ctx = makeCtx(batch);
|
|
66
|
+
await handleUserBanChange({
|
|
67
|
+
uid: "user-1",
|
|
68
|
+
before: { isDisabled: false },
|
|
69
|
+
after: { isDisabled: true, hardBanReason: "Fraud", hardBannedBy: "admin-2" },
|
|
70
|
+
}, ctx);
|
|
71
|
+
const call = batch.set.mock.calls[0][1];
|
|
72
|
+
expect(call.performedBy).toBe("admin-2");
|
|
73
|
+
});
|
|
74
|
+
it("does NOT write hard_ban when isDisabled stays true (no transition)", async () => {
|
|
75
|
+
const batch = makeBatch();
|
|
76
|
+
const ctx = makeCtx(batch);
|
|
77
|
+
await handleUserBanChange({
|
|
78
|
+
uid: "user-1",
|
|
79
|
+
before: { isDisabled: true },
|
|
80
|
+
after: { isDisabled: true, hardBanReason: "Fraud" },
|
|
81
|
+
}, ctx);
|
|
82
|
+
const hardBanCalls = batch.set.mock.calls.filter((c) => c[1].type === "hard_ban");
|
|
83
|
+
expect(hardBanCalls).toHaveLength(0);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
describe("handleUserBanChange — hard ban lifted (unban)", () => {
|
|
87
|
+
it("writes a hard_unban entry when isDisabled transitions true→false", async () => {
|
|
88
|
+
const batch = makeBatch();
|
|
89
|
+
const ctx = makeCtx(batch);
|
|
90
|
+
await handleUserBanChange({
|
|
91
|
+
uid: "user-1",
|
|
92
|
+
before: { isDisabled: true },
|
|
93
|
+
after: { isDisabled: false },
|
|
94
|
+
}, ctx);
|
|
95
|
+
expect(batch.set).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ type: "hard_unban" }));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe("handleUserBanChange — soft ban changes", () => {
|
|
99
|
+
it("writes a soft_ban entry for each newly added soft ban action", async () => {
|
|
100
|
+
const batch = makeBatch();
|
|
101
|
+
const ctx = makeCtx(batch);
|
|
102
|
+
await handleUserBanChange({
|
|
103
|
+
uid: "user-1",
|
|
104
|
+
before: { softBans: [] },
|
|
105
|
+
after: {
|
|
106
|
+
softBans: [
|
|
107
|
+
{ action: "place_bids", reason: "Shill bidding", bannedBy: "admin-1" },
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
}, ctx);
|
|
111
|
+
expect(batch.set).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ type: "soft_ban", action: "place_bids" }));
|
|
112
|
+
});
|
|
113
|
+
it("writes soft_unban for each removed soft ban", async () => {
|
|
114
|
+
const batch = makeBatch();
|
|
115
|
+
const ctx = makeCtx(batch);
|
|
116
|
+
await handleUserBanChange({
|
|
117
|
+
uid: "user-1",
|
|
118
|
+
before: { softBans: [{ action: "place_bids" }] },
|
|
119
|
+
after: { softBans: [] },
|
|
120
|
+
}, ctx);
|
|
121
|
+
expect(batch.set).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ type: "soft_unban", action: "place_bids" }));
|
|
122
|
+
});
|
|
123
|
+
it("does NOT write entry for soft bans that are unchanged", async () => {
|
|
124
|
+
const batch = makeBatch();
|
|
125
|
+
const ctx = makeCtx(batch);
|
|
126
|
+
await handleUserBanChange({
|
|
127
|
+
uid: "user-1",
|
|
128
|
+
before: { softBans: [{ action: "place_bids" }] },
|
|
129
|
+
after: { softBans: [{ action: "place_bids" }] }, // same ban, no change
|
|
130
|
+
}, ctx);
|
|
131
|
+
expect(batch.commit).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
it("handles multiple simultaneous soft ban additions", async () => {
|
|
134
|
+
const batch = makeBatch();
|
|
135
|
+
const ctx = makeCtx(batch);
|
|
136
|
+
await handleUserBanChange({
|
|
137
|
+
uid: "user-1",
|
|
138
|
+
before: { softBans: [] },
|
|
139
|
+
after: {
|
|
140
|
+
softBans: [
|
|
141
|
+
{ action: "place_bids" },
|
|
142
|
+
{ action: "post_reviews" },
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
}, ctx);
|
|
146
|
+
expect(batch.set).toHaveBeenCalledTimes(2);
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
describe("handleUserBanChange — batch failure", () => {
|
|
150
|
+
it("batch.commit failure is non-fatal — logs error but does not throw", async () => {
|
|
151
|
+
const batch = makeBatch();
|
|
152
|
+
batch.commit.mockRejectedValue(new Error("Firestore error"));
|
|
153
|
+
const ctx = makeCtx(batch);
|
|
154
|
+
await expect(handleUserBanChange({
|
|
155
|
+
uid: "user-1",
|
|
156
|
+
before: { isDisabled: false },
|
|
157
|
+
after: { isDisabled: true, hardBanReason: "Test" },
|
|
158
|
+
}, ctx)).resolves.toBeUndefined();
|
|
159
|
+
expect(ctx.logger.error).toHaveBeenCalled();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
const { mockGetPending, mockMarkProcessing, mockRecordSuccess, mockRecordFailure, mockFetch, } = vi.hoisted(() => ({
|
|
3
|
+
mockGetPending: vi.fn(),
|
|
4
|
+
mockMarkProcessing: vi.fn(),
|
|
5
|
+
mockRecordSuccess: vi.fn(),
|
|
6
|
+
mockRecordFailure: vi.fn(),
|
|
7
|
+
mockFetch: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock("../../../../../repositories", () => ({
|
|
10
|
+
payoutRepository: {
|
|
11
|
+
getPending: mockGetPending,
|
|
12
|
+
markProcessing: mockMarkProcessing,
|
|
13
|
+
recordSuccess: mockRecordSuccess,
|
|
14
|
+
recordFailure: mockRecordFailure,
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
vi.mock("../../../../../errors/normalize", () => ({
|
|
18
|
+
normalizeError: vi.fn(),
|
|
19
|
+
}));
|
|
20
|
+
import { runPayoutBatch } from "../payoutBatch";
|
|
21
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
22
|
+
function makeCtx(overrides = {}) {
|
|
23
|
+
return {
|
|
24
|
+
env: (key) => {
|
|
25
|
+
const defaults = {
|
|
26
|
+
RAZORPAY_KEY_ID: "rzp_test_key",
|
|
27
|
+
RAZORPAY_KEY_SECRET: "secret",
|
|
28
|
+
RAZORPAY_ACCOUNT_NUMBER: "ACC123",
|
|
29
|
+
RAZORPAY_API_BASE_URL: "https://api.razorpay.com/v1",
|
|
30
|
+
APP_BRAND_NAME: "LetItRip",
|
|
31
|
+
};
|
|
32
|
+
return defaults[key];
|
|
33
|
+
},
|
|
34
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
35
|
+
...overrides,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function makeRef() {
|
|
39
|
+
return { id: "payout-ref-1" };
|
|
40
|
+
}
|
|
41
|
+
function makePayout(overrides = {}) {
|
|
42
|
+
return {
|
|
43
|
+
id: "payout-store-A-001",
|
|
44
|
+
storeId: "store-A",
|
|
45
|
+
sellerEmail: "seller@example.com",
|
|
46
|
+
amount: 1000,
|
|
47
|
+
netAmount: 1000,
|
|
48
|
+
currency: "INR",
|
|
49
|
+
paymentMethod: "upi",
|
|
50
|
+
upiId: "seller@upi",
|
|
51
|
+
failureCount: 0,
|
|
52
|
+
...overrides,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function makeBankPayout(overrides = {}) {
|
|
56
|
+
return makePayout({
|
|
57
|
+
paymentMethod: "bank",
|
|
58
|
+
upiId: undefined,
|
|
59
|
+
bankAccount: {
|
|
60
|
+
accountHolderName: "Seller A",
|
|
61
|
+
ifscCode: "HDFC0000001",
|
|
62
|
+
accountNumberMasked: "xxxx1234",
|
|
63
|
+
},
|
|
64
|
+
...overrides,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
function makeEntry(payoutOverrides = {}) {
|
|
68
|
+
return {
|
|
69
|
+
ref: makeRef(),
|
|
70
|
+
data: makePayout(payoutOverrides),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function mockRazorpaySuccess(id = "rzp_payout_1", status = "processing") {
|
|
74
|
+
mockFetch.mockResolvedValue({
|
|
75
|
+
ok: true,
|
|
76
|
+
json: async () => ({ id, status }),
|
|
77
|
+
text: async () => JSON.stringify({ id, status }),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
function mockRazorpayFailure(statusCode = 500, body = "Internal Server Error") {
|
|
81
|
+
mockFetch.mockResolvedValue({
|
|
82
|
+
ok: false,
|
|
83
|
+
status: statusCode,
|
|
84
|
+
text: async () => body,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
vi.clearAllMocks();
|
|
89
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
90
|
+
mockMarkProcessing.mockResolvedValue(undefined);
|
|
91
|
+
mockRecordSuccess.mockResolvedValue(undefined);
|
|
92
|
+
mockRecordFailure.mockResolvedValue(undefined);
|
|
93
|
+
});
|
|
94
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
95
|
+
describe("runPayoutBatch — no pending payouts", () => {
|
|
96
|
+
it("returns early without calling fetch when list is empty", async () => {
|
|
97
|
+
mockGetPending.mockResolvedValue([]);
|
|
98
|
+
await runPayoutBatch(makeCtx());
|
|
99
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
100
|
+
});
|
|
101
|
+
it("logs 'No pending payouts' when list is empty", async () => {
|
|
102
|
+
mockGetPending.mockResolvedValue([]);
|
|
103
|
+
const ctx = makeCtx();
|
|
104
|
+
await runPayoutBatch(ctx);
|
|
105
|
+
expect(ctx.logger.info).toHaveBeenCalledWith(expect.stringMatching(/No pending payouts/i));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("runPayoutBatch — UPI payout", () => {
|
|
109
|
+
it("marks payout as PROCESSING before dispatching", async () => {
|
|
110
|
+
const entry = makeEntry();
|
|
111
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
112
|
+
mockRazorpaySuccess();
|
|
113
|
+
await runPayoutBatch(makeCtx());
|
|
114
|
+
expect(mockMarkProcessing).toHaveBeenCalledWith(entry.ref);
|
|
115
|
+
});
|
|
116
|
+
it("builds correct UPI payload (account_type=vpa)", async () => {
|
|
117
|
+
const entry = makeEntry({ upiId: "seller@okaxis" });
|
|
118
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
119
|
+
mockRazorpaySuccess();
|
|
120
|
+
await runPayoutBatch(makeCtx());
|
|
121
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
122
|
+
expect(body.fund_account.account_type).toBe("vpa");
|
|
123
|
+
expect(body.fund_account.vpa.address).toBe("seller@okaxis");
|
|
124
|
+
expect(body.mode).toBe("UPI");
|
|
125
|
+
});
|
|
126
|
+
it("converts amount to paise (multiplies by 100)", async () => {
|
|
127
|
+
const entry = makeEntry({ amount: 500, netAmount: 500 });
|
|
128
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
129
|
+
mockRazorpaySuccess();
|
|
130
|
+
await runPayoutBatch(makeCtx());
|
|
131
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
132
|
+
expect(body.amount).toBe(50000);
|
|
133
|
+
});
|
|
134
|
+
it("uses netAmount over gross amount when set (after refund deduction)", async () => {
|
|
135
|
+
const entry = makeEntry({ amount: 1000, netAmount: 800 });
|
|
136
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
137
|
+
mockRazorpaySuccess();
|
|
138
|
+
await runPayoutBatch(makeCtx());
|
|
139
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
140
|
+
expect(body.amount).toBe(80000); // netAmount 800 * 100
|
|
141
|
+
});
|
|
142
|
+
it("sends Authorization: Basic header with base64-encoded credentials", async () => {
|
|
143
|
+
const entry = makeEntry();
|
|
144
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
145
|
+
mockRazorpaySuccess();
|
|
146
|
+
await runPayoutBatch(makeCtx());
|
|
147
|
+
const headers = mockFetch.mock.calls[0][1].headers;
|
|
148
|
+
const expected = "Basic " + Buffer.from("rzp_test_key:secret").toString("base64");
|
|
149
|
+
expect(headers["Authorization"]).toBe(expected);
|
|
150
|
+
});
|
|
151
|
+
it("sets X-Payout-Idempotency header to payout.id", async () => {
|
|
152
|
+
const entry = makeEntry({ id: "payout-001" });
|
|
153
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
154
|
+
mockRazorpaySuccess();
|
|
155
|
+
await runPayoutBatch(makeCtx());
|
|
156
|
+
const headers = mockFetch.mock.calls[0][1].headers;
|
|
157
|
+
expect(headers["X-Payout-Idempotency"]).toBe("payout-001");
|
|
158
|
+
});
|
|
159
|
+
it("calls recordSuccess with razorpayId and status on success", async () => {
|
|
160
|
+
const entry = makeEntry();
|
|
161
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
162
|
+
mockRazorpaySuccess("rzp_pout_xyz", "processing");
|
|
163
|
+
await runPayoutBatch(makeCtx());
|
|
164
|
+
expect(mockRecordSuccess).toHaveBeenCalledWith(entry.ref, "rzp_pout_xyz", "processing");
|
|
165
|
+
});
|
|
166
|
+
it("uses reference_id = payout.id in the request body", async () => {
|
|
167
|
+
const entry = makeEntry({ id: "payout-store-B-42" });
|
|
168
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
169
|
+
mockRazorpaySuccess();
|
|
170
|
+
await runPayoutBatch(makeCtx());
|
|
171
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
172
|
+
expect(body.reference_id).toBe("payout-store-B-42");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
describe("runPayoutBatch — bank payout", () => {
|
|
176
|
+
it("builds correct bank payload (account_type=bank_account)", async () => {
|
|
177
|
+
const entry = { ref: makeRef(), data: makeBankPayout() };
|
|
178
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
179
|
+
mockRazorpaySuccess();
|
|
180
|
+
await runPayoutBatch(makeCtx());
|
|
181
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
182
|
+
expect(body.fund_account.account_type).toBe("bank_account");
|
|
183
|
+
expect(body.fund_account.bank_account.ifsc).toBe("HDFC0000001");
|
|
184
|
+
expect(body.mode).toBe("NEFT");
|
|
185
|
+
});
|
|
186
|
+
it("bank payout: account_number_masked used in payload", async () => {
|
|
187
|
+
const entry = { ref: makeRef(), data: makeBankPayout() };
|
|
188
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
189
|
+
mockRazorpaySuccess();
|
|
190
|
+
await runPayoutBatch(makeCtx());
|
|
191
|
+
const body = JSON.parse(mockFetch.mock.calls[0][1].body);
|
|
192
|
+
expect(body.fund_account.bank_account.account_number).toBe("xxxx1234");
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
describe("runPayoutBatch — no fund account configured", () => {
|
|
196
|
+
it("throws and calls recordFailure when paymentMethod is upi but no upiId", async () => {
|
|
197
|
+
const entry = makeEntry({ paymentMethod: "upi", upiId: undefined });
|
|
198
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
199
|
+
await runPayoutBatch(makeCtx());
|
|
200
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
201
|
+
expect(mockRecordFailure).toHaveBeenCalled();
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
describe("runPayoutBatch — missing Razorpay credentials", () => {
|
|
205
|
+
it("records failure when RAZORPAY_KEY_ID is missing", async () => {
|
|
206
|
+
const entry = makeEntry();
|
|
207
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
208
|
+
const ctx = makeCtx({ env: (k) => (k === "RAZORPAY_KEY_ID" ? undefined : "value") });
|
|
209
|
+
await runPayoutBatch(ctx);
|
|
210
|
+
expect(mockRecordFailure).toHaveBeenCalled();
|
|
211
|
+
expect(mockFetch).not.toHaveBeenCalled();
|
|
212
|
+
});
|
|
213
|
+
it("records failure when RAZORPAY_KEY_SECRET is missing", async () => {
|
|
214
|
+
const entry = makeEntry();
|
|
215
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
216
|
+
const ctx = makeCtx({ env: (k) => (k === "RAZORPAY_KEY_SECRET" ? undefined : "value") });
|
|
217
|
+
await runPayoutBatch(ctx);
|
|
218
|
+
expect(mockRecordFailure).toHaveBeenCalled();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
describe("runPayoutBatch — Razorpay API failure", () => {
|
|
222
|
+
it("Razorpay returns non-OK → calls recordFailure with incremented failureCount", async () => {
|
|
223
|
+
const entry = makeEntry({ failureCount: 0 });
|
|
224
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
225
|
+
mockRazorpayFailure(500);
|
|
226
|
+
await runPayoutBatch(makeCtx());
|
|
227
|
+
expect(mockRecordFailure).toHaveBeenCalledWith(entry.ref, 1, // failureCount incremented from 0 to 1
|
|
228
|
+
expect.any(String), false);
|
|
229
|
+
});
|
|
230
|
+
it("failureCount=1 (2nd attempt) → isFinal = false", async () => {
|
|
231
|
+
const entry = makeEntry({ failureCount: 1 });
|
|
232
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
233
|
+
mockRazorpayFailure();
|
|
234
|
+
await runPayoutBatch(makeCtx());
|
|
235
|
+
expect(mockRecordFailure).toHaveBeenCalledWith(entry.ref, 2, expect.any(String), false);
|
|
236
|
+
});
|
|
237
|
+
it("failureCount=2 (3rd attempt, MAX_FAILURES) → isFinal = true", async () => {
|
|
238
|
+
const entry = makeEntry({ failureCount: 2 });
|
|
239
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
240
|
+
mockRazorpayFailure();
|
|
241
|
+
await runPayoutBatch(makeCtx());
|
|
242
|
+
expect(mockRecordFailure).toHaveBeenCalledWith(entry.ref, 3, expect.any(String), true);
|
|
243
|
+
});
|
|
244
|
+
it("final failure → ctx.logger.error called (not just warn)", async () => {
|
|
245
|
+
const entry = makeEntry({ failureCount: 2 });
|
|
246
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
247
|
+
mockRazorpayFailure();
|
|
248
|
+
const ctx = makeCtx();
|
|
249
|
+
await runPayoutBatch(ctx);
|
|
250
|
+
expect(ctx.logger.error).toHaveBeenCalled();
|
|
251
|
+
expect(ctx.logger.warn).not.toHaveBeenCalled();
|
|
252
|
+
});
|
|
253
|
+
it("non-final failure → ctx.logger.warn called (not error)", async () => {
|
|
254
|
+
const entry = makeEntry({ failureCount: 0 });
|
|
255
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
256
|
+
mockRazorpayFailure();
|
|
257
|
+
const ctx = makeCtx();
|
|
258
|
+
await runPayoutBatch(ctx);
|
|
259
|
+
expect(ctx.logger.warn).toHaveBeenCalled();
|
|
260
|
+
expect(ctx.logger.error).not.toHaveBeenCalled();
|
|
261
|
+
});
|
|
262
|
+
it("failure does NOT call recordSuccess", async () => {
|
|
263
|
+
const entry = makeEntry();
|
|
264
|
+
mockGetPending.mockResolvedValue([entry]);
|
|
265
|
+
mockRazorpayFailure();
|
|
266
|
+
await runPayoutBatch(makeCtx());
|
|
267
|
+
expect(mockRecordSuccess).not.toHaveBeenCalled();
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
describe("runPayoutBatch — multiple payouts", () => {
|
|
271
|
+
it("dispatches all payouts independently", async () => {
|
|
272
|
+
const e1 = makeEntry({ id: "payout-A" });
|
|
273
|
+
const e2 = makeEntry({ id: "payout-B" });
|
|
274
|
+
mockGetPending.mockResolvedValue([e1, e2]);
|
|
275
|
+
mockRazorpaySuccess();
|
|
276
|
+
await runPayoutBatch(makeCtx());
|
|
277
|
+
expect(mockMarkProcessing).toHaveBeenCalledTimes(2);
|
|
278
|
+
expect(mockRecordSuccess).toHaveBeenCalledTimes(2);
|
|
279
|
+
});
|
|
280
|
+
it("one payout failure does NOT abort others (Promise.allSettled)", async () => {
|
|
281
|
+
const e1 = makeEntry({ id: "payout-A", failureCount: 2 });
|
|
282
|
+
const e2 = makeEntry({ id: "payout-B" });
|
|
283
|
+
mockGetPending.mockResolvedValue([e1, e2]);
|
|
284
|
+
// First call fails, second succeeds
|
|
285
|
+
mockFetch
|
|
286
|
+
.mockResolvedValueOnce({ ok: false, status: 500, text: async () => "err" })
|
|
287
|
+
.mockResolvedValueOnce({ ok: true, json: async () => ({ id: "rzp_1", status: "processing" }) });
|
|
288
|
+
await expect(runPayoutBatch(makeCtx())).resolves.toBeUndefined();
|
|
289
|
+
expect(mockRecordSuccess).toHaveBeenCalledTimes(1);
|
|
290
|
+
expect(mockRecordFailure).toHaveBeenCalledTimes(1);
|
|
291
|
+
});
|
|
292
|
+
it("logs batch summary with total/succeeded/failed counts", async () => {
|
|
293
|
+
const e1 = makeEntry({ id: "payout-A" });
|
|
294
|
+
const e2 = makeEntry({ id: "payout-B" });
|
|
295
|
+
mockGetPending.mockResolvedValue([e1, e2]);
|
|
296
|
+
mockRazorpaySuccess();
|
|
297
|
+
const ctx = makeCtx();
|
|
298
|
+
await runPayoutBatch(ctx);
|
|
299
|
+
const summaryCall = ctx.logger.info.mock.calls.find((c) => typeof c[0] === "string" && c[0].toLowerCase().includes("complete"));
|
|
300
|
+
expect(summaryCall).toBeTruthy();
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|