@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,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for lottery entry submission and validation.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
export declare const lotteryEntrySubmitSchema: z.ZodObject<{
|
|
6
|
+
transactionId: z.ZodString;
|
|
7
|
+
paymentTime: z.ZodString;
|
|
8
|
+
purchasedItemNumber: z.ZodNumber;
|
|
9
|
+
userPhone: z.ZodString;
|
|
10
|
+
}, "strip", z.ZodTypeAny, {
|
|
11
|
+
transactionId: string;
|
|
12
|
+
userPhone: string;
|
|
13
|
+
paymentTime: string;
|
|
14
|
+
purchasedItemNumber: number;
|
|
15
|
+
}, {
|
|
16
|
+
transactionId: string;
|
|
17
|
+
userPhone: string;
|
|
18
|
+
paymentTime: string;
|
|
19
|
+
purchasedItemNumber: number;
|
|
20
|
+
}>;
|
|
21
|
+
export type LotteryEntrySubmitInput = z.infer<typeof lotteryEntrySubmitSchema>;
|
|
22
|
+
/** Schema for flag action */
|
|
23
|
+
export declare const flagLotteryEntrySchema: z.ZodObject<{
|
|
24
|
+
entryId: z.ZodString;
|
|
25
|
+
flagNote: z.ZodString;
|
|
26
|
+
flaggedByUserId: z.ZodString;
|
|
27
|
+
}, "strip", z.ZodTypeAny, {
|
|
28
|
+
flagNote: string;
|
|
29
|
+
entryId: string;
|
|
30
|
+
flaggedByUserId: string;
|
|
31
|
+
}, {
|
|
32
|
+
flagNote: string;
|
|
33
|
+
entryId: string;
|
|
34
|
+
flaggedByUserId: string;
|
|
35
|
+
}>;
|
|
36
|
+
export type FlagLotteryEntryInput = z.infer<typeof flagLotteryEntrySchema>;
|
|
37
|
+
/** Schema for reopen-slot action */
|
|
38
|
+
export declare const reopenLotterySlotSchema: z.ZodObject<{
|
|
39
|
+
sourceType: z.ZodEnum<["event", "product"]>;
|
|
40
|
+
sourceId: z.ZodString;
|
|
41
|
+
slotNumber: z.ZodNumber;
|
|
42
|
+
adminUserId: z.ZodString;
|
|
43
|
+
}, "strip", z.ZodTypeAny, {
|
|
44
|
+
slotNumber: number;
|
|
45
|
+
sourceType: "product" | "event";
|
|
46
|
+
sourceId: string;
|
|
47
|
+
adminUserId: string;
|
|
48
|
+
}, {
|
|
49
|
+
slotNumber: number;
|
|
50
|
+
sourceType: "product" | "event";
|
|
51
|
+
sourceId: string;
|
|
52
|
+
adminUserId: string;
|
|
53
|
+
}>;
|
|
54
|
+
export type ReopenLotterySlotInput = z.infer<typeof reopenLotterySlotSchema>;
|
|
55
|
+
/** Schema for submit lottery pull (full, server-side) */
|
|
56
|
+
export declare const submitLotteryPullSchema: z.ZodEffects<z.ZodObject<{
|
|
57
|
+
sourceType: z.ZodEnum<["event", "product"]>;
|
|
58
|
+
eventId: z.ZodOptional<z.ZodString>;
|
|
59
|
+
productId: z.ZodOptional<z.ZodString>;
|
|
60
|
+
userId: z.ZodString;
|
|
61
|
+
userDisplayName: z.ZodOptional<z.ZodString>;
|
|
62
|
+
userEmail: z.ZodOptional<z.ZodString>;
|
|
63
|
+
} & {
|
|
64
|
+
transactionId: z.ZodString;
|
|
65
|
+
paymentTime: z.ZodString;
|
|
66
|
+
purchasedItemNumber: z.ZodNumber;
|
|
67
|
+
userPhone: z.ZodString;
|
|
68
|
+
}, "strip", z.ZodTypeAny, {
|
|
69
|
+
userId: string;
|
|
70
|
+
transactionId: string;
|
|
71
|
+
userPhone: string;
|
|
72
|
+
sourceType: "product" | "event";
|
|
73
|
+
paymentTime: string;
|
|
74
|
+
purchasedItemNumber: number;
|
|
75
|
+
productId?: string | undefined;
|
|
76
|
+
userEmail?: string | undefined;
|
|
77
|
+
userDisplayName?: string | undefined;
|
|
78
|
+
eventId?: string | undefined;
|
|
79
|
+
}, {
|
|
80
|
+
userId: string;
|
|
81
|
+
transactionId: string;
|
|
82
|
+
userPhone: string;
|
|
83
|
+
sourceType: "product" | "event";
|
|
84
|
+
paymentTime: string;
|
|
85
|
+
purchasedItemNumber: number;
|
|
86
|
+
productId?: string | undefined;
|
|
87
|
+
userEmail?: string | undefined;
|
|
88
|
+
userDisplayName?: string | undefined;
|
|
89
|
+
eventId?: string | undefined;
|
|
90
|
+
}>, {
|
|
91
|
+
userId: string;
|
|
92
|
+
transactionId: string;
|
|
93
|
+
userPhone: string;
|
|
94
|
+
sourceType: "product" | "event";
|
|
95
|
+
paymentTime: string;
|
|
96
|
+
purchasedItemNumber: number;
|
|
97
|
+
productId?: string | undefined;
|
|
98
|
+
userEmail?: string | undefined;
|
|
99
|
+
userDisplayName?: string | undefined;
|
|
100
|
+
eventId?: string | undefined;
|
|
101
|
+
}, {
|
|
102
|
+
userId: string;
|
|
103
|
+
transactionId: string;
|
|
104
|
+
userPhone: string;
|
|
105
|
+
sourceType: "product" | "event";
|
|
106
|
+
paymentTime: string;
|
|
107
|
+
purchasedItemNumber: number;
|
|
108
|
+
productId?: string | undefined;
|
|
109
|
+
userEmail?: string | undefined;
|
|
110
|
+
userDisplayName?: string | undefined;
|
|
111
|
+
eventId?: string | undefined;
|
|
112
|
+
}>;
|
|
113
|
+
export type SubmitLotteryPullInput = z.infer<typeof submitLotteryPullSchema>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod schemas for lottery entry submission and validation.
|
|
3
|
+
*/
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
export const lotteryEntrySubmitSchema = z.object({
|
|
6
|
+
transactionId: z.string().min(4, "Transaction ID must be at least 4 characters"),
|
|
7
|
+
paymentTime: z.string().datetime({ message: "Payment time must be a valid ISO datetime" }),
|
|
8
|
+
purchasedItemNumber: z.number().int().min(1, "Item number must be at least 1"),
|
|
9
|
+
userPhone: z
|
|
10
|
+
.string()
|
|
11
|
+
.min(10, "Phone number must be at least 10 digits")
|
|
12
|
+
.regex(/^\+?[\d\s\-()]{10,}$/, "Enter a valid phone number"),
|
|
13
|
+
});
|
|
14
|
+
/** Schema for flag action */
|
|
15
|
+
export const flagLotteryEntrySchema = z.object({
|
|
16
|
+
entryId: z.string().min(1),
|
|
17
|
+
flagNote: z.string().min(1, "Flag note is required"),
|
|
18
|
+
flaggedByUserId: z.string().min(1),
|
|
19
|
+
});
|
|
20
|
+
/** Schema for reopen-slot action */
|
|
21
|
+
export const reopenLotterySlotSchema = z.object({
|
|
22
|
+
sourceType: z.enum(["event", "product"]),
|
|
23
|
+
sourceId: z.string().min(1),
|
|
24
|
+
slotNumber: z.number().int().min(1),
|
|
25
|
+
adminUserId: z.string().min(1),
|
|
26
|
+
});
|
|
27
|
+
/** Schema for submit lottery pull (full, server-side) */
|
|
28
|
+
export const submitLotteryPullSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
sourceType: z.enum(["event", "product"]),
|
|
31
|
+
eventId: z.string().optional(),
|
|
32
|
+
productId: z.string().optional(),
|
|
33
|
+
userId: z.string().min(1),
|
|
34
|
+
userDisplayName: z.string().optional(),
|
|
35
|
+
userEmail: z.string().email().optional(),
|
|
36
|
+
})
|
|
37
|
+
.merge(lotteryEntrySubmitSchema)
|
|
38
|
+
.refine((data) => (data.sourceType === "event" && !!data.eventId) ||
|
|
39
|
+
(data.sourceType === "product" && !!data.productId), { message: "eventId or productId must be provided matching sourceType" });
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lottery feature shared types
|
|
3
|
+
*
|
|
4
|
+
* Used by both event-type lottery and prize-draw-mode lottery.
|
|
5
|
+
* Weight + price fields are server-only — never sent to client consumers.
|
|
6
|
+
*/
|
|
7
|
+
export interface LotterySlot {
|
|
8
|
+
slotNumber: number;
|
|
9
|
+
name: string;
|
|
10
|
+
priceInPaise: number;
|
|
11
|
+
weight: number;
|
|
12
|
+
isBooked: boolean;
|
|
13
|
+
bookedByUserLotteryNumber?: number;
|
|
14
|
+
bookedByUserId?: string;
|
|
15
|
+
bookedByDisplayName?: string;
|
|
16
|
+
}
|
|
17
|
+
/** Slot shape safe to expose to clients — price and weight stripped by adapter. */
|
|
18
|
+
export interface ClientLotterySlot {
|
|
19
|
+
slotNumber: number;
|
|
20
|
+
name: string;
|
|
21
|
+
isBooked: boolean;
|
|
22
|
+
bookedByUserLotteryNumber?: number;
|
|
23
|
+
bookedByDisplayName?: string;
|
|
24
|
+
}
|
|
25
|
+
export type LotteryPricingMode = "uniform" | "variable";
|
|
26
|
+
export interface LotteryConfig {
|
|
27
|
+
slots: LotterySlot[];
|
|
28
|
+
totalSlots: number;
|
|
29
|
+
pricingMode: LotteryPricingMode;
|
|
30
|
+
uniformPriceInPaise?: number;
|
|
31
|
+
drawWindowDurationMinutes: number;
|
|
32
|
+
maxPullsPerTransaction: number;
|
|
33
|
+
maxPullsPerUser: number;
|
|
34
|
+
}
|
|
35
|
+
/** Client-safe config shape — slots have price/weight stripped. */
|
|
36
|
+
export interface ClientLotteryConfig {
|
|
37
|
+
slots: ClientLotterySlot[];
|
|
38
|
+
totalSlots: number;
|
|
39
|
+
pricingMode: LotteryPricingMode;
|
|
40
|
+
drawWindowDurationMinutes: number;
|
|
41
|
+
maxPullsPerTransaction: number;
|
|
42
|
+
maxPullsPerUser: number;
|
|
43
|
+
}
|
|
44
|
+
export type LotteryEntryStatus = "pending" | "drawn" | "won" | "flagged" | "cancelled";
|
|
45
|
+
/** Error codes thrown by lottery actions */
|
|
46
|
+
export type LotteryErrorCode = "LOTTERY_FULL" | "LOTTERY_WINDOW_CLOSED" | "USER_LIMIT_REACHED" | "TX_ALREADY_USED" | "SLOT_NOT_FOUND" | "ENTRY_NOT_FLAGGED";
|
|
47
|
+
export declare class LotteryError extends Error {
|
|
48
|
+
readonly code: LotteryErrorCode;
|
|
49
|
+
constructor(code: LotteryErrorCode, message: string);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Compute the weight for a slot given its price.
|
|
53
|
+
* Lower price → higher weight → more likely to be selected.
|
|
54
|
+
* Weight is always at least 1 (never 0).
|
|
55
|
+
* For uniform pricing: all slots get weight 50.
|
|
56
|
+
*/
|
|
57
|
+
export declare function computeWeight(priceInPaise: number, maxPriceInPaise: number): number;
|
|
58
|
+
/**
|
|
59
|
+
* Pick a slot using weighted random selection.
|
|
60
|
+
* @throws {LotteryError} LOTTERY_FULL when unclaimed is empty.
|
|
61
|
+
*/
|
|
62
|
+
export declare function pickWeightedSlot(unclaimed: LotterySlot[]): LotterySlot;
|
|
63
|
+
/**
|
|
64
|
+
* Assign weights to slots based on pricing mode.
|
|
65
|
+
* Returns a new array with weight populated on every slot.
|
|
66
|
+
*/
|
|
67
|
+
export declare function assignSlotWeights(config: LotteryConfig): LotterySlot[];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lottery feature shared types
|
|
3
|
+
*
|
|
4
|
+
* Used by both event-type lottery and prize-draw-mode lottery.
|
|
5
|
+
* Weight + price fields are server-only — never sent to client consumers.
|
|
6
|
+
*/
|
|
7
|
+
export class LotteryError extends Error {
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.name = "LotteryError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Compute the weight for a slot given its price.
|
|
16
|
+
* Lower price → higher weight → more likely to be selected.
|
|
17
|
+
* Weight is always at least 1 (never 0).
|
|
18
|
+
* For uniform pricing: all slots get weight 50.
|
|
19
|
+
*/
|
|
20
|
+
export function computeWeight(priceInPaise, maxPriceInPaise) {
|
|
21
|
+
if (maxPriceInPaise === 0)
|
|
22
|
+
return 50;
|
|
23
|
+
return Math.max(1, Math.round((1 - priceInPaise / maxPriceInPaise) * 99) + 1);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Pick a slot using weighted random selection.
|
|
27
|
+
* @throws {LotteryError} LOTTERY_FULL when unclaimed is empty.
|
|
28
|
+
*/
|
|
29
|
+
export function pickWeightedSlot(unclaimed) {
|
|
30
|
+
if (unclaimed.length === 0) {
|
|
31
|
+
throw new LotteryError("LOTTERY_FULL", "No unclaimed slots remain.");
|
|
32
|
+
}
|
|
33
|
+
const total = unclaimed.reduce((s, sl) => s + sl.weight, 0);
|
|
34
|
+
let r = Math.random() * total;
|
|
35
|
+
for (const sl of unclaimed) {
|
|
36
|
+
r -= sl.weight;
|
|
37
|
+
if (r <= 0)
|
|
38
|
+
return sl;
|
|
39
|
+
}
|
|
40
|
+
return unclaimed[unclaimed.length - 1];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Assign weights to slots based on pricing mode.
|
|
44
|
+
* Returns a new array with weight populated on every slot.
|
|
45
|
+
*/
|
|
46
|
+
export function assignSlotWeights(config) {
|
|
47
|
+
if (config.pricingMode === "uniform") {
|
|
48
|
+
return config.slots.map((sl) => ({ ...sl, weight: 50 }));
|
|
49
|
+
}
|
|
50
|
+
const maxPrice = Math.max(...config.slots.map((sl) => sl.priceInPaise), 0);
|
|
51
|
+
return config.slots.map((sl) => ({
|
|
52
|
+
...sl,
|
|
53
|
+
weight: computeWeight(sl.priceInPaise, maxPrice),
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { makeMockDb } from "../../../../../tests/helpers/mock-firestore";
|
|
3
|
+
const { db, mockDocRef, mockCollection, mockQuery, mockTxn } = makeMockDb();
|
|
4
|
+
vi.mock("../../../../providers/db-firebase", () => ({
|
|
5
|
+
getAdminDb: () => db,
|
|
6
|
+
}));
|
|
7
|
+
vi.mock("../../../../monitoring", () => ({
|
|
8
|
+
serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("../../../../errors/normalize", () => ({
|
|
11
|
+
normalizeError: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
import { ConversationsRepository } from "../conversations.repository";
|
|
14
|
+
const repo = new ConversationsRepository();
|
|
15
|
+
function makeConvData(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
buyerId: "user-ravi",
|
|
18
|
+
buyerDisplayName: "Ravi Kumar",
|
|
19
|
+
storeId: "store-pokemon-palace",
|
|
20
|
+
storeName: "Pokemon Palace",
|
|
21
|
+
sellerDisplayName: "Misty Singh",
|
|
22
|
+
productId: "product-charizard",
|
|
23
|
+
productTitle: "Charizard PSA9",
|
|
24
|
+
messages: [],
|
|
25
|
+
lastMessage: "",
|
|
26
|
+
lastMessageAt: new Date(),
|
|
27
|
+
unreadBuyer: 0,
|
|
28
|
+
unreadSeller: 0,
|
|
29
|
+
status: "active",
|
|
30
|
+
createdAt: new Date(),
|
|
31
|
+
updatedAt: new Date(),
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
db.collection.mockReturnValue(mockCollection);
|
|
38
|
+
mockCollection.doc.mockReturnValue(mockDocRef);
|
|
39
|
+
mockCollection.where.mockReturnValue(mockQuery);
|
|
40
|
+
mockQuery.where.mockReturnValue(mockQuery);
|
|
41
|
+
mockQuery.orderBy.mockReturnValue(mockQuery);
|
|
42
|
+
mockQuery.get.mockResolvedValue({ docs: [], empty: true, size: 0 });
|
|
43
|
+
mockDocRef.get.mockResolvedValue({ exists: false, id: "mock-id", data: () => ({}) });
|
|
44
|
+
mockDocRef.update.mockResolvedValue(undefined);
|
|
45
|
+
db.runTransaction.mockImplementation(async (fn) => fn(mockTxn));
|
|
46
|
+
mockTxn.get.mockResolvedValue({ exists: false, id: "mock-id", data: () => ({}) });
|
|
47
|
+
mockTxn.set.mockReturnValue(undefined);
|
|
48
|
+
mockTxn.update.mockReturnValue(undefined);
|
|
49
|
+
});
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// findOrCreateByContext
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
describe("ConversationsRepository.findOrCreateByContext", () => {
|
|
54
|
+
it("generates stable ID: conv-{buyerId}-{storeId}-{productId}", async () => {
|
|
55
|
+
const convData = makeConvData();
|
|
56
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-user-ravi-store-pokemon-palace-product-charizard", data: () => convData });
|
|
57
|
+
const result = await repo.findOrCreateByContext({
|
|
58
|
+
buyerId: "user-ravi",
|
|
59
|
+
buyerDisplayName: "Ravi Kumar",
|
|
60
|
+
storeId: "store-pokemon-palace",
|
|
61
|
+
storeName: "Pokemon Palace",
|
|
62
|
+
sellerDisplayName: "Misty Singh",
|
|
63
|
+
productId: "product-charizard",
|
|
64
|
+
});
|
|
65
|
+
expect(mockCollection.doc).toHaveBeenCalledWith("conv-user-ravi-store-pokemon-palace-product-charizard");
|
|
66
|
+
expect(result.buyerId).toBe("user-ravi");
|
|
67
|
+
});
|
|
68
|
+
it("uses 'general' productKey when productId is not provided", async () => {
|
|
69
|
+
const convData = makeConvData({ productId: undefined });
|
|
70
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-user-ravi-store-1-general", data: () => convData });
|
|
71
|
+
await repo.findOrCreateByContext({
|
|
72
|
+
buyerId: "user-ravi",
|
|
73
|
+
buyerDisplayName: "Ravi",
|
|
74
|
+
storeId: "store-1",
|
|
75
|
+
storeName: "Store 1",
|
|
76
|
+
sellerDisplayName: "Seller 1",
|
|
77
|
+
});
|
|
78
|
+
expect(mockCollection.doc).toHaveBeenCalledWith("conv-user-ravi-store-1-general");
|
|
79
|
+
});
|
|
80
|
+
it("returns existing conversation without creating a new one (idempotent)", async () => {
|
|
81
|
+
const convData = makeConvData();
|
|
82
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-abc", data: () => convData });
|
|
83
|
+
await repo.findOrCreateByContext({
|
|
84
|
+
buyerId: "user-ravi",
|
|
85
|
+
buyerDisplayName: "Ravi",
|
|
86
|
+
storeId: "store-1",
|
|
87
|
+
storeName: "Store 1",
|
|
88
|
+
sellerDisplayName: "Seller",
|
|
89
|
+
});
|
|
90
|
+
expect(mockTxn.set).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
it("creates a new conversation when one does not exist", async () => {
|
|
93
|
+
mockTxn.get.mockResolvedValue({ exists: false, id: "conv-new", data: () => ({}) });
|
|
94
|
+
await repo.findOrCreateByContext({
|
|
95
|
+
buyerId: "user-new",
|
|
96
|
+
buyerDisplayName: "New Buyer",
|
|
97
|
+
storeId: "store-new",
|
|
98
|
+
storeName: "New Store",
|
|
99
|
+
sellerDisplayName: "New Seller",
|
|
100
|
+
});
|
|
101
|
+
expect(mockTxn.set).toHaveBeenCalledOnce();
|
|
102
|
+
});
|
|
103
|
+
it("new conversation starts with messages=[], unreadBuyer=0, unreadSeller=0", async () => {
|
|
104
|
+
mockTxn.get.mockResolvedValue({ exists: false, id: "conv-new", data: () => ({}) });
|
|
105
|
+
const result = await repo.findOrCreateByContext({
|
|
106
|
+
buyerId: "user-new",
|
|
107
|
+
buyerDisplayName: "New",
|
|
108
|
+
storeId: "store-new",
|
|
109
|
+
storeName: "New Store",
|
|
110
|
+
sellerDisplayName: "Seller",
|
|
111
|
+
});
|
|
112
|
+
expect(result.messages).toHaveLength(0);
|
|
113
|
+
expect(result.unreadBuyer).toBe(0);
|
|
114
|
+
expect(result.unreadSeller).toBe(0);
|
|
115
|
+
});
|
|
116
|
+
it("uses Firestore transaction for atomicity", async () => {
|
|
117
|
+
mockTxn.get.mockResolvedValue({ exists: false, id: "conv-new", data: () => ({}) });
|
|
118
|
+
await repo.findOrCreateByContext({
|
|
119
|
+
buyerId: "user-a",
|
|
120
|
+
buyerDisplayName: "A",
|
|
121
|
+
storeId: "store-b",
|
|
122
|
+
storeName: "B",
|
|
123
|
+
sellerDisplayName: "Seller",
|
|
124
|
+
});
|
|
125
|
+
expect(db.runTransaction).toHaveBeenCalledOnce();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// appendMessage
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
describe("ConversationsRepository.appendMessage", () => {
|
|
132
|
+
it("increments unreadBuyer when seller sends a message", async () => {
|
|
133
|
+
const convData = makeConvData({ unreadBuyer: 0, unreadSeller: 0 });
|
|
134
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
135
|
+
const result = await repo.appendMessage("conv-1", {
|
|
136
|
+
senderId: "seller-uid",
|
|
137
|
+
senderRole: "seller",
|
|
138
|
+
body: "Hello buyer!",
|
|
139
|
+
});
|
|
140
|
+
expect(result.unreadBuyer).toBe(1);
|
|
141
|
+
expect(result.unreadSeller).toBe(0);
|
|
142
|
+
});
|
|
143
|
+
it("increments unreadSeller when buyer sends a message", async () => {
|
|
144
|
+
const convData = makeConvData({ unreadBuyer: 0, unreadSeller: 2 });
|
|
145
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
146
|
+
const result = await repo.appendMessage("conv-1", {
|
|
147
|
+
senderId: "buyer-uid",
|
|
148
|
+
senderRole: "buyer",
|
|
149
|
+
body: "Hello seller!",
|
|
150
|
+
});
|
|
151
|
+
expect(result.unreadSeller).toBe(3);
|
|
152
|
+
expect(result.unreadBuyer).toBe(0);
|
|
153
|
+
});
|
|
154
|
+
it("appends the new message to the existing messages array", async () => {
|
|
155
|
+
const existingMsg = {
|
|
156
|
+
id: "msg-old",
|
|
157
|
+
senderId: "seller-uid",
|
|
158
|
+
senderRole: "seller",
|
|
159
|
+
body: "Previous message",
|
|
160
|
+
isRead: false,
|
|
161
|
+
sentAt: new Date(),
|
|
162
|
+
};
|
|
163
|
+
const convData = makeConvData({ messages: [existingMsg] });
|
|
164
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
165
|
+
const result = await repo.appendMessage("conv-1", {
|
|
166
|
+
senderId: "buyer-uid",
|
|
167
|
+
senderRole: "buyer",
|
|
168
|
+
body: "New message",
|
|
169
|
+
});
|
|
170
|
+
expect(result.messages).toHaveLength(2);
|
|
171
|
+
expect(result.messages[1].body).toBe("New message");
|
|
172
|
+
});
|
|
173
|
+
it("sets isRead: false on the new message", async () => {
|
|
174
|
+
const convData = makeConvData();
|
|
175
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
176
|
+
const result = await repo.appendMessage("conv-1", {
|
|
177
|
+
senderId: "buyer-uid",
|
|
178
|
+
senderRole: "buyer",
|
|
179
|
+
body: "Test",
|
|
180
|
+
});
|
|
181
|
+
expect(result.messages[0].isRead).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
it("updates lastMessage and lastMessageAt", async () => {
|
|
184
|
+
const convData = makeConvData();
|
|
185
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
186
|
+
const result = await repo.appendMessage("conv-1", {
|
|
187
|
+
senderId: "buyer-uid",
|
|
188
|
+
senderRole: "buyer",
|
|
189
|
+
body: "Latest message",
|
|
190
|
+
});
|
|
191
|
+
expect(result.lastMessage).toBe("Latest message");
|
|
192
|
+
expect(result.lastMessageAt).toBeInstanceOf(Date);
|
|
193
|
+
});
|
|
194
|
+
it("uses Firestore transaction", async () => {
|
|
195
|
+
const convData = makeConvData();
|
|
196
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
197
|
+
await repo.appendMessage("conv-1", { senderId: "uid", senderRole: "buyer", body: "msg" });
|
|
198
|
+
expect(db.runTransaction).toHaveBeenCalledOnce();
|
|
199
|
+
expect(mockTxn.update).toHaveBeenCalledOnce();
|
|
200
|
+
});
|
|
201
|
+
it("throws when conversation not found", async () => {
|
|
202
|
+
mockTxn.get.mockResolvedValue({ exists: false, id: "conv-ghost", data: () => ({}) });
|
|
203
|
+
await expect(repo.appendMessage("conv-ghost", { senderId: "uid", senderRole: "buyer", body: "msg" })).rejects.toThrow(/not found/i);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// markRead
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
describe("ConversationsRepository.markRead", () => {
|
|
210
|
+
it("zeroes unreadBuyer when role=buyer", async () => {
|
|
211
|
+
const sellerMsg = { id: "m1", senderId: "seller", senderRole: "seller", body: "Hi", isRead: false, sentAt: new Date() };
|
|
212
|
+
const convData = makeConvData({ unreadBuyer: 3, unreadSeller: 1, messages: [sellerMsg] });
|
|
213
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
214
|
+
await repo.markRead("conv-1", "buyer");
|
|
215
|
+
const updateArg = mockTxn.update.mock.calls[0][1];
|
|
216
|
+
expect(updateArg.unreadBuyer).toBe(0);
|
|
217
|
+
});
|
|
218
|
+
it("does NOT touch unreadSeller when buyer reads", async () => {
|
|
219
|
+
const convData = makeConvData({ unreadBuyer: 2, unreadSeller: 5 });
|
|
220
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
221
|
+
await repo.markRead("conv-1", "buyer");
|
|
222
|
+
const updateArg = mockTxn.update.mock.calls[0][1];
|
|
223
|
+
expect(updateArg).not.toHaveProperty("unreadSeller");
|
|
224
|
+
});
|
|
225
|
+
it("zeroes unreadSeller when role=seller", async () => {
|
|
226
|
+
const buyerMsg = { id: "m1", senderId: "buyer", senderRole: "buyer", body: "Hi", isRead: false, sentAt: new Date() };
|
|
227
|
+
const convData = makeConvData({ unreadBuyer: 2, unreadSeller: 4, messages: [buyerMsg] });
|
|
228
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
229
|
+
await repo.markRead("conv-1", "seller");
|
|
230
|
+
const updateArg = mockTxn.update.mock.calls[0][1];
|
|
231
|
+
expect(updateArg.unreadSeller).toBe(0);
|
|
232
|
+
});
|
|
233
|
+
it("marks seller-sent messages as isRead=true when buyer reads", async () => {
|
|
234
|
+
const sellerMsg = { id: "m1", senderId: "seller", senderRole: "seller", body: "Hi", isRead: false, sentAt: new Date() };
|
|
235
|
+
const convData = makeConvData({ messages: [sellerMsg] });
|
|
236
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
237
|
+
await repo.markRead("conv-1", "buyer");
|
|
238
|
+
const updateArg = mockTxn.update.mock.calls[0][1];
|
|
239
|
+
const msgs = updateArg.messages;
|
|
240
|
+
expect(msgs[0].isRead).toBe(true);
|
|
241
|
+
});
|
|
242
|
+
it("does NOT mark buyer-sent messages when buyer reads (only marks counterparty)", async () => {
|
|
243
|
+
const buyerMsg = { id: "m1", senderId: "buyer", senderRole: "buyer", body: "Hi", isRead: false, sentAt: new Date() };
|
|
244
|
+
const convData = makeConvData({ messages: [buyerMsg] });
|
|
245
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
246
|
+
await repo.markRead("conv-1", "buyer");
|
|
247
|
+
const updateArg = mockTxn.update.mock.calls[0][1];
|
|
248
|
+
const msgs = updateArg.messages;
|
|
249
|
+
expect(msgs[0].isRead).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
it("uses Firestore transaction", async () => {
|
|
252
|
+
const convData = makeConvData();
|
|
253
|
+
mockTxn.get.mockResolvedValue({ exists: true, id: "conv-1", data: () => convData });
|
|
254
|
+
await repo.markRead("conv-1", "buyer");
|
|
255
|
+
expect(db.runTransaction).toHaveBeenCalledOnce();
|
|
256
|
+
});
|
|
257
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|