@mohasinac/appkit 3.0.6 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +42 -0
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +86 -0
- package/dist/_internal/client/features/lottery/LotteryAdminSlotView.d.ts +14 -0
- package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +46 -0
- package/dist/_internal/client/features/lottery/LotteryDetailView.d.ts +28 -0
- package/dist/_internal/client/features/lottery/LotteryDetailView.js +17 -0
- package/dist/_internal/client/features/lottery/LotteryEntriesView.d.ts +23 -0
- package/dist/_internal/client/features/lottery/LotteryEntriesView.js +58 -0
- package/dist/_internal/client/features/lottery/LotteryListView.d.ts +21 -0
- package/dist/_internal/client/features/lottery/LotteryListView.js +25 -0
- package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +19 -0
- package/dist/_internal/client/features/lottery/LotteryPullForm.js +65 -0
- package/dist/_internal/client/features/lottery/LotterySlotGrid.d.ts +13 -0
- package/dist/_internal/client/features/lottery/LotterySlotGrid.js +21 -0
- package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.d.ts +28 -0
- package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.js +18 -0
- package/dist/_internal/server/features/auctions/__tests__/service.test.d.ts +1 -0
- package/dist/_internal/server/features/auctions/__tests__/service.test.js +140 -0
- package/dist/_internal/server/features/checkout/__tests__/actions.test.d.ts +1 -0
- package/dist/_internal/server/features/checkout/__tests__/actions.test.js +600 -0
- package/dist/_internal/server/features/lottery/actions.d.ts +43 -0
- package/dist/_internal/server/features/lottery/actions.js +243 -0
- package/dist/_internal/server/features/lottery/adapters.d.ts +9 -0
- package/dist/_internal/server/features/lottery/adapters.js +21 -0
- package/dist/_internal/server/features/lottery/data.d.ts +20 -0
- package/dist/_internal/server/features/lottery/data.js +41 -0
- package/dist/_internal/server/features/lottery/metadata.d.ts +6 -0
- package/dist/_internal/server/features/lottery/metadata.js +31 -0
- package/dist/_internal/server/features/refunds/__tests__/actions.test.d.ts +1 -0
- package/dist/_internal/server/features/refunds/__tests__/actions.test.js +263 -0
- package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.js +249 -0
- package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.js +201 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.js +169 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.js +148 -0
- package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.js +210 -0
- package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.js +190 -0
- package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.js +161 -0
- package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.js +302 -0
- package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.js +252 -0
- package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.d.ts +1 -0
- package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.js +129 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +46 -0
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/client.d.ts +9 -0
- package/dist/client.js +9 -0
- package/dist/constants/field-names.d.ts +5 -1
- package/dist/constants/field-names.js +5 -1
- package/dist/features/addresses/repository/__tests__/addresses.repository.test.d.ts +1 -0
- package/dist/features/addresses/repository/__tests__/addresses.repository.test.js +386 -0
- package/dist/features/admin/api/bids/route.d.ts +2 -2
- package/dist/features/admin/api/coupons/route.d.ts +2 -2
- package/dist/features/admin/api/products/route.d.ts +2 -2
- package/dist/features/admin/api/reviews/route.d.ts +2 -2
- package/dist/features/admin/repository/__tests__/notification.repository.test.d.ts +1 -0
- package/dist/features/admin/repository/__tests__/notification.repository.test.js +356 -0
- package/dist/features/admin/repository/__tests__/site-settings.repository.test.d.ts +1 -0
- package/dist/features/admin/repository/__tests__/site-settings.repository.test.js +298 -0
- package/dist/features/auctions/repository/__tests__/bid.repository.test.d.ts +1 -0
- package/dist/features/auctions/repository/__tests__/bid.repository.test.js +290 -0
- package/dist/features/auth/api/route.d.ts +2 -2
- package/dist/features/auth/repository/__tests__/session.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/session.repository.test.js +310 -0
- package/dist/features/auth/repository/__tests__/session.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/session.test.js +146 -0
- package/dist/features/auth/repository/__tests__/sms-counter.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/sms-counter.repository.test.js +171 -0
- package/dist/features/auth/repository/__tests__/sms-counter.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/sms-counter.test.js +88 -0
- package/dist/features/auth/repository/__tests__/token.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/token.repository.test.js +260 -0
- package/dist/features/auth/repository/__tests__/token.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/token.test.js +140 -0
- package/dist/features/auth/repository/__tests__/user.repository.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/user.repository.test.js +352 -0
- package/dist/features/auth/repository/__tests__/user.test.d.ts +1 -0
- package/dist/features/auth/repository/__tests__/user.test.js +226 -0
- package/dist/features/auth/repository/session.repository.js +10 -1
- package/dist/features/cart/repository/__tests__/cart.repository.test.d.ts +1 -0
- package/dist/features/cart/repository/__tests__/cart.repository.test.js +371 -0
- package/dist/features/cart/repository/cart.repository.js +2 -2
- package/dist/features/categories/api/[id]/route.d.ts +4 -4
- package/dist/features/categories/api/route.d.ts +2 -2
- package/dist/features/categories/repository/__tests__/categories.repository.test.d.ts +1 -0
- package/dist/features/categories/repository/__tests__/categories.repository.test.js +813 -0
- package/dist/features/events/components/EventCard.js +1 -0
- package/dist/features/events/repository/__tests__/event.repository.test.d.ts +1 -0
- package/dist/features/events/repository/__tests__/event.repository.test.js +299 -0
- package/dist/features/events/schemas/firestore.d.ts +3 -0
- package/dist/features/events/schemas/index.d.ts +834 -14
- package/dist/features/events/schemas/index.js +80 -1
- package/dist/features/events/types/index.d.ts +4 -1
- package/dist/features/faq/repository/__tests__/faqs.repository.test.d.ts +1 -0
- package/dist/features/faq/repository/__tests__/faqs.repository.test.js +311 -0
- package/dist/features/history/repository/__tests__/user-history.repository.test.d.ts +1 -0
- package/dist/features/history/repository/__tests__/user-history.repository.test.js +180 -0
- package/dist/features/homepage/api/[id]/route.d.ts +4 -4
- package/dist/features/homepage/api/carousel/[id]/route.d.ts +4 -4
- package/dist/features/homepage/api/carousel/route.d.ts +2 -2
- package/dist/features/homepage/api/route.d.ts +2 -2
- package/dist/features/homepage/repository/__tests__/carousel.repository.test.d.ts +1 -0
- package/dist/features/homepage/repository/__tests__/carousel.repository.test.js +182 -0
- package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.d.ts +1 -0
- package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.js +185 -0
- package/dist/features/lottery/repository/lottery-entry.repository.d.ts +33 -0
- package/dist/features/lottery/repository/lottery-entry.repository.js +130 -0
- package/dist/features/lottery/schemas/firestore.d.ts +49 -0
- package/dist/features/lottery/schemas/firestore.js +25 -0
- package/dist/features/lottery/schemas/zod.d.ts +113 -0
- package/dist/features/lottery/schemas/zod.js +39 -0
- package/dist/features/lottery/types/index.d.ts +67 -0
- package/dist/features/lottery/types/index.js +55 -0
- package/dist/features/messages/repository/__tests__/conversations.repository.test.d.ts +1 -0
- package/dist/features/messages/repository/__tests__/conversations.repository.test.js +257 -0
- package/dist/features/orders/repository/__tests__/orders.repository.test.d.ts +1 -0
- package/dist/features/orders/repository/__tests__/orders.repository.test.js +211 -0
- package/dist/features/payments/repository/__tests__/payout.repository.test.d.ts +1 -0
- package/dist/features/payments/repository/__tests__/payout.repository.test.js +292 -0
- package/dist/features/products/api/[id]/route.d.ts +4 -4
- package/dist/features/products/api/route.d.ts +2 -2
- package/dist/features/products/repository/__tests__/product-features.repository.test.d.ts +1 -0
- package/dist/features/products/repository/__tests__/product-features.repository.test.js +329 -0
- package/dist/features/products/repository/__tests__/products.repository.test.d.ts +1 -0
- package/dist/features/products/repository/__tests__/products.repository.test.js +607 -0
- package/dist/features/products/schemas/firestore.d.ts +15 -1
- package/dist/features/products/schemas/index.d.ts +12 -12
- package/dist/features/products/schemas/index.js +4 -2
- package/dist/features/products/types/index.d.ts +1 -1
- package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.d.ts +1 -0
- package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.js +219 -0
- package/dist/features/promotions/repository/__tests__/coupons.repository.test.d.ts +1 -0
- package/dist/features/promotions/repository/__tests__/coupons.repository.test.js +480 -0
- package/dist/features/reviews/api/[id]/route.d.ts +4 -4
- package/dist/features/reviews/api/route.d.ts +2 -2
- package/dist/features/reviews/repository/__tests__/reviews.repository.test.d.ts +1 -0
- package/dist/features/reviews/repository/__tests__/reviews.repository.test.js +327 -0
- package/dist/features/scams/repository/__tests__/scammer.repository.test.d.ts +1 -0
- package/dist/features/scams/repository/__tests__/scammer.repository.test.js +385 -0
- package/dist/features/search/repository/__tests__/search.repository.test.d.ts +1 -0
- package/dist/features/search/repository/__tests__/search.repository.test.js +369 -0
- package/dist/features/seller/api/coupons/route.d.ts +2 -2
- package/dist/features/seller/api/offers/route.d.ts +2 -2
- package/dist/features/seller/api/products/route.d.ts +2 -2
- package/dist/features/seller/api/store/route.d.ts +2 -2
- package/dist/features/seller/repository/__tests__/offer.repository.test.d.ts +1 -0
- package/dist/features/seller/repository/__tests__/offer.repository.test.js +458 -0
- package/dist/features/stores/repository/__tests__/store.repository.test.d.ts +1 -0
- package/dist/features/stores/repository/__tests__/store.repository.test.js +261 -0
- package/dist/features/support/repository/__tests__/support.repository.test.d.ts +1 -0
- package/dist/features/support/repository/__tests__/support.repository.test.js +312 -0
- package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.d.ts +1 -0
- package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.js +139 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +17 -0
- package/dist/next/api/routeHandler.d.ts +1 -1
- package/dist/next/routing/route-map.d.ts +13 -0
- package/dist/next/routing/route-map.js +6 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +2 -0
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-schemas.d.ts +2 -0
- package/dist/security/pii-schemas.js +6 -0
- package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
- package/dist/seed/events-seed-data.js +85 -2
- package/dist/seed/grouped-listings-seed-data.d.ts +3 -1
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/lottery-entries-seed-data.d.ts +2 -0
- package/dist/seed/lottery-entries-seed-data.js +108 -0
- package/dist/seed/manifest.js +1 -0
- package/dist/seed/products-preorders-seed-data.d.ts +3 -1
- package/dist/server.d.ts +6 -0
- package/dist/server.js +6 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,607 @@
|
|
|
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
|
+
const { mockCacheManager } = vi.hoisted(() => ({
|
|
5
|
+
mockCacheManager: {
|
|
6
|
+
get: vi.fn().mockReturnValue(null),
|
|
7
|
+
set: vi.fn(),
|
|
8
|
+
delete: vi.fn(),
|
|
9
|
+
},
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("../../../../providers/db-firebase/admin", () => ({
|
|
12
|
+
getAdminDb: () => db,
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
|
|
15
|
+
const actual = await importOriginal();
|
|
16
|
+
return {
|
|
17
|
+
...actual,
|
|
18
|
+
prepareForFirestore: (d) => d,
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
vi.mock("../../../../contracts/field-ops", () => ({
|
|
22
|
+
serverTimestamp: () => new Date(),
|
|
23
|
+
increment: (n) => n,
|
|
24
|
+
arrayUnion: (...args) => args,
|
|
25
|
+
arrayRemove: (...args) => args,
|
|
26
|
+
deleteField: () => null,
|
|
27
|
+
registerFieldOps: vi.fn(),
|
|
28
|
+
}));
|
|
29
|
+
vi.mock("../../../../core", () => ({
|
|
30
|
+
cacheManager: mockCacheManager,
|
|
31
|
+
}));
|
|
32
|
+
vi.mock("../../../../errors/normalize", () => ({ normalizeError: vi.fn() }));
|
|
33
|
+
vi.mock("../../../../monitoring", () => ({
|
|
34
|
+
serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
35
|
+
}));
|
|
36
|
+
// buildSearchTokens returns an array of tokens — mock as passthrough
|
|
37
|
+
vi.mock("../../../../utils", () => ({
|
|
38
|
+
generateUniqueId: vi.fn(async (gen, exists) => {
|
|
39
|
+
const candidate = gen(0);
|
|
40
|
+
const taken = await exists(candidate);
|
|
41
|
+
return taken ? gen(1) : candidate;
|
|
42
|
+
}),
|
|
43
|
+
slugify: (s) => s.toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, ""),
|
|
44
|
+
buildSearchTokens: (...args) => args.flat().filter(Boolean),
|
|
45
|
+
tokenizeQuery: (q) => q.trim() ? q.trim().toLowerCase().split(/\s+/) : [],
|
|
46
|
+
generateBarcodeId: (id) => Promise.resolve(`barcode-${id}`),
|
|
47
|
+
}));
|
|
48
|
+
import { ProductRepository } from "../products.repository";
|
|
49
|
+
const repo = new ProductRepository();
|
|
50
|
+
function makeProductDoc(overrides = {}) {
|
|
51
|
+
return {
|
|
52
|
+
id: "product-charizard-psa9",
|
|
53
|
+
slug: "product-charizard-psa9",
|
|
54
|
+
title: "Charizard PSA 9",
|
|
55
|
+
description: "1st Edition base set Charizard PSA 9",
|
|
56
|
+
brand: "Pokemon",
|
|
57
|
+
brandSlug: "brand-pokemon",
|
|
58
|
+
storeId: "store-pokemon-palace",
|
|
59
|
+
categorySlug: "category-trading-cards",
|
|
60
|
+
categorySlugs: ["category-trading-cards", "category-pokemon"],
|
|
61
|
+
listingType: "standard",
|
|
62
|
+
status: "published",
|
|
63
|
+
price: 50000,
|
|
64
|
+
currency: "INR",
|
|
65
|
+
stockQuantity: 1,
|
|
66
|
+
availableQuantity: 1,
|
|
67
|
+
currentBid: 0,
|
|
68
|
+
bidCount: 0,
|
|
69
|
+
viewCount: 0,
|
|
70
|
+
searchTokens: ["charizard", "psa9"],
|
|
71
|
+
barcodeId: "barcode-product-charizard-psa9",
|
|
72
|
+
images: [],
|
|
73
|
+
tags: [],
|
|
74
|
+
features: [],
|
|
75
|
+
createdAt: new Date(),
|
|
76
|
+
updatedAt: new Date(),
|
|
77
|
+
...overrides,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
beforeEach(() => {
|
|
81
|
+
vi.clearAllMocks();
|
|
82
|
+
mockCacheManager.get.mockReturnValue(null);
|
|
83
|
+
db.collection.mockReturnValue(mockCollection);
|
|
84
|
+
mockCollection.doc.mockReturnValue(mockDocRef);
|
|
85
|
+
mockCollection.where.mockReturnValue(mockQuery);
|
|
86
|
+
mockCollection.get = vi.fn().mockResolvedValue(makeQuerySnap([]));
|
|
87
|
+
mockQuery.where.mockReturnValue(mockQuery);
|
|
88
|
+
mockQuery.orderBy.mockReturnValue(mockQuery);
|
|
89
|
+
mockQuery.limit.mockReturnValue(mockQuery);
|
|
90
|
+
mockQuery.offset = vi.fn().mockReturnValue(mockQuery);
|
|
91
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
92
|
+
mockQuery.count.mockReturnValue({
|
|
93
|
+
get: vi.fn().mockResolvedValue({ data: () => ({ count: 0 }) }),
|
|
94
|
+
});
|
|
95
|
+
mockDocRef.get.mockResolvedValue(makeSnap(makeProductDoc(), "product-charizard-psa9"));
|
|
96
|
+
mockDocRef.set.mockResolvedValue(undefined);
|
|
97
|
+
mockDocRef.update.mockResolvedValue(undefined);
|
|
98
|
+
mockDocRef.delete.mockResolvedValue(undefined);
|
|
99
|
+
db.batch.mockReturnValue(mockBatch);
|
|
100
|
+
db.getAll = vi.fn().mockResolvedValue([]);
|
|
101
|
+
});
|
|
102
|
+
// ---------------------------------------------------------------------------
|
|
103
|
+
// findById — caching
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
describe("ProductRepository.findById — caching", () => {
|
|
106
|
+
it("returns cached product without hitting Firestore on warm cache", async () => {
|
|
107
|
+
const cached = makeProductDoc();
|
|
108
|
+
mockCacheManager.get.mockReturnValue(cached);
|
|
109
|
+
const result = await repo.findById("product-charizard-psa9");
|
|
110
|
+
expect(result?.id).toBe("product-charizard-psa9");
|
|
111
|
+
expect(mockDocRef.get).not.toHaveBeenCalled();
|
|
112
|
+
});
|
|
113
|
+
it("fetches from Firestore on cache miss and caches result", async () => {
|
|
114
|
+
mockCacheManager.get.mockReturnValue(null);
|
|
115
|
+
const product = makeProductDoc();
|
|
116
|
+
mockDocRef.get.mockResolvedValue(makeSnap(product, "product-charizard-psa9"));
|
|
117
|
+
await repo.findById("product-charizard-psa9");
|
|
118
|
+
expect(mockDocRef.get).toHaveBeenCalledOnce();
|
|
119
|
+
expect(mockCacheManager.set).toHaveBeenCalledWith(expect.stringContaining("product-charizard-psa9"), expect.any(Object), expect.objectContaining({ ttl: 30000 }));
|
|
120
|
+
});
|
|
121
|
+
it("returns null when product does not exist (Firestore miss)", async () => {
|
|
122
|
+
mockCacheManager.get.mockReturnValue(null);
|
|
123
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
124
|
+
const result = await repo.findById("nonexistent");
|
|
125
|
+
expect(result).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
it("does NOT cache null results (product not found)", async () => {
|
|
128
|
+
mockCacheManager.get.mockReturnValue(null);
|
|
129
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
130
|
+
await repo.findById("nonexistent");
|
|
131
|
+
expect(mockCacheManager.set).not.toHaveBeenCalled();
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// delete — cache invalidation
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
describe("ProductRepository.delete — cache invalidation", () => {
|
|
138
|
+
it("invalidates cache entry before deleting", async () => {
|
|
139
|
+
await repo.delete("product-charizard-psa9");
|
|
140
|
+
expect(mockCacheManager.delete).toHaveBeenCalledWith(expect.stringContaining("product-charizard-psa9"));
|
|
141
|
+
// delete happens before or alongside cache invalidation
|
|
142
|
+
expect(mockDocRef.delete).toHaveBeenCalledOnce();
|
|
143
|
+
});
|
|
144
|
+
it("deletes the Firestore document", async () => {
|
|
145
|
+
await repo.delete("product-charizard-psa9");
|
|
146
|
+
expect(mockDocRef.delete).toHaveBeenCalledOnce();
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
// ---------------------------------------------------------------------------
|
|
150
|
+
// create
|
|
151
|
+
// ---------------------------------------------------------------------------
|
|
152
|
+
describe("ProductRepository.create", () => {
|
|
153
|
+
beforeEach(() => {
|
|
154
|
+
// After doc.set, findById is called to re-read — return the stored product
|
|
155
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
156
|
+
});
|
|
157
|
+
it("uses title to generate the slug when no explicit slug provided", async () => {
|
|
158
|
+
await repo.create({
|
|
159
|
+
title: "Hot Wheels Redline Vintage",
|
|
160
|
+
storeId: "store-diecast-depot",
|
|
161
|
+
price: 5000,
|
|
162
|
+
currency: "INR",
|
|
163
|
+
stockQuantity: 3,
|
|
164
|
+
listingType: "standard",
|
|
165
|
+
});
|
|
166
|
+
// generateUniqueId calls gen(0) = slugify("Hot Wheels Redline Vintage")
|
|
167
|
+
expect(mockCollection.doc).toHaveBeenCalledWith("hot-wheels-redline-vintage");
|
|
168
|
+
});
|
|
169
|
+
it("uses explicit slug when provided", async () => {
|
|
170
|
+
await repo.create({
|
|
171
|
+
title: "Anything",
|
|
172
|
+
slug: "product-custom-slug",
|
|
173
|
+
storeId: "store-1",
|
|
174
|
+
price: 1000,
|
|
175
|
+
currency: "INR",
|
|
176
|
+
stockQuantity: 1,
|
|
177
|
+
listingType: "standard",
|
|
178
|
+
});
|
|
179
|
+
expect(mockCollection.doc).toHaveBeenCalledWith("product-custom-slug");
|
|
180
|
+
});
|
|
181
|
+
it("builds and stores searchTokens", async () => {
|
|
182
|
+
await repo.create({
|
|
183
|
+
title: "Charizard",
|
|
184
|
+
storeId: "store-1",
|
|
185
|
+
price: 1000,
|
|
186
|
+
currency: "INR",
|
|
187
|
+
stockQuantity: 1,
|
|
188
|
+
listingType: "standard",
|
|
189
|
+
});
|
|
190
|
+
const setArg = mockDocRef.set.mock.calls[0][0];
|
|
191
|
+
expect(Array.isArray(setArg.searchTokens)).toBe(true);
|
|
192
|
+
});
|
|
193
|
+
it("sets availableQuantity from stockQuantity", async () => {
|
|
194
|
+
await repo.create({
|
|
195
|
+
title: "Test Product",
|
|
196
|
+
storeId: "store-1",
|
|
197
|
+
price: 1000,
|
|
198
|
+
currency: "INR",
|
|
199
|
+
stockQuantity: 5,
|
|
200
|
+
listingType: "standard",
|
|
201
|
+
});
|
|
202
|
+
const setArg = mockDocRef.set.mock.calls[0][0];
|
|
203
|
+
expect(setArg.availableQuantity).toBe(5);
|
|
204
|
+
});
|
|
205
|
+
it("generates barcodeId when not provided", async () => {
|
|
206
|
+
await repo.create({
|
|
207
|
+
title: "New Product",
|
|
208
|
+
storeId: "store-1",
|
|
209
|
+
price: 1000,
|
|
210
|
+
currency: "INR",
|
|
211
|
+
stockQuantity: 1,
|
|
212
|
+
listingType: "standard",
|
|
213
|
+
});
|
|
214
|
+
const setArg = mockDocRef.set.mock.calls[0][0];
|
|
215
|
+
expect(setArg.barcodeId).toBeDefined();
|
|
216
|
+
expect(typeof setArg.barcodeId).toBe("string");
|
|
217
|
+
});
|
|
218
|
+
it("uses provided barcodeId when given", async () => {
|
|
219
|
+
await repo.create({
|
|
220
|
+
title: "New Product",
|
|
221
|
+
barcodeId: "custom-barcode-123",
|
|
222
|
+
storeId: "store-1",
|
|
223
|
+
price: 1000,
|
|
224
|
+
currency: "INR",
|
|
225
|
+
stockQuantity: 1,
|
|
226
|
+
listingType: "standard",
|
|
227
|
+
});
|
|
228
|
+
const setArg = mockDocRef.set.mock.calls[0][0];
|
|
229
|
+
expect(setArg.barcodeId).toBe("custom-barcode-123");
|
|
230
|
+
});
|
|
231
|
+
it("caches the newly created product", async () => {
|
|
232
|
+
await repo.create({
|
|
233
|
+
title: "Cache Me",
|
|
234
|
+
storeId: "store-1",
|
|
235
|
+
price: 1000,
|
|
236
|
+
currency: "INR",
|
|
237
|
+
stockQuantity: 1,
|
|
238
|
+
listingType: "standard",
|
|
239
|
+
});
|
|
240
|
+
expect(mockCacheManager.set).toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
it("sets createdAt and updatedAt timestamps", async () => {
|
|
243
|
+
await repo.create({
|
|
244
|
+
title: "New Product",
|
|
245
|
+
storeId: "store-1",
|
|
246
|
+
price: 1000,
|
|
247
|
+
currency: "INR",
|
|
248
|
+
stockQuantity: 1,
|
|
249
|
+
listingType: "standard",
|
|
250
|
+
});
|
|
251
|
+
const setArg = mockDocRef.set.mock.calls[0][0];
|
|
252
|
+
expect(setArg.createdAt).toBeInstanceOf(Date);
|
|
253
|
+
expect(setArg.updatedAt).toBeInstanceOf(Date);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
// ---------------------------------------------------------------------------
|
|
257
|
+
// updateBid
|
|
258
|
+
// ---------------------------------------------------------------------------
|
|
259
|
+
describe("ProductRepository.updateBid", () => {
|
|
260
|
+
it("updates currentBid field", async () => {
|
|
261
|
+
const product = makeProductDoc();
|
|
262
|
+
mockDocRef.get.mockResolvedValue(makeSnap(product, product.id));
|
|
263
|
+
await repo.updateBid("product-charizard-psa9", 75000, 5);
|
|
264
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
265
|
+
expect(updateArg.currentBid).toBe(75000);
|
|
266
|
+
});
|
|
267
|
+
it("updates bidCount field", async () => {
|
|
268
|
+
const product = makeProductDoc();
|
|
269
|
+
mockDocRef.get.mockResolvedValue(makeSnap(product, product.id));
|
|
270
|
+
await repo.updateBid("product-charizard-psa9", 75000, 5);
|
|
271
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
272
|
+
expect(updateArg.bidCount).toBe(5);
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// startGroup
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
describe("ProductRepository.startGroup", () => {
|
|
279
|
+
it("sets groupId, isGroupParent: true, and groupChildSlugs: []", async () => {
|
|
280
|
+
await repo.startGroup("product-charizard-psa9", "group-pokemon-starter-bundle");
|
|
281
|
+
expect(mockDocRef.update).toHaveBeenCalledWith({
|
|
282
|
+
groupId: "group-pokemon-starter-bundle",
|
|
283
|
+
isGroupParent: true,
|
|
284
|
+
groupChildSlugs: [],
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
it("calls doc(productId) to target the correct product", async () => {
|
|
288
|
+
await repo.startGroup("product-abc", "group-xyz");
|
|
289
|
+
expect(mockCollection.doc).toHaveBeenCalledWith("product-abc");
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
// ---------------------------------------------------------------------------
|
|
293
|
+
// dissolveGroup
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
describe("ProductRepository.dissolveGroup", () => {
|
|
296
|
+
it("clears groupId and group fields from all members in a batch", async () => {
|
|
297
|
+
const members = [
|
|
298
|
+
makeProductDoc({ id: "product-1", groupId: "group-1" }),
|
|
299
|
+
makeProductDoc({ id: "product-2", groupId: "group-1" }),
|
|
300
|
+
];
|
|
301
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(members.map((m) => ({ id: m.id, data: m }))));
|
|
302
|
+
await repo.dissolveGroup("group-1");
|
|
303
|
+
expect(mockBatch.update).toHaveBeenCalledTimes(2);
|
|
304
|
+
const calls = mockBatch.update.mock.calls;
|
|
305
|
+
expect(calls.every((c) => c[1].groupId === null)).toBe(true);
|
|
306
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
307
|
+
});
|
|
308
|
+
it("does not call batch.update when group has no members", async () => {
|
|
309
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
310
|
+
await repo.dissolveGroup("group-empty");
|
|
311
|
+
expect(mockBatch.update).not.toHaveBeenCalled();
|
|
312
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
313
|
+
});
|
|
314
|
+
it("clears all group fields (isGroupParent, groupParentSlug, groupChildSlugs, groupTitle)", async () => {
|
|
315
|
+
const parent = makeProductDoc({ id: "p-1", groupId: "group-1", isGroupParent: true });
|
|
316
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "p-1", data: parent }]));
|
|
317
|
+
await repo.dissolveGroup("group-1");
|
|
318
|
+
const updateArg = mockBatch.update.mock.calls[0][1];
|
|
319
|
+
expect(updateArg.isGroupParent).toBeNull();
|
|
320
|
+
expect(updateArg.groupParentSlug).toBeNull();
|
|
321
|
+
expect(updateArg.groupChildSlugs).toBeNull();
|
|
322
|
+
expect(updateArg.groupTitle).toBeNull();
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
// ---------------------------------------------------------------------------
|
|
326
|
+
// linkChildToGroup
|
|
327
|
+
// ---------------------------------------------------------------------------
|
|
328
|
+
describe("ProductRepository.linkChildToGroup", () => {
|
|
329
|
+
it("batch-updates child with groupId, groupParentSlug", async () => {
|
|
330
|
+
const parent = makeProductDoc({
|
|
331
|
+
id: "product-parent",
|
|
332
|
+
groupId: "group-1",
|
|
333
|
+
groupChildSlugs: [],
|
|
334
|
+
slug: "product-parent",
|
|
335
|
+
});
|
|
336
|
+
const child = makeProductDoc({ id: "product-child", slug: "product-child" });
|
|
337
|
+
await repo.linkChildToGroup(parent, child);
|
|
338
|
+
const calls = mockBatch.update.mock.calls;
|
|
339
|
+
const childUpdate = calls.find((c) => c[1].groupId === "group-1");
|
|
340
|
+
expect(childUpdate).toBeDefined();
|
|
341
|
+
expect(childUpdate[1].groupParentSlug).toBe("product-parent");
|
|
342
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
343
|
+
});
|
|
344
|
+
it("appends child id to parent's groupChildSlugs", async () => {
|
|
345
|
+
const parent = makeProductDoc({
|
|
346
|
+
id: "product-parent",
|
|
347
|
+
groupId: "group-1",
|
|
348
|
+
groupChildSlugs: ["product-existing"],
|
|
349
|
+
slug: "product-parent",
|
|
350
|
+
});
|
|
351
|
+
const child = makeProductDoc({ id: "product-new-child", slug: "product-new-child" });
|
|
352
|
+
await repo.linkChildToGroup(parent, child);
|
|
353
|
+
const calls = mockBatch.update.mock.calls;
|
|
354
|
+
const parentUpdate = calls.find((c) => Array.isArray(c[1].groupChildSlugs));
|
|
355
|
+
expect(parentUpdate[1].groupChildSlugs).toContain("product-new-child");
|
|
356
|
+
expect(parentUpdate[1].groupChildSlugs).toContain("product-existing");
|
|
357
|
+
});
|
|
358
|
+
});
|
|
359
|
+
// ---------------------------------------------------------------------------
|
|
360
|
+
// unlinkChildFromGroup
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
describe("ProductRepository.unlinkChildFromGroup", () => {
|
|
363
|
+
it("clears child's groupId and groupParentSlug", async () => {
|
|
364
|
+
const parent = makeProductDoc({ id: "p-parent", groupChildSlugs: ["p-child"], slug: "p-parent" });
|
|
365
|
+
const child = makeProductDoc({ id: "p-child", groupId: "group-1", groupParentSlug: "p-parent" });
|
|
366
|
+
await repo.unlinkChildFromGroup(parent, child);
|
|
367
|
+
const calls = mockBatch.update.mock.calls;
|
|
368
|
+
const childUpdate = calls.find((c) => c[1].groupId === null);
|
|
369
|
+
expect(childUpdate).toBeDefined();
|
|
370
|
+
expect(childUpdate[1].groupParentSlug).toBeNull();
|
|
371
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
372
|
+
});
|
|
373
|
+
it("removes child from parent's groupChildSlugs", async () => {
|
|
374
|
+
const parent = makeProductDoc({
|
|
375
|
+
id: "p-parent",
|
|
376
|
+
groupChildSlugs: ["p-child", "p-other"],
|
|
377
|
+
slug: "p-parent",
|
|
378
|
+
});
|
|
379
|
+
const child = makeProductDoc({ id: "p-child", slug: "p-child" });
|
|
380
|
+
await repo.unlinkChildFromGroup(parent, child);
|
|
381
|
+
const calls = mockBatch.update.mock.calls;
|
|
382
|
+
const parentUpdate = calls.find((c) => Array.isArray(c[1].groupChildSlugs));
|
|
383
|
+
expect(parentUpdate[1].groupChildSlugs).not.toContain("p-child");
|
|
384
|
+
expect(parentUpdate[1].groupChildSlugs).toContain("p-other");
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
// ---------------------------------------------------------------------------
|
|
388
|
+
// getExpiredAuctions
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
describe("ProductRepository.getExpiredAuctions", () => {
|
|
391
|
+
it("queries listingType==auction, auctionEndDate < now, status==published", async () => {
|
|
392
|
+
const now = new Date();
|
|
393
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
394
|
+
await repo.getExpiredAuctions(now);
|
|
395
|
+
expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("listingType"), "==", "auction");
|
|
396
|
+
expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("auctionEndDate"), "<", now);
|
|
397
|
+
expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("status"), "==", "published");
|
|
398
|
+
});
|
|
399
|
+
it("limits to 500 documents", async () => {
|
|
400
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
401
|
+
await repo.getExpiredAuctions(new Date());
|
|
402
|
+
expect(mockQuery.limit).toHaveBeenCalledWith(500);
|
|
403
|
+
});
|
|
404
|
+
it("returns { id, ref, data } tuples", async () => {
|
|
405
|
+
const auction = makeProductDoc({ id: "auction-expired", listingType: "auction" });
|
|
406
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "auction-expired", data: auction }]));
|
|
407
|
+
const result = await repo.getExpiredAuctions(new Date());
|
|
408
|
+
expect(result).toHaveLength(1);
|
|
409
|
+
expect(result[0].id).toBe("auction-expired");
|
|
410
|
+
expect(result[0].data).toBeDefined();
|
|
411
|
+
expect(result[0].ref).toBeDefined();
|
|
412
|
+
});
|
|
413
|
+
it("returns empty array when no expired auctions", async () => {
|
|
414
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
415
|
+
const result = await repo.getExpiredAuctions(new Date());
|
|
416
|
+
expect(result).toEqual([]);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
// ---------------------------------------------------------------------------
|
|
420
|
+
// updateStatusInBatch
|
|
421
|
+
// ---------------------------------------------------------------------------
|
|
422
|
+
describe("ProductRepository.updateStatusInBatch", () => {
|
|
423
|
+
it("stages status update in the caller's batch without committing", () => {
|
|
424
|
+
const callerBatch = { update: vi.fn(), commit: vi.fn() };
|
|
425
|
+
repo.updateStatusInBatch(callerBatch, "product-1", "draft");
|
|
426
|
+
expect(callerBatch.update).toHaveBeenCalledOnce();
|
|
427
|
+
expect(callerBatch.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ status: "draft" }));
|
|
428
|
+
expect(callerBatch.commit).not.toHaveBeenCalled();
|
|
429
|
+
});
|
|
430
|
+
it("sets updatedAt timestamp in the staged update", () => {
|
|
431
|
+
const callerBatch = { update: vi.fn(), commit: vi.fn() };
|
|
432
|
+
repo.updateStatusInBatch(callerBatch, "product-1", "suspended");
|
|
433
|
+
expect(callerBatch.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
434
|
+
});
|
|
435
|
+
});
|
|
436
|
+
// ---------------------------------------------------------------------------
|
|
437
|
+
// incrementBidCountInBatch
|
|
438
|
+
// ---------------------------------------------------------------------------
|
|
439
|
+
describe("ProductRepository.incrementBidCountInBatch", () => {
|
|
440
|
+
it("stages bid count update without committing", () => {
|
|
441
|
+
const callerBatch = { update: vi.fn(), commit: vi.fn() };
|
|
442
|
+
repo.incrementBidCountInBatch(callerBatch, "auction-1", 5000);
|
|
443
|
+
expect(callerBatch.update).toHaveBeenCalledOnce();
|
|
444
|
+
expect(callerBatch.commit).not.toHaveBeenCalled();
|
|
445
|
+
});
|
|
446
|
+
it("sets currentBid to the provided amount", () => {
|
|
447
|
+
const callerBatch = { update: vi.fn(), commit: vi.fn() };
|
|
448
|
+
repo.incrementBidCountInBatch(callerBatch, "auction-1", 7500);
|
|
449
|
+
expect(callerBatch.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ currentBid: 7500 }));
|
|
450
|
+
});
|
|
451
|
+
it("sets bidsHaveStarted: true on first bid", () => {
|
|
452
|
+
const callerBatch = { update: vi.fn(), commit: vi.fn() };
|
|
453
|
+
repo.incrementBidCountInBatch(callerBatch, "auction-1", 1000);
|
|
454
|
+
expect(callerBatch.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ bidsHaveStarted: true }));
|
|
455
|
+
});
|
|
456
|
+
it("includes leadingBidderId when provided", () => {
|
|
457
|
+
const callerBatch = { update: vi.fn(), commit: vi.fn() };
|
|
458
|
+
repo.incrementBidCountInBatch(callerBatch, "auction-1", 5000, "user-ravi");
|
|
459
|
+
expect(callerBatch.update).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({ leadingBidderId: "user-ravi" }));
|
|
460
|
+
});
|
|
461
|
+
it("does NOT include leadingBidderId when not provided", () => {
|
|
462
|
+
const callerBatch = { update: vi.fn(), commit: vi.fn() };
|
|
463
|
+
repo.incrementBidCountInBatch(callerBatch, "auction-1", 5000);
|
|
464
|
+
const updateArg = callerBatch.update.mock.calls[0][1];
|
|
465
|
+
expect(updateArg).not.toHaveProperty("leadingBidderId");
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
// ---------------------------------------------------------------------------
|
|
469
|
+
// FILTER_ALIASES — pure function tests
|
|
470
|
+
// ---------------------------------------------------------------------------
|
|
471
|
+
describe("ProductRepository.FILTER_ALIASES — listingType", () => {
|
|
472
|
+
const aliases = ProductRepository.FILTER_ALIASES;
|
|
473
|
+
it("canonical 'auction' → listingType==auction", () => {
|
|
474
|
+
expect(aliases.listingType("auction", "==")).toContain("auction");
|
|
475
|
+
});
|
|
476
|
+
it("legacy 'preorder' → normalises to pre-order", () => {
|
|
477
|
+
const result = aliases.listingType("preorder", "==");
|
|
478
|
+
expect(result).toContain("pre-order");
|
|
479
|
+
});
|
|
480
|
+
it("legacy 'product' → normalises to standard", () => {
|
|
481
|
+
const result = aliases.listingType("product", "==");
|
|
482
|
+
expect(result).toContain("standard");
|
|
483
|
+
});
|
|
484
|
+
it("'standard' → listingType==standard", () => {
|
|
485
|
+
const result = aliases.listingType("standard", "==");
|
|
486
|
+
expect(result).toContain("standard");
|
|
487
|
+
});
|
|
488
|
+
it("unknown value → returns empty string (dropped)", () => {
|
|
489
|
+
expect(aliases.listingType("unknown-type", "==")).toBe("");
|
|
490
|
+
});
|
|
491
|
+
it("!= operator produces listingType!=<value>", () => {
|
|
492
|
+
const result = aliases.listingType("auction", "!=");
|
|
493
|
+
expect(result).toContain("!=");
|
|
494
|
+
expect(result).toContain("auction");
|
|
495
|
+
});
|
|
496
|
+
it("unsupported operator → returns empty string", () => {
|
|
497
|
+
expect(aliases.listingType("auction", ">")).toBe("");
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
describe("ProductRepository.FILTER_ALIASES — scope", () => {
|
|
501
|
+
const aliases = ProductRepository.FILTER_ALIASES;
|
|
502
|
+
it("scope==publicProducts → status==published,listingType==standard", () => {
|
|
503
|
+
const result = aliases.scope("publicProducts", "==");
|
|
504
|
+
expect(result).toContain("status");
|
|
505
|
+
expect(result).toContain("published");
|
|
506
|
+
expect(result).toContain("standard");
|
|
507
|
+
});
|
|
508
|
+
it("scope==publicAuctions → status==published,listingType==auction", () => {
|
|
509
|
+
const result = aliases.scope("publicAuctions", "==");
|
|
510
|
+
expect(result).toContain("published");
|
|
511
|
+
expect(result).toContain("auction");
|
|
512
|
+
});
|
|
513
|
+
it("scope==published → status==published only", () => {
|
|
514
|
+
const result = aliases.scope("published", "==");
|
|
515
|
+
expect(result).toContain("published");
|
|
516
|
+
expect(result).not.toContain("listingType");
|
|
517
|
+
});
|
|
518
|
+
it("scope with != operator → returns empty string", () => {
|
|
519
|
+
expect(aliases.scope("published", "!=")).toBe("");
|
|
520
|
+
});
|
|
521
|
+
it("unknown scope value → returns empty string", () => {
|
|
522
|
+
expect(aliases.scope("someUnknownScope", "==")).toBe("");
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
describe("ProductRepository.FILTER_ALIASES — other aliases", () => {
|
|
526
|
+
const aliases = ProductRepository.FILTER_ALIASES;
|
|
527
|
+
it("promoted==true → status==published,isPromoted==true", () => {
|
|
528
|
+
const result = aliases.promoted("true", "==");
|
|
529
|
+
expect(result).toContain("published");
|
|
530
|
+
expect(result).toContain("isPromoted");
|
|
531
|
+
});
|
|
532
|
+
it("promoted==false → returns empty string (not supported)", () => {
|
|
533
|
+
expect(aliases.promoted("false", "==")).toBe("");
|
|
534
|
+
});
|
|
535
|
+
it("featuredPublic==true → status==published,isFeatured==true", () => {
|
|
536
|
+
const result = aliases.featuredPublic("true", "==");
|
|
537
|
+
expect(result).toContain("published");
|
|
538
|
+
expect(result).toContain("featured");
|
|
539
|
+
});
|
|
540
|
+
it("category==slug → expands to categorySlugs@=slug (array-contains)", () => {
|
|
541
|
+
const result = aliases.category("category-trading-cards", "==");
|
|
542
|
+
expect(result).toContain("categorySlugs");
|
|
543
|
+
expect(result).toContain("category-trading-cards");
|
|
544
|
+
expect(result).toContain("@=");
|
|
545
|
+
});
|
|
546
|
+
});
|
|
547
|
+
// ---------------------------------------------------------------------------
|
|
548
|
+
// findByCategory
|
|
549
|
+
// ---------------------------------------------------------------------------
|
|
550
|
+
describe("ProductRepository.findByCategory", () => {
|
|
551
|
+
it("queries categorySlugs array-contains the category slug", async () => {
|
|
552
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
553
|
+
await repo.findByCategory("category-trading-cards");
|
|
554
|
+
expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("categorySlugs"), "array-contains", "category-trading-cards");
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
// incrementViewCount
|
|
559
|
+
// ---------------------------------------------------------------------------
|
|
560
|
+
describe("ProductRepository.incrementViewCount", () => {
|
|
561
|
+
it("increments viewCount field (fire-and-forget)", async () => {
|
|
562
|
+
await repo.incrementViewCount("product-1");
|
|
563
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ viewCount: 1 }));
|
|
564
|
+
});
|
|
565
|
+
it("does not throw when Firestore update fails", async () => {
|
|
566
|
+
mockDocRef.update.mockRejectedValue(new Error("Firestore error"));
|
|
567
|
+
await expect(repo.incrementViewCount("product-1")).resolves.toBeUndefined();
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
// ---------------------------------------------------------------------------
|
|
571
|
+
// deleteByStore
|
|
572
|
+
// ---------------------------------------------------------------------------
|
|
573
|
+
describe("ProductRepository.deleteByStore", () => {
|
|
574
|
+
it("returns 0 when no products exist for the store", async () => {
|
|
575
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
576
|
+
const count = await repo.deleteByStore("store-empty");
|
|
577
|
+
expect(count).toBe(0);
|
|
578
|
+
});
|
|
579
|
+
it("batch-deletes all products for the store and returns count", async () => {
|
|
580
|
+
const products = [
|
|
581
|
+
makeProductDoc({ id: "p-1", storeId: "store-1" }),
|
|
582
|
+
makeProductDoc({ id: "p-2", storeId: "store-1" }),
|
|
583
|
+
];
|
|
584
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(products.map((p) => ({ id: p.id, data: p }))));
|
|
585
|
+
const count = await repo.deleteByStore("store-1");
|
|
586
|
+
expect(count).toBe(2);
|
|
587
|
+
expect(mockBatch.delete).toHaveBeenCalledTimes(2);
|
|
588
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
589
|
+
});
|
|
590
|
+
});
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
// getStaleDraftRefs
|
|
593
|
+
// ---------------------------------------------------------------------------
|
|
594
|
+
describe("ProductRepository.getStaleDraftRefs", () => {
|
|
595
|
+
it("queries status==draft and updatedAt < cutoff", async () => {
|
|
596
|
+
const cutoff = new Date("2026-01-01");
|
|
597
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
598
|
+
await repo.getStaleDraftRefs(cutoff);
|
|
599
|
+
expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("status"), "==", "draft");
|
|
600
|
+
expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("updatedAt"), "<", cutoff);
|
|
601
|
+
});
|
|
602
|
+
it("limits to 200 documents", async () => {
|
|
603
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
604
|
+
await repo.getStaleDraftRefs(new Date());
|
|
605
|
+
expect(mockQuery.limit).toHaveBeenCalledWith(200);
|
|
606
|
+
});
|
|
607
|
+
});
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { type GenerateProductIdInput, type GenerateAuctionIdInput, type GeneratePreOrderIdInput } from "../../../utils/id-generators";
|
|
5
5
|
import type { ProductStatus, ListingType } from "../types";
|
|
6
6
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
7
|
+
import type { LotteryConfig } from "../../lottery/types";
|
|
7
8
|
export interface ProductVideoField {
|
|
8
9
|
url: string;
|
|
9
10
|
thumbnailUrl: string;
|
|
@@ -142,7 +143,7 @@ export interface ProductDocument extends BaseDocument {
|
|
|
142
143
|
features?: string[];
|
|
143
144
|
shippingInfo?: string;
|
|
144
145
|
returnPolicy?: string;
|
|
145
|
-
condition?: "new" | "used" | "refurbished" | "broken" | "graded";
|
|
146
|
+
condition?: "new" | "like_new" | "good" | "fair" | "poor" | "used" | "refurbished" | "broken" | "graded";
|
|
146
147
|
insurance?: boolean;
|
|
147
148
|
insuranceCost?: number;
|
|
148
149
|
shippingPaidBy?: "seller" | "buyer";
|
|
@@ -236,6 +237,17 @@ export interface ProductDocument extends BaseDocument {
|
|
|
236
237
|
prizeRevealDeadlineDays?: number;
|
|
237
238
|
/** Public proof-of-fairness file (commit-reveal scheme). */
|
|
238
239
|
prizeGithubFileUrl?: string;
|
|
240
|
+
/**
|
|
241
|
+
* Draw mode for prize-draw listings.
|
|
242
|
+
* "reveal" = classic buyer-reveal flow (default, backward-compat).
|
|
243
|
+
* "lottery" = user self-pull with weighted random slot assignment.
|
|
244
|
+
*/
|
|
245
|
+
prizeDrawMode?: "reveal" | "lottery";
|
|
246
|
+
/**
|
|
247
|
+
* Full lottery config. Only set when prizeDrawMode === "lottery".
|
|
248
|
+
* Includes server-only price/weight fields — stripped by adapter before any client response.
|
|
249
|
+
*/
|
|
250
|
+
lotteryConfig?: LotteryConfig;
|
|
239
251
|
/**
|
|
240
252
|
* Optional per-item shipping constraints. When absent the store's
|
|
241
253
|
* shippingConfig applies unchanged.
|
|
@@ -286,6 +298,8 @@ export interface PrizeDrawItem {
|
|
|
286
298
|
};
|
|
287
299
|
condition: string;
|
|
288
300
|
estimatedValue?: number;
|
|
301
|
+
/** Per-slot price in paise for lottery mode. Lower price = higher weight = higher chance. */
|
|
302
|
+
prizeSlotPrice?: number;
|
|
289
303
|
isWon: boolean;
|
|
290
304
|
}
|
|
291
305
|
/** Runtime-accessible product status values — use instead of bare string literals. */
|