@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.
- 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,249 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
const { mockGetExpiredAuctions, mockGetActiveByProduct, mockUpdateStatusInBatch, mockMarkWon, mockMarkLost, mockCreateFromAuction, mockBatchUpdate, mockBatchCommit, mockSendNotification, } = vi.hoisted(() => ({
|
|
3
|
+
mockGetExpiredAuctions: vi.fn(),
|
|
4
|
+
mockGetActiveByProduct: vi.fn(),
|
|
5
|
+
mockUpdateStatusInBatch: vi.fn(),
|
|
6
|
+
mockMarkWon: vi.fn(),
|
|
7
|
+
mockMarkLost: vi.fn(),
|
|
8
|
+
mockCreateFromAuction: vi.fn(),
|
|
9
|
+
mockBatchUpdate: vi.fn(),
|
|
10
|
+
mockBatchCommit: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
mockSendNotification: vi.fn().mockResolvedValue(undefined),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("../../../../../repositories", () => ({
|
|
14
|
+
productRepository: {
|
|
15
|
+
getExpiredAuctions: mockGetExpiredAuctions,
|
|
16
|
+
updateStatusInBatch: mockUpdateStatusInBatch,
|
|
17
|
+
},
|
|
18
|
+
bidRepository: {
|
|
19
|
+
getActiveByProduct: mockGetActiveByProduct,
|
|
20
|
+
markWon: mockMarkWon,
|
|
21
|
+
markLost: mockMarkLost,
|
|
22
|
+
},
|
|
23
|
+
orderRepository: {
|
|
24
|
+
createFromAuction: mockCreateFromAuction,
|
|
25
|
+
},
|
|
26
|
+
}));
|
|
27
|
+
vi.mock("../../../../../features/admin/actions/notification-actions", () => ({
|
|
28
|
+
sendNotification: mockSendNotification,
|
|
29
|
+
}));
|
|
30
|
+
import { runAuctionSettlement } from "../auctionSettlement";
|
|
31
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
32
|
+
function makeCtx(overrides = {}) {
|
|
33
|
+
const mockDoc = vi.fn().mockReturnValue({ id: "products/auction-1" });
|
|
34
|
+
const mockCollection = vi.fn().mockReturnValue({ doc: mockDoc });
|
|
35
|
+
const batch = {
|
|
36
|
+
update: mockBatchUpdate,
|
|
37
|
+
commit: mockBatchCommit,
|
|
38
|
+
set: vi.fn(),
|
|
39
|
+
delete: vi.fn(),
|
|
40
|
+
};
|
|
41
|
+
return {
|
|
42
|
+
db: {
|
|
43
|
+
collection: mockCollection,
|
|
44
|
+
batch: vi.fn().mockReturnValue(batch),
|
|
45
|
+
},
|
|
46
|
+
now: new Date(),
|
|
47
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function makeAuction(overrides = {}) {
|
|
52
|
+
return {
|
|
53
|
+
id: "auction-pokemon-charizard",
|
|
54
|
+
title: "Pokemon Charizard",
|
|
55
|
+
storeId: "store-A",
|
|
56
|
+
listingType: "auction",
|
|
57
|
+
availableQuantity: 1,
|
|
58
|
+
...overrides,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function makeBid(overrides = {}) {
|
|
62
|
+
return {
|
|
63
|
+
ref: { id: "bid-ref-1" },
|
|
64
|
+
data: {
|
|
65
|
+
userId: "user-ravi",
|
|
66
|
+
userName: "Ravi Kumar",
|
|
67
|
+
userEmail: "ravi@example.com",
|
|
68
|
+
bidAmount: 25000,
|
|
69
|
+
currency: "INR",
|
|
70
|
+
...overrides,
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
vi.clearAllMocks();
|
|
76
|
+
mockBatchCommit.mockResolvedValue(undefined);
|
|
77
|
+
mockSendNotification.mockResolvedValue(undefined);
|
|
78
|
+
mockCreateFromAuction.mockReturnValue({ id: "order-1" });
|
|
79
|
+
});
|
|
80
|
+
describe("runAuctionSettlement — no expired auctions", () => {
|
|
81
|
+
it("returns early when no expired auctions exist", async () => {
|
|
82
|
+
mockGetExpiredAuctions.mockResolvedValue([]);
|
|
83
|
+
const ctx = makeCtx();
|
|
84
|
+
await runAuctionSettlement(ctx);
|
|
85
|
+
expect(mockGetActiveByProduct).not.toHaveBeenCalled();
|
|
86
|
+
});
|
|
87
|
+
it("logs 'No expired auctions found' when list is empty", async () => {
|
|
88
|
+
mockGetExpiredAuctions.mockResolvedValue([]);
|
|
89
|
+
const ctx = makeCtx();
|
|
90
|
+
await runAuctionSettlement(ctx);
|
|
91
|
+
expect(ctx.logger.info).toHaveBeenCalledWith(expect.stringMatching(/No expired auctions/i));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("runAuctionSettlement — auction with no bids", () => {
|
|
95
|
+
it("archives product when no active bids", async () => {
|
|
96
|
+
const auction = makeAuction();
|
|
97
|
+
mockGetExpiredAuctions.mockResolvedValue([{ data: auction }]);
|
|
98
|
+
mockGetActiveByProduct.mockResolvedValue([]);
|
|
99
|
+
const ctx = makeCtx();
|
|
100
|
+
await runAuctionSettlement(ctx);
|
|
101
|
+
expect(mockUpdateStatusInBatch).toHaveBeenCalledWith(expect.any(Object), // batch
|
|
102
|
+
auction.id, "archived");
|
|
103
|
+
expect(mockBatchCommit).toHaveBeenCalled();
|
|
104
|
+
});
|
|
105
|
+
it("does NOT call createFromAuction when no bids", async () => {
|
|
106
|
+
mockGetExpiredAuctions.mockResolvedValue([{ data: makeAuction() }]);
|
|
107
|
+
mockGetActiveByProduct.mockResolvedValue([]);
|
|
108
|
+
await runAuctionSettlement(makeCtx());
|
|
109
|
+
expect(mockCreateFromAuction).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
it("does NOT send win/loss notifications when no bids", async () => {
|
|
112
|
+
mockGetExpiredAuctions.mockResolvedValue([{ data: makeAuction() }]);
|
|
113
|
+
mockGetActiveByProduct.mockResolvedValue([]);
|
|
114
|
+
await runAuctionSettlement(makeCtx());
|
|
115
|
+
expect(mockSendNotification).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe("runAuctionSettlement — auction with bids", () => {
|
|
119
|
+
it("marks the highest bid as won", async () => {
|
|
120
|
+
const winner = makeBid({ bidAmount: 30000 });
|
|
121
|
+
const loser = makeBid({ bidAmount: 20000, userId: "user-ankit" });
|
|
122
|
+
mockGetExpiredAuctions.mockResolvedValue([{ data: makeAuction() }]);
|
|
123
|
+
mockGetActiveByProduct.mockResolvedValue([winner, loser]);
|
|
124
|
+
await runAuctionSettlement(makeCtx());
|
|
125
|
+
expect(mockMarkWon).toHaveBeenCalledWith(expect.any(Object), winner.ref);
|
|
126
|
+
});
|
|
127
|
+
it("marks all other bids as lost", async () => {
|
|
128
|
+
const winner = makeBid({ bidAmount: 30000 });
|
|
129
|
+
const loser1 = makeBid({ userId: "user-ankit", bidAmount: 20000 });
|
|
130
|
+
const loser2 = makeBid({ userId: "user-priya", bidAmount: 15000 });
|
|
131
|
+
mockGetExpiredAuctions.mockResolvedValue([{ data: makeAuction() }]);
|
|
132
|
+
mockGetActiveByProduct.mockResolvedValue([winner, loser1, loser2]);
|
|
133
|
+
await runAuctionSettlement(makeCtx());
|
|
134
|
+
expect(mockMarkLost).toHaveBeenCalledTimes(2);
|
|
135
|
+
expect(mockMarkLost).toHaveBeenCalledWith(expect.any(Object), loser1.ref);
|
|
136
|
+
expect(mockMarkLost).toHaveBeenCalledWith(expect.any(Object), loser2.ref);
|
|
137
|
+
});
|
|
138
|
+
it("creates an order from auction data (winner becomes buyer)", async () => {
|
|
139
|
+
const winner = makeBid({ bidAmount: 25000, userId: "user-ravi" });
|
|
140
|
+
const auction = makeAuction({ id: "auction-1", title: "Charizard", storeId: "store-B" });
|
|
141
|
+
mockGetExpiredAuctions.mockResolvedValue([{ data: auction }]);
|
|
142
|
+
mockGetActiveByProduct.mockResolvedValue([winner]);
|
|
143
|
+
await runAuctionSettlement(makeCtx());
|
|
144
|
+
expect(mockCreateFromAuction).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({
|
|
145
|
+
productId: "auction-1",
|
|
146
|
+
productTitle: "Charizard",
|
|
147
|
+
userId: "user-ravi",
|
|
148
|
+
storeId: "store-B",
|
|
149
|
+
amount: 25000,
|
|
150
|
+
}));
|
|
151
|
+
});
|
|
152
|
+
it("sets isSold=true and availableQuantity=0 on product", async () => {
|
|
153
|
+
const winner = makeBid();
|
|
154
|
+
mockGetExpiredAuctions.mockResolvedValue([{ data: makeAuction() }]);
|
|
155
|
+
mockGetActiveByProduct.mockResolvedValue([winner]);
|
|
156
|
+
await runAuctionSettlement(makeCtx());
|
|
157
|
+
expect(mockBatchUpdate).toHaveBeenCalledWith(expect.any(Object), // doc ref
|
|
158
|
+
expect.objectContaining({ isSold: true, availableQuantity: 0 }));
|
|
159
|
+
});
|
|
160
|
+
it("commits the batch before sending notifications", async () => {
|
|
161
|
+
const winner = makeBid();
|
|
162
|
+
mockGetExpiredAuctions.mockResolvedValue([{ data: makeAuction() }]);
|
|
163
|
+
mockGetActiveByProduct.mockResolvedValue([winner]);
|
|
164
|
+
const callOrder = [];
|
|
165
|
+
mockBatchCommit.mockImplementation(async () => { callOrder.push("commit"); });
|
|
166
|
+
mockSendNotification.mockImplementation(async () => { callOrder.push("notify"); });
|
|
167
|
+
await runAuctionSettlement(makeCtx());
|
|
168
|
+
const commitIdx = callOrder.indexOf("commit");
|
|
169
|
+
const notifyIdx = callOrder.indexOf("notify");
|
|
170
|
+
expect(commitIdx).toBeLessThan(notifyIdx);
|
|
171
|
+
});
|
|
172
|
+
it("sends bid_won notification to the winning bidder", async () => {
|
|
173
|
+
const winner = makeBid({ userId: "user-ravi", bidAmount: 25000, currency: "INR" });
|
|
174
|
+
mockGetExpiredAuctions.mockResolvedValue([{ data: makeAuction({ title: "Charizard" }) }]);
|
|
175
|
+
mockGetActiveByProduct.mockResolvedValue([winner]);
|
|
176
|
+
await runAuctionSettlement(makeCtx());
|
|
177
|
+
expect(mockSendNotification).toHaveBeenCalledWith(expect.objectContaining({ userId: "user-ravi", type: "bid_won" }));
|
|
178
|
+
});
|
|
179
|
+
it("sends bid_lost notification to each losing bidder", async () => {
|
|
180
|
+
const winner = makeBid({ userId: "user-ravi" });
|
|
181
|
+
const loser1 = makeBid({ userId: "user-ankit" });
|
|
182
|
+
const loser2 = makeBid({ userId: "user-priya" });
|
|
183
|
+
mockGetExpiredAuctions.mockResolvedValue([{ data: makeAuction() }]);
|
|
184
|
+
mockGetActiveByProduct.mockResolvedValue([winner, loser1, loser2]);
|
|
185
|
+
await runAuctionSettlement(makeCtx());
|
|
186
|
+
expect(mockSendNotification).toHaveBeenCalledWith(expect.objectContaining({ userId: "user-ankit", type: "bid_lost" }));
|
|
187
|
+
expect(mockSendNotification).toHaveBeenCalledWith(expect.objectContaining({ userId: "user-priya", type: "bid_lost" }));
|
|
188
|
+
});
|
|
189
|
+
it("notification failure for one loser does NOT abort others (allSettled)", async () => {
|
|
190
|
+
const winner = makeBid({ userId: "user-ravi" });
|
|
191
|
+
const loser1 = makeBid({ userId: "user-ankit" });
|
|
192
|
+
const loser2 = makeBid({ userId: "user-priya" });
|
|
193
|
+
mockGetExpiredAuctions.mockResolvedValue([{ data: makeAuction() }]);
|
|
194
|
+
mockGetActiveByProduct.mockResolvedValue([winner, loser1, loser2]);
|
|
195
|
+
let callCount = 0;
|
|
196
|
+
mockSendNotification.mockImplementation(async ({ type, userId }) => {
|
|
197
|
+
callCount++;
|
|
198
|
+
if (type === "bid_lost" && userId === "user-ankit") {
|
|
199
|
+
throw new Error("Notification service down");
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
// Should not throw
|
|
203
|
+
await expect(runAuctionSettlement(makeCtx())).resolves.toBeUndefined();
|
|
204
|
+
// loser2 still got notified
|
|
205
|
+
expect(callCount).toBeGreaterThanOrEqual(2);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
describe("runAuctionSettlement — multiple expired auctions", () => {
|
|
209
|
+
it("settles each auction independently", async () => {
|
|
210
|
+
const auction1 = makeAuction({ id: "auction-1" });
|
|
211
|
+
const auction2 = makeAuction({ id: "auction-2" });
|
|
212
|
+
mockGetExpiredAuctions.mockResolvedValue([
|
|
213
|
+
{ data: auction1 },
|
|
214
|
+
{ data: auction2 },
|
|
215
|
+
]);
|
|
216
|
+
mockGetActiveByProduct.mockResolvedValue([]);
|
|
217
|
+
await runAuctionSettlement(makeCtx());
|
|
218
|
+
expect(mockGetActiveByProduct).toHaveBeenCalledTimes(2);
|
|
219
|
+
expect(mockGetActiveByProduct).toHaveBeenCalledWith("auction-1");
|
|
220
|
+
expect(mockGetActiveByProduct).toHaveBeenCalledWith("auction-2");
|
|
221
|
+
});
|
|
222
|
+
it("one auction error does NOT abort others (allSettled)", async () => {
|
|
223
|
+
const auction1 = makeAuction({ id: "auction-1" });
|
|
224
|
+
const auction2 = makeAuction({ id: "auction-2" });
|
|
225
|
+
mockGetExpiredAuctions.mockResolvedValue([
|
|
226
|
+
{ data: auction1 },
|
|
227
|
+
{ data: auction2 },
|
|
228
|
+
]);
|
|
229
|
+
// auction-1 throws, auction-2 succeeds
|
|
230
|
+
mockGetActiveByProduct
|
|
231
|
+
.mockRejectedValueOnce(new Error("Firestore query failed"))
|
|
232
|
+
.mockResolvedValueOnce([]);
|
|
233
|
+
await expect(runAuctionSettlement(makeCtx())).resolves.toBeUndefined();
|
|
234
|
+
// auction-2 was still processed (batch committed)
|
|
235
|
+
expect(mockBatchCommit).toHaveBeenCalledTimes(1);
|
|
236
|
+
});
|
|
237
|
+
it("logs the count of failed settlements when some fail", async () => {
|
|
238
|
+
mockGetExpiredAuctions.mockResolvedValue([
|
|
239
|
+
{ data: makeAuction({ id: "auction-1" }) },
|
|
240
|
+
{ data: makeAuction({ id: "auction-2" }) },
|
|
241
|
+
]);
|
|
242
|
+
mockGetActiveByProduct
|
|
243
|
+
.mockRejectedValueOnce(new Error("DB error"))
|
|
244
|
+
.mockResolvedValueOnce([]);
|
|
245
|
+
const ctx = makeCtx();
|
|
246
|
+
await runAuctionSettlement(ctx);
|
|
247
|
+
expect(ctx.logger.error).toHaveBeenCalledWith(expect.stringMatching(/1 auction\(s\) failed/i), expect.any(Array));
|
|
248
|
+
});
|
|
249
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
const { mockGetWinningBid, mockMarkWinning, mockMarkOutbid, mockIncrementBidCount, mockBatchCommit, mockBatchUpdate, mockSendNotification, mockRtdbPush, mockDecryptPii, } = vi.hoisted(() => ({
|
|
3
|
+
mockGetWinningBid: vi.fn(),
|
|
4
|
+
mockMarkWinning: vi.fn(),
|
|
5
|
+
mockMarkOutbid: vi.fn(),
|
|
6
|
+
mockIncrementBidCount: vi.fn(),
|
|
7
|
+
mockBatchCommit: vi.fn().mockResolvedValue(undefined),
|
|
8
|
+
mockBatchUpdate: vi.fn(),
|
|
9
|
+
mockSendNotification: vi.fn().mockResolvedValue(undefined),
|
|
10
|
+
mockRtdbPush: vi.fn().mockResolvedValue(undefined),
|
|
11
|
+
mockDecryptPii: vi.fn((v) => v),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("../../../../../repositories", () => ({
|
|
14
|
+
bidRepository: {
|
|
15
|
+
getWinningBid: mockGetWinningBid,
|
|
16
|
+
markWinning: mockMarkWinning,
|
|
17
|
+
markOutbid: mockMarkOutbid,
|
|
18
|
+
},
|
|
19
|
+
productRepository: {
|
|
20
|
+
incrementBidCountInBatch: mockIncrementBidCount,
|
|
21
|
+
},
|
|
22
|
+
}));
|
|
23
|
+
vi.mock("../../../../../features/admin/actions/notification-actions", () => ({
|
|
24
|
+
sendNotification: mockSendNotification,
|
|
25
|
+
}));
|
|
26
|
+
vi.mock("../../../../../providers/db-firebase", () => ({
|
|
27
|
+
getAdminRealtimeDb: vi.fn(() => ({
|
|
28
|
+
ref: vi.fn(() => ({ push: mockRtdbPush })),
|
|
29
|
+
})),
|
|
30
|
+
}));
|
|
31
|
+
vi.mock("../../../../../security/index", () => ({
|
|
32
|
+
decryptPii: mockDecryptPii,
|
|
33
|
+
}));
|
|
34
|
+
vi.mock("../../../../../errors/normalize", () => ({
|
|
35
|
+
normalizeError: vi.fn(),
|
|
36
|
+
}));
|
|
37
|
+
import { handleBidPlaced } from "../onBidPlaced";
|
|
38
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
39
|
+
function makeCtx() {
|
|
40
|
+
const batch = {
|
|
41
|
+
update: mockBatchUpdate,
|
|
42
|
+
commit: mockBatchCommit,
|
|
43
|
+
set: vi.fn(),
|
|
44
|
+
delete: vi.fn(),
|
|
45
|
+
};
|
|
46
|
+
return {
|
|
47
|
+
db: {
|
|
48
|
+
collection: vi.fn().mockReturnValue({
|
|
49
|
+
doc: vi.fn().mockReturnValue({ id: "bid-ref-1" }),
|
|
50
|
+
}),
|
|
51
|
+
batch: vi.fn().mockReturnValue(batch),
|
|
52
|
+
},
|
|
53
|
+
now: new Date(),
|
|
54
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
function makeInput(overrides = {}) {
|
|
58
|
+
return {
|
|
59
|
+
bidId: "bid-charizard-ravi-001",
|
|
60
|
+
bid: {
|
|
61
|
+
id: "bid-charizard-ravi-001",
|
|
62
|
+
productId: "auction-charizard",
|
|
63
|
+
productTitle: "Pokemon Charizard",
|
|
64
|
+
userId: "user-ravi",
|
|
65
|
+
userName: "Ravi Kumar",
|
|
66
|
+
bidAmount: 25000,
|
|
67
|
+
currency: "INR",
|
|
68
|
+
...overrides,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function makePrevWinning(userId = "user-ankit") {
|
|
73
|
+
return {
|
|
74
|
+
ref: { id: "bid-ref-prev" },
|
|
75
|
+
data: { userId, bidAmount: 20000 },
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
beforeEach(() => {
|
|
79
|
+
vi.clearAllMocks();
|
|
80
|
+
mockBatchCommit.mockResolvedValue(undefined);
|
|
81
|
+
mockSendNotification.mockResolvedValue(undefined);
|
|
82
|
+
mockRtdbPush.mockResolvedValue(undefined);
|
|
83
|
+
mockDecryptPii.mockImplementation((v) => v);
|
|
84
|
+
mockGetWinningBid.mockResolvedValue(null);
|
|
85
|
+
});
|
|
86
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
87
|
+
describe("handleBidPlaced — marks new bid as winning", () => {
|
|
88
|
+
it("calls bidRepository.markWinning with new bid ref", async () => {
|
|
89
|
+
const ctx = makeCtx();
|
|
90
|
+
await handleBidPlaced(makeInput(), ctx);
|
|
91
|
+
expect(mockMarkWinning).toHaveBeenCalledWith(expect.any(Object), expect.any(Object));
|
|
92
|
+
});
|
|
93
|
+
it("calls productRepository.incrementBidCountInBatch with correct args", async () => {
|
|
94
|
+
const ctx = makeCtx();
|
|
95
|
+
await handleBidPlaced(makeInput(), ctx);
|
|
96
|
+
expect(mockIncrementBidCount).toHaveBeenCalledWith(expect.any(Object), "auction-charizard", 25000, "user-ravi");
|
|
97
|
+
});
|
|
98
|
+
it("commits the batch", async () => {
|
|
99
|
+
const ctx = makeCtx();
|
|
100
|
+
await handleBidPlaced(makeInput(), ctx);
|
|
101
|
+
expect(mockBatchCommit).toHaveBeenCalledOnce();
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
describe("handleBidPlaced — first bid (no previous winner)", () => {
|
|
105
|
+
it("does NOT call markOutbid when no previous winner", async () => {
|
|
106
|
+
mockGetWinningBid.mockResolvedValue(null);
|
|
107
|
+
const ctx = makeCtx();
|
|
108
|
+
await handleBidPlaced(makeInput(), ctx);
|
|
109
|
+
expect(mockMarkOutbid).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
it("does NOT send outbid notification when no previous winner", async () => {
|
|
112
|
+
mockGetWinningBid.mockResolvedValue(null);
|
|
113
|
+
const ctx = makeCtx();
|
|
114
|
+
await handleBidPlaced(makeInput(), ctx);
|
|
115
|
+
expect(mockSendNotification).not.toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
it("does NOT push to RTDB when no previous winner", async () => {
|
|
118
|
+
mockGetWinningBid.mockResolvedValue(null);
|
|
119
|
+
const ctx = makeCtx();
|
|
120
|
+
await handleBidPlaced(makeInput(), ctx);
|
|
121
|
+
expect(mockRtdbPush).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe("handleBidPlaced — outbid notification", () => {
|
|
125
|
+
it("marks previous winner as outbid when different user", async () => {
|
|
126
|
+
mockGetWinningBid.mockResolvedValue(makePrevWinning("user-ankit"));
|
|
127
|
+
const ctx = makeCtx();
|
|
128
|
+
await handleBidPlaced(makeInput(), ctx);
|
|
129
|
+
expect(mockMarkOutbid).toHaveBeenCalledWith(expect.any(Object), expect.any(Object));
|
|
130
|
+
});
|
|
131
|
+
it("sends bid_outbid notification to the outbid user", async () => {
|
|
132
|
+
mockGetWinningBid.mockResolvedValue(makePrevWinning("user-ankit"));
|
|
133
|
+
const ctx = makeCtx();
|
|
134
|
+
await handleBidPlaced(makeInput(), ctx);
|
|
135
|
+
expect(mockSendNotification).toHaveBeenCalledWith(expect.objectContaining({ userId: "user-ankit", type: "bid_outbid" }));
|
|
136
|
+
});
|
|
137
|
+
it("pushes outbid event to RTDB for real-time notification", async () => {
|
|
138
|
+
mockGetWinningBid.mockResolvedValue(makePrevWinning("user-ankit"));
|
|
139
|
+
const ctx = makeCtx();
|
|
140
|
+
await handleBidPlaced(makeInput(), ctx);
|
|
141
|
+
expect(mockRtdbPush).toHaveBeenCalledWith(expect.objectContaining({ type: "bid_outbid" }));
|
|
142
|
+
});
|
|
143
|
+
it("outbid notification message includes bid amount and product title", async () => {
|
|
144
|
+
mockGetWinningBid.mockResolvedValue(makePrevWinning("user-ankit"));
|
|
145
|
+
const ctx = makeCtx();
|
|
146
|
+
await handleBidPlaced(makeInput({ productTitle: "Charizard 1st Ed", bidAmount: 30000 }), ctx);
|
|
147
|
+
const notifCall = mockSendNotification.mock.calls[0][0];
|
|
148
|
+
expect(notifCall.message).toMatch(/Charizard 1st Ed/);
|
|
149
|
+
});
|
|
150
|
+
it("does NOT outbid the same user who placed the new bid", async () => {
|
|
151
|
+
// Same user re-bids (bidder is already the winner)
|
|
152
|
+
mockGetWinningBid.mockResolvedValue(makePrevWinning("user-ravi")); // same as input.bid.userId
|
|
153
|
+
const ctx = makeCtx();
|
|
154
|
+
await handleBidPlaced(makeInput({ userId: "user-ravi" }), ctx);
|
|
155
|
+
expect(mockMarkOutbid).not.toHaveBeenCalled();
|
|
156
|
+
expect(mockSendNotification).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
describe("handleBidPlaced — RTDB failure is non-fatal", () => {
|
|
160
|
+
it("RTDB push failure → logs but does NOT re-throw", async () => {
|
|
161
|
+
mockGetWinningBid.mockResolvedValue(makePrevWinning("user-ankit"));
|
|
162
|
+
mockRtdbPush.mockRejectedValue(new Error("RTDB unavailable"));
|
|
163
|
+
const ctx = makeCtx();
|
|
164
|
+
await expect(handleBidPlaced(makeInput(), ctx)).resolves.toBeUndefined();
|
|
165
|
+
expect(ctx.logger.error).toHaveBeenCalledWith(expect.stringMatching(/Realtime DB/i), expect.any(Error));
|
|
166
|
+
});
|
|
167
|
+
it("Firestore outbid notification still sent even if RTDB fails", async () => {
|
|
168
|
+
mockGetWinningBid.mockResolvedValue(makePrevWinning("user-ankit"));
|
|
169
|
+
mockRtdbPush.mockRejectedValue(new Error("RTDB unavailable"));
|
|
170
|
+
const ctx = makeCtx();
|
|
171
|
+
await handleBidPlaced(makeInput(), ctx);
|
|
172
|
+
// sendNotification (Firestore) should still have been called before RTDB
|
|
173
|
+
expect(mockSendNotification).toHaveBeenCalledWith(expect.objectContaining({ type: "bid_outbid" }));
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
describe("handleBidPlaced — PII decryption", () => {
|
|
177
|
+
it("decrypts userName via decryptPii before use", async () => {
|
|
178
|
+
mockDecryptPii.mockImplementation((v) => `decrypted:${v}`);
|
|
179
|
+
const ctx = makeCtx();
|
|
180
|
+
await handleBidPlaced(makeInput({ userName: "encrypted_name" }), ctx);
|
|
181
|
+
expect(mockDecryptPii).toHaveBeenCalledWith("encrypted_name");
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
describe("handleBidPlaced — error propagation", () => {
|
|
185
|
+
it("Firestore batch.commit failure → re-throws", async () => {
|
|
186
|
+
mockBatchCommit.mockRejectedValue(new Error("Firestore unavailable"));
|
|
187
|
+
const ctx = makeCtx();
|
|
188
|
+
await expect(handleBidPlaced(makeInput(), ctx)).rejects.toThrow("Firestore unavailable");
|
|
189
|
+
});
|
|
190
|
+
it("logs error with bidId and productId before re-throwing", async () => {
|
|
191
|
+
mockBatchCommit.mockRejectedValue(new Error("Firestore unavailable"));
|
|
192
|
+
const ctx = makeCtx();
|
|
193
|
+
try {
|
|
194
|
+
await handleBidPlaced(makeInput(), ctx);
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
// expected
|
|
198
|
+
}
|
|
199
|
+
expect(ctx.logger.error).toHaveBeenCalledWith(expect.any(String), expect.any(Error), expect.objectContaining({ bidId: "bid-charizard-ravi-001" }));
|
|
200
|
+
});
|
|
201
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
const { mockSendWhatsApp, mockBuildMessage, mockStoreFindBySlug, mockUserFindById, mockDecryptPii, } = vi.hoisted(() => ({
|
|
3
|
+
mockSendWhatsApp: vi.fn(),
|
|
4
|
+
mockBuildMessage: vi.fn().mockReturnValue("Purchase announcement message"),
|
|
5
|
+
mockStoreFindBySlug: vi.fn(),
|
|
6
|
+
mockUserFindById: vi.fn(),
|
|
7
|
+
mockDecryptPii: vi.fn((v) => v),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock("../../../../../repositories", () => ({
|
|
10
|
+
storeRepository: { findBySlug: mockStoreFindBySlug },
|
|
11
|
+
userRepository: { findById: mockUserFindById },
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("../../../../../features/whatsapp-bot/server", () => ({
|
|
14
|
+
sendWhatsAppBusinessMessage: mockSendWhatsApp,
|
|
15
|
+
buildPurchaseAnnouncementMessage: mockBuildMessage,
|
|
16
|
+
}));
|
|
17
|
+
vi.mock("../../../../../security/index", () => ({
|
|
18
|
+
decryptPii: mockDecryptPii,
|
|
19
|
+
}));
|
|
20
|
+
vi.mock("../../../../../errors/normalize", () => ({
|
|
21
|
+
normalizeError: vi.fn(),
|
|
22
|
+
}));
|
|
23
|
+
import { handleOrderCreate } from "../onOrderCreate";
|
|
24
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
25
|
+
function makeCtx(envOverrides = {}) {
|
|
26
|
+
const defaults = {
|
|
27
|
+
WHATSAPP_PHONE_NUMBER_ID: "phone-number-id-123",
|
|
28
|
+
WHATSAPP_CLOUD_API_TOKEN: "token-abc",
|
|
29
|
+
WHATSAPP_ADMIN_NOTIFY_NUMBERS: "919876543210",
|
|
30
|
+
...envOverrides,
|
|
31
|
+
};
|
|
32
|
+
return {
|
|
33
|
+
env: (key) => defaults[key],
|
|
34
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function makeInput(overrides = {}) {
|
|
38
|
+
return {
|
|
39
|
+
orderId: "order-1",
|
|
40
|
+
order: {
|
|
41
|
+
buyerDisplayName: "Ravi Kumar",
|
|
42
|
+
buyerId: "user-ravi",
|
|
43
|
+
items: [{ title: "Pokemon Charizard" }],
|
|
44
|
+
totalAmount: 25000,
|
|
45
|
+
storeId: "store-pokemon-palace",
|
|
46
|
+
...overrides,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.clearAllMocks();
|
|
52
|
+
mockSendWhatsApp.mockResolvedValue(true);
|
|
53
|
+
mockBuildMessage.mockReturnValue("Purchase announcement message");
|
|
54
|
+
mockDecryptPii.mockImplementation((v) => v);
|
|
55
|
+
mockStoreFindBySlug.mockResolvedValue(null);
|
|
56
|
+
mockUserFindById.mockResolvedValue(null);
|
|
57
|
+
});
|
|
58
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
59
|
+
describe("handleOrderCreate — WhatsApp not configured", () => {
|
|
60
|
+
it("skips when WHATSAPP_PHONE_NUMBER_ID is missing", async () => {
|
|
61
|
+
const ctx = makeCtx({ WHATSAPP_PHONE_NUMBER_ID: undefined });
|
|
62
|
+
await handleOrderCreate(makeInput(), ctx);
|
|
63
|
+
expect(mockSendWhatsApp).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
it("skips when WHATSAPP_CLOUD_API_TOKEN is missing", async () => {
|
|
66
|
+
const ctx = makeCtx({ WHATSAPP_CLOUD_API_TOKEN: undefined });
|
|
67
|
+
await handleOrderCreate(makeInput(), ctx);
|
|
68
|
+
expect(mockSendWhatsApp).not.toHaveBeenCalled();
|
|
69
|
+
});
|
|
70
|
+
it("logs that WhatsApp is not configured", async () => {
|
|
71
|
+
const ctx = makeCtx({ WHATSAPP_PHONE_NUMBER_ID: undefined });
|
|
72
|
+
await handleOrderCreate(makeInput(), ctx);
|
|
73
|
+
expect(ctx.logger.info).toHaveBeenCalledWith(expect.stringMatching(/not configured/i), expect.any(Object));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("handleOrderCreate — admin notification", () => {
|
|
77
|
+
it("sends announcement to each admin number in WHATSAPP_ADMIN_NOTIFY_NUMBERS", async () => {
|
|
78
|
+
const ctx = makeCtx({ WHATSAPP_ADMIN_NOTIFY_NUMBERS: "919876543210,918765432109" });
|
|
79
|
+
await handleOrderCreate(makeInput(), ctx);
|
|
80
|
+
expect(mockSendWhatsApp).toHaveBeenCalledTimes(2);
|
|
81
|
+
});
|
|
82
|
+
it("sends to 0 admin numbers when env var is empty", async () => {
|
|
83
|
+
const ctx = makeCtx({ WHATSAPP_ADMIN_NOTIFY_NUMBERS: "" });
|
|
84
|
+
await handleOrderCreate(makeInput(), ctx);
|
|
85
|
+
// No admin calls, store lookup still happens
|
|
86
|
+
const waCallArgs = mockSendWhatsApp.mock.calls.map((c) => c[0].toPhone);
|
|
87
|
+
expect(waCallArgs).toHaveLength(0);
|
|
88
|
+
});
|
|
89
|
+
it("strips non-digit chars from admin numbers", async () => {
|
|
90
|
+
const ctx = makeCtx({ WHATSAPP_ADMIN_NOTIFY_NUMBERS: "+91 987-654-3210" });
|
|
91
|
+
await handleOrderCreate(makeInput(), ctx);
|
|
92
|
+
const call = mockSendWhatsApp.mock.calls[0][0];
|
|
93
|
+
expect(call.toPhone).toBe("919876543210");
|
|
94
|
+
});
|
|
95
|
+
it("sends announcement with the built message", async () => {
|
|
96
|
+
const ctx = makeCtx();
|
|
97
|
+
mockBuildMessage.mockReturnValue("Custom message content");
|
|
98
|
+
await handleOrderCreate(makeInput(), ctx);
|
|
99
|
+
const call = mockSendWhatsApp.mock.calls[0][0];
|
|
100
|
+
expect(call.message).toBe("Custom message content");
|
|
101
|
+
});
|
|
102
|
+
it("one admin send failure is non-fatal, function still completes", async () => {
|
|
103
|
+
const ctx = makeCtx({ WHATSAPP_ADMIN_NOTIFY_NUMBERS: "91111,91222" });
|
|
104
|
+
mockSendWhatsApp.mockRejectedValueOnce(new Error("WA error")).mockResolvedValue(true);
|
|
105
|
+
await expect(handleOrderCreate(makeInput(), ctx)).resolves.toBeUndefined();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("handleOrderCreate — store owner notification", () => {
|
|
109
|
+
it("sends to store owner phone when store + owner + phone all exist", async () => {
|
|
110
|
+
mockStoreFindBySlug.mockResolvedValue({ ownerId: "user-seller-1" });
|
|
111
|
+
mockUserFindById.mockResolvedValue({ phoneNumber: "encrypted_phone_91999" });
|
|
112
|
+
mockDecryptPii.mockReturnValue("919999999999");
|
|
113
|
+
const ctx = makeCtx({ WHATSAPP_ADMIN_NOTIFY_NUMBERS: "" });
|
|
114
|
+
await handleOrderCreate(makeInput({ storeId: "store-A" }), ctx);
|
|
115
|
+
expect(mockSendWhatsApp).toHaveBeenCalledWith(expect.objectContaining({ toPhone: "919999999999" }));
|
|
116
|
+
});
|
|
117
|
+
it("skips store owner if store not found", async () => {
|
|
118
|
+
mockStoreFindBySlug.mockResolvedValue(null);
|
|
119
|
+
const ctx = makeCtx({ WHATSAPP_ADMIN_NOTIFY_NUMBERS: "" });
|
|
120
|
+
await handleOrderCreate(makeInput({ storeId: "store-nonexistent" }), ctx);
|
|
121
|
+
expect(mockSendWhatsApp).not.toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
it("skips store owner if owner has no phone number", async () => {
|
|
124
|
+
mockStoreFindBySlug.mockResolvedValue({ ownerId: "user-seller-1" });
|
|
125
|
+
mockUserFindById.mockResolvedValue({ phoneNumber: null });
|
|
126
|
+
const ctx = makeCtx({ WHATSAPP_ADMIN_NOTIFY_NUMBERS: "" });
|
|
127
|
+
await handleOrderCreate(makeInput({ storeId: "store-A" }), ctx);
|
|
128
|
+
expect(mockSendWhatsApp).not.toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
it("skips store owner notification when storeId is absent from order", async () => {
|
|
131
|
+
const ctx = makeCtx({ WHATSAPP_ADMIN_NOTIFY_NUMBERS: "" });
|
|
132
|
+
await handleOrderCreate(makeInput({ storeId: undefined }), ctx);
|
|
133
|
+
expect(mockStoreFindBySlug).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
it("store owner lookup failure is non-fatal", async () => {
|
|
136
|
+
mockStoreFindBySlug.mockRejectedValue(new Error("Firestore error"));
|
|
137
|
+
const ctx = makeCtx();
|
|
138
|
+
await expect(handleOrderCreate(makeInput(), ctx)).resolves.toBeUndefined();
|
|
139
|
+
expect(ctx.logger.error).toHaveBeenCalledWith(expect.stringMatching(/Store owner lookup failed/i), expect.any(Error), expect.any(Object));
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe("handleOrderCreate — message content", () => {
|
|
143
|
+
it("uses buyerDisplayName in announcement", async () => {
|
|
144
|
+
const ctx = makeCtx();
|
|
145
|
+
await handleOrderCreate(makeInput({ buyerDisplayName: "Priya Singh" }), ctx);
|
|
146
|
+
expect(mockBuildMessage).toHaveBeenCalledWith(expect.objectContaining({ buyerName: "Priya Singh" }));
|
|
147
|
+
});
|
|
148
|
+
it("uses 'A customer' as fallback when buyerDisplayName is missing", async () => {
|
|
149
|
+
const ctx = makeCtx();
|
|
150
|
+
await handleOrderCreate(makeInput({ buyerDisplayName: undefined }), ctx);
|
|
151
|
+
expect(mockBuildMessage).toHaveBeenCalledWith(expect.objectContaining({ buyerName: "A customer" }));
|
|
152
|
+
});
|
|
153
|
+
it("uses first item title as firstItemName", async () => {
|
|
154
|
+
const ctx = makeCtx();
|
|
155
|
+
await handleOrderCreate(makeInput({ items: [{ title: "Charizard PSA 9" }] }), ctx);
|
|
156
|
+
expect(mockBuildMessage).toHaveBeenCalledWith(expect.objectContaining({ firstItemName: "Charizard PSA 9" }));
|
|
157
|
+
});
|
|
158
|
+
it("falls back to item.name when title is missing", async () => {
|
|
159
|
+
const ctx = makeCtx();
|
|
160
|
+
await handleOrderCreate(makeInput({ items: [{ name: "Blastoise" }] }), ctx);
|
|
161
|
+
expect(mockBuildMessage).toHaveBeenCalledWith(expect.objectContaining({ firstItemName: "Blastoise" }));
|
|
162
|
+
});
|
|
163
|
+
it("additionalItemCount = items.length - 1", async () => {
|
|
164
|
+
const ctx = makeCtx();
|
|
165
|
+
const items = [{ title: "A" }, { title: "B" }, { title: "C" }];
|
|
166
|
+
await handleOrderCreate(makeInput({ items }), ctx);
|
|
167
|
+
expect(mockBuildMessage).toHaveBeenCalledWith(expect.objectContaining({ additionalItemCount: 2 }));
|
|
168
|
+
});
|
|
169
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|