@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.
Files changed (186) hide show
  1. package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +42 -0
  2. package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +86 -0
  3. package/dist/_internal/client/features/lottery/LotteryAdminSlotView.d.ts +14 -0
  4. package/dist/_internal/client/features/lottery/LotteryAdminSlotView.js +46 -0
  5. package/dist/_internal/client/features/lottery/LotteryDetailView.d.ts +28 -0
  6. package/dist/_internal/client/features/lottery/LotteryDetailView.js +17 -0
  7. package/dist/_internal/client/features/lottery/LotteryEntriesView.d.ts +23 -0
  8. package/dist/_internal/client/features/lottery/LotteryEntriesView.js +58 -0
  9. package/dist/_internal/client/features/lottery/LotteryListView.d.ts +21 -0
  10. package/dist/_internal/client/features/lottery/LotteryListView.js +25 -0
  11. package/dist/_internal/client/features/lottery/LotteryPullForm.d.ts +19 -0
  12. package/dist/_internal/client/features/lottery/LotteryPullForm.js +65 -0
  13. package/dist/_internal/client/features/lottery/LotterySlotGrid.d.ts +13 -0
  14. package/dist/_internal/client/features/lottery/LotterySlotGrid.js +21 -0
  15. package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.d.ts +28 -0
  16. package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.js +18 -0
  17. package/dist/_internal/server/features/auctions/__tests__/service.test.d.ts +1 -0
  18. package/dist/_internal/server/features/auctions/__tests__/service.test.js +140 -0
  19. package/dist/_internal/server/features/checkout/__tests__/actions.test.d.ts +1 -0
  20. package/dist/_internal/server/features/checkout/__tests__/actions.test.js +600 -0
  21. package/dist/_internal/server/features/lottery/actions.d.ts +43 -0
  22. package/dist/_internal/server/features/lottery/actions.js +243 -0
  23. package/dist/_internal/server/features/lottery/adapters.d.ts +9 -0
  24. package/dist/_internal/server/features/lottery/adapters.js +21 -0
  25. package/dist/_internal/server/features/lottery/data.d.ts +20 -0
  26. package/dist/_internal/server/features/lottery/data.js +41 -0
  27. package/dist/_internal/server/features/lottery/metadata.d.ts +6 -0
  28. package/dist/_internal/server/features/lottery/metadata.js +31 -0
  29. package/dist/_internal/server/features/refunds/__tests__/actions.test.d.ts +1 -0
  30. package/dist/_internal/server/features/refunds/__tests__/actions.test.js +263 -0
  31. package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.d.ts +1 -0
  32. package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.js +249 -0
  33. package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.d.ts +1 -0
  34. package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.js +201 -0
  35. package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.d.ts +1 -0
  36. package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.js +169 -0
  37. package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.d.ts +1 -0
  38. package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.js +148 -0
  39. package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.d.ts +1 -0
  40. package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.js +210 -0
  41. package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.d.ts +1 -0
  42. package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.js +190 -0
  43. package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.d.ts +1 -0
  44. package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.js +161 -0
  45. package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.d.ts +1 -0
  46. package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.js +302 -0
  47. package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.d.ts +1 -0
  48. package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.js +252 -0
  49. package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.d.ts +1 -0
  50. package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.js +129 -0
  51. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  52. package/dist/_internal/shared/actions/action-registry.js +46 -0
  53. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  54. package/dist/client.d.ts +9 -0
  55. package/dist/client.js +9 -0
  56. package/dist/constants/field-names.d.ts +5 -1
  57. package/dist/constants/field-names.js +5 -1
  58. package/dist/features/addresses/repository/__tests__/addresses.repository.test.d.ts +1 -0
  59. package/dist/features/addresses/repository/__tests__/addresses.repository.test.js +386 -0
  60. package/dist/features/admin/api/bids/route.d.ts +2 -2
  61. package/dist/features/admin/api/coupons/route.d.ts +2 -2
  62. package/dist/features/admin/api/products/route.d.ts +2 -2
  63. package/dist/features/admin/api/reviews/route.d.ts +2 -2
  64. package/dist/features/admin/repository/__tests__/notification.repository.test.d.ts +1 -0
  65. package/dist/features/admin/repository/__tests__/notification.repository.test.js +356 -0
  66. package/dist/features/admin/repository/__tests__/site-settings.repository.test.d.ts +1 -0
  67. package/dist/features/admin/repository/__tests__/site-settings.repository.test.js +298 -0
  68. package/dist/features/auctions/repository/__tests__/bid.repository.test.d.ts +1 -0
  69. package/dist/features/auctions/repository/__tests__/bid.repository.test.js +290 -0
  70. package/dist/features/auth/api/route.d.ts +2 -2
  71. package/dist/features/auth/repository/__tests__/session.repository.test.d.ts +1 -0
  72. package/dist/features/auth/repository/__tests__/session.repository.test.js +310 -0
  73. package/dist/features/auth/repository/__tests__/session.test.d.ts +1 -0
  74. package/dist/features/auth/repository/__tests__/session.test.js +146 -0
  75. package/dist/features/auth/repository/__tests__/sms-counter.repository.test.d.ts +1 -0
  76. package/dist/features/auth/repository/__tests__/sms-counter.repository.test.js +171 -0
  77. package/dist/features/auth/repository/__tests__/sms-counter.test.d.ts +1 -0
  78. package/dist/features/auth/repository/__tests__/sms-counter.test.js +88 -0
  79. package/dist/features/auth/repository/__tests__/token.repository.test.d.ts +1 -0
  80. package/dist/features/auth/repository/__tests__/token.repository.test.js +260 -0
  81. package/dist/features/auth/repository/__tests__/token.test.d.ts +1 -0
  82. package/dist/features/auth/repository/__tests__/token.test.js +140 -0
  83. package/dist/features/auth/repository/__tests__/user.repository.test.d.ts +1 -0
  84. package/dist/features/auth/repository/__tests__/user.repository.test.js +352 -0
  85. package/dist/features/auth/repository/__tests__/user.test.d.ts +1 -0
  86. package/dist/features/auth/repository/__tests__/user.test.js +226 -0
  87. package/dist/features/auth/repository/session.repository.js +10 -1
  88. package/dist/features/cart/repository/__tests__/cart.repository.test.d.ts +1 -0
  89. package/dist/features/cart/repository/__tests__/cart.repository.test.js +371 -0
  90. package/dist/features/cart/repository/cart.repository.js +2 -2
  91. package/dist/features/categories/api/[id]/route.d.ts +4 -4
  92. package/dist/features/categories/api/route.d.ts +2 -2
  93. package/dist/features/categories/repository/__tests__/categories.repository.test.d.ts +1 -0
  94. package/dist/features/categories/repository/__tests__/categories.repository.test.js +813 -0
  95. package/dist/features/events/components/EventCard.js +1 -0
  96. package/dist/features/events/repository/__tests__/event.repository.test.d.ts +1 -0
  97. package/dist/features/events/repository/__tests__/event.repository.test.js +299 -0
  98. package/dist/features/events/schemas/firestore.d.ts +3 -0
  99. package/dist/features/events/schemas/index.d.ts +834 -14
  100. package/dist/features/events/schemas/index.js +80 -1
  101. package/dist/features/events/types/index.d.ts +4 -1
  102. package/dist/features/faq/repository/__tests__/faqs.repository.test.d.ts +1 -0
  103. package/dist/features/faq/repository/__tests__/faqs.repository.test.js +311 -0
  104. package/dist/features/history/repository/__tests__/user-history.repository.test.d.ts +1 -0
  105. package/dist/features/history/repository/__tests__/user-history.repository.test.js +180 -0
  106. package/dist/features/homepage/api/[id]/route.d.ts +4 -4
  107. package/dist/features/homepage/api/carousel/[id]/route.d.ts +4 -4
  108. package/dist/features/homepage/api/carousel/route.d.ts +2 -2
  109. package/dist/features/homepage/api/route.d.ts +2 -2
  110. package/dist/features/homepage/repository/__tests__/carousel.repository.test.d.ts +1 -0
  111. package/dist/features/homepage/repository/__tests__/carousel.repository.test.js +182 -0
  112. package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.d.ts +1 -0
  113. package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.js +185 -0
  114. package/dist/features/lottery/repository/lottery-entry.repository.d.ts +33 -0
  115. package/dist/features/lottery/repository/lottery-entry.repository.js +130 -0
  116. package/dist/features/lottery/schemas/firestore.d.ts +49 -0
  117. package/dist/features/lottery/schemas/firestore.js +25 -0
  118. package/dist/features/lottery/schemas/zod.d.ts +113 -0
  119. package/dist/features/lottery/schemas/zod.js +39 -0
  120. package/dist/features/lottery/types/index.d.ts +67 -0
  121. package/dist/features/lottery/types/index.js +55 -0
  122. package/dist/features/messages/repository/__tests__/conversations.repository.test.d.ts +1 -0
  123. package/dist/features/messages/repository/__tests__/conversations.repository.test.js +257 -0
  124. package/dist/features/orders/repository/__tests__/orders.repository.test.d.ts +1 -0
  125. package/dist/features/orders/repository/__tests__/orders.repository.test.js +211 -0
  126. package/dist/features/payments/repository/__tests__/payout.repository.test.d.ts +1 -0
  127. package/dist/features/payments/repository/__tests__/payout.repository.test.js +292 -0
  128. package/dist/features/products/api/[id]/route.d.ts +4 -4
  129. package/dist/features/products/api/route.d.ts +2 -2
  130. package/dist/features/products/repository/__tests__/product-features.repository.test.d.ts +1 -0
  131. package/dist/features/products/repository/__tests__/product-features.repository.test.js +329 -0
  132. package/dist/features/products/repository/__tests__/products.repository.test.d.ts +1 -0
  133. package/dist/features/products/repository/__tests__/products.repository.test.js +607 -0
  134. package/dist/features/products/schemas/firestore.d.ts +15 -1
  135. package/dist/features/products/schemas/index.d.ts +12 -12
  136. package/dist/features/products/schemas/index.js +4 -2
  137. package/dist/features/products/types/index.d.ts +1 -1
  138. package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.d.ts +1 -0
  139. package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.js +219 -0
  140. package/dist/features/promotions/repository/__tests__/coupons.repository.test.d.ts +1 -0
  141. package/dist/features/promotions/repository/__tests__/coupons.repository.test.js +480 -0
  142. package/dist/features/reviews/api/[id]/route.d.ts +4 -4
  143. package/dist/features/reviews/api/route.d.ts +2 -2
  144. package/dist/features/reviews/repository/__tests__/reviews.repository.test.d.ts +1 -0
  145. package/dist/features/reviews/repository/__tests__/reviews.repository.test.js +327 -0
  146. package/dist/features/scams/repository/__tests__/scammer.repository.test.d.ts +1 -0
  147. package/dist/features/scams/repository/__tests__/scammer.repository.test.js +385 -0
  148. package/dist/features/search/repository/__tests__/search.repository.test.d.ts +1 -0
  149. package/dist/features/search/repository/__tests__/search.repository.test.js +369 -0
  150. package/dist/features/seller/api/coupons/route.d.ts +2 -2
  151. package/dist/features/seller/api/offers/route.d.ts +2 -2
  152. package/dist/features/seller/api/products/route.d.ts +2 -2
  153. package/dist/features/seller/api/store/route.d.ts +2 -2
  154. package/dist/features/seller/repository/__tests__/offer.repository.test.d.ts +1 -0
  155. package/dist/features/seller/repository/__tests__/offer.repository.test.js +458 -0
  156. package/dist/features/stores/repository/__tests__/store.repository.test.d.ts +1 -0
  157. package/dist/features/stores/repository/__tests__/store.repository.test.js +261 -0
  158. package/dist/features/support/repository/__tests__/support.repository.test.d.ts +1 -0
  159. package/dist/features/support/repository/__tests__/support.repository.test.js +312 -0
  160. package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.d.ts +1 -0
  161. package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.js +139 -0
  162. package/dist/index.d.ts +14 -0
  163. package/dist/index.js +17 -0
  164. package/dist/next/api/routeHandler.d.ts +1 -1
  165. package/dist/next/routing/route-map.d.ts +13 -0
  166. package/dist/next/routing/route-map.js +6 -0
  167. package/dist/repositories/index.d.ts +1 -0
  168. package/dist/repositories/index.js +2 -0
  169. package/dist/security/index.d.ts +1 -1
  170. package/dist/security/index.js +1 -1
  171. package/dist/security/pii-schemas.d.ts +2 -0
  172. package/dist/security/pii-schemas.js +6 -0
  173. package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
  174. package/dist/seed/events-seed-data.js +85 -2
  175. package/dist/seed/grouped-listings-seed-data.d.ts +3 -1
  176. package/dist/seed/index.d.ts +1 -0
  177. package/dist/seed/index.js +1 -0
  178. package/dist/seed/lottery-entries-seed-data.d.ts +2 -0
  179. package/dist/seed/lottery-entries-seed-data.js +108 -0
  180. package/dist/seed/manifest.js +1 -0
  181. package/dist/seed/products-preorders-seed-data.d.ts +3 -1
  182. package/dist/server.d.ts +6 -0
  183. package/dist/server.js +6 -0
  184. package/dist/styles.css +1 -1
  185. package/dist/tailwind-utilities.css +1 -1
  186. package/package.json +1 -1
@@ -0,0 +1,329 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { makeMockDb, makeSnap, makeQuerySnap } from "../../../../../tests/helpers/mock-firestore";
3
+ import { ValidationError } from "../../../../errors";
4
+ const { db, mockDocRef, mockCollection, mockQuery } = 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("../../../../contracts/field-ops", () => ({
16
+ serverTimestamp: () => new Date(),
17
+ increment: (n) => n,
18
+ arrayUnion: (...args) => args,
19
+ arrayRemove: (...args) => args,
20
+ deleteField: () => null,
21
+ registerFieldOps: vi.fn(),
22
+ }));
23
+ vi.mock("../../../../errors/normalize", () => ({ normalizeError: vi.fn() }));
24
+ vi.mock("../../../../monitoring", () => ({
25
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
26
+ }));
27
+ import { ProductFeaturesRepository } from "../product-features.repository";
28
+ import { MAX_STORE_CUSTOM_FEATURES } from "../../schemas/product-features";
29
+ const repo = new ProductFeaturesRepository();
30
+ function makeFeatureDoc(overrides = {}) {
31
+ return {
32
+ id: "feature-free-shipping",
33
+ slug: "feature-free-shipping",
34
+ label: "Free Shipping",
35
+ description: "This item ships for free",
36
+ icon: "truck",
37
+ scope: "platform",
38
+ storeId: undefined,
39
+ productTypes: ["all"],
40
+ displayOrder: 0,
41
+ isActive: true,
42
+ createdAt: new Date(),
43
+ updatedAt: new Date(),
44
+ ...overrides,
45
+ };
46
+ }
47
+ beforeEach(() => {
48
+ vi.clearAllMocks();
49
+ db.collection.mockReturnValue(mockCollection);
50
+ mockCollection.doc.mockReturnValue(mockDocRef);
51
+ mockCollection.where.mockReturnValue(mockQuery);
52
+ mockCollection.get = vi.fn().mockResolvedValue(makeQuerySnap([]));
53
+ mockQuery.where.mockReturnValue(mockQuery);
54
+ mockQuery.orderBy.mockReturnValue(mockQuery);
55
+ mockQuery.limit.mockReturnValue(mockQuery);
56
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
57
+ mockDocRef.get.mockResolvedValue(makeSnap(makeFeatureDoc(), "feature-free-shipping"));
58
+ mockDocRef.set.mockResolvedValue(undefined);
59
+ mockDocRef.update.mockResolvedValue(undefined);
60
+ mockDocRef.delete.mockResolvedValue(undefined);
61
+ });
62
+ // ---------------------------------------------------------------------------
63
+ // generateId
64
+ // ---------------------------------------------------------------------------
65
+ describe("ProductFeaturesRepository.generateId", () => {
66
+ it("generates ID with feature- prefix", () => {
67
+ const id = repo.generateId("Free Shipping");
68
+ expect(id).toMatch(/^feature-/);
69
+ });
70
+ it("slugifies the label (spaces → hyphens, lowercase)", () => {
71
+ const id = repo.generateId("Free Shipping");
72
+ expect(id).toBe("feature-free-shipping");
73
+ });
74
+ it("does not double-prefix when label already starts with feature-", () => {
75
+ const id = repo.generateId("feature-checkout");
76
+ expect(id).toBe("feature-checkout");
77
+ expect(id).not.toContain("feature-feature-");
78
+ });
79
+ it("strips special characters from label", () => {
80
+ const id = repo.generateId("Fast & Reliable!");
81
+ expect(id).not.toContain("&");
82
+ expect(id).not.toContain("!");
83
+ expect(id).toMatch(/^feature-/);
84
+ });
85
+ });
86
+ // ---------------------------------------------------------------------------
87
+ // create — scope validations
88
+ // ---------------------------------------------------------------------------
89
+ describe("ProductFeaturesRepository.create — scope validations", () => {
90
+ it("throws ValidationError when scope=store and storeId is not provided", async () => {
91
+ await expect(repo.create({
92
+ label: "Custom Badge",
93
+ description: "A store badge",
94
+ icon: "star",
95
+ scope: "store",
96
+ productTypes: ["all"],
97
+ displayOrder: 0,
98
+ isActive: true,
99
+ })).rejects.toThrow(ValidationError);
100
+ });
101
+ it("throws ValidationError when scope=platform and storeId is provided", async () => {
102
+ await expect(repo.create({
103
+ label: "Platform Badge",
104
+ description: "A platform badge",
105
+ icon: "award",
106
+ scope: "platform",
107
+ storeId: "store-pokemon-palace",
108
+ productTypes: ["all"],
109
+ displayOrder: 0,
110
+ isActive: true,
111
+ })).rejects.toThrow(ValidationError);
112
+ });
113
+ it("throws ValidationError when store feature count reaches MAX_STORE_CUSTOM_FEATURES", async () => {
114
+ // countByStore will query and return snap with size = MAX_STORE_CUSTOM_FEATURES
115
+ mockQuery.get.mockResolvedValue({ size: MAX_STORE_CUSTOM_FEATURES, docs: [], empty: false });
116
+ await expect(repo.create({
117
+ label: "One Too Many",
118
+ description: "Exceeds limit",
119
+ icon: "x",
120
+ scope: "store",
121
+ storeId: "store-pokemon-palace",
122
+ productTypes: ["all"],
123
+ displayOrder: 0,
124
+ isActive: true,
125
+ })).rejects.toThrow(ValidationError);
126
+ });
127
+ it("does NOT throw when store feature count is below MAX_STORE_CUSTOM_FEATURES", async () => {
128
+ mockQuery.get.mockResolvedValue({ size: MAX_STORE_CUSTOM_FEATURES - 1, docs: [], empty: false });
129
+ // After count check, doc().get() for findByIdOrFail
130
+ mockDocRef.get.mockResolvedValue(makeSnap(makeFeatureDoc({ scope: "store", storeId: "store-1" }), "feature-custom-badge"));
131
+ await expect(repo.create({
132
+ label: "Custom Badge",
133
+ description: "A store badge",
134
+ icon: "star",
135
+ scope: "store",
136
+ storeId: "store-pokemon-palace",
137
+ productTypes: ["all"],
138
+ displayOrder: 0,
139
+ isActive: true,
140
+ })).resolves.toBeDefined();
141
+ });
142
+ it("platform scope with no storeId creates the feature successfully", async () => {
143
+ mockDocRef.get.mockResolvedValue(makeSnap(makeFeatureDoc(), "feature-free-shipping"));
144
+ await expect(repo.create({
145
+ label: "Free Shipping",
146
+ description: "Ships for free",
147
+ icon: "truck",
148
+ scope: "platform",
149
+ productTypes: ["all"],
150
+ displayOrder: 0,
151
+ isActive: true,
152
+ })).resolves.toBeDefined();
153
+ });
154
+ });
155
+ // ---------------------------------------------------------------------------
156
+ // create — field storage
157
+ // ---------------------------------------------------------------------------
158
+ describe("ProductFeaturesRepository.create — field storage", () => {
159
+ it("uses generated ID from label as document ID", async () => {
160
+ mockDocRef.get.mockResolvedValue(makeSnap(makeFeatureDoc(), "feature-fast-checkout"));
161
+ await repo.create({
162
+ label: "Fast Checkout",
163
+ description: "Quick buy",
164
+ icon: "zap",
165
+ scope: "platform",
166
+ productTypes: ["all"],
167
+ displayOrder: 1,
168
+ isActive: true,
169
+ });
170
+ expect(mockDocRef.set).toHaveBeenCalledWith(expect.objectContaining({ slug: "feature-fast-checkout" }));
171
+ });
172
+ it("sets createdAt and updatedAt timestamps", async () => {
173
+ mockDocRef.get.mockResolvedValue(makeSnap(makeFeatureDoc(), "feature-fast-checkout"));
174
+ await repo.create({
175
+ label: "Fast Checkout",
176
+ description: "Quick buy",
177
+ icon: "zap",
178
+ scope: "platform",
179
+ productTypes: ["all"],
180
+ displayOrder: 1,
181
+ isActive: true,
182
+ });
183
+ const setArg = mockDocRef.set.mock.calls[0][0];
184
+ expect(setArg.createdAt).toBeInstanceOf(Date);
185
+ expect(setArg.updatedAt).toBeInstanceOf(Date);
186
+ });
187
+ });
188
+ // ---------------------------------------------------------------------------
189
+ // delete
190
+ // ---------------------------------------------------------------------------
191
+ describe("ProductFeaturesRepository.delete", () => {
192
+ it("throws ValidationError when a product references the feature", async () => {
193
+ // The where chain for the product reference check returns a non-empty snap
194
+ mockQuery.get.mockResolvedValueOnce({ empty: false, size: 1, docs: [{ id: "prod-1" }] });
195
+ await expect(repo.delete("feature-free-shipping")).rejects.toThrow(ValidationError);
196
+ });
197
+ it("deletes the feature when no product references it", async () => {
198
+ mockQuery.get.mockResolvedValueOnce(makeQuerySnap([]));
199
+ await expect(repo.delete("feature-free-shipping")).resolves.toBeUndefined();
200
+ expect(mockDocRef.delete).toHaveBeenCalledOnce();
201
+ });
202
+ it("limits the product reference check to 1 document (efficiency)", async () => {
203
+ mockQuery.get.mockResolvedValueOnce(makeQuerySnap([]));
204
+ await repo.delete("feature-free-shipping");
205
+ expect(mockQuery.limit).toHaveBeenCalledWith(1);
206
+ });
207
+ it("does not delete the feature when a product references it", async () => {
208
+ mockQuery.get.mockResolvedValueOnce({ empty: false, size: 1, docs: [{ id: "prod-1" }] });
209
+ try {
210
+ await repo.delete("feature-free-shipping");
211
+ }
212
+ catch {
213
+ // expected
214
+ }
215
+ expect(mockDocRef.delete).not.toHaveBeenCalled();
216
+ });
217
+ });
218
+ // ---------------------------------------------------------------------------
219
+ // countByStore
220
+ // ---------------------------------------------------------------------------
221
+ describe("ProductFeaturesRepository.countByStore", () => {
222
+ it("returns snap.size as the count", async () => {
223
+ mockQuery.get.mockResolvedValue({ size: 3, docs: [], empty: false });
224
+ const count = await repo.countByStore("store-pokemon-palace");
225
+ expect(count).toBe(3);
226
+ });
227
+ it("returns 0 when no store features exist", async () => {
228
+ mockQuery.get.mockResolvedValue({ size: 0, docs: [], empty: true });
229
+ const count = await repo.countByStore("store-no-features");
230
+ expect(count).toBe(0);
231
+ });
232
+ it("filters by scope=store and storeId", async () => {
233
+ mockQuery.get.mockResolvedValue({ size: 0, docs: [], empty: true });
234
+ await repo.countByStore("store-pokemon-palace");
235
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("scope"), "==", "store");
236
+ expect(mockQuery.where).toHaveBeenCalledWith(expect.stringContaining("storeId"), "==", "store-pokemon-palace");
237
+ });
238
+ });
239
+ // ---------------------------------------------------------------------------
240
+ // update
241
+ // ---------------------------------------------------------------------------
242
+ describe("ProductFeaturesRepository.update", () => {
243
+ it("updates provided fields plus updatedAt", async () => {
244
+ await repo.update("feature-free-shipping", { label: "Free Delivery", isActive: false });
245
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ label: "Free Delivery", isActive: false, updatedAt: expect.any(Date) }));
246
+ });
247
+ it("calls doc(id) to target the correct feature", async () => {
248
+ await repo.update("feature-abc", { label: "Updated" });
249
+ expect(mockCollection.doc).toHaveBeenCalledWith("feature-abc");
250
+ });
251
+ });
252
+ // ---------------------------------------------------------------------------
253
+ // listFiltered
254
+ // ---------------------------------------------------------------------------
255
+ describe("ProductFeaturesRepository.listFiltered", () => {
256
+ it("applies scope filter when provided", async () => {
257
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
258
+ await repo.listFiltered({ scope: "platform" });
259
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("scope"), "==", "platform");
260
+ });
261
+ it("applies storeId filter when provided", async () => {
262
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
263
+ await repo.listFiltered({ storeId: "store-1" });
264
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("storeId"), "==", "store-1");
265
+ });
266
+ it("applies isActive filter when provided", async () => {
267
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
268
+ await repo.listFiltered({ isActive: true });
269
+ expect(mockCollection.where).toHaveBeenCalledWith("isActive", "==", true);
270
+ });
271
+ it("filters by productType in-memory (not Firestore)", async () => {
272
+ const feature1 = makeFeatureDoc({ id: "f-1", productTypes: ["all"] });
273
+ const feature2 = makeFeatureDoc({ id: "f-2", productTypes: ["product"] });
274
+ const feature3 = makeFeatureDoc({ id: "f-3", productTypes: ["auction"] });
275
+ // No Firestore filters applied for productType-only query → uses mockCollection.get
276
+ mockCollection.get.mockResolvedValue(makeQuerySnap([
277
+ { id: "f-1", data: feature1 },
278
+ { id: "f-2", data: feature2 },
279
+ { id: "f-3", data: feature3 },
280
+ ]));
281
+ const results = await repo.listFiltered({ productType: "product" });
282
+ // "all" matches everything; "product" matches "product"; "auction" does NOT match "product"
283
+ expect(results).toHaveLength(2);
284
+ const ids = results.map((r) => r.id);
285
+ expect(ids).toContain("f-1");
286
+ expect(ids).toContain("f-2");
287
+ expect(ids).not.toContain("f-3");
288
+ });
289
+ it("returns results sorted by displayOrder ascending", async () => {
290
+ const features = [
291
+ makeFeatureDoc({ id: "f-b", displayOrder: 2 }),
292
+ makeFeatureDoc({ id: "f-a", displayOrder: 0 }),
293
+ makeFeatureDoc({ id: "f-c", displayOrder: 5 }),
294
+ ];
295
+ // No Firestore filters → uses mockCollection.get
296
+ mockCollection.get.mockResolvedValue(makeQuerySnap(features.map((f) => ({ id: f.id, data: f }))));
297
+ const results = await repo.listFiltered({});
298
+ expect(results[0].id).toBe("f-a");
299
+ expect(results[1].id).toBe("f-b");
300
+ expect(results[2].id).toBe("f-c");
301
+ });
302
+ it("returns empty array when no features match", async () => {
303
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
304
+ const results = await repo.listFiltered({ scope: "store", storeId: "store-empty" });
305
+ expect(results).toEqual([]);
306
+ });
307
+ });
308
+ // ---------------------------------------------------------------------------
309
+ // listPlatform
310
+ // ---------------------------------------------------------------------------
311
+ describe("ProductFeaturesRepository.listPlatform", () => {
312
+ it("calls listFiltered with scope=platform and isActive=true", async () => {
313
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
314
+ await repo.listPlatform();
315
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("scope"), "==", "platform");
316
+ expect(mockCollection.where).toHaveBeenCalledWith("isActive", "==", true);
317
+ });
318
+ });
319
+ // ---------------------------------------------------------------------------
320
+ // listForStore
321
+ // ---------------------------------------------------------------------------
322
+ describe("ProductFeaturesRepository.listForStore", () => {
323
+ it("calls listFiltered with scope=store, storeId, and isActive=true", async () => {
324
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
325
+ await repo.listForStore("store-pokemon-palace");
326
+ expect(mockCollection.where).toHaveBeenCalledWith(expect.stringContaining("storeId"), "==", "store-pokemon-palace");
327
+ expect(mockCollection.where).toHaveBeenCalledWith("isActive", "==", true);
328
+ });
329
+ });