@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,261 @@
|
|
|
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
|
+
vi.mock("../../../../providers/db-firebase/admin", () => ({
|
|
6
|
+
getAdminDb: () => db,
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
|
|
9
|
+
const actual = await importOriginal();
|
|
10
|
+
return {
|
|
11
|
+
...actual,
|
|
12
|
+
prepareForFirestore: (d) => d,
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
vi.mock("../../../../security/settings-encryption", () => ({
|
|
16
|
+
encryptSecret: (v) => `enc:${v}`,
|
|
17
|
+
decryptSecret: (v) => v.replace(/^enc:/, ""),
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("../../../../errors/normalize", () => ({
|
|
20
|
+
normalizeError: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
import { StoreRepository } from "../store.repository";
|
|
23
|
+
const repo = new StoreRepository();
|
|
24
|
+
function makeStoreDoc(overrides = {}) {
|
|
25
|
+
return {
|
|
26
|
+
id: "store-test",
|
|
27
|
+
storeSlug: "store-test",
|
|
28
|
+
ownerId: "uid-seller-1",
|
|
29
|
+
storeName: "Test Store",
|
|
30
|
+
status: "active",
|
|
31
|
+
isPublic: true,
|
|
32
|
+
isVerified: false,
|
|
33
|
+
createdAt: new Date(),
|
|
34
|
+
updatedAt: new Date(),
|
|
35
|
+
stats: { totalProducts: 0, itemsSold: 0, totalReviews: 0, averageRating: 0 },
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function makeCreateInput(overrides = {}) {
|
|
40
|
+
return {
|
|
41
|
+
storeSlug: "store-test",
|
|
42
|
+
ownerId: "uid-seller-1",
|
|
43
|
+
storeName: "Test Store",
|
|
44
|
+
status: "pending",
|
|
45
|
+
isPublic: false,
|
|
46
|
+
...overrides,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
vi.clearAllMocks();
|
|
51
|
+
db.collection.mockReturnValue(mockCollection);
|
|
52
|
+
mockCollection.doc.mockReturnValue(mockDocRef);
|
|
53
|
+
mockCollection.where.mockReturnValue(mockQuery);
|
|
54
|
+
mockQuery.where.mockReturnValue(mockQuery);
|
|
55
|
+
mockQuery.orderBy.mockReturnValue(mockQuery);
|
|
56
|
+
mockQuery.limit.mockReturnValue(mockQuery);
|
|
57
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
58
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
59
|
+
mockDocRef.create.mockResolvedValue(undefined);
|
|
60
|
+
mockDocRef.set.mockResolvedValue(undefined);
|
|
61
|
+
mockDocRef.update.mockResolvedValue(undefined);
|
|
62
|
+
mockDocRef.delete.mockResolvedValue(undefined);
|
|
63
|
+
db.batch.mockReturnValue(mockBatch);
|
|
64
|
+
});
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// create
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
describe("StoreRepository.create", () => {
|
|
69
|
+
it("throws when storeSlug equals ownerId", async () => {
|
|
70
|
+
const input = makeCreateInput({ storeSlug: "uid-same", ownerId: "uid-same" });
|
|
71
|
+
await expect(repo.create(input)).rejects.toThrow(DatabaseError);
|
|
72
|
+
});
|
|
73
|
+
it("creates document using .create() semantics (not .set())", async () => {
|
|
74
|
+
await repo.create(makeCreateInput());
|
|
75
|
+
expect(mockDocRef.create).toHaveBeenCalledOnce();
|
|
76
|
+
expect(mockDocRef.set).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
it("sets document ID to storeSlug", async () => {
|
|
79
|
+
await repo.create(makeCreateInput({ storeSlug: "store-pokemon-palace" }));
|
|
80
|
+
expect(mockCollection.doc).toHaveBeenCalledWith("store-pokemon-palace");
|
|
81
|
+
});
|
|
82
|
+
it("throws DatabaseError(slug already taken) when .create() fails with gRPC code 6", async () => {
|
|
83
|
+
const grpcAlreadyExists = Object.assign(new Error("ALREADY_EXISTS"), { code: 6 });
|
|
84
|
+
mockDocRef.create.mockRejectedValue(grpcAlreadyExists);
|
|
85
|
+
await expect(repo.create(makeCreateInput({ storeSlug: "store-taken" }))).rejects.toThrow(DatabaseError);
|
|
86
|
+
});
|
|
87
|
+
it("re-throws non-ALREADY_EXISTS errors as-is", async () => {
|
|
88
|
+
const networkErr = new Error("NETWORK_FAILURE");
|
|
89
|
+
mockDocRef.create.mockRejectedValue(networkErr);
|
|
90
|
+
await expect(repo.create(makeCreateInput())).rejects.toThrow("NETWORK_FAILURE");
|
|
91
|
+
});
|
|
92
|
+
it("returns the store document with id=storeSlug", async () => {
|
|
93
|
+
const result = await repo.create(makeCreateInput({ storeSlug: "store-abc" }));
|
|
94
|
+
expect(result.id).toBe("store-abc");
|
|
95
|
+
expect(result.storeSlug).toBe("store-abc");
|
|
96
|
+
});
|
|
97
|
+
it("encrypts whatsappConfig.accessToken before writing", async () => {
|
|
98
|
+
const input = makeCreateInput({
|
|
99
|
+
whatsappConfig: { accessToken: "secret-token", phoneNumberId: "ph-1", businessId: "biz-1" },
|
|
100
|
+
});
|
|
101
|
+
await repo.create(input);
|
|
102
|
+
const callArg = mockDocRef.create.mock.calls[0][0];
|
|
103
|
+
const config = callArg.whatsappConfig;
|
|
104
|
+
expect(config.accessToken).toBe("enc:secret-token");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// isSlugAvailable
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
describe("StoreRepository.isSlugAvailable", () => {
|
|
111
|
+
it("returns true when no document exists for that slug", async () => {
|
|
112
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
113
|
+
const result = await repo.isSlugAvailable("store-new");
|
|
114
|
+
expect(result).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
it("returns false when a document exists for that slug", async () => {
|
|
117
|
+
const store = makeStoreDoc();
|
|
118
|
+
mockDocRef.get.mockResolvedValue(makeSnap(store, "store-test"));
|
|
119
|
+
const result = await repo.isSlugAvailable("store-test");
|
|
120
|
+
expect(result).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// findByOwnerId
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
describe("StoreRepository.findByOwnerId", () => {
|
|
127
|
+
it("returns store when found", async () => {
|
|
128
|
+
const store = makeStoreDoc();
|
|
129
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: store.id, data: store }]));
|
|
130
|
+
const result = await repo.findByOwnerId("uid-seller-1");
|
|
131
|
+
expect(result).not.toBeNull();
|
|
132
|
+
});
|
|
133
|
+
it("queries by ownerId field", async () => {
|
|
134
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
135
|
+
await repo.findByOwnerId("uid-seller-1");
|
|
136
|
+
expect(mockCollection.where).toHaveBeenCalledWith("ownerId", "==", "uid-seller-1");
|
|
137
|
+
});
|
|
138
|
+
it("returns null when no store matches", async () => {
|
|
139
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
140
|
+
const result = await repo.findByOwnerId("uid-nobody");
|
|
141
|
+
expect(result).toBeNull();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// setStatus
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
describe("StoreRepository.setStatus", () => {
|
|
148
|
+
it("status=active → sets isPublic: true", async () => {
|
|
149
|
+
const store = makeStoreDoc({ status: "pending", isPublic: false });
|
|
150
|
+
mockDocRef.get.mockResolvedValue(makeSnap(store, "store-test"));
|
|
151
|
+
await repo.setStatus("store-test", "active");
|
|
152
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "active", isPublic: true }));
|
|
153
|
+
});
|
|
154
|
+
it("status=suspended → sets isPublic: false", async () => {
|
|
155
|
+
const store = makeStoreDoc({ status: "active", isPublic: true });
|
|
156
|
+
mockDocRef.get.mockResolvedValue(makeSnap(store, "store-test"));
|
|
157
|
+
await repo.setStatus("store-test", "suspended");
|
|
158
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "suspended", isPublic: false }));
|
|
159
|
+
});
|
|
160
|
+
it("status=pending → sets isPublic: false", async () => {
|
|
161
|
+
const store = makeStoreDoc({ status: "active", isPublic: true });
|
|
162
|
+
mockDocRef.get.mockResolvedValue(makeSnap(store, "store-test"));
|
|
163
|
+
await repo.setStatus("store-test", "pending");
|
|
164
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "pending", isPublic: false }));
|
|
165
|
+
});
|
|
166
|
+
it("status=rejected → sets isPublic: false", async () => {
|
|
167
|
+
const store = makeStoreDoc({ status: "active", isPublic: true });
|
|
168
|
+
mockDocRef.get.mockResolvedValue(makeSnap(store, "store-test"));
|
|
169
|
+
await repo.setStatus("store-test", "rejected");
|
|
170
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ isPublic: false }));
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// ---------------------------------------------------------------------------
|
|
174
|
+
// incrementTotalProducts
|
|
175
|
+
// ---------------------------------------------------------------------------
|
|
176
|
+
describe("StoreRepository.incrementTotalProducts", () => {
|
|
177
|
+
it("calls update with increment on stats.totalProducts", async () => {
|
|
178
|
+
await repo.incrementTotalProducts("store-1", 1);
|
|
179
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ "stats.totalProducts": expect.anything() }));
|
|
180
|
+
});
|
|
181
|
+
it("no-op when storeId is empty string", async () => {
|
|
182
|
+
await repo.incrementTotalProducts("", 1);
|
|
183
|
+
expect(mockDocRef.update).not.toHaveBeenCalled();
|
|
184
|
+
});
|
|
185
|
+
it("accepts negative delta (decrement)", async () => {
|
|
186
|
+
await repo.incrementTotalProducts("store-1", -1);
|
|
187
|
+
expect(mockDocRef.update).toHaveBeenCalledOnce();
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
// updateReviewStats
|
|
192
|
+
// ---------------------------------------------------------------------------
|
|
193
|
+
describe("StoreRepository.updateReviewStats", () => {
|
|
194
|
+
it("updates totalReviews and averageRating on the store doc", async () => {
|
|
195
|
+
await repo.updateReviewStats("store-1", 10, 4.5);
|
|
196
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({
|
|
197
|
+
"stats.totalReviews": 10,
|
|
198
|
+
"stats.averageRating": 4.5,
|
|
199
|
+
}));
|
|
200
|
+
});
|
|
201
|
+
it("no-op when storeId is empty string", async () => {
|
|
202
|
+
await repo.updateReviewStats("", 10, 4.5);
|
|
203
|
+
expect(mockDocRef.update).not.toHaveBeenCalled();
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// changeSlug
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
describe("StoreRepository.changeSlug", () => {
|
|
210
|
+
it("throws when currentSlug === newSlug", async () => {
|
|
211
|
+
await expect(repo.changeSlug("store-same", "store-same")).rejects.toThrow(DatabaseError);
|
|
212
|
+
});
|
|
213
|
+
it("throws when newSlug is empty", async () => {
|
|
214
|
+
await expect(repo.changeSlug("store-old", "")).rejects.toThrow(DatabaseError);
|
|
215
|
+
});
|
|
216
|
+
it("throws when newSlug contains invalid characters (uppercase)", async () => {
|
|
217
|
+
await expect(repo.changeSlug("store-old", "Store-BAD")).rejects.toThrow(DatabaseError);
|
|
218
|
+
});
|
|
219
|
+
it("throws when current store is not found", async () => {
|
|
220
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
221
|
+
await expect(repo.changeSlug("store-ghost", "store-new")).rejects.toThrow(DatabaseError);
|
|
222
|
+
});
|
|
223
|
+
it("throws when newSlug is already taken", async () => {
|
|
224
|
+
const currentStore = makeStoreDoc({ storeSlug: "store-old" });
|
|
225
|
+
const takenStore = makeStoreDoc({ storeSlug: "store-taken", id: "store-taken" });
|
|
226
|
+
mockDocRef.get
|
|
227
|
+
.mockResolvedValueOnce(makeSnap(currentStore, "store-old")) // findById(currentSlug)
|
|
228
|
+
.mockResolvedValueOnce(makeSnap(takenStore, "store-taken")); // isSlugAvailable check
|
|
229
|
+
await expect(repo.changeSlug("store-old", "store-taken")).rejects.toThrow(DatabaseError);
|
|
230
|
+
});
|
|
231
|
+
it("atomically creates new doc and deletes old doc in one batch", async () => {
|
|
232
|
+
const currentStore = makeStoreDoc({ storeSlug: "store-old" });
|
|
233
|
+
mockDocRef.get
|
|
234
|
+
.mockResolvedValueOnce(makeSnap(currentStore, "store-old")) // findById(currentSlug)
|
|
235
|
+
.mockResolvedValueOnce(makeSnap(null)); // isSlugAvailable(newSlug) → available
|
|
236
|
+
await repo.changeSlug("store-old", "store-new");
|
|
237
|
+
expect(mockBatch.create).toHaveBeenCalledOnce();
|
|
238
|
+
expect(mockBatch.delete).toHaveBeenCalledOnce();
|
|
239
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
240
|
+
});
|
|
241
|
+
it("returns result with id=newSlug and storeSlug=newSlug", async () => {
|
|
242
|
+
const currentStore = makeStoreDoc({ storeSlug: "store-old" });
|
|
243
|
+
mockDocRef.get
|
|
244
|
+
.mockResolvedValueOnce(makeSnap(currentStore, "store-old"))
|
|
245
|
+
.mockResolvedValueOnce(makeSnap(null));
|
|
246
|
+
const result = await repo.changeSlug("store-old", "store-new");
|
|
247
|
+
expect(result.id).toBe("store-new");
|
|
248
|
+
expect(result.storeSlug).toBe("store-new");
|
|
249
|
+
});
|
|
250
|
+
it("creates new doc at newSlug path, deletes old doc at currentSlug path", async () => {
|
|
251
|
+
const currentStore = makeStoreDoc({ storeSlug: "store-old" });
|
|
252
|
+
mockDocRef.get
|
|
253
|
+
.mockResolvedValueOnce(makeSnap(currentStore, "store-old"))
|
|
254
|
+
.mockResolvedValueOnce(makeSnap(null));
|
|
255
|
+
await repo.changeSlug("store-old", "store-new");
|
|
256
|
+
// doc() was called for newSlug and currentSlug
|
|
257
|
+
const docCalls = mockCollection.doc.mock.calls.map((c) => c[0]);
|
|
258
|
+
expect(docCalls).toContain("store-new");
|
|
259
|
+
expect(docCalls).toContain("store-old");
|
|
260
|
+
});
|
|
261
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,312 @@
|
|
|
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 } = 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
|
+
firebaseFieldOps: {
|
|
13
|
+
arrayUnion: (...args) => args,
|
|
14
|
+
arrayRemove: (...args) => args,
|
|
15
|
+
increment: (n) => n,
|
|
16
|
+
serverTimestamp: () => new Date(),
|
|
17
|
+
delete: () => null,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
});
|
|
21
|
+
vi.mock("../../../../errors/normalize", () => ({
|
|
22
|
+
normalizeError: vi.fn(),
|
|
23
|
+
}));
|
|
24
|
+
vi.mock("../../../../monitoring", () => ({
|
|
25
|
+
serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
26
|
+
}));
|
|
27
|
+
import { SupportRepository } from "../support.repository";
|
|
28
|
+
import { ACTIVE_TICKET_STATUSES } from "../../schemas/firestore";
|
|
29
|
+
const repo = new SupportRepository();
|
|
30
|
+
function makeTicketDoc(overrides = {}) {
|
|
31
|
+
return {
|
|
32
|
+
id: "ticket-order-issue-ravi-20260627",
|
|
33
|
+
userId: "user-ravi",
|
|
34
|
+
userEmail: "ravi@test.com",
|
|
35
|
+
userDisplayName: "Ravi Kumar",
|
|
36
|
+
category: "order_issue",
|
|
37
|
+
subject: "My order is missing",
|
|
38
|
+
description: "I placed an order but never received it.",
|
|
39
|
+
orderId: "order-3-20260601-abc123",
|
|
40
|
+
status: "open",
|
|
41
|
+
priority: "normal",
|
|
42
|
+
messages: [],
|
|
43
|
+
createdAt: new Date(),
|
|
44
|
+
updatedAt: new Date(),
|
|
45
|
+
...overrides,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function makeMessage(overrides = {}) {
|
|
49
|
+
return {
|
|
50
|
+
id: "msg-1",
|
|
51
|
+
authorId: "user-ravi",
|
|
52
|
+
authorRole: "user",
|
|
53
|
+
body: "I need help with my order.",
|
|
54
|
+
createdAt: new Date(),
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
vi.clearAllMocks();
|
|
60
|
+
db.collection.mockReturnValue(mockCollection);
|
|
61
|
+
mockCollection.doc.mockReturnValue(mockDocRef);
|
|
62
|
+
mockCollection.where.mockReturnValue(mockQuery);
|
|
63
|
+
mockCollection.add = vi.fn().mockResolvedValue({ id: "new-ticket-id", get: vi.fn().mockResolvedValue(makeSnap(makeTicketDoc(), "new-ticket-id")) });
|
|
64
|
+
mockQuery.where.mockReturnValue(mockQuery);
|
|
65
|
+
mockQuery.orderBy.mockReturnValue(mockQuery);
|
|
66
|
+
mockQuery.limit.mockReturnValue(mockQuery);
|
|
67
|
+
mockQuery.select = vi.fn().mockReturnValue(mockQuery);
|
|
68
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
69
|
+
mockDocRef.get.mockResolvedValue(makeSnap(makeTicketDoc(), "ticket-1"));
|
|
70
|
+
mockDocRef.set.mockResolvedValue(undefined);
|
|
71
|
+
mockDocRef.update.mockResolvedValue(undefined);
|
|
72
|
+
});
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// createTicket
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
describe("SupportRepository.createTicket", () => {
|
|
77
|
+
it("sets status: open on new tickets", async () => {
|
|
78
|
+
const input = {
|
|
79
|
+
userId: "user-ravi",
|
|
80
|
+
userEmail: "ravi@test.com",
|
|
81
|
+
userDisplayName: "Ravi Kumar",
|
|
82
|
+
category: "order_issue",
|
|
83
|
+
subject: "Missing order",
|
|
84
|
+
description: "Order never arrived.",
|
|
85
|
+
};
|
|
86
|
+
await repo.createTicket(input);
|
|
87
|
+
// create() calls collection.add (BaseRepository.create uses add for auto-ID)
|
|
88
|
+
const addArg = mockCollection.add.mock.calls[0][0];
|
|
89
|
+
expect(addArg.status).toBe("open");
|
|
90
|
+
});
|
|
91
|
+
it("sets priority: normal by default", async () => {
|
|
92
|
+
await repo.createTicket({
|
|
93
|
+
userId: "user-ravi",
|
|
94
|
+
userEmail: "ravi@test.com",
|
|
95
|
+
userDisplayName: "Ravi Kumar",
|
|
96
|
+
category: "general",
|
|
97
|
+
subject: "Question",
|
|
98
|
+
description: "I have a question.",
|
|
99
|
+
});
|
|
100
|
+
const addArg = mockCollection.add.mock.calls[0][0];
|
|
101
|
+
expect(addArg.priority).toBe("normal");
|
|
102
|
+
});
|
|
103
|
+
it("initialises messages: [] on new ticket", async () => {
|
|
104
|
+
await repo.createTicket({
|
|
105
|
+
userId: "user-ravi",
|
|
106
|
+
userEmail: "ravi@test.com",
|
|
107
|
+
userDisplayName: "Ravi Kumar",
|
|
108
|
+
category: "general",
|
|
109
|
+
subject: "Question",
|
|
110
|
+
description: "I have a question.",
|
|
111
|
+
});
|
|
112
|
+
const addArg = mockCollection.add.mock.calls[0][0];
|
|
113
|
+
expect(addArg.messages).toEqual([]);
|
|
114
|
+
});
|
|
115
|
+
it("stores orderId when provided", async () => {
|
|
116
|
+
await repo.createTicket({
|
|
117
|
+
userId: "user-ravi",
|
|
118
|
+
userEmail: "ravi@test.com",
|
|
119
|
+
userDisplayName: "Ravi Kumar",
|
|
120
|
+
category: "order_issue",
|
|
121
|
+
subject: "Order missing",
|
|
122
|
+
description: "My order is gone.",
|
|
123
|
+
orderId: "order-3-20260601-abc123",
|
|
124
|
+
});
|
|
125
|
+
const addArg = mockCollection.add.mock.calls[0][0];
|
|
126
|
+
expect(addArg.orderId).toBe("order-3-20260601-abc123");
|
|
127
|
+
});
|
|
128
|
+
it("sets createdAt and updatedAt timestamps", async () => {
|
|
129
|
+
await repo.createTicket({
|
|
130
|
+
userId: "user-ravi",
|
|
131
|
+
userEmail: "ravi@test.com",
|
|
132
|
+
userDisplayName: "Ravi Kumar",
|
|
133
|
+
category: "general",
|
|
134
|
+
subject: "Q",
|
|
135
|
+
description: "D",
|
|
136
|
+
});
|
|
137
|
+
const addArg = mockCollection.add.mock.calls[0][0];
|
|
138
|
+
expect(addArg.createdAt).toBeInstanceOf(Date);
|
|
139
|
+
expect(addArg.updatedAt).toBeInstanceOf(Date);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// countActiveTickets
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
describe("SupportRepository.countActiveTickets", () => {
|
|
146
|
+
it("runs a query per ACTIVE_TICKET_STATUS in parallel", async () => {
|
|
147
|
+
mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
|
|
148
|
+
await repo.countActiveTickets("user-ravi");
|
|
149
|
+
// One query per active status
|
|
150
|
+
expect(mockQuery.get).toHaveBeenCalledTimes(ACTIVE_TICKET_STATUSES.length);
|
|
151
|
+
});
|
|
152
|
+
it("returns sum of sizes across all active status queries", async () => {
|
|
153
|
+
// Each of the 3 queries returns size=2 → total should be 6
|
|
154
|
+
mockQuery.get.mockResolvedValue({ empty: false, size: 2, docs: [] });
|
|
155
|
+
const count = await repo.countActiveTickets("user-ravi");
|
|
156
|
+
expect(count).toBe(ACTIVE_TICKET_STATUSES.length * 2);
|
|
157
|
+
});
|
|
158
|
+
it("returns 0 when no active tickets", async () => {
|
|
159
|
+
mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
|
|
160
|
+
const count = await repo.countActiveTickets("user-nobody");
|
|
161
|
+
expect(count).toBe(0);
|
|
162
|
+
});
|
|
163
|
+
it("queries by userId on each sub-query", async () => {
|
|
164
|
+
mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
|
|
165
|
+
await repo.countActiveTickets("user-specific");
|
|
166
|
+
expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("userId"), "==", "user-specific");
|
|
167
|
+
});
|
|
168
|
+
it("uses select() stub reads (minimal cost)", async () => {
|
|
169
|
+
mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
|
|
170
|
+
await repo.countActiveTickets("user-ravi");
|
|
171
|
+
expect(mockQuery.select).toHaveBeenCalled();
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
// getActiveCategoryTicket
|
|
176
|
+
// ---------------------------------------------------------------------------
|
|
177
|
+
describe("SupportRepository.getActiveCategoryTicket", () => {
|
|
178
|
+
it("returns existing ticket for userId + category with status waiting_on_user", async () => {
|
|
179
|
+
const ticket = makeTicketDoc({ status: "waiting_on_user", category: "order_issue" });
|
|
180
|
+
mockQuery.get.mockResolvedValue({
|
|
181
|
+
empty: false,
|
|
182
|
+
size: 1,
|
|
183
|
+
docs: [makeSnap(ticket, ticket.id)],
|
|
184
|
+
});
|
|
185
|
+
const result = await repo.getActiveCategoryTicket("user-ravi", "order_issue");
|
|
186
|
+
expect(result).not.toBeNull();
|
|
187
|
+
expect(result?.category).toBe("order_issue");
|
|
188
|
+
});
|
|
189
|
+
it("returns null when no matching ticket found", async () => {
|
|
190
|
+
mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
|
|
191
|
+
const result = await repo.getActiveCategoryTicket("user-ravi", "general");
|
|
192
|
+
expect(result).toBeNull();
|
|
193
|
+
});
|
|
194
|
+
it("queries specifically for status == waiting_on_user", async () => {
|
|
195
|
+
mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
|
|
196
|
+
await repo.getActiveCategoryTicket("user-ravi", "billing_payment");
|
|
197
|
+
expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("status"), "==", "waiting_on_user");
|
|
198
|
+
});
|
|
199
|
+
it("queries by userId and category", async () => {
|
|
200
|
+
mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
|
|
201
|
+
await repo.getActiveCategoryTicket("user-xyz", "account");
|
|
202
|
+
expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("userId"), "==", "user-xyz");
|
|
203
|
+
expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("category"), "==", "account");
|
|
204
|
+
});
|
|
205
|
+
it("uses limit(1) to avoid scanning all tickets", async () => {
|
|
206
|
+
mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
|
|
207
|
+
await repo.getActiveCategoryTicket("user-ravi", "general");
|
|
208
|
+
expect(mockQuery.limit).toHaveBeenCalledWith(1);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// addMessage
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
describe("SupportRepository.addMessage", () => {
|
|
215
|
+
it("appends message using arrayUnion (does not overwrite messages array)", async () => {
|
|
216
|
+
const message = makeMessage();
|
|
217
|
+
await repo.addMessage("ticket-1", message);
|
|
218
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
219
|
+
// arrayUnion mock returns its arguments as an array
|
|
220
|
+
expect(updateArg.messages).toBeDefined();
|
|
221
|
+
});
|
|
222
|
+
it("updates updatedAt timestamp on message add", async () => {
|
|
223
|
+
await repo.addMessage("ticket-1", makeMessage());
|
|
224
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
225
|
+
expect(updateArg.updatedAt).toBeInstanceOf(Date);
|
|
226
|
+
});
|
|
227
|
+
it("sets newStatus when provided", async () => {
|
|
228
|
+
await repo.addMessage("ticket-1", makeMessage(), "in_progress");
|
|
229
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
230
|
+
expect(updateArg.status).toBe("in_progress");
|
|
231
|
+
});
|
|
232
|
+
it("does NOT set status field when newStatus not provided", async () => {
|
|
233
|
+
await repo.addMessage("ticket-1", makeMessage());
|
|
234
|
+
const updateArg = mockDocRef.update.mock.calls[0][0];
|
|
235
|
+
expect(Object.keys(updateArg)).not.toContain("status");
|
|
236
|
+
});
|
|
237
|
+
it("calls doc(ticketId).update — not collection.add", async () => {
|
|
238
|
+
await repo.addMessage("ticket-1", makeMessage());
|
|
239
|
+
expect(mockCollection.doc).toHaveBeenCalledWith("ticket-1");
|
|
240
|
+
expect(mockDocRef.update).toHaveBeenCalledOnce();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
// assignTicket
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
describe("SupportRepository.assignTicket", () => {
|
|
247
|
+
it("sets assignedTo field", async () => {
|
|
248
|
+
await repo.assignTicket("ticket-1", "employee-uid-1", "Support Agent");
|
|
249
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ assignedTo: "employee-uid-1" }));
|
|
250
|
+
});
|
|
251
|
+
it("sets assignedToName field", async () => {
|
|
252
|
+
await repo.assignTicket("ticket-1", "emp-1", "Alice Support");
|
|
253
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ assignedToName: "Alice Support" }));
|
|
254
|
+
});
|
|
255
|
+
it("sets updatedAt timestamp", async () => {
|
|
256
|
+
await repo.assignTicket("ticket-1", "emp-1", "Agent");
|
|
257
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
// ---------------------------------------------------------------------------
|
|
261
|
+
// updateTicketStatus
|
|
262
|
+
// ---------------------------------------------------------------------------
|
|
263
|
+
describe("SupportRepository.updateTicketStatus", () => {
|
|
264
|
+
it("applies provided status update", async () => {
|
|
265
|
+
await repo.updateTicketStatus("ticket-1", { status: "resolved" });
|
|
266
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ status: "resolved" }));
|
|
267
|
+
});
|
|
268
|
+
it("sets updatedAt on every status change", async () => {
|
|
269
|
+
await repo.updateTicketStatus("ticket-1", { status: "closed" });
|
|
270
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
271
|
+
});
|
|
272
|
+
it("stores resolvedAt when provided", async () => {
|
|
273
|
+
const resolvedAt = new Date();
|
|
274
|
+
await repo.updateTicketStatus("ticket-1", { status: "resolved", resolvedAt });
|
|
275
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ resolvedAt }));
|
|
276
|
+
});
|
|
277
|
+
it("stores priority update when provided", async () => {
|
|
278
|
+
await repo.updateTicketStatus("ticket-1", { priority: "high" });
|
|
279
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ priority: "high" }));
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
// ---------------------------------------------------------------------------
|
|
283
|
+
// getActiveOrderTicket
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
describe("SupportRepository.getActiveOrderTicket", () => {
|
|
286
|
+
it("returns ticket matching userId + orderId with active status", async () => {
|
|
287
|
+
const ticket = makeTicketDoc({ status: "open", orderId: "order-1" });
|
|
288
|
+
mockQuery.get.mockResolvedValue({
|
|
289
|
+
empty: false,
|
|
290
|
+
size: 1,
|
|
291
|
+
docs: [{ id: ticket.id, data: () => ticket }],
|
|
292
|
+
});
|
|
293
|
+
const result = await repo.getActiveOrderTicket("user-ravi", "order-1");
|
|
294
|
+
expect(result).not.toBeNull();
|
|
295
|
+
expect(result?.orderId).toBe("order-1");
|
|
296
|
+
});
|
|
297
|
+
it("returns null when no active order ticket exists", async () => {
|
|
298
|
+
mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
|
|
299
|
+
const result = await repo.getActiveOrderTicket("user-ravi", "order-nonexistent");
|
|
300
|
+
expect(result).toBeNull();
|
|
301
|
+
});
|
|
302
|
+
it("queries status in ACTIVE_TICKET_STATUSES", async () => {
|
|
303
|
+
mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
|
|
304
|
+
await repo.getActiveOrderTicket("user-ravi", "order-1");
|
|
305
|
+
expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("status"), "in", ACTIVE_TICKET_STATUSES);
|
|
306
|
+
});
|
|
307
|
+
it("uses limit(1)", async () => {
|
|
308
|
+
mockQuery.get.mockResolvedValue({ empty: true, size: 0, docs: [] });
|
|
309
|
+
await repo.getActiveOrderTicket("user-ravi", "order-1");
|
|
310
|
+
expect(mockQuery.limit).toHaveBeenCalledWith(1);
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|