@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
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { NextResponse } from "next/server.js";
|
|
11
11
|
export declare function GET(request: Request): Promise<NextResponse>;
|
|
12
|
-
export declare const POST: (request: Request, context
|
|
12
|
+
export declare const POST: (request: Request, context?: {
|
|
13
13
|
params: Promise<Record<string, string>>;
|
|
14
|
-
}) => Promise<Response>;
|
|
14
|
+
} | undefined) => Promise<Response>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { makeMockDb, makeSnap, makeQuerySnap } from "../../../../../tests/helpers/mock-firestore";
|
|
3
|
+
import { DatabaseError } from "../../../../errors";
|
|
4
|
+
const { db, mockDocRef, mockCollection, mockQuery, mockBatch } = makeMockDb();
|
|
5
|
+
const { mockGetFirestoreCount } = vi.hoisted(() => ({
|
|
6
|
+
mockGetFirestoreCount: vi.fn().mockResolvedValue(0),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("../../../../providers/db-firebase/admin", () => ({
|
|
9
|
+
getAdminDb: () => db,
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal();
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
prepareForFirestore: (d) => d,
|
|
16
|
+
getFirestoreCount: mockGetFirestoreCount,
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
vi.mock("../../../../errors/normalize", () => ({
|
|
20
|
+
normalizeError: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
import { CarouselRepository } from "../carousel.repository";
|
|
23
|
+
import { MAX_ACTIVE_SLIDES } from "../../schemas";
|
|
24
|
+
const repo = new CarouselRepository();
|
|
25
|
+
function makeSlideDoc(overrides = {}) {
|
|
26
|
+
return {
|
|
27
|
+
id: "slide-hero",
|
|
28
|
+
title: "Hero Slide",
|
|
29
|
+
active: true,
|
|
30
|
+
order: 0,
|
|
31
|
+
cards: [],
|
|
32
|
+
background: { type: "image", url: "https://example.com/img.jpg" },
|
|
33
|
+
createdBy: "admin-1",
|
|
34
|
+
createdAt: new Date(),
|
|
35
|
+
updatedAt: new Date(),
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function makeActiveSlides(count) {
|
|
40
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
41
|
+
id: `slide-${i}`,
|
|
42
|
+
data: makeSlideDoc({ id: `slide-${i}`, active: true, order: i }),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
db.collection.mockReturnValue(mockCollection);
|
|
48
|
+
mockCollection.doc.mockReturnValue(mockDocRef);
|
|
49
|
+
mockCollection.where.mockReturnValue(mockQuery);
|
|
50
|
+
mockQuery.where.mockReturnValue(mockQuery);
|
|
51
|
+
mockQuery.orderBy.mockReturnValue(mockQuery);
|
|
52
|
+
mockQuery.limit.mockReturnValue(mockQuery);
|
|
53
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
54
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
55
|
+
mockDocRef.set.mockResolvedValue(undefined);
|
|
56
|
+
mockDocRef.update.mockResolvedValue(undefined);
|
|
57
|
+
db.batch.mockReturnValue(mockBatch);
|
|
58
|
+
mockGetFirestoreCount.mockResolvedValue(0);
|
|
59
|
+
});
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// activateSlide
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
describe("CarouselRepository.activateSlide", () => {
|
|
64
|
+
it("activates when active count < MAX_ACTIVE_SLIDES", async () => {
|
|
65
|
+
// getActiveSlides returns fewer than max
|
|
66
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(makeActiveSlides(MAX_ACTIVE_SLIDES - 1)));
|
|
67
|
+
const slide = makeSlideDoc({ active: true });
|
|
68
|
+
mockDocRef.get.mockResolvedValue(makeSnap(slide, "slide-hero"));
|
|
69
|
+
await repo.activateSlide("slide-hero");
|
|
70
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ active: true }));
|
|
71
|
+
});
|
|
72
|
+
it("throws DatabaseError when active count = MAX_ACTIVE_SLIDES", async () => {
|
|
73
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(makeActiveSlides(MAX_ACTIVE_SLIDES)));
|
|
74
|
+
await expect(repo.activateSlide("slide-7")).rejects.toThrow(DatabaseError);
|
|
75
|
+
});
|
|
76
|
+
it("throws DatabaseError when active count > MAX_ACTIVE_SLIDES (defensive)", async () => {
|
|
77
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(makeActiveSlides(MAX_ACTIVE_SLIDES + 1)));
|
|
78
|
+
await expect(repo.activateSlide("slide-7")).rejects.toThrow(DatabaseError);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// deactivateSlide
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
describe("CarouselRepository.deactivateSlide", () => {
|
|
85
|
+
it("sets active: false on the slide", async () => {
|
|
86
|
+
const slide = makeSlideDoc({ active: false });
|
|
87
|
+
mockDocRef.get.mockResolvedValue(makeSnap(slide, "slide-hero"));
|
|
88
|
+
await repo.deactivateSlide("slide-hero");
|
|
89
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ active: false }));
|
|
90
|
+
});
|
|
91
|
+
it("sets updatedAt timestamp", async () => {
|
|
92
|
+
const slide = makeSlideDoc();
|
|
93
|
+
mockDocRef.get.mockResolvedValue(makeSnap(slide, "slide-hero"));
|
|
94
|
+
await repo.deactivateSlide("slide-hero");
|
|
95
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// canActivateMore
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
describe("CarouselRepository.canActivateMore", () => {
|
|
102
|
+
it("returns true when active count < MAX_ACTIVE_SLIDES", async () => {
|
|
103
|
+
mockGetFirestoreCount.mockResolvedValue(MAX_ACTIVE_SLIDES - 1);
|
|
104
|
+
const result = await repo.canActivateMore();
|
|
105
|
+
expect(result).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
it("returns false when active count = MAX_ACTIVE_SLIDES", async () => {
|
|
108
|
+
mockGetFirestoreCount.mockResolvedValue(MAX_ACTIVE_SLIDES);
|
|
109
|
+
const result = await repo.canActivateMore();
|
|
110
|
+
expect(result).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
it("returns false when active count > MAX_ACTIVE_SLIDES", async () => {
|
|
113
|
+
mockGetFirestoreCount.mockResolvedValue(MAX_ACTIVE_SLIDES + 2);
|
|
114
|
+
const result = await repo.canActivateMore();
|
|
115
|
+
expect(result).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// reorderSlides
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
describe("CarouselRepository.reorderSlides", () => {
|
|
122
|
+
it("batch-updates order field on each slide", async () => {
|
|
123
|
+
await repo.reorderSlides([
|
|
124
|
+
{ id: "slide-1", order: 0 },
|
|
125
|
+
{ id: "slide-2", order: 1 },
|
|
126
|
+
{ id: "slide-3", order: 2 },
|
|
127
|
+
]);
|
|
128
|
+
expect(mockBatch.update).toHaveBeenCalledTimes(3);
|
|
129
|
+
});
|
|
130
|
+
it("commits the batch", async () => {
|
|
131
|
+
await repo.reorderSlides([{ id: "slide-1", order: 0 }]);
|
|
132
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
133
|
+
});
|
|
134
|
+
it("each update carries the correct order value", async () => {
|
|
135
|
+
await repo.reorderSlides([
|
|
136
|
+
{ id: "slide-a", order: 2 },
|
|
137
|
+
{ id: "slide-b", order: 5 },
|
|
138
|
+
]);
|
|
139
|
+
const updateCalls = mockBatch.update.mock.calls;
|
|
140
|
+
const orders = updateCalls.map((c) => c[1].order);
|
|
141
|
+
expect(orders).toContain(2);
|
|
142
|
+
expect(orders).toContain(5);
|
|
143
|
+
});
|
|
144
|
+
it("empty list → no batch update called, commits empty batch", async () => {
|
|
145
|
+
await repo.reorderSlides([]);
|
|
146
|
+
expect(mockBatch.update).not.toHaveBeenCalled();
|
|
147
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// incrementViews
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
describe("CarouselRepository.incrementViews", () => {
|
|
154
|
+
it("increments stats.views by 1", async () => {
|
|
155
|
+
await repo.incrementViews("slide-hero");
|
|
156
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ "stats.views": expect.anything() }));
|
|
157
|
+
});
|
|
158
|
+
it("updates stats.lastViewed timestamp", async () => {
|
|
159
|
+
await repo.incrementViews("slide-hero");
|
|
160
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ "stats.lastViewed": expect.any(Date) }));
|
|
161
|
+
});
|
|
162
|
+
it("swallows errors — analytics must not break callers", async () => {
|
|
163
|
+
mockDocRef.update.mockRejectedValue(new Error("Firestore error"));
|
|
164
|
+
await expect(repo.incrementViews("slide-hero")).resolves.toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// getActiveSlides
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
describe("CarouselRepository.getActiveSlides", () => {
|
|
171
|
+
it("queries active=true, ordered by order ascending", async () => {
|
|
172
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
173
|
+
await repo.getActiveSlides();
|
|
174
|
+
expect(mockCollection.where).toHaveBeenCalledWith("active", "==", true);
|
|
175
|
+
expect(mockQuery.orderBy).toHaveBeenCalledWith("order", "asc");
|
|
176
|
+
});
|
|
177
|
+
it("limits results to MAX_ACTIVE_SLIDES", async () => {
|
|
178
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
179
|
+
await repo.getActiveSlides();
|
|
180
|
+
expect(mockQuery.limit).toHaveBeenCalledWith(MAX_ACTIVE_SLIDES);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { makeMockDb, makeSnap, makeQuerySnap } from "../../../../../tests/helpers/mock-firestore";
|
|
3
|
+
const { db, mockDocRef, mockCollection, mockQuery, mockBatch } = makeMockDb();
|
|
4
|
+
vi.mock("../../../../providers/db-firebase/admin", () => ({
|
|
5
|
+
getAdminDb: () => db,
|
|
6
|
+
}));
|
|
7
|
+
vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal();
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
prepareForFirestore: (d) => d,
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
vi.mock("../../../../errors/normalize", () => ({
|
|
15
|
+
normalizeError: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
import { HomepageSectionsRepository } from "../homepage-sections.repository";
|
|
18
|
+
const repo = new HomepageSectionsRepository();
|
|
19
|
+
function makeSectionDoc(overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
id: "section-featured-products",
|
|
22
|
+
type: "featured-products",
|
|
23
|
+
enabled: true,
|
|
24
|
+
order: 0,
|
|
25
|
+
config: {},
|
|
26
|
+
createdAt: new Date(),
|
|
27
|
+
updatedAt: new Date(),
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
db.collection.mockReturnValue(mockCollection);
|
|
34
|
+
mockCollection.doc.mockReturnValue(mockDocRef);
|
|
35
|
+
mockCollection.where.mockReturnValue(mockQuery);
|
|
36
|
+
mockQuery.where.mockReturnValue(mockQuery);
|
|
37
|
+
mockQuery.orderBy.mockReturnValue(mockQuery);
|
|
38
|
+
mockQuery.limit.mockReturnValue(mockQuery);
|
|
39
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
40
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
41
|
+
mockDocRef.set.mockResolvedValue(undefined);
|
|
42
|
+
mockDocRef.update.mockResolvedValue(undefined);
|
|
43
|
+
db.batch.mockReturnValue(mockBatch);
|
|
44
|
+
});
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// getEnabledSections
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
describe("HomepageSectionsRepository.getEnabledSections", () => {
|
|
49
|
+
it("queries where enabled=true, ordered by order ascending", async () => {
|
|
50
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
51
|
+
await repo.getEnabledSections();
|
|
52
|
+
expect(mockCollection.where).toHaveBeenCalledWith("enabled", "==", true);
|
|
53
|
+
expect(mockQuery.orderBy).toHaveBeenCalledWith("order", "asc");
|
|
54
|
+
});
|
|
55
|
+
it("returns only enabled sections", async () => {
|
|
56
|
+
const section = makeSectionDoc({ enabled: true });
|
|
57
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: section.id, data: section }]));
|
|
58
|
+
const results = await repo.getEnabledSections();
|
|
59
|
+
expect(results).toHaveLength(1);
|
|
60
|
+
});
|
|
61
|
+
it("returns empty array when no enabled sections", async () => {
|
|
62
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
63
|
+
const results = await repo.getEnabledSections();
|
|
64
|
+
expect(results).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// enableSection
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
describe("HomepageSectionsRepository.enableSection", () => {
|
|
71
|
+
it("sets enabled: true on the section", async () => {
|
|
72
|
+
const section = makeSectionDoc({ enabled: false });
|
|
73
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
74
|
+
await repo.enableSection("section-1");
|
|
75
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ enabled: true }));
|
|
76
|
+
});
|
|
77
|
+
it("sets updatedAt timestamp", async () => {
|
|
78
|
+
const section = makeSectionDoc();
|
|
79
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
80
|
+
await repo.enableSection("section-1");
|
|
81
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// disableSection
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
describe("HomepageSectionsRepository.disableSection", () => {
|
|
88
|
+
it("sets enabled: false on the section", async () => {
|
|
89
|
+
const section = makeSectionDoc({ enabled: true });
|
|
90
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
91
|
+
await repo.disableSection("section-1");
|
|
92
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ enabled: false }));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// toggleSection
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
describe("HomepageSectionsRepository.toggleSection", () => {
|
|
99
|
+
it("enabled:true → sets enabled:false", async () => {
|
|
100
|
+
const section = makeSectionDoc({ enabled: true });
|
|
101
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
102
|
+
await repo.toggleSection("section-1");
|
|
103
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ enabled: false }));
|
|
104
|
+
});
|
|
105
|
+
it("enabled:false → sets enabled:true", async () => {
|
|
106
|
+
const section = makeSectionDoc({ enabled: false });
|
|
107
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
108
|
+
await repo.toggleSection("section-1");
|
|
109
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ enabled: true }));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// reorderSections
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
describe("HomepageSectionsRepository.reorderSections", () => {
|
|
116
|
+
it("batch-updates order field for each section", async () => {
|
|
117
|
+
await repo.reorderSections([
|
|
118
|
+
{ id: "section-1", order: 0 },
|
|
119
|
+
{ id: "section-2", order: 1 },
|
|
120
|
+
{ id: "section-3", order: 2 },
|
|
121
|
+
]);
|
|
122
|
+
expect(mockBatch.update).toHaveBeenCalledTimes(3);
|
|
123
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
124
|
+
});
|
|
125
|
+
it("each update sets the correct order value", async () => {
|
|
126
|
+
await repo.reorderSections([
|
|
127
|
+
{ id: "section-a", order: 5 },
|
|
128
|
+
{ id: "section-b", order: 10 },
|
|
129
|
+
]);
|
|
130
|
+
const calls = mockBatch.update.mock.calls;
|
|
131
|
+
expect(calls.some((c) => c[1].order === 5)).toBe(true);
|
|
132
|
+
expect(calls.some((c) => c[1].order === 10)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
it("empty list → no batch update, commits empty batch", async () => {
|
|
135
|
+
await repo.reorderSections([]);
|
|
136
|
+
expect(mockBatch.update).not.toHaveBeenCalled();
|
|
137
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// batchToggleSections
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
describe("HomepageSectionsRepository.batchToggleSections", () => {
|
|
144
|
+
it("sets enabled:true on all provided sections in one batch", async () => {
|
|
145
|
+
await repo.batchToggleSections(["section-1", "section-2", "section-3"], true);
|
|
146
|
+
expect(mockBatch.update).toHaveBeenCalledTimes(3);
|
|
147
|
+
const calls = mockBatch.update.mock.calls;
|
|
148
|
+
expect(calls.every((c) => c[1].enabled === true)).toBe(true);
|
|
149
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
150
|
+
});
|
|
151
|
+
it("sets enabled:false on all provided sections", async () => {
|
|
152
|
+
await repo.batchToggleSections(["section-x", "section-y"], false);
|
|
153
|
+
const calls = mockBatch.update.mock.calls;
|
|
154
|
+
expect(calls.every((c) => c[1].enabled === false)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
it("empty list → no updates, commits empty batch", async () => {
|
|
157
|
+
await repo.batchToggleSections([], true);
|
|
158
|
+
expect(mockBatch.update).not.toHaveBeenCalled();
|
|
159
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// resetSectionToDefault
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
describe("HomepageSectionsRepository.resetSectionToDefault", () => {
|
|
166
|
+
it("overwrites config with the provided default config", async () => {
|
|
167
|
+
const section = makeSectionDoc({ config: { customKey: "old-value" } });
|
|
168
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
169
|
+
const defaultCfg = { title: "Default Title", itemCount: 6 };
|
|
170
|
+
await repo.resetSectionToDefault("section-1", defaultCfg);
|
|
171
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ config: defaultCfg }));
|
|
172
|
+
});
|
|
173
|
+
it("does NOT delete the section document", async () => {
|
|
174
|
+
const section = makeSectionDoc();
|
|
175
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
176
|
+
await repo.resetSectionToDefault("section-1", {});
|
|
177
|
+
expect(mockDocRef.delete).not.toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
it("sets updatedAt timestamp on reset", async () => {
|
|
180
|
+
const section = makeSectionDoc();
|
|
181
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
182
|
+
await repo.resetSectionToDefault("section-1", {});
|
|
183
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
|
|
2
|
+
import { BaseRepository } from "../../../providers/db-firebase";
|
|
3
|
+
import { type LotteryEntryDocument } from "../schemas/firestore";
|
|
4
|
+
declare class LotteryEntryRepository extends BaseRepository<LotteryEntryDocument> {
|
|
5
|
+
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
6
|
+
constructor();
|
|
7
|
+
protected mapDoc<D = LotteryEntryDocument>(snap: import("../../../providers/db-firebase").DocumentSnapshot): D;
|
|
8
|
+
/**
|
|
9
|
+
* Get the next sequential userLotteryNumber for a source atomically.
|
|
10
|
+
* Returns current count + 1.
|
|
11
|
+
*/
|
|
12
|
+
nextUserLotteryNumber(sourceId: string, field: "eventId" | "productId"): Promise<number>;
|
|
13
|
+
/**
|
|
14
|
+
* Count entries for a given source and transaction ID.
|
|
15
|
+
* Used to enforce maxPullsPerTransaction.
|
|
16
|
+
*/
|
|
17
|
+
countByTransactionId(sourceId: string, sourceField: "eventId" | "productId", transactionId: string): Promise<number>;
|
|
18
|
+
/**
|
|
19
|
+
* Count entries for a given source and user.
|
|
20
|
+
* Used to enforce maxPullsPerUser.
|
|
21
|
+
*/
|
|
22
|
+
countByUser(sourceId: string, sourceField: "eventId" | "productId", userId: string): Promise<number>;
|
|
23
|
+
/** List all entries for an event/product source. Paginated via Sieve. */
|
|
24
|
+
listForSource(sourceId: string, sourceField: "eventId" | "productId", model: SieveModel): Promise<FirebaseSieveResult<LotteryEntryDocument>>;
|
|
25
|
+
/** List entries for a specific user across all sources. */
|
|
26
|
+
listForUser(userId: string, model: SieveModel): Promise<FirebaseSieveResult<LotteryEntryDocument>>;
|
|
27
|
+
/** Create a lottery entry with PII encryption. */
|
|
28
|
+
createEntry(data: Omit<LotteryEntryDocument, "id">): Promise<LotteryEntryDocument>;
|
|
29
|
+
/** Flag a lottery entry as fraudulent. */
|
|
30
|
+
flagEntry(entryId: string, flagNote: string, flaggedBy: string): Promise<LotteryEntryDocument>;
|
|
31
|
+
}
|
|
32
|
+
export { LotteryEntryRepository };
|
|
33
|
+
export declare const lotteryEntryRepository: LotteryEntryRepository;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { normalizeError } from "../../../errors/normalize";
|
|
2
|
+
import { DatabaseError } from "../../../errors";
|
|
3
|
+
import { serverLogger } from "../../../monitoring";
|
|
4
|
+
import { BaseRepository, prepareForFirestore, } from "../../../providers/db-firebase";
|
|
5
|
+
import { decryptPiiFields, encryptPiiFields, LOTTERY_ENTRY_PII_FIELDS, } from "../../../security";
|
|
6
|
+
import { LOTTERY_ENTRIES_COLLECTION, LOTTERY_ENTRY_FIELDS, } from "../schemas/firestore";
|
|
7
|
+
class LotteryEntryRepository extends BaseRepository {
|
|
8
|
+
constructor() {
|
|
9
|
+
super(LOTTERY_ENTRIES_COLLECTION);
|
|
10
|
+
}
|
|
11
|
+
mapDoc(snap) {
|
|
12
|
+
const raw = super.mapDoc(snap);
|
|
13
|
+
return decryptPiiFields(raw, [
|
|
14
|
+
...LOTTERY_ENTRY_PII_FIELDS,
|
|
15
|
+
]);
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Get the next sequential userLotteryNumber for a source atomically.
|
|
19
|
+
* Returns current count + 1.
|
|
20
|
+
*/
|
|
21
|
+
async nextUserLotteryNumber(sourceId, field) {
|
|
22
|
+
try {
|
|
23
|
+
const snapshot = await this.getCollection()
|
|
24
|
+
.where(field, "==", sourceId)
|
|
25
|
+
.get();
|
|
26
|
+
return snapshot.size + 1;
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
void normalizeError(error);
|
|
30
|
+
throw new DatabaseError(`Failed to get next lottery number for ${field}=${sourceId}`, error);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Count entries for a given source and transaction ID.
|
|
35
|
+
* Used to enforce maxPullsPerTransaction.
|
|
36
|
+
*/
|
|
37
|
+
async countByTransactionId(sourceId, sourceField, transactionId) {
|
|
38
|
+
try {
|
|
39
|
+
const snapshot = await this.getCollection()
|
|
40
|
+
.where(sourceField, "==", sourceId)
|
|
41
|
+
.where(LOTTERY_ENTRY_FIELDS.TRANSACTION_ID, "==", transactionId)
|
|
42
|
+
.get();
|
|
43
|
+
return snapshot.size;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
void normalizeError(error);
|
|
47
|
+
throw new DatabaseError(`Failed to count entries for transactionId=${transactionId}`, error);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Count entries for a given source and user.
|
|
52
|
+
* Used to enforce maxPullsPerUser.
|
|
53
|
+
*/
|
|
54
|
+
async countByUser(sourceId, sourceField, userId) {
|
|
55
|
+
try {
|
|
56
|
+
const snapshot = await this.getCollection()
|
|
57
|
+
.where(sourceField, "==", sourceId)
|
|
58
|
+
.where(LOTTERY_ENTRY_FIELDS.USER_ID, "==", userId)
|
|
59
|
+
.where(LOTTERY_ENTRY_FIELDS.STATUS, "!=", "cancelled")
|
|
60
|
+
.get();
|
|
61
|
+
return snapshot.size;
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
void normalizeError(error);
|
|
65
|
+
throw new DatabaseError(`Failed to count entries for userId=${userId}`, error);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** List all entries for an event/product source. Paginated via Sieve. */
|
|
69
|
+
async listForSource(sourceId, sourceField, model) {
|
|
70
|
+
return this.sieveQuery(model, LotteryEntryRepository.SIEVE_FIELDS, {
|
|
71
|
+
baseQuery: this.getCollection().where(sourceField, "==", sourceId),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** List entries for a specific user across all sources. */
|
|
75
|
+
async listForUser(userId, model) {
|
|
76
|
+
return this.sieveQuery(model, LotteryEntryRepository.SIEVE_FIELDS, {
|
|
77
|
+
baseQuery: this.getCollection().where(LOTTERY_ENTRY_FIELDS.USER_ID, "==", userId),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** Create a lottery entry with PII encryption. */
|
|
81
|
+
async createEntry(data) {
|
|
82
|
+
try {
|
|
83
|
+
const encrypted = encryptPiiFields(data, [...LOTTERY_ENTRY_PII_FIELDS]);
|
|
84
|
+
const prepared = prepareForFirestore({ ...encrypted });
|
|
85
|
+
const ref = await this.getCollection().add(prepared);
|
|
86
|
+
const created = await ref.get();
|
|
87
|
+
serverLogger.info("Lottery entry created", {
|
|
88
|
+
entryId: ref.id,
|
|
89
|
+
sourceType: data.sourceType,
|
|
90
|
+
eventId: data.eventId,
|
|
91
|
+
productId: data.productId,
|
|
92
|
+
userId: data.userId,
|
|
93
|
+
userLotteryNumber: data.userLotteryNumber,
|
|
94
|
+
});
|
|
95
|
+
return this.mapDoc(created);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
void normalizeError(error);
|
|
99
|
+
throw new DatabaseError("Failed to create lottery entry", error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** Flag a lottery entry as fraudulent. */
|
|
103
|
+
async flagEntry(entryId, flagNote, flaggedBy) {
|
|
104
|
+
try {
|
|
105
|
+
await this.getCollection().doc(entryId).update(prepareForFirestore({
|
|
106
|
+
isFlagged: true,
|
|
107
|
+
flagNote,
|
|
108
|
+
flaggedBy,
|
|
109
|
+
flaggedAt: new Date(),
|
|
110
|
+
status: "flagged",
|
|
111
|
+
}));
|
|
112
|
+
return this.findByIdOrFail(entryId);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
void normalizeError(error);
|
|
116
|
+
throw new DatabaseError(`Failed to flag lottery entry ${entryId}`, error);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
LotteryEntryRepository.SIEVE_FIELDS = {
|
|
121
|
+
sourceType: { canFilter: true, canSort: false },
|
|
122
|
+
eventId: { canFilter: true, canSort: false },
|
|
123
|
+
productId: { canFilter: true, canSort: false },
|
|
124
|
+
userId: { canFilter: true, canSort: false },
|
|
125
|
+
status: { canFilter: true, canSort: false },
|
|
126
|
+
submittedAt: { canFilter: true, canSort: true },
|
|
127
|
+
userLotteryNumber: { canFilter: false, canSort: true },
|
|
128
|
+
};
|
|
129
|
+
export { LotteryEntryRepository };
|
|
130
|
+
export const lotteryEntryRepository = new LotteryEntryRepository();
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LotteryEntry Firestore Document Type & Constants
|
|
3
|
+
*/
|
|
4
|
+
import type { LotteryEntryStatus } from "../types";
|
|
5
|
+
export declare const LOTTERY_ENTRIES_COLLECTION: "lotteryEntries";
|
|
6
|
+
export interface LotteryEntryDocument {
|
|
7
|
+
id: string;
|
|
8
|
+
sourceType: "event" | "product";
|
|
9
|
+
/** Set when sourceType === "event" */
|
|
10
|
+
eventId?: string;
|
|
11
|
+
/** Set when sourceType === "product" */
|
|
12
|
+
productId?: string;
|
|
13
|
+
userId: string;
|
|
14
|
+
userDisplayName?: string;
|
|
15
|
+
/** PII — encrypted at rest */
|
|
16
|
+
userEmail?: string;
|
|
17
|
+
/** PII — encrypted at rest; required */
|
|
18
|
+
userPhone: string;
|
|
19
|
+
/** User-provided external payment TX ID */
|
|
20
|
+
transactionId: string;
|
|
21
|
+
/** User-provided time of payment */
|
|
22
|
+
paymentTime: Date;
|
|
23
|
+
/** Slot/item number the user submitted for */
|
|
24
|
+
purchasedItemNumber: number;
|
|
25
|
+
/** Sequential per source (1, 2, 3…) — assigned atomically */
|
|
26
|
+
userLotteryNumber: number;
|
|
27
|
+
/** Filled after draw — assigned slot number */
|
|
28
|
+
assignedPrizeSlotNumber?: number;
|
|
29
|
+
status: LotteryEntryStatus;
|
|
30
|
+
isFlagged: boolean;
|
|
31
|
+
flagNote?: string;
|
|
32
|
+
flaggedBy?: string;
|
|
33
|
+
flaggedAt?: Date;
|
|
34
|
+
submittedAt: Date;
|
|
35
|
+
drawnAt?: Date;
|
|
36
|
+
}
|
|
37
|
+
export declare const LOTTERY_ENTRY_INDEXED_FIELDS: readonly ["sourceType", "eventId", "productId", "userId", "status", "submittedAt", "userLotteryNumber"];
|
|
38
|
+
/** Canonical field name constants for lotteryEntries collection — use instead of hardcoded strings. */
|
|
39
|
+
export declare const LOTTERY_ENTRY_FIELDS: {
|
|
40
|
+
readonly SOURCE_TYPE: "sourceType";
|
|
41
|
+
readonly EVENT_ID: "eventId";
|
|
42
|
+
readonly PRODUCT_ID: "productId";
|
|
43
|
+
readonly USER_ID: "userId";
|
|
44
|
+
readonly STATUS: "status";
|
|
45
|
+
readonly TRANSACTION_ID: "transactionId";
|
|
46
|
+
readonly SUBMITTED_AT: "submittedAt";
|
|
47
|
+
readonly USER_LOTTERY_NUMBER: "userLotteryNumber";
|
|
48
|
+
readonly IS_FLAGGED: "isFlagged";
|
|
49
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LotteryEntry Firestore Document Type & Constants
|
|
3
|
+
*/
|
|
4
|
+
export const LOTTERY_ENTRIES_COLLECTION = "lotteryEntries";
|
|
5
|
+
export const LOTTERY_ENTRY_INDEXED_FIELDS = [
|
|
6
|
+
"sourceType",
|
|
7
|
+
"eventId",
|
|
8
|
+
"productId",
|
|
9
|
+
"userId",
|
|
10
|
+
"status",
|
|
11
|
+
"submittedAt",
|
|
12
|
+
"userLotteryNumber",
|
|
13
|
+
];
|
|
14
|
+
/** Canonical field name constants for lotteryEntries collection — use instead of hardcoded strings. */
|
|
15
|
+
export const LOTTERY_ENTRY_FIELDS = {
|
|
16
|
+
SOURCE_TYPE: "sourceType",
|
|
17
|
+
EVENT_ID: "eventId",
|
|
18
|
+
PRODUCT_ID: "productId",
|
|
19
|
+
USER_ID: "userId",
|
|
20
|
+
STATUS: "status",
|
|
21
|
+
TRANSACTION_ID: "transactionId",
|
|
22
|
+
SUBMITTED_AT: "submittedAt",
|
|
23
|
+
USER_LOTTERY_NUMBER: "userLotteryNumber",
|
|
24
|
+
IS_FLAGGED: "isFlagged",
|
|
25
|
+
};
|