@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,600 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
// ── vi.hoisted: mock vars used in vi.mock factory closures ──────────────────
|
|
3
|
+
const { mockGetOrCreate, mockAddressFindById, mockOrdersCreate, mockProductsFindById, mockRunBatch, mockStoresFindById, mockUsersFindById, mockSiteSettingsGetSingleton, mockNotificationCreate, mockCouponsApply, mockCouponsFindById, mockCouponsGetUsageCount, mockClaimedMarkUsed, mockRunTransaction, mockOtpGet, mockFailedLogCheckout, mockSendEmail, } = vi.hoisted(() => ({
|
|
4
|
+
mockGetOrCreate: vi.fn(),
|
|
5
|
+
mockAddressFindById: vi.fn(),
|
|
6
|
+
mockOrdersCreate: vi.fn(),
|
|
7
|
+
mockProductsFindById: vi.fn(),
|
|
8
|
+
mockRunBatch: vi.fn(),
|
|
9
|
+
mockStoresFindById: vi.fn(),
|
|
10
|
+
mockUsersFindById: vi.fn(),
|
|
11
|
+
mockSiteSettingsGetSingleton: vi.fn(),
|
|
12
|
+
mockNotificationCreate: vi.fn(),
|
|
13
|
+
mockCouponsApply: vi.fn(),
|
|
14
|
+
mockCouponsFindById: vi.fn(),
|
|
15
|
+
mockCouponsGetUsageCount: vi.fn(),
|
|
16
|
+
mockClaimedMarkUsed: vi.fn(),
|
|
17
|
+
mockRunTransaction: vi.fn(),
|
|
18
|
+
mockOtpGet: vi.fn(),
|
|
19
|
+
mockFailedLogCheckout: vi.fn(),
|
|
20
|
+
mockSendEmail: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
// ── Repositories barrel (unitOfWork lives here) ─────────────────────────────
|
|
23
|
+
vi.mock("../../../../../repositories", () => ({
|
|
24
|
+
unitOfWork: {
|
|
25
|
+
carts: {
|
|
26
|
+
getOrCreate: mockGetOrCreate,
|
|
27
|
+
updateInBatch: vi.fn(),
|
|
28
|
+
},
|
|
29
|
+
addresses: { findById: mockAddressFindById },
|
|
30
|
+
orders: { create: mockOrdersCreate },
|
|
31
|
+
products: { findById: mockProductsFindById, updateInBatch: vi.fn() },
|
|
32
|
+
runBatch: mockRunBatch,
|
|
33
|
+
},
|
|
34
|
+
siteSettingsRepository: { getSingleton: mockSiteSettingsGetSingleton },
|
|
35
|
+
userRepository: { findById: mockUsersFindById },
|
|
36
|
+
storeRepository: { findById: mockStoresFindById },
|
|
37
|
+
couponsRepository: {
|
|
38
|
+
applyCoupon: mockCouponsApply,
|
|
39
|
+
findById: mockCouponsFindById,
|
|
40
|
+
getUserCouponUsageCount: mockCouponsGetUsageCount,
|
|
41
|
+
},
|
|
42
|
+
notificationRepository: { create: mockNotificationCreate },
|
|
43
|
+
claimedCouponsRepository: { markUsed: mockClaimedMarkUsed },
|
|
44
|
+
}));
|
|
45
|
+
vi.mock("../../../../../features/checkout/repository/failed-checkout.repository", () => ({
|
|
46
|
+
failedCheckoutRepository: {
|
|
47
|
+
logCheckout: mockFailedLogCheckout,
|
|
48
|
+
logPayment: vi.fn(),
|
|
49
|
+
},
|
|
50
|
+
}));
|
|
51
|
+
vi.mock("../../../../../features/contact/server", () => ({
|
|
52
|
+
sendOrderConfirmationEmail: mockSendEmail,
|
|
53
|
+
}));
|
|
54
|
+
vi.mock("../../../../../features/orders/index", () => ({
|
|
55
|
+
splitCartIntoOrderGroups: vi.fn((items) => [{ items, orderType: "standard" }]),
|
|
56
|
+
}));
|
|
57
|
+
vi.mock("../../../../../providers/db-firebase", () => ({
|
|
58
|
+
getAdminDb: vi.fn(() => ({
|
|
59
|
+
runTransaction: mockRunTransaction,
|
|
60
|
+
collection: vi.fn(() => ({
|
|
61
|
+
doc: vi.fn(() => ({
|
|
62
|
+
collection: vi.fn(() => ({
|
|
63
|
+
where: vi.fn().mockReturnThis(),
|
|
64
|
+
limit: vi.fn().mockReturnThis(),
|
|
65
|
+
get: vi.fn().mockResolvedValue({ empty: true, docs: [] }),
|
|
66
|
+
})),
|
|
67
|
+
get: vi.fn().mockResolvedValue({ exists: false }),
|
|
68
|
+
})),
|
|
69
|
+
})),
|
|
70
|
+
})),
|
|
71
|
+
getAdminRealtimeDb: vi.fn(() => ({
|
|
72
|
+
ref: vi.fn(() => ({ update: vi.fn().mockResolvedValue(undefined) })),
|
|
73
|
+
})),
|
|
74
|
+
RTDB_PATHS: { PAYMENT_EVENTS: "paymentEvents" },
|
|
75
|
+
}));
|
|
76
|
+
vi.mock("../../../../../providers/db-firebase/admin", () => ({
|
|
77
|
+
getAdminDb: vi.fn(() => ({
|
|
78
|
+
runTransaction: mockRunTransaction,
|
|
79
|
+
collection: vi.fn(() => ({
|
|
80
|
+
doc: vi.fn(() => ({
|
|
81
|
+
collection: vi.fn(() => ({
|
|
82
|
+
where: vi.fn().mockReturnThis(),
|
|
83
|
+
limit: vi.fn().mockReturnThis(),
|
|
84
|
+
get: vi.fn().mockResolvedValue({ empty: true, docs: [] }),
|
|
85
|
+
})),
|
|
86
|
+
})),
|
|
87
|
+
})),
|
|
88
|
+
})),
|
|
89
|
+
getAdminAuth: vi.fn(),
|
|
90
|
+
}));
|
|
91
|
+
// consentOtpRef returns a DocumentReference whose .get() controls OTP behavior
|
|
92
|
+
vi.mock("../../../../../features/auth/server", () => ({
|
|
93
|
+
consentOtpRef: vi.fn(() => ({ get: mockOtpGet })),
|
|
94
|
+
consentOtpRateLimitRef: vi.fn(() => ({
|
|
95
|
+
get: vi.fn().mockResolvedValue({ exists: false }),
|
|
96
|
+
set: vi.fn().mockResolvedValue(undefined),
|
|
97
|
+
})),
|
|
98
|
+
CONSENT_OTP_MAX_BYPASS_CREDITS: 3,
|
|
99
|
+
}));
|
|
100
|
+
vi.mock("../../../../shared/checkout/rules", () => ({
|
|
101
|
+
getListingRule: vi.fn(() => ({
|
|
102
|
+
decorateOrderItem: vi.fn((base) => base),
|
|
103
|
+
decorateOrderDoc: vi.fn(() => ({})),
|
|
104
|
+
stockDecrementExtras: vi.fn(() => ({})),
|
|
105
|
+
})),
|
|
106
|
+
runSyncPreflight: vi.fn(),
|
|
107
|
+
}));
|
|
108
|
+
vi.mock("../../../../shared/listing-types/cart-shipping", () => ({
|
|
109
|
+
cartIsDigitalOnly: vi.fn(() => false),
|
|
110
|
+
}));
|
|
111
|
+
vi.mock("../bundle-expansion", () => ({
|
|
112
|
+
getCartItemMemberIds: vi.fn((item) => [item.productId]),
|
|
113
|
+
getExpandedDecrements: vi.fn((items) => ({
|
|
114
|
+
productIds: [...new Set(items.map((i) => i.productId))],
|
|
115
|
+
decrements: new Map(items.map((i) => [i.productId, 1])),
|
|
116
|
+
})),
|
|
117
|
+
validateCartItemStock: vi.fn(() => null), // null = no shortfall
|
|
118
|
+
}));
|
|
119
|
+
vi.mock("../prize-bundle-gates", () => ({
|
|
120
|
+
enforceMaxPerUserForCart: vi.fn(),
|
|
121
|
+
computePrizeRevealDeadline: vi.fn(() => new Date()),
|
|
122
|
+
}));
|
|
123
|
+
vi.mock("../data", () => ({
|
|
124
|
+
formatShippingAddress: vi.fn((addr) => addr.addressLine1),
|
|
125
|
+
}));
|
|
126
|
+
vi.mock("../../../../../errors/normalize", () => ({ normalizeError: vi.fn() }));
|
|
127
|
+
vi.mock("../../../../../monitoring", () => ({
|
|
128
|
+
serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() },
|
|
129
|
+
}));
|
|
130
|
+
vi.mock("../../../../../providers/payment-razorpay/index", () => ({
|
|
131
|
+
verifyPaymentSignatureWithKeys: vi.fn(() => true),
|
|
132
|
+
fetchRazorpayOrder: vi.fn(() => ({ amount: 100000 })), // 1000 rupees in paise
|
|
133
|
+
paiseToRupees: vi.fn((p) => p / 100),
|
|
134
|
+
}));
|
|
135
|
+
vi.mock("../../../../../core/index", () => ({
|
|
136
|
+
getDefaultCurrency: vi.fn(() => "INR"),
|
|
137
|
+
}));
|
|
138
|
+
vi.mock("../../../../../utils", () => ({
|
|
139
|
+
resolveDate: vi.fn((d) => (d instanceof Date ? d : new Date())),
|
|
140
|
+
}));
|
|
141
|
+
import { createCheckoutOrderAction } from "../actions";
|
|
142
|
+
import { cartIsDigitalOnly } from "../../../../shared/listing-types/cart-shipping";
|
|
143
|
+
// ── Shared helpers ──────────────────────────────────────────────────────────
|
|
144
|
+
function makeCartItem(overrides = {}) {
|
|
145
|
+
return {
|
|
146
|
+
itemId: "item-1",
|
|
147
|
+
productId: "product-hot-wheels",
|
|
148
|
+
productTitle: "Hot Wheels",
|
|
149
|
+
quantity: 1,
|
|
150
|
+
price: 5000,
|
|
151
|
+
currency: "INR",
|
|
152
|
+
storeId: "store-A",
|
|
153
|
+
storeName: "Toy Store",
|
|
154
|
+
listingType: "standard",
|
|
155
|
+
...overrides,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
function makeCart(overrides = {}) {
|
|
159
|
+
return {
|
|
160
|
+
id: "cart-user-1",
|
|
161
|
+
userId: "user-1",
|
|
162
|
+
items: [makeCartItem()],
|
|
163
|
+
appliedCoupons: [],
|
|
164
|
+
selectedItemIds: null,
|
|
165
|
+
updatedAt: new Date(),
|
|
166
|
+
...overrides,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function makeAddress(overrides = {}) {
|
|
170
|
+
return {
|
|
171
|
+
id: "addr-1",
|
|
172
|
+
ownerType: "user",
|
|
173
|
+
ownerId: "user-1",
|
|
174
|
+
fullName: "Ravi Kumar",
|
|
175
|
+
addressLine1: "123 Main St",
|
|
176
|
+
city: "Mumbai",
|
|
177
|
+
state: "Maharashtra",
|
|
178
|
+
postalCode: "400001",
|
|
179
|
+
country: "IN",
|
|
180
|
+
...overrides,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function makeOrder(overrides = {}) {
|
|
184
|
+
return {
|
|
185
|
+
id: "order-1",
|
|
186
|
+
userId: "user-1",
|
|
187
|
+
status: "pending",
|
|
188
|
+
totalPrice: 5000,
|
|
189
|
+
...overrides,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function makeProduct(overrides = {}) {
|
|
193
|
+
return {
|
|
194
|
+
id: "product-hot-wheels",
|
|
195
|
+
title: "Hot Wheels",
|
|
196
|
+
price: 5000,
|
|
197
|
+
availableQuantity: 10,
|
|
198
|
+
listingType: "standard",
|
|
199
|
+
mainImage: "/media/img.jpg",
|
|
200
|
+
...overrides,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function makeSiteSettings(overrides = {}) {
|
|
204
|
+
return {
|
|
205
|
+
payment: { codEnabled: true },
|
|
206
|
+
commissions: {
|
|
207
|
+
platformFeePercent: 5,
|
|
208
|
+
gstPercent: 18,
|
|
209
|
+
platformShippingPercent: 2,
|
|
210
|
+
platformShippingFixedMin: 50,
|
|
211
|
+
codDepositPercent: 10,
|
|
212
|
+
},
|
|
213
|
+
...overrides,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function makeInput(overrides = {}) {
|
|
217
|
+
return {
|
|
218
|
+
userId: "user-1",
|
|
219
|
+
userName: "Ravi Kumar",
|
|
220
|
+
userEmail: "ravi@example.com",
|
|
221
|
+
addressId: "addr-1",
|
|
222
|
+
paymentMethod: "cod",
|
|
223
|
+
...overrides,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
// OTP snap helpers
|
|
227
|
+
function makeValidOtp() {
|
|
228
|
+
return {
|
|
229
|
+
exists: true,
|
|
230
|
+
data: () => ({
|
|
231
|
+
verified: true,
|
|
232
|
+
expiresAt: new Date(Date.now() + 5 * 60 * 1000), // 5 mins from now
|
|
233
|
+
verifiedVia: "sms",
|
|
234
|
+
}),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
function makeExpiredOtp() {
|
|
238
|
+
return {
|
|
239
|
+
exists: true,
|
|
240
|
+
data: () => ({
|
|
241
|
+
verified: true,
|
|
242
|
+
expiresAt: new Date(Date.now() - 1000), // 1 second ago
|
|
243
|
+
verifiedVia: "sms",
|
|
244
|
+
}),
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// The runTransaction mock simulates the Firestore tx by calling the callback.
|
|
248
|
+
// By default, make it succeed with the provided items.
|
|
249
|
+
function setupTransactionSuccess(availableItems = [makeCartItem()], unavailableItems = []) {
|
|
250
|
+
mockRunTransaction.mockImplementation(async (callback) => {
|
|
251
|
+
const mockTx = {
|
|
252
|
+
get: vi.fn().mockResolvedValue(makeValidOtp()), // OTP valid by default
|
|
253
|
+
update: vi.fn(),
|
|
254
|
+
set: vi.fn(),
|
|
255
|
+
delete: vi.fn(),
|
|
256
|
+
};
|
|
257
|
+
// When the tx calls get() for product docs, return a valid product snap
|
|
258
|
+
const product = makeProduct();
|
|
259
|
+
mockTx.get.mockImplementation(async (ref) => {
|
|
260
|
+
// OTP ref has no id; product refs do
|
|
261
|
+
if (ref && typeof ref === "object" && "id" in ref && ref.id) {
|
|
262
|
+
return {
|
|
263
|
+
exists: true,
|
|
264
|
+
data: () => product,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return makeValidOtp();
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
available: availableItems.map((item) => ({ item, product: makeProduct() })),
|
|
271
|
+
unavailable: unavailableItems,
|
|
272
|
+
emailOtpUsed: false,
|
|
273
|
+
};
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
beforeEach(() => {
|
|
277
|
+
vi.clearAllMocks();
|
|
278
|
+
// Default happy-path setup
|
|
279
|
+
mockSiteSettingsGetSingleton.mockResolvedValue(makeSiteSettings());
|
|
280
|
+
mockGetOrCreate.mockResolvedValue(makeCart());
|
|
281
|
+
mockAddressFindById.mockResolvedValue(makeAddress());
|
|
282
|
+
mockProductsFindById.mockResolvedValue(makeProduct());
|
|
283
|
+
mockStoresFindById.mockResolvedValue({ id: "store-A", ownerId: "owner-1" });
|
|
284
|
+
mockUsersFindById.mockResolvedValue({ id: "owner-1", shippingConfig: { isConfigured: false } });
|
|
285
|
+
mockOrdersCreate.mockResolvedValue(makeOrder());
|
|
286
|
+
mockRunBatch.mockResolvedValue(undefined);
|
|
287
|
+
mockNotificationCreate.mockResolvedValue(undefined);
|
|
288
|
+
mockCouponsApply.mockResolvedValue(undefined);
|
|
289
|
+
mockCouponsFindById.mockResolvedValue(null);
|
|
290
|
+
mockCouponsGetUsageCount.mockResolvedValue(0);
|
|
291
|
+
mockClaimedMarkUsed.mockResolvedValue(undefined);
|
|
292
|
+
mockSendEmail.mockResolvedValue(undefined);
|
|
293
|
+
mockFailedLogCheckout.mockResolvedValue(undefined);
|
|
294
|
+
mockOtpGet.mockResolvedValue(makeValidOtp());
|
|
295
|
+
setupTransactionSuccess();
|
|
296
|
+
cartIsDigitalOnly.mockReturnValue(false);
|
|
297
|
+
});
|
|
298
|
+
// ── COD gate ─────────────────────────────────────────────────────────────────
|
|
299
|
+
describe("createCheckoutOrderAction — COD gate", () => {
|
|
300
|
+
it("COD + codEnabled=false → ValidationError", async () => {
|
|
301
|
+
mockSiteSettingsGetSingleton.mockResolvedValue(makeSiteSettings({ payment: { codEnabled: false } }));
|
|
302
|
+
await expect(createCheckoutOrderAction(makeInput({ paymentMethod: "cod" }))).rejects.toThrow(/Cash on Delivery/i);
|
|
303
|
+
});
|
|
304
|
+
it("COD + codEnabled=false + adminBypass=true → does NOT throw", async () => {
|
|
305
|
+
mockSiteSettingsGetSingleton.mockResolvedValue(makeSiteSettings({ payment: { codEnabled: false } }));
|
|
306
|
+
const result = await createCheckoutOrderAction(makeInput({ paymentMethod: "cod", adminBypass: true }));
|
|
307
|
+
expect(result.orderIds).toHaveLength(1);
|
|
308
|
+
});
|
|
309
|
+
it("COD + codEnabled=true → proceeds normally", async () => {
|
|
310
|
+
const result = await createCheckoutOrderAction(makeInput({ paymentMethod: "cod" }));
|
|
311
|
+
expect(result.orderIds).toHaveLength(1);
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
// ── Cart validation ───────────────────────────────────────────────────────────
|
|
315
|
+
describe("createCheckoutOrderAction — cart validation", () => {
|
|
316
|
+
it("empty cart (no items) → ValidationError CART_EMPTY", async () => {
|
|
317
|
+
mockGetOrCreate.mockResolvedValue(makeCart({ items: [] }));
|
|
318
|
+
await expect(createCheckoutOrderAction(makeInput())).rejects.toThrow(/empty/i);
|
|
319
|
+
});
|
|
320
|
+
it("all items in excludedProductIds → ValidationError CART_EMPTY", async () => {
|
|
321
|
+
mockGetOrCreate.mockResolvedValue(makeCart({
|
|
322
|
+
items: [makeCartItem({ productId: "product-A" })],
|
|
323
|
+
}));
|
|
324
|
+
await expect(createCheckoutOrderAction(makeInput({ excludedProductIds: ["product-A"] }))).rejects.toThrow(/empty/i);
|
|
325
|
+
});
|
|
326
|
+
it("selectedItemIds filters to only matching items", async () => {
|
|
327
|
+
mockGetOrCreate.mockResolvedValue(makeCart({
|
|
328
|
+
items: [
|
|
329
|
+
makeCartItem({ itemId: "item-A", productId: "product-A" }),
|
|
330
|
+
makeCartItem({ itemId: "item-B", productId: "product-B" }),
|
|
331
|
+
],
|
|
332
|
+
selectedItemIds: ["item-A"],
|
|
333
|
+
}));
|
|
334
|
+
setupTransactionSuccess([makeCartItem({ itemId: "item-A", productId: "product-A" })]);
|
|
335
|
+
const result = await createCheckoutOrderAction(makeInput());
|
|
336
|
+
// Only item-A should be processed; item-B excluded
|
|
337
|
+
expect(result.orderIds).toHaveLength(1);
|
|
338
|
+
});
|
|
339
|
+
it("selectedItemIds = [] (empty) → processes all items", async () => {
|
|
340
|
+
mockGetOrCreate.mockResolvedValue(makeCart({ selectedItemIds: [] }));
|
|
341
|
+
const result = await createCheckoutOrderAction(makeInput());
|
|
342
|
+
expect(result.orderIds).toHaveLength(1);
|
|
343
|
+
});
|
|
344
|
+
});
|
|
345
|
+
// ── Address validation ────────────────────────────────────────────────────────
|
|
346
|
+
describe("createCheckoutOrderAction — address validation", () => {
|
|
347
|
+
it("physical cart + no addressId → NotFoundError ADDRESS_REQUIRED", async () => {
|
|
348
|
+
cartIsDigitalOnly.mockReturnValue(false);
|
|
349
|
+
await expect(createCheckoutOrderAction(makeInput({ addressId: undefined }))).rejects.toThrow(/address/i);
|
|
350
|
+
});
|
|
351
|
+
it("physical cart + addressId belonging to different user → NotFoundError", async () => {
|
|
352
|
+
cartIsDigitalOnly.mockReturnValue(false);
|
|
353
|
+
mockAddressFindById.mockResolvedValue(makeAddress({ ownerId: "different-user" }));
|
|
354
|
+
await expect(createCheckoutOrderAction(makeInput())).rejects.toThrow(/address/i);
|
|
355
|
+
});
|
|
356
|
+
it("physical cart + valid addressId → proceeds (no address error)", async () => {
|
|
357
|
+
cartIsDigitalOnly.mockReturnValue(false);
|
|
358
|
+
mockAddressFindById.mockResolvedValue(makeAddress({ ownerId: "user-1" }));
|
|
359
|
+
const result = await createCheckoutOrderAction(makeInput());
|
|
360
|
+
expect(result.orderIds).toHaveLength(1);
|
|
361
|
+
});
|
|
362
|
+
it("digital-only cart + no addressId → does NOT throw (address not required)", async () => {
|
|
363
|
+
cartIsDigitalOnly.mockReturnValue(true);
|
|
364
|
+
// Provide a digital item
|
|
365
|
+
mockGetOrCreate.mockResolvedValue(makeCart({
|
|
366
|
+
items: [makeCartItem({ listingType: "digital-code" })],
|
|
367
|
+
}));
|
|
368
|
+
const result = await createCheckoutOrderAction(makeInput({ addressId: undefined }));
|
|
369
|
+
expect(result.orderIds).toHaveLength(1);
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
// ── OTP gate ─────────────────────────────────────────────────────────────────
|
|
373
|
+
describe("createCheckoutOrderAction — OTP gate (inside transaction)", () => {
|
|
374
|
+
function setupOtpInTransaction(otpSnap) {
|
|
375
|
+
mockRunTransaction.mockImplementation(async (callback) => {
|
|
376
|
+
const mockTx = {
|
|
377
|
+
get: vi.fn().mockResolvedValue(otpSnap),
|
|
378
|
+
update: vi.fn(),
|
|
379
|
+
set: vi.fn(),
|
|
380
|
+
delete: vi.fn(),
|
|
381
|
+
};
|
|
382
|
+
try {
|
|
383
|
+
return await callback(mockTx);
|
|
384
|
+
}
|
|
385
|
+
catch (err) {
|
|
386
|
+
throw err;
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
it("no OTP record → 403 ApiError (otp_not_verified)", async () => {
|
|
391
|
+
setupOtpInTransaction({ exists: false, data: () => null });
|
|
392
|
+
await expect(createCheckoutOrderAction(makeInput())).rejects.toMatchObject({
|
|
393
|
+
statusCode: 403,
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
it("OTP exists but verified=false → 403 ApiError", async () => {
|
|
397
|
+
setupOtpInTransaction({
|
|
398
|
+
exists: true,
|
|
399
|
+
data: () => ({ verified: false, expiresAt: new Date(Date.now() + 60000), verifiedVia: "sms" }),
|
|
400
|
+
});
|
|
401
|
+
await expect(createCheckoutOrderAction(makeInput())).rejects.toMatchObject({ statusCode: 403 });
|
|
402
|
+
});
|
|
403
|
+
it("OTP expired (expiresAt in past) → 403 ApiError", async () => {
|
|
404
|
+
setupOtpInTransaction({
|
|
405
|
+
exists: true,
|
|
406
|
+
data: () => ({
|
|
407
|
+
verified: true,
|
|
408
|
+
expiresAt: new Date(Date.now() - 1000),
|
|
409
|
+
verifiedVia: "sms",
|
|
410
|
+
}),
|
|
411
|
+
});
|
|
412
|
+
await expect(createCheckoutOrderAction(makeInput())).rejects.toMatchObject({ statusCode: 403 });
|
|
413
|
+
});
|
|
414
|
+
it("valid OTP → proceeds, no 403 thrown", async () => {
|
|
415
|
+
setupOtpInTransaction({
|
|
416
|
+
exists: true,
|
|
417
|
+
data: () => ({
|
|
418
|
+
verified: true,
|
|
419
|
+
expiresAt: new Date(Date.now() + 60000),
|
|
420
|
+
verifiedVia: "sms",
|
|
421
|
+
}),
|
|
422
|
+
});
|
|
423
|
+
// Also provide product snap in transaction
|
|
424
|
+
mockRunTransaction.mockImplementation(async (callback) => {
|
|
425
|
+
return {
|
|
426
|
+
available: [{ item: makeCartItem(), product: makeProduct() }],
|
|
427
|
+
unavailable: [],
|
|
428
|
+
emailOtpUsed: false,
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
const result = await createCheckoutOrderAction(makeInput());
|
|
432
|
+
expect(result.orderIds).toHaveLength(1);
|
|
433
|
+
});
|
|
434
|
+
it("adminBypass=true → OTP gate is skipped entirely", async () => {
|
|
435
|
+
// runTransaction returns a result without OTP check being executed
|
|
436
|
+
mockRunTransaction.mockResolvedValue({
|
|
437
|
+
available: [{ item: makeCartItem(), product: makeProduct() }],
|
|
438
|
+
unavailable: [],
|
|
439
|
+
emailOtpUsed: false,
|
|
440
|
+
});
|
|
441
|
+
const result = await createCheckoutOrderAction(makeInput({ adminBypass: true }));
|
|
442
|
+
expect(result.orderIds).toHaveLength(1);
|
|
443
|
+
});
|
|
444
|
+
it("digital-only cart → OTP gate is skipped (otpRef is null)", async () => {
|
|
445
|
+
cartIsDigitalOnly.mockReturnValue(true);
|
|
446
|
+
mockGetOrCreate.mockResolvedValue(makeCart({
|
|
447
|
+
items: [makeCartItem({ listingType: "digital-code" })],
|
|
448
|
+
}));
|
|
449
|
+
mockRunTransaction.mockResolvedValue({
|
|
450
|
+
available: [{ item: makeCartItem({ listingType: "digital-code" }), product: makeProduct({ listingType: "digital-code" }) }],
|
|
451
|
+
unavailable: [],
|
|
452
|
+
emailOtpUsed: false,
|
|
453
|
+
});
|
|
454
|
+
const result = await createCheckoutOrderAction(makeInput({ addressId: undefined }));
|
|
455
|
+
expect(result.orderIds).toHaveLength(1);
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
// ── Stock validation ──────────────────────────────────────────────────────────
|
|
459
|
+
describe("createCheckoutOrderAction — stock results", () => {
|
|
460
|
+
it("all items OOS → ValidationError INSUFFICIENT_STOCK", async () => {
|
|
461
|
+
mockRunTransaction.mockResolvedValue({
|
|
462
|
+
available: [],
|
|
463
|
+
unavailable: [{ productId: "product-hot-wheels", productTitle: "Hot Wheels", requestedQty: 1, availableQty: 0 }],
|
|
464
|
+
emailOtpUsed: false,
|
|
465
|
+
});
|
|
466
|
+
await expect(createCheckoutOrderAction(makeInput())).rejects.toThrow(/stock|unavailable/i);
|
|
467
|
+
});
|
|
468
|
+
it("some items OOS → unavailableItems in result", async () => {
|
|
469
|
+
const available = [{ item: makeCartItem({ productId: "prod-A" }), product: makeProduct({ id: "prod-A" }) }];
|
|
470
|
+
const unavailable = [{ productId: "prod-B", productTitle: "B", requestedQty: 2, availableQty: 1 }];
|
|
471
|
+
mockRunTransaction.mockResolvedValue({ available, unavailable, emailOtpUsed: false });
|
|
472
|
+
const result = await createCheckoutOrderAction(makeInput());
|
|
473
|
+
expect(result.unavailableItems).toHaveLength(1);
|
|
474
|
+
expect(result.unavailableItems[0].productId).toBe("prod-B");
|
|
475
|
+
expect(result.orderIds).toHaveLength(1);
|
|
476
|
+
});
|
|
477
|
+
it("all items in stock → no unavailableItems field", async () => {
|
|
478
|
+
const result = await createCheckoutOrderAction(makeInput());
|
|
479
|
+
expect(result.unavailableItems).toBeUndefined();
|
|
480
|
+
});
|
|
481
|
+
});
|
|
482
|
+
// ── Order creation ─────────────────────────────────────────────────────────────
|
|
483
|
+
describe("createCheckoutOrderAction — order creation", () => {
|
|
484
|
+
it("returns { orderIds, total, itemCount }", async () => {
|
|
485
|
+
const result = await createCheckoutOrderAction(makeInput());
|
|
486
|
+
expect(result.orderIds).toBeInstanceOf(Array);
|
|
487
|
+
expect(typeof result.total).toBe("number");
|
|
488
|
+
expect(typeof result.itemCount).toBe("number");
|
|
489
|
+
expect(result.itemCount).toBe(result.orderIds.length);
|
|
490
|
+
});
|
|
491
|
+
it("calls unitOfWork.orders.create with userId, paymentMethod", async () => {
|
|
492
|
+
await createCheckoutOrderAction(makeInput({ paymentMethod: "cod" }));
|
|
493
|
+
expect(mockOrdersCreate).toHaveBeenCalledWith(expect.objectContaining({ userId: "user-1", paymentMethod: "cod" }));
|
|
494
|
+
});
|
|
495
|
+
it("adminBypass order has status PROCESSING and paymentStatus PAID", async () => {
|
|
496
|
+
await createCheckoutOrderAction(makeInput({ adminBypass: true }));
|
|
497
|
+
expect(mockOrdersCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
498
|
+
status: expect.stringMatching(/processing/i),
|
|
499
|
+
paymentStatus: expect.stringMatching(/paid/i),
|
|
500
|
+
}));
|
|
501
|
+
});
|
|
502
|
+
it("non-bypass order has status PENDING and paymentStatus PENDING", async () => {
|
|
503
|
+
await createCheckoutOrderAction(makeInput({ adminBypass: false, paymentMethod: "cod" }));
|
|
504
|
+
expect(mockOrdersCreate).toHaveBeenCalledWith(expect.objectContaining({
|
|
505
|
+
status: expect.stringMatching(/pending/i),
|
|
506
|
+
paymentStatus: expect.stringMatching(/pending/i),
|
|
507
|
+
}));
|
|
508
|
+
});
|
|
509
|
+
it("order receives shippingAddress from formatShippingAddress", async () => {
|
|
510
|
+
cartIsDigitalOnly.mockReturnValue(false);
|
|
511
|
+
mockAddressFindById.mockResolvedValue(makeAddress({ addressLine1: "456 Park Ave" }));
|
|
512
|
+
await createCheckoutOrderAction(makeInput());
|
|
513
|
+
expect(mockOrdersCreate).toHaveBeenCalledWith(expect.objectContaining({ shippingAddress: "456 Park Ave" }));
|
|
514
|
+
});
|
|
515
|
+
it("digital-only cart order has no shippingAddress", async () => {
|
|
516
|
+
cartIsDigitalOnly.mockReturnValue(true);
|
|
517
|
+
mockGetOrCreate.mockResolvedValue(makeCart({
|
|
518
|
+
items: [makeCartItem({ listingType: "digital-code" })],
|
|
519
|
+
}));
|
|
520
|
+
mockRunTransaction.mockResolvedValue({
|
|
521
|
+
available: [{ item: makeCartItem({ listingType: "digital-code" }), product: makeProduct({ listingType: "digital-code" }) }],
|
|
522
|
+
unavailable: [],
|
|
523
|
+
emailOtpUsed: false,
|
|
524
|
+
});
|
|
525
|
+
await createCheckoutOrderAction(makeInput({ addressId: undefined }));
|
|
526
|
+
const call = mockOrdersCreate.mock.calls[0][0];
|
|
527
|
+
expect(call.shippingAddress).toBeUndefined();
|
|
528
|
+
});
|
|
529
|
+
it("notes are forwarded to order create", async () => {
|
|
530
|
+
await createCheckoutOrderAction(makeInput({ notes: "Leave at door" }));
|
|
531
|
+
expect(mockOrdersCreate).toHaveBeenCalledWith(expect.objectContaining({ notes: "Leave at door" }));
|
|
532
|
+
});
|
|
533
|
+
it("coupon discount pro-rated to order total", async () => {
|
|
534
|
+
// Single item cart, coupon with discountAmount=500 (₹5)
|
|
535
|
+
mockGetOrCreate.mockResolvedValue(makeCart({
|
|
536
|
+
appliedCoupons: [{
|
|
537
|
+
code: "SAVE5",
|
|
538
|
+
couponId: "coupon-1",
|
|
539
|
+
scope: "admin",
|
|
540
|
+
discountAmount: 500,
|
|
541
|
+
storeId: undefined,
|
|
542
|
+
}],
|
|
543
|
+
}));
|
|
544
|
+
await createCheckoutOrderAction(makeInput());
|
|
545
|
+
const call = mockOrdersCreate.mock.calls[0][0];
|
|
546
|
+
// With a single order group, the full discount applies to this group
|
|
547
|
+
expect(call.couponDiscount).toBeGreaterThan(0);
|
|
548
|
+
expect(call.couponCode).toBe("SAVE5");
|
|
549
|
+
});
|
|
550
|
+
});
|
|
551
|
+
// ── Side effects ──────────────────────────────────────────────────────────────
|
|
552
|
+
describe("createCheckoutOrderAction — side effects", () => {
|
|
553
|
+
it("coupon usage flushed after order creation", async () => {
|
|
554
|
+
mockGetOrCreate.mockResolvedValue(makeCart({
|
|
555
|
+
appliedCoupons: [{ code: "SAVE10", couponId: "coupon-1", scope: "admin", discountAmount: 1000 }],
|
|
556
|
+
}));
|
|
557
|
+
await createCheckoutOrderAction(makeInput());
|
|
558
|
+
expect(mockCouponsApply).toHaveBeenCalledWith("coupon-1", "SAVE10", "user-1", expect.any(Array), expect.any(Number));
|
|
559
|
+
});
|
|
560
|
+
it("no coupons → couponUsageAccumulator is empty, applyCoupon NOT called", async () => {
|
|
561
|
+
mockGetOrCreate.mockResolvedValue(makeCart({ appliedCoupons: [] }));
|
|
562
|
+
await createCheckoutOrderAction(makeInput());
|
|
563
|
+
expect(mockCouponsApply).not.toHaveBeenCalled();
|
|
564
|
+
});
|
|
565
|
+
it("order placed notifications emitted (fire-and-forget — notificationRepository.create called)", async () => {
|
|
566
|
+
await createCheckoutOrderAction(makeInput());
|
|
567
|
+
// Buyer notification
|
|
568
|
+
expect(mockNotificationCreate).toHaveBeenCalledWith(expect.objectContaining({ userId: "user-1", type: "order_placed" }));
|
|
569
|
+
});
|
|
570
|
+
it("confirmation email dispatched (fire-and-forget)", async () => {
|
|
571
|
+
await createCheckoutOrderAction(makeInput({ userEmail: "ravi@example.com" }));
|
|
572
|
+
expect(mockSendEmail).toHaveBeenCalledWith(expect.objectContaining({ to: "ravi@example.com" }));
|
|
573
|
+
});
|
|
574
|
+
it("no email dispatched when userEmail is empty", async () => {
|
|
575
|
+
await createCheckoutOrderAction(makeInput({ userEmail: "" }));
|
|
576
|
+
expect(mockSendEmail).not.toHaveBeenCalled();
|
|
577
|
+
});
|
|
578
|
+
it("emailOtpUsed=true + unavailableItems → bypass credit granted (consentOtpRateLimitRef called)", async () => {
|
|
579
|
+
mockRunTransaction.mockResolvedValue({
|
|
580
|
+
available: [{ item: makeCartItem({ productId: "prod-A" }), product: makeProduct() }],
|
|
581
|
+
unavailable: [{ productId: "prod-B", productTitle: "B", requestedQty: 1, availableQty: 0 }],
|
|
582
|
+
emailOtpUsed: true,
|
|
583
|
+
});
|
|
584
|
+
const { consentOtpRateLimitRef } = await import("../../../../../features/auth/server");
|
|
585
|
+
await createCheckoutOrderAction(makeInput());
|
|
586
|
+
// grantConsentOtpBypassCredit reads the meta ref when emailOtpUsed=true and unavailable>0
|
|
587
|
+
expect(consentOtpRateLimitRef).toHaveBeenCalled();
|
|
588
|
+
});
|
|
589
|
+
it("emailOtpUsed=false → bypass credit NOT granted", async () => {
|
|
590
|
+
mockRunTransaction.mockResolvedValue({
|
|
591
|
+
available: [{ item: makeCartItem(), product: makeProduct() }],
|
|
592
|
+
unavailable: [{ productId: "prod-B", productTitle: "B", requestedQty: 1, availableQty: 0 }],
|
|
593
|
+
emailOtpUsed: false,
|
|
594
|
+
});
|
|
595
|
+
const { consentOtpRateLimitRef } = await import("../../../../../features/auth/server");
|
|
596
|
+
vi.clearAllMocks();
|
|
597
|
+
await createCheckoutOrderAction(makeInput());
|
|
598
|
+
expect(consentOtpRateLimitRef).not.toHaveBeenCalled();
|
|
599
|
+
});
|
|
600
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ActionResult } from "@mohasinac/appkit/server";
|
|
2
|
+
import type { LotteryConfig } from "../../../../features/lottery/types";
|
|
3
|
+
import type { LotteryEntryDocument } from "../../../../features/lottery/schemas/firestore";
|
|
4
|
+
/**
|
|
5
|
+
* User-initiated lottery pull.
|
|
6
|
+
* Atomically:
|
|
7
|
+
* 1. Validates inputs + limits
|
|
8
|
+
* 2. Runs weighted slot selection on unclaimed slots
|
|
9
|
+
* 3. Marks the slot as booked
|
|
10
|
+
* 4. Creates the LotteryEntryDocument with assignedPrizeSlotNumber set
|
|
11
|
+
*
|
|
12
|
+
* Returns the assigned slot number so the user sees their prize immediately.
|
|
13
|
+
*/
|
|
14
|
+
export declare function submitLotteryPullAction(input: unknown): Promise<ActionResult<{
|
|
15
|
+
userLotteryNumber: number;
|
|
16
|
+
assignedPrizeSlotNumber: number;
|
|
17
|
+
slotName: string;
|
|
18
|
+
}>>;
|
|
19
|
+
/**
|
|
20
|
+
* Admin — flag a lottery entry as fraudulent.
|
|
21
|
+
* Slot stays booked; use reopenLotterySlotAction to free it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function flagLotteryEntryAction(input: unknown): Promise<ActionResult<LotteryEntryDocument>>;
|
|
24
|
+
/**
|
|
25
|
+
* Admin — reopen a slot from a flagged entry.
|
|
26
|
+
* Clears isBooked on the slot so it can be claimed in a future pull.
|
|
27
|
+
*/
|
|
28
|
+
export declare function reopenLotterySlotAction(input: unknown): Promise<ActionResult<void>>;
|
|
29
|
+
/**
|
|
30
|
+
* Admin — create a new lottery event.
|
|
31
|
+
* Weights computed server-side; never returned to client.
|
|
32
|
+
*/
|
|
33
|
+
export declare function createLotteryEventAction(data: unknown): Promise<ActionResult<unknown>>;
|
|
34
|
+
/**
|
|
35
|
+
* Admin — update a lottery event.
|
|
36
|
+
* Recomputes slot weights after update.
|
|
37
|
+
*/
|
|
38
|
+
export declare function updateLotteryEventAction(id: string, data: unknown): Promise<ActionResult<unknown>>;
|
|
39
|
+
/**
|
|
40
|
+
* Store owner or Admin — switch a prize-draw product into lottery mode.
|
|
41
|
+
* Store owners provide prizeSlotPrice but weight is NEVER returned to them.
|
|
42
|
+
*/
|
|
43
|
+
export declare function setProductLotteryModeAction(productId: string, lotteryConfig: LotteryConfig): Promise<ActionResult<void>>;
|