@mohasinac/appkit 3.0.4 → 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 (320) 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/auctions/data.d.ts +2 -4
  20. package/dist/_internal/server/features/auctions/data.js +3 -9
  21. package/dist/_internal/server/features/checkout/__tests__/actions.test.d.ts +1 -0
  22. package/dist/_internal/server/features/checkout/__tests__/actions.test.js +600 -0
  23. package/dist/_internal/server/features/classified/adapters.d.ts +2 -15
  24. package/dist/_internal/server/features/classified/adapters.js +2 -17
  25. package/dist/_internal/server/features/classified/data.d.ts +1 -6
  26. package/dist/_internal/server/features/classified/data.js +2 -10
  27. package/dist/_internal/server/features/digital-code/adapters.d.ts +2 -15
  28. package/dist/_internal/server/features/digital-code/adapters.js +2 -17
  29. package/dist/_internal/server/features/digital-code/data.d.ts +1 -6
  30. package/dist/_internal/server/features/digital-code/data.js +2 -10
  31. package/dist/_internal/server/features/live/adapters.d.ts +2 -15
  32. package/dist/_internal/server/features/live/adapters.js +2 -17
  33. package/dist/_internal/server/features/live/data.d.ts +1 -6
  34. package/dist/_internal/server/features/live/data.js +2 -10
  35. package/dist/_internal/server/features/lottery/actions.d.ts +43 -0
  36. package/dist/_internal/server/features/lottery/actions.js +243 -0
  37. package/dist/_internal/server/features/lottery/adapters.d.ts +9 -0
  38. package/dist/_internal/server/features/lottery/adapters.js +21 -0
  39. package/dist/_internal/server/features/lottery/data.d.ts +20 -0
  40. package/dist/_internal/server/features/lottery/data.js +41 -0
  41. package/dist/_internal/server/features/lottery/metadata.d.ts +6 -0
  42. package/dist/_internal/server/features/lottery/metadata.js +31 -0
  43. package/dist/_internal/server/features/pre-orders/data.d.ts +2 -4
  44. package/dist/_internal/server/features/pre-orders/data.js +3 -9
  45. package/dist/_internal/server/features/prize-draws/actions.d.ts +2 -0
  46. package/dist/_internal/server/features/prize-draws/actions.js +22 -0
  47. package/dist/_internal/server/features/prize-draws/data.d.ts +1 -17
  48. package/dist/_internal/server/features/prize-draws/data.js +2 -20
  49. package/dist/_internal/server/features/prize-draws/index.d.ts +3 -1
  50. package/dist/_internal/server/features/prize-draws/index.js +2 -0
  51. package/dist/_internal/server/features/prize-draws/service.d.ts +3 -0
  52. package/dist/_internal/server/features/prize-draws/service.js +19 -0
  53. package/dist/_internal/server/features/refunds/__tests__/actions.test.d.ts +1 -0
  54. package/dist/_internal/server/features/refunds/__tests__/actions.test.js +263 -0
  55. package/dist/_internal/server/features/shared/listing-adapters.d.ts +19 -0
  56. package/dist/_internal/server/features/shared/listing-adapters.js +20 -0
  57. package/dist/_internal/server/features/shared/listing-data-factory.d.ts +9 -0
  58. package/dist/_internal/server/features/shared/listing-data-factory.js +33 -0
  59. package/dist/_internal/server/features/stores/data.d.ts +3 -14
  60. package/dist/_internal/server/features/stores/data.js +6 -44
  61. package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.d.ts +1 -0
  62. package/dist/_internal/server/jobs/core/__tests__/auctionSettlement.test.js +249 -0
  63. package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.d.ts +1 -0
  64. package/dist/_internal/server/jobs/core/__tests__/onBidPlaced.test.js +201 -0
  65. package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.d.ts +1 -0
  66. package/dist/_internal/server/jobs/core/__tests__/onOrderCreate.test.js +169 -0
  67. package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.d.ts +1 -0
  68. package/dist/_internal/server/jobs/core/__tests__/onOrderStatusChange.test.js +148 -0
  69. package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.d.ts +1 -0
  70. package/dist/_internal/server/jobs/core/__tests__/onProductStockChange.test.js +210 -0
  71. package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.d.ts +1 -0
  72. package/dist/_internal/server/jobs/core/__tests__/onReviewWrite.test.js +190 -0
  73. package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.d.ts +1 -0
  74. package/dist/_internal/server/jobs/core/__tests__/onUserBanChange.test.js +161 -0
  75. package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.d.ts +1 -0
  76. package/dist/_internal/server/jobs/core/__tests__/payoutBatch.test.js +302 -0
  77. package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.d.ts +1 -0
  78. package/dist/_internal/server/jobs/core/__tests__/triggerEventRaffle.test.js +252 -0
  79. package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.d.ts +1 -0
  80. package/dist/_internal/server/jobs/core/__tests__/wrapJobHandler.test.js +129 -0
  81. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  82. package/dist/_internal/shared/actions/action-registry.js +46 -0
  83. package/dist/_internal/shared/features/cart/schema.d.ts +8 -8
  84. package/dist/_internal/shared/features/cart/schema.js +3 -0
  85. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  86. package/dist/_internal/shared/listing-types/_registry.js +2 -0
  87. package/dist/_internal/shared/listing-types/auction/schema.d.ts +3 -1
  88. package/dist/_internal/shared/listing-types/auction/schema.js +7 -1
  89. package/dist/_internal/shared/listing-types/classified/schema.d.ts +2 -1
  90. package/dist/_internal/shared/listing-types/classified/schema.js +3 -1
  91. package/dist/_internal/shared/listing-types/digital-code/schema.d.ts +3 -1
  92. package/dist/_internal/shared/listing-types/digital-code/schema.js +7 -1
  93. package/dist/_internal/shared/listing-types/live/schema.d.ts +2 -1
  94. package/dist/_internal/shared/listing-types/live/schema.js +3 -1
  95. package/dist/_internal/shared/listing-types/pre-order/schema.d.ts +3 -1
  96. package/dist/_internal/shared/listing-types/pre-order/schema.js +7 -1
  97. package/dist/_internal/shared/listing-types/prize-draw/schema.d.ts +3 -1
  98. package/dist/_internal/shared/listing-types/prize-draw/schema.js +13 -1
  99. package/dist/_internal/shared/listing-types/standard/schema.d.ts +2 -1
  100. package/dist/_internal/shared/listing-types/standard/schema.js +4 -1
  101. package/dist/_internal/shared/types/base-document.d.ts +15 -0
  102. package/dist/_internal/shared/types/base-document.js +1 -0
  103. package/dist/_internal/shared/types/index.d.ts +2 -0
  104. package/dist/_internal/shared/types/index.js +1 -0
  105. package/dist/client.d.ts +13 -4
  106. package/dist/client.js +11 -2
  107. package/dist/constants/api-endpoints.d.ts +12 -0
  108. package/dist/constants/api-endpoints.js +4 -0
  109. package/dist/constants/field-names.d.ts +5 -1
  110. package/dist/constants/field-names.js +5 -1
  111. package/dist/features/addresses/repository/__tests__/addresses.repository.test.d.ts +1 -0
  112. package/dist/features/addresses/repository/__tests__/addresses.repository.test.js +386 -0
  113. package/dist/features/addresses/schemas/firestore.d.ts +2 -4
  114. package/dist/features/admin/api/bids/route.d.ts +2 -2
  115. package/dist/features/admin/api/coupons/route.d.ts +2 -2
  116. package/dist/features/admin/api/products/route.d.ts +2 -2
  117. package/dist/features/admin/api/reviews/route.d.ts +2 -2
  118. package/dist/features/admin/components/AdminFulfillmentView.d.ts +6 -0
  119. package/dist/features/admin/components/AdminFulfillmentView.js +107 -0
  120. package/dist/features/admin/components/AdminProductEditorView.js +21 -9
  121. package/dist/features/admin/components/AdminProductsView.js +3 -0
  122. package/dist/features/admin/components/DataListingView.d.ts +1 -0
  123. package/dist/features/admin/components/index.d.ts +2 -0
  124. package/dist/features/admin/components/index.js +1 -0
  125. package/dist/features/admin/repository/__tests__/notification.repository.test.d.ts +1 -0
  126. package/dist/features/admin/repository/__tests__/notification.repository.test.js +356 -0
  127. package/dist/features/admin/repository/__tests__/site-settings.repository.test.d.ts +1 -0
  128. package/dist/features/admin/repository/__tests__/site-settings.repository.test.js +298 -0
  129. package/dist/features/admin/schemas/firestore.d.ts +3 -8
  130. package/dist/features/auctions/repository/__tests__/bid.repository.test.d.ts +1 -0
  131. package/dist/features/auctions/repository/__tests__/bid.repository.test.js +290 -0
  132. package/dist/features/auctions/schemas/firestore.d.ts +2 -4
  133. package/dist/features/auctions/schemas/index.d.ts +2 -2
  134. package/dist/features/auth/api/route.d.ts +2 -2
  135. package/dist/features/auth/repository/__tests__/session.repository.test.d.ts +1 -0
  136. package/dist/features/auth/repository/__tests__/session.repository.test.js +310 -0
  137. package/dist/features/auth/repository/__tests__/session.test.d.ts +1 -0
  138. package/dist/features/auth/repository/__tests__/session.test.js +146 -0
  139. package/dist/features/auth/repository/__tests__/sms-counter.repository.test.d.ts +1 -0
  140. package/dist/features/auth/repository/__tests__/sms-counter.repository.test.js +171 -0
  141. package/dist/features/auth/repository/__tests__/sms-counter.test.d.ts +1 -0
  142. package/dist/features/auth/repository/__tests__/sms-counter.test.js +88 -0
  143. package/dist/features/auth/repository/__tests__/token.repository.test.d.ts +1 -0
  144. package/dist/features/auth/repository/__tests__/token.repository.test.js +260 -0
  145. package/dist/features/auth/repository/__tests__/token.test.d.ts +1 -0
  146. package/dist/features/auth/repository/__tests__/token.test.js +140 -0
  147. package/dist/features/auth/repository/__tests__/user.repository.test.d.ts +1 -0
  148. package/dist/features/auth/repository/__tests__/user.repository.test.js +352 -0
  149. package/dist/features/auth/repository/__tests__/user.test.d.ts +1 -0
  150. package/dist/features/auth/repository/__tests__/user.test.js +226 -0
  151. package/dist/features/auth/repository/session.repository.js +10 -1
  152. package/dist/features/before-after/schemas/index.d.ts +2 -2
  153. package/dist/features/blog/schemas/firestore.d.ts +2 -4
  154. package/dist/features/blog/schemas/index.d.ts +4 -4
  155. package/dist/features/cart/repository/__tests__/cart.repository.test.d.ts +1 -0
  156. package/dist/features/cart/repository/__tests__/cart.repository.test.js +371 -0
  157. package/dist/features/cart/repository/cart.repository.js +2 -2
  158. package/dist/features/cart/schemas/firestore.d.ts +2 -4
  159. package/dist/features/categories/api/[id]/route.d.ts +4 -4
  160. package/dist/features/categories/api/route.d.ts +2 -2
  161. package/dist/features/categories/repository/__tests__/categories.repository.test.d.ts +1 -0
  162. package/dist/features/categories/repository/__tests__/categories.repository.test.js +813 -0
  163. package/dist/features/categories/schemas/firestore.d.ts +2 -4
  164. package/dist/features/categories/schemas/index.d.ts +4 -4
  165. package/dist/features/events/components/EventCard.js +1 -0
  166. package/dist/features/events/repository/__tests__/event.repository.test.d.ts +1 -0
  167. package/dist/features/events/repository/__tests__/event.repository.test.js +299 -0
  168. package/dist/features/events/schemas/firestore.d.ts +5 -4
  169. package/dist/features/events/schemas/index.d.ts +836 -16
  170. package/dist/features/events/schemas/index.js +80 -1
  171. package/dist/features/events/types/index.d.ts +4 -1
  172. package/dist/features/faq/repository/__tests__/faqs.repository.test.d.ts +1 -0
  173. package/dist/features/faq/repository/__tests__/faqs.repository.test.js +311 -0
  174. package/dist/features/faq/schemas/firestore.d.ts +2 -4
  175. package/dist/features/faq/schemas/index.d.ts +2 -2
  176. package/dist/features/grouped/schemas/index.d.ts +2 -2
  177. package/dist/features/history/repository/__tests__/user-history.repository.test.d.ts +1 -0
  178. package/dist/features/history/repository/__tests__/user-history.repository.test.js +180 -0
  179. package/dist/features/homepage/api/[id]/route.d.ts +4 -4
  180. package/dist/features/homepage/api/carousel/[id]/route.d.ts +4 -4
  181. package/dist/features/homepage/api/carousel/route.d.ts +2 -2
  182. package/dist/features/homepage/api/route.d.ts +2 -2
  183. package/dist/features/homepage/repository/__tests__/carousel.repository.test.d.ts +1 -0
  184. package/dist/features/homepage/repository/__tests__/carousel.repository.test.js +182 -0
  185. package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.d.ts +1 -0
  186. package/dist/features/homepage/repository/__tests__/homepage-sections.repository.test.js +185 -0
  187. package/dist/features/homepage/schemas/firestore.d.ts +4 -12
  188. package/dist/features/homepage/schemas/index.d.ts +2 -2
  189. package/dist/features/lottery/repository/lottery-entry.repository.d.ts +33 -0
  190. package/dist/features/lottery/repository/lottery-entry.repository.js +130 -0
  191. package/dist/features/lottery/schemas/firestore.d.ts +49 -0
  192. package/dist/features/lottery/schemas/firestore.js +25 -0
  193. package/dist/features/lottery/schemas/zod.d.ts +113 -0
  194. package/dist/features/lottery/schemas/zod.js +39 -0
  195. package/dist/features/lottery/types/index.d.ts +67 -0
  196. package/dist/features/lottery/types/index.js +55 -0
  197. package/dist/features/messages/repository/__tests__/conversations.repository.test.d.ts +1 -0
  198. package/dist/features/messages/repository/__tests__/conversations.repository.test.js +257 -0
  199. package/dist/features/messages/schemas/firestore.d.ts +2 -4
  200. package/dist/features/messages/schemas/index.d.ts +2 -2
  201. package/dist/features/orders/repository/__tests__/orders.repository.test.d.ts +1 -0
  202. package/dist/features/orders/repository/__tests__/orders.repository.test.js +211 -0
  203. package/dist/features/orders/repository/orders.repository.d.ts +5 -0
  204. package/dist/features/orders/repository/orders.repository.js +26 -0
  205. package/dist/features/orders/schemas/firestore.d.ts +9 -5
  206. package/dist/features/orders/schemas/firestore.js +3 -0
  207. package/dist/features/payments/repository/__tests__/payout.repository.test.d.ts +1 -0
  208. package/dist/features/payments/repository/__tests__/payout.repository.test.js +292 -0
  209. package/dist/features/payments/schemas/firestore.d.ts +2 -4
  210. package/dist/features/pre-orders/schemas/index.d.ts +2 -2
  211. package/dist/features/products/api/[id]/route.d.ts +4 -4
  212. package/dist/features/products/api/route.d.ts +2 -2
  213. package/dist/features/products/constants/action-defs.d.ts +16 -0
  214. package/dist/features/products/constants/action-defs.js +16 -0
  215. package/dist/features/products/constants/listing-tabs.d.ts +48 -0
  216. package/dist/features/products/constants/listing-tabs.js +12 -0
  217. package/dist/features/products/repository/__tests__/product-features.repository.test.d.ts +1 -0
  218. package/dist/features/products/repository/__tests__/product-features.repository.test.js +329 -0
  219. package/dist/features/products/repository/__tests__/products.repository.test.d.ts +1 -0
  220. package/dist/features/products/repository/__tests__/products.repository.test.js +607 -0
  221. package/dist/features/products/repository/products.repository.d.ts +37 -0
  222. package/dist/features/products/repository/products.repository.js +28 -1
  223. package/dist/features/products/schemas/firestore.d.ts +23 -7
  224. package/dist/features/products/schemas/firestore.js +2 -0
  225. package/dist/features/products/schemas/index.d.ts +18 -18
  226. package/dist/features/products/schemas/index.js +4 -2
  227. package/dist/features/products/types/index.d.ts +3 -1
  228. package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.d.ts +1 -0
  229. package/dist/features/promotions/repository/__tests__/claimed-coupons.repository.test.js +219 -0
  230. package/dist/features/promotions/repository/__tests__/coupons.repository.test.d.ts +1 -0
  231. package/dist/features/promotions/repository/__tests__/coupons.repository.test.js +480 -0
  232. package/dist/features/promotions/schemas/firestore.d.ts +2 -4
  233. package/dist/features/promotions/schemas/index.d.ts +4 -4
  234. package/dist/features/reviews/api/[id]/route.d.ts +4 -4
  235. package/dist/features/reviews/api/route.d.ts +2 -2
  236. package/dist/features/reviews/repository/__tests__/reviews.repository.test.d.ts +1 -0
  237. package/dist/features/reviews/repository/__tests__/reviews.repository.test.js +327 -0
  238. package/dist/features/reviews/schemas/firestore.d.ts +2 -4
  239. package/dist/features/reviews/schemas/index.d.ts +2 -2
  240. package/dist/features/scams/repository/__tests__/scammer.repository.test.d.ts +1 -0
  241. package/dist/features/scams/repository/__tests__/scammer.repository.test.js +385 -0
  242. package/dist/features/scams/schemas/firestore.d.ts +2 -4
  243. package/dist/features/scams/schemas/index.d.ts +4 -4
  244. package/dist/features/search/repository/__tests__/search.repository.test.d.ts +1 -0
  245. package/dist/features/search/repository/__tests__/search.repository.test.js +369 -0
  246. package/dist/features/search/schemas/index.d.ts +2 -2
  247. package/dist/features/seller/api/coupons/route.d.ts +2 -2
  248. package/dist/features/seller/api/offers/route.d.ts +2 -2
  249. package/dist/features/seller/api/products/route.d.ts +2 -2
  250. package/dist/features/seller/api/store/route.d.ts +2 -2
  251. package/dist/features/seller/components/BarcodeField.d.ts +17 -0
  252. package/dist/features/seller/components/BarcodeField.js +30 -0
  253. package/dist/features/seller/components/FulfillmentView.d.ts +5 -0
  254. package/dist/features/seller/components/FulfillmentView.js +120 -0
  255. package/dist/features/seller/components/PrintCenterView.d.ts +3 -1
  256. package/dist/features/seller/components/PrintCenterView.js +52 -4
  257. package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
  258. package/dist/features/seller/components/SellerProductShell.js +2 -1
  259. package/dist/features/seller/components/index.d.ts +4 -0
  260. package/dist/features/seller/components/index.js +2 -0
  261. package/dist/features/seller/repository/__tests__/offer.repository.test.d.ts +1 -0
  262. package/dist/features/seller/repository/__tests__/offer.repository.test.js +458 -0
  263. package/dist/features/seller/schemas/index.d.ts +2 -2
  264. package/dist/features/store-extensions/schemas/firestore.d.ts +12 -44
  265. package/dist/features/stores/components/StoreClassifiedsListing.d.ts +5 -0
  266. package/dist/features/stores/components/StoreClassifiedsListing.js +48 -0
  267. package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -0
  268. package/dist/features/stores/components/StoreClassifiedsPageView.js +20 -0
  269. package/dist/features/stores/components/StoreDetailLayoutView.js +23 -2
  270. package/dist/features/stores/components/StoreDigitalCodesListing.d.ts +5 -0
  271. package/dist/features/stores/components/StoreDigitalCodesListing.js +48 -0
  272. package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -0
  273. package/dist/features/stores/components/StoreDigitalCodesPageView.js +20 -0
  274. package/dist/features/stores/components/StoreLiveItemsListing.d.ts +5 -0
  275. package/dist/features/stores/components/StoreLiveItemsListing.js +48 -0
  276. package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -0
  277. package/dist/features/stores/components/StoreLiveItemsPageView.js +20 -0
  278. package/dist/features/stores/components/index.d.ts +9 -0
  279. package/dist/features/stores/components/index.js +3 -0
  280. package/dist/features/stores/repository/__tests__/store.repository.test.d.ts +1 -0
  281. package/dist/features/stores/repository/__tests__/store.repository.test.js +261 -0
  282. package/dist/features/stores/schemas/firestore.d.ts +2 -4
  283. package/dist/features/stores/schemas/index.d.ts +4 -4
  284. package/dist/features/support/repository/__tests__/support.repository.test.d.ts +1 -0
  285. package/dist/features/support/repository/__tests__/support.repository.test.js +312 -0
  286. package/dist/features/support/schemas/firestore.d.ts +2 -4
  287. package/dist/features/support/schemas/index.d.ts +2 -2
  288. package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.d.ts +1 -0
  289. package/dist/features/wishlist/repository/__tests__/user-wishlist.repository.test.js +139 -0
  290. package/dist/index.d.ts +14 -0
  291. package/dist/index.js +17 -0
  292. package/dist/next/api/routeHandler.d.ts +1 -1
  293. package/dist/next/routing/route-map.d.ts +24 -0
  294. package/dist/next/routing/route-map.js +11 -0
  295. package/dist/repositories/index.d.ts +1 -0
  296. package/dist/repositories/index.js +2 -0
  297. package/dist/schemas/registry.d.ts +24 -24
  298. package/dist/security/index.d.ts +1 -1
  299. package/dist/security/index.js +1 -1
  300. package/dist/security/pii-schemas.d.ts +2 -0
  301. package/dist/security/pii-schemas.js +6 -0
  302. package/dist/seed/actions/demo-seed-actions.d.ts +1 -1
  303. package/dist/seed/events-seed-data.js +85 -2
  304. package/dist/seed/grouped-listings-seed-data.d.ts +5 -2
  305. package/dist/seed/index.d.ts +1 -0
  306. package/dist/seed/index.js +1 -0
  307. package/dist/seed/lottery-entries-seed-data.d.ts +2 -0
  308. package/dist/seed/lottery-entries-seed-data.js +108 -0
  309. package/dist/seed/manifest.js +1 -0
  310. package/dist/seed/products-preorders-seed-data.d.ts +5 -2
  311. package/dist/seed/products-standard-seed-data.js +8 -0
  312. package/dist/server-entry.d.ts +3 -0
  313. package/dist/server-entry.js +3 -0
  314. package/dist/server.d.ts +10 -4
  315. package/dist/server.js +6 -0
  316. package/dist/styles.css +1 -1
  317. package/dist/tailwind-utilities.css +1 -1
  318. package/dist/utils/id-generators.d.ts +8 -0
  319. package/dist/utils/id-generators.js +14 -0
  320. package/package.json +2 -1
@@ -0,0 +1,171 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { makeMockDb, makeSnap } from "../../../../../tests/helpers/mock-firestore";
3
+ const { db, mockDocRef, mockCollection, mockTxn } = 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 { ...actual, prepareForFirestore: (d) => d };
10
+ });
11
+ vi.mock("../../../../contracts/field-ops", () => ({
12
+ serverTimestamp: () => new Date(),
13
+ increment: (n) => n,
14
+ arrayUnion: (...args) => args,
15
+ arrayRemove: (...args) => args,
16
+ deleteField: () => null,
17
+ registerFieldOps: vi.fn(),
18
+ }));
19
+ vi.mock("../../../../errors/normalize", () => ({ normalizeError: vi.fn() }));
20
+ vi.mock("../../../../monitoring", () => ({
21
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
22
+ }));
23
+ import { SmsCounterRepository } from "../sms-counter.repository";
24
+ import { SMS_DAILY_LIMIT } from "../../schemas";
25
+ const repo = new SmsCounterRepository();
26
+ beforeEach(() => {
27
+ vi.clearAllMocks();
28
+ db.collection.mockReturnValue(mockCollection);
29
+ mockCollection.doc.mockReturnValue(mockDocRef);
30
+ mockTxn.get.mockResolvedValue(makeSnap(null));
31
+ mockTxn.set.mockReturnValue(undefined);
32
+ mockTxn.update.mockReturnValue(undefined);
33
+ db.runTransaction.mockImplementation(async (cb) => cb(mockTxn));
34
+ });
35
+ // ---------------------------------------------------------------------------
36
+ // checkAndIncrement
37
+ // ---------------------------------------------------------------------------
38
+ describe("SmsCounterRepository.checkAndIncrement", () => {
39
+ it("first call of the day (no doc) → returns { allowed: true, count: 1 }", async () => {
40
+ mockTxn.get.mockResolvedValue(makeSnap(null));
41
+ const result = await repo.checkAndIncrement("2026-06-27");
42
+ expect(result.allowed).toBe(true);
43
+ expect(result.count).toBe(1);
44
+ });
45
+ it("uses Firestore transaction (runTransaction called)", async () => {
46
+ mockTxn.get.mockResolvedValue(makeSnap(null));
47
+ await repo.checkAndIncrement("2026-06-27");
48
+ expect(db.runTransaction).toHaveBeenCalledOnce();
49
+ });
50
+ it("increments existing count by 1", async () => {
51
+ mockTxn.get.mockResolvedValue(makeSnap({ count: 4 }));
52
+ const result = await repo.checkAndIncrement("2026-06-27");
53
+ expect(result.count).toBe(5);
54
+ expect(result.allowed).toBe(true);
55
+ });
56
+ it("count at SMS_DAILY_LIMIT - 1 → increments to limit, returns allowed: true", async () => {
57
+ mockTxn.get.mockResolvedValue(makeSnap({ count: SMS_DAILY_LIMIT - 1 }));
58
+ const result = await repo.checkAndIncrement("2026-06-27");
59
+ expect(result.allowed).toBe(true);
60
+ expect(result.count).toBe(SMS_DAILY_LIMIT);
61
+ });
62
+ it("count at SMS_DAILY_LIMIT → returns { allowed: false, count: limit }", async () => {
63
+ mockTxn.get.mockResolvedValue(makeSnap({ count: SMS_DAILY_LIMIT }));
64
+ const result = await repo.checkAndIncrement("2026-06-27");
65
+ expect(result.allowed).toBe(false);
66
+ expect(result.count).toBe(SMS_DAILY_LIMIT);
67
+ });
68
+ it("count exceeds SMS_DAILY_LIMIT → still returns { allowed: false }", async () => {
69
+ mockTxn.get.mockResolvedValue(makeSnap({ count: SMS_DAILY_LIMIT + 5 }));
70
+ const result = await repo.checkAndIncrement("2026-06-27");
71
+ expect(result.allowed).toBe(false);
72
+ });
73
+ it("when allowed, calls tx.set with incremented count", async () => {
74
+ mockTxn.get.mockResolvedValue(makeSnap({ count: 2 }));
75
+ await repo.checkAndIncrement("2026-06-27");
76
+ expect(mockTxn.set).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ count: 3 }), { merge: true });
77
+ });
78
+ it("when NOT allowed, does NOT call tx.set", async () => {
79
+ mockTxn.get.mockResolvedValue(makeSnap({ count: SMS_DAILY_LIMIT }));
80
+ await repo.checkAndIncrement("2026-06-27");
81
+ expect(mockTxn.set).not.toHaveBeenCalled();
82
+ });
83
+ it("uses dateStr as the document ID (one doc per date)", async () => {
84
+ mockTxn.get.mockResolvedValue(makeSnap(null));
85
+ await repo.checkAndIncrement("2026-06-27");
86
+ expect(mockCollection.doc).toHaveBeenCalledWith("2026-06-27");
87
+ });
88
+ it("stores updatedAt timestamp in the tx.set payload", async () => {
89
+ mockTxn.get.mockResolvedValue(makeSnap(null));
90
+ await repo.checkAndIncrement("2026-06-27");
91
+ const setArg = mockTxn.set.mock.calls[0][1];
92
+ expect(setArg).toHaveProperty("updatedAt");
93
+ });
94
+ });
95
+ // ---------------------------------------------------------------------------
96
+ // getCount
97
+ // ---------------------------------------------------------------------------
98
+ describe("SmsCounterRepository.getCount", () => {
99
+ it("returns 0 when no document exists for the date", async () => {
100
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
101
+ const count = await repo.getCount("2026-06-27");
102
+ expect(count).toBe(0);
103
+ });
104
+ it("returns the stored count", async () => {
105
+ mockDocRef.get.mockResolvedValue(makeSnap({ count: 7 }));
106
+ const count = await repo.getCount("2026-06-27");
107
+ expect(count).toBe(7);
108
+ });
109
+ it("returns 0 if document exists but count field is missing", async () => {
110
+ mockDocRef.get.mockResolvedValue(makeSnap({}));
111
+ const count = await repo.getCount("2026-06-27");
112
+ expect(count).toBe(0);
113
+ });
114
+ });
115
+ // ---------------------------------------------------------------------------
116
+ // checkAndSetUserCooldown
117
+ // ---------------------------------------------------------------------------
118
+ describe("SmsCounterRepository.checkAndSetUserCooldown", () => {
119
+ it("first call (no existing cooldown doc) → returns { allowed: true }", async () => {
120
+ mockTxn.get.mockResolvedValue(makeSnap(null));
121
+ const result = await repo.checkAndSetUserCooldown("user-ravi");
122
+ expect(result.allowed).toBe(true);
123
+ expect(result.retryAfterSeconds).toBe(0);
124
+ });
125
+ it("uses Firestore transaction", async () => {
126
+ mockTxn.get.mockResolvedValue(makeSnap(null));
127
+ await repo.checkAndSetUserCooldown("user-ravi");
128
+ expect(db.runTransaction).toHaveBeenCalledOnce();
129
+ });
130
+ it("cooldown expired (last request >15 min ago) → returns { allowed: true }", async () => {
131
+ const sixteenMinutesAgo = new Date(Date.now() - 16 * 60 * 1000);
132
+ mockTxn.get.mockResolvedValue(makeSnap({ lastRequestedAt: { toDate: () => sixteenMinutesAgo } }));
133
+ const result = await repo.checkAndSetUserCooldown("user-ravi");
134
+ expect(result.allowed).toBe(true);
135
+ expect(result.retryAfterSeconds).toBe(0);
136
+ });
137
+ it("within 15 min cooldown → returns { allowed: false, retryAfterSeconds > 0 }", async () => {
138
+ const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
139
+ mockTxn.get.mockResolvedValue(makeSnap({ lastRequestedAt: { toDate: () => twoMinutesAgo } }));
140
+ const result = await repo.checkAndSetUserCooldown("user-ravi");
141
+ expect(result.allowed).toBe(false);
142
+ expect(result.retryAfterSeconds).toBeGreaterThan(0);
143
+ // 15 min - 2 min = 13 min = 780s (±1s tolerance)
144
+ expect(result.retryAfterSeconds).toBeGreaterThanOrEqual(778);
145
+ expect(result.retryAfterSeconds).toBeLessThanOrEqual(780);
146
+ });
147
+ it("just within cooldown (1 second in) → retryAfterSeconds ≈ 899", async () => {
148
+ const oneSecondAgo = new Date(Date.now() - 1000);
149
+ mockTxn.get.mockResolvedValue(makeSnap({ lastRequestedAt: { toDate: () => oneSecondAgo } }));
150
+ const result = await repo.checkAndSetUserCooldown("user-ravi");
151
+ expect(result.allowed).toBe(false);
152
+ expect(result.retryAfterSeconds).toBeGreaterThanOrEqual(898);
153
+ expect(result.retryAfterSeconds).toBeLessThanOrEqual(900);
154
+ });
155
+ it("document ID is user_cooldown_{userId}", async () => {
156
+ mockTxn.get.mockResolvedValue(makeSnap(null));
157
+ await repo.checkAndSetUserCooldown("user-ravi");
158
+ expect(mockCollection.doc).toHaveBeenCalledWith("user_cooldown_user-ravi");
159
+ });
160
+ it("when allowed, calls tx.set with lastRequestedAt timestamp", async () => {
161
+ mockTxn.get.mockResolvedValue(makeSnap(null));
162
+ await repo.checkAndSetUserCooldown("user-ravi");
163
+ expect(mockTxn.set).toHaveBeenCalledWith(expect.anything(), expect.objectContaining({ lastRequestedAt: expect.any(Date) }), { merge: true });
164
+ });
165
+ it("when NOT allowed, does NOT call tx.set", async () => {
166
+ const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000);
167
+ mockTxn.get.mockResolvedValue(makeSnap({ lastRequestedAt: { toDate: () => twoMinutesAgo } }));
168
+ await repo.checkAndSetUserCooldown("user-ravi");
169
+ expect(mockTxn.set).not.toHaveBeenCalled();
170
+ });
171
+ });
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { makeMockDb, makeSnap } from "../../../../../tests/helpers/mock-firestore";
3
+ const { db, mockTxn } = makeMockDb();
4
+ vi.mock("../../../../providers/db-firebase", () => ({
5
+ getAdminDb: () => db,
6
+ BaseRepository: class {
7
+ get db() { return db; }
8
+ constructor(col) { this.collection = col; }
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ getCollection() { return db.collection(this.collection); }
11
+ },
12
+ }));
13
+ vi.mock("../../../../monitoring", () => ({
14
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
15
+ }));
16
+ import { SmsCounterRepository } from "../sms-counter.repository";
17
+ import { SMS_DAILY_LIMIT } from "../../schemas";
18
+ const repo = new SmsCounterRepository();
19
+ beforeEach(() => {
20
+ vi.clearAllMocks();
21
+ db.runTransaction.mockImplementation(async (fn) => fn(mockTxn));
22
+ mockTxn.set.mockReturnValue(undefined);
23
+ });
24
+ describe("SmsCounterRepository.checkAndIncrement", () => {
25
+ it("first call of the day → count becomes 1, allowed: true", async () => {
26
+ mockTxn.get.mockResolvedValue(makeSnap(null));
27
+ const result = await repo.checkAndIncrement("2026-01-01");
28
+ expect(result.allowed).toBe(true);
29
+ expect(result.count).toBe(1);
30
+ });
31
+ it("count at LIMIT - 1 → increments, allowed: true", async () => {
32
+ mockTxn.get.mockResolvedValue(makeSnap({ count: SMS_DAILY_LIMIT - 1, date: "2026-01-01", updatedAt: new Date() }));
33
+ const result = await repo.checkAndIncrement("2026-01-01");
34
+ expect(result.allowed).toBe(true);
35
+ expect(result.count).toBe(SMS_DAILY_LIMIT);
36
+ });
37
+ it("count at LIMIT → not allowed, count unchanged", async () => {
38
+ mockTxn.get.mockResolvedValue(makeSnap({ count: SMS_DAILY_LIMIT, date: "2026-01-01", updatedAt: new Date() }));
39
+ const result = await repo.checkAndIncrement("2026-01-01");
40
+ expect(result.allowed).toBe(false);
41
+ expect(result.count).toBe(SMS_DAILY_LIMIT);
42
+ expect(mockTxn.set).not.toHaveBeenCalled();
43
+ });
44
+ it("uses Firestore transaction (runTransaction called)", async () => {
45
+ mockTxn.get.mockResolvedValue(makeSnap(null));
46
+ await repo.checkAndIncrement("2026-01-01");
47
+ expect(db.runTransaction).toHaveBeenCalledOnce();
48
+ });
49
+ it("next UTC day uses different doc ID → starts at 1", async () => {
50
+ mockTxn.get.mockResolvedValue(makeSnap(null));
51
+ const result = await repo.checkAndIncrement("2026-01-02");
52
+ expect(result.allowed).toBe(true);
53
+ expect(result.count).toBe(1);
54
+ });
55
+ });
56
+ describe("SmsCounterRepository.checkAndSetUserCooldown", () => {
57
+ it("first call → allowed: true, sets cooldown doc", async () => {
58
+ mockTxn.get.mockResolvedValue(makeSnap(null));
59
+ const result = await repo.checkAndSetUserCooldown("user-1");
60
+ expect(result.allowed).toBe(true);
61
+ expect(result.retryAfterSeconds).toBe(0);
62
+ expect(mockTxn.set).toHaveBeenCalledOnce();
63
+ });
64
+ it("called within 15 minutes → allowed: false with retryAfterSeconds", async () => {
65
+ const recentlyRequested = new Date(Date.now() - 5 * 60 * 1000); // 5 min ago
66
+ mockTxn.get.mockResolvedValue(makeSnap({
67
+ lastRequestedAt: { toDate: () => recentlyRequested },
68
+ }));
69
+ const result = await repo.checkAndSetUserCooldown("user-1");
70
+ expect(result.allowed).toBe(false);
71
+ expect(result.retryAfterSeconds).toBeGreaterThan(0);
72
+ expect(result.retryAfterSeconds).toBeLessThanOrEqual(10 * 60); // ~10 min remaining
73
+ });
74
+ it("called after 15 minutes → allowed: true", async () => {
75
+ const old = new Date(Date.now() - 20 * 60 * 1000); // 20 min ago
76
+ mockTxn.get.mockResolvedValue(makeSnap({
77
+ lastRequestedAt: { toDate: () => old },
78
+ }));
79
+ const result = await repo.checkAndSetUserCooldown("user-1");
80
+ expect(result.allowed).toBe(true);
81
+ expect(mockTxn.set).toHaveBeenCalledOnce();
82
+ });
83
+ it("uses Firestore transaction", async () => {
84
+ mockTxn.get.mockResolvedValue(makeSnap(null));
85
+ await repo.checkAndSetUserCooldown("user-1");
86
+ expect(db.runTransaction).toHaveBeenCalledOnce();
87
+ });
88
+ });
@@ -0,0 +1,260 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { makeMockDb, makeSnap, makeQuerySnap } from "../../../../../tests/helpers/mock-firestore";
3
+ const { db, mockDocRef, mockCollection, mockQuery, mockBatch } = makeMockDb();
4
+ vi.mock("../../../../providers/db-firebase/admin", () => ({
5
+ getAdminDb: () => db,
6
+ }));
7
+ vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
8
+ const actual = await importOriginal();
9
+ return { ...actual, prepareForFirestore: (d) => d };
10
+ });
11
+ vi.mock("../../../../contracts/field-ops", () => ({
12
+ serverTimestamp: () => new Date(),
13
+ increment: (n) => n,
14
+ arrayUnion: (...args) => args,
15
+ arrayRemove: (...args) => args,
16
+ deleteField: () => null,
17
+ registerFieldOps: vi.fn(),
18
+ }));
19
+ vi.mock("../../../../security", () => ({
20
+ encryptPiiFields: (d) => d,
21
+ decryptPiiFields: (d) => d,
22
+ addPiiIndices: (d) => d,
23
+ piiBlindIndex: (v) => `blind:${v}`,
24
+ encryptPayoutDetails: (d) => d,
25
+ decryptPayoutDetails: (d) => d,
26
+ encryptShippingConfig: (d) => d,
27
+ decryptShippingConfig: (d) => d,
28
+ USER_PII_FIELDS: [],
29
+ TOKEN_PII_FIELDS: [],
30
+ TOKEN_PII_INDEX_MAP: {},
31
+ }));
32
+ vi.mock("../../../../utils", async (importOriginal) => {
33
+ const actual = await importOriginal();
34
+ return { ...actual, resolveDate: (d) => (d instanceof Date ? d : d ? new Date(d) : null) };
35
+ });
36
+ vi.mock("../../../../errors/normalize", () => ({ normalizeError: vi.fn() }));
37
+ vi.mock("../../../../monitoring", () => ({
38
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
39
+ }));
40
+ import { EmailVerificationTokenRepository, PasswordResetTokenRepository, } from "../token.repository";
41
+ const emailRepo = new EmailVerificationTokenRepository();
42
+ const passwordRepo = new PasswordResetTokenRepository();
43
+ function makeEmailToken(overrides = {}) {
44
+ return {
45
+ id: "token-email-abc123",
46
+ userId: "user-ravi",
47
+ token: "secure-random-token",
48
+ email: "ravi@example.com",
49
+ expiresAt: new Date(Date.now() + 60 * 60 * 1000), // 1 hour from now
50
+ createdAt: new Date(),
51
+ updatedAt: new Date(),
52
+ ...overrides,
53
+ };
54
+ }
55
+ function makePasswordResetToken(overrides = {}) {
56
+ return {
57
+ id: "pw-token-abc123",
58
+ userId: "user-ravi",
59
+ token: "pw-reset-token",
60
+ email: "ravi@example.com",
61
+ expiresAt: new Date(Date.now() + 30 * 60 * 1000), // 30 min from now
62
+ used: false,
63
+ createdAt: new Date(),
64
+ updatedAt: new Date(),
65
+ ...overrides,
66
+ };
67
+ }
68
+ beforeEach(() => {
69
+ vi.clearAllMocks();
70
+ db.collection.mockReturnValue(mockCollection);
71
+ mockCollection.doc.mockReturnValue(mockDocRef);
72
+ mockCollection.where.mockReturnValue(mockQuery);
73
+ mockCollection.get = vi.fn().mockResolvedValue(makeQuerySnap([]));
74
+ mockQuery.where.mockReturnValue(mockQuery);
75
+ mockQuery.orderBy.mockReturnValue(mockQuery);
76
+ mockQuery.limit.mockReturnValue(mockQuery);
77
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
78
+ mockDocRef.get.mockResolvedValue(makeSnap(makeEmailToken(), "token-email-abc123"));
79
+ mockDocRef.set.mockResolvedValue(undefined);
80
+ mockDocRef.update.mockResolvedValue(undefined);
81
+ mockDocRef.delete.mockResolvedValue(undefined);
82
+ db.batch.mockReturnValue(mockBatch);
83
+ });
84
+ // ---------------------------------------------------------------------------
85
+ // EmailVerificationTokenRepository.findByToken
86
+ // ---------------------------------------------------------------------------
87
+ describe("EmailVerificationTokenRepository.findByToken", () => {
88
+ it("returns token document when found", async () => {
89
+ const token = makeEmailToken();
90
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: token.id, data: token }]));
91
+ const result = await emailRepo.findByToken("secure-random-token");
92
+ expect(result).not.toBeNull();
93
+ expect(result?.token).toBe("secure-random-token");
94
+ });
95
+ it("returns null when token does not exist", async () => {
96
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
97
+ const result = await emailRepo.findByToken("nonexistent-token");
98
+ expect(result).toBeNull();
99
+ });
100
+ it("queries by token field", async () => {
101
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
102
+ await emailRepo.findByToken("abc");
103
+ expect(mockCollection.where).toHaveBeenCalledWith("token", "==", "abc");
104
+ });
105
+ });
106
+ // ---------------------------------------------------------------------------
107
+ // EmailVerificationTokenRepository.isExpired
108
+ // ---------------------------------------------------------------------------
109
+ describe("EmailVerificationTokenRepository.isExpired", () => {
110
+ it("returns false when expiresAt is in the future", () => {
111
+ const token = makeEmailToken({ expiresAt: new Date(Date.now() + 60000) });
112
+ expect(emailRepo.isExpired(token)).toBe(false);
113
+ });
114
+ it("returns true when expiresAt is in the past", () => {
115
+ const token = makeEmailToken({ expiresAt: new Date(Date.now() - 1000) });
116
+ expect(emailRepo.isExpired(token)).toBe(true);
117
+ });
118
+ it("returns true when expiresAt is null/undefined", () => {
119
+ const token = makeEmailToken({ expiresAt: null });
120
+ expect(emailRepo.isExpired(token)).toBe(true);
121
+ });
122
+ });
123
+ // ---------------------------------------------------------------------------
124
+ // EmailVerificationTokenRepository.deleteAllForUser
125
+ // ---------------------------------------------------------------------------
126
+ describe("EmailVerificationTokenRepository.deleteAllForUser", () => {
127
+ it("deletes all tokens for the user in batch", async () => {
128
+ const tokens = [
129
+ makeEmailToken({ id: "t-1" }),
130
+ makeEmailToken({ id: "t-2" }),
131
+ ];
132
+ mockQuery.get.mockResolvedValue(makeQuerySnap(tokens.map((t) => ({ id: t.id, data: t }))));
133
+ await emailRepo.deleteAllForUser("user-ravi");
134
+ expect(mockBatch.delete).toHaveBeenCalledTimes(2);
135
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
136
+ });
137
+ it("commits even when no tokens exist for the user", async () => {
138
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
139
+ await emailRepo.deleteAllForUser("user-no-tokens");
140
+ expect(mockBatch.delete).not.toHaveBeenCalled();
141
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
142
+ });
143
+ });
144
+ // ---------------------------------------------------------------------------
145
+ // EmailVerificationTokenRepository.findByEmail (blind-index lookup)
146
+ // ---------------------------------------------------------------------------
147
+ describe("EmailVerificationTokenRepository.findByEmail", () => {
148
+ it("first queries emailIndex (blind index), not raw email", async () => {
149
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
150
+ await emailRepo.findByEmail("ravi@example.com");
151
+ expect(mockCollection.where).toHaveBeenCalledWith("emailIndex", "==", "blind:ravi@example.com");
152
+ });
153
+ it("falls back to plaintext email lookup when index returns empty", async () => {
154
+ // First call (index) returns empty, second call (email) also empty
155
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
156
+ await emailRepo.findByEmail("ravi@example.com");
157
+ // Two where calls — first for emailIndex, second for email
158
+ expect(mockCollection.where).toHaveBeenCalledTimes(2);
159
+ });
160
+ it("does NOT fall back if index returns results", async () => {
161
+ const token = makeEmailToken();
162
+ mockQuery.get.mockResolvedValueOnce(makeQuerySnap([{ id: token.id, data: token }]));
163
+ await emailRepo.findByEmail("ravi@example.com");
164
+ expect(mockCollection.where).toHaveBeenCalledTimes(1);
165
+ });
166
+ });
167
+ // ---------------------------------------------------------------------------
168
+ // EmailVerificationTokenRepository.deleteExpired
169
+ // ---------------------------------------------------------------------------
170
+ describe("EmailVerificationTokenRepository.deleteExpired", () => {
171
+ it("batch-deletes all expired tokens", async () => {
172
+ const tokens = [makeEmailToken({ id: "t-expired-1" }), makeEmailToken({ id: "t-expired-2" })];
173
+ mockQuery.get.mockResolvedValue(makeQuerySnap(tokens.map((t) => ({ id: t.id, data: t }))));
174
+ const count = await emailRepo.deleteExpired();
175
+ expect(count).toBe(2);
176
+ expect(mockBatch.delete).toHaveBeenCalledTimes(2);
177
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
178
+ });
179
+ it("queries expiresAt < now", async () => {
180
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
181
+ await emailRepo.deleteExpired();
182
+ expect(mockCollection.where).toHaveBeenCalledWith("expiresAt", "<", expect.any(Date));
183
+ });
184
+ });
185
+ // ---------------------------------------------------------------------------
186
+ // PasswordResetTokenRepository.markAsUsed
187
+ // ---------------------------------------------------------------------------
188
+ describe("PasswordResetTokenRepository.markAsUsed", () => {
189
+ it("sets used: true on the token", async () => {
190
+ const token = makePasswordResetToken({ used: false });
191
+ mockDocRef.get.mockResolvedValue(makeSnap(token, "pw-token-abc123"));
192
+ await passwordRepo.markAsUsed("pw-token-abc123");
193
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ used: true }));
194
+ });
195
+ it("sets usedAt timestamp", async () => {
196
+ const token = makePasswordResetToken();
197
+ mockDocRef.get.mockResolvedValue(makeSnap(token, "pw-token-abc123"));
198
+ await passwordRepo.markAsUsed("pw-token-abc123");
199
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ usedAt: expect.any(Date) }));
200
+ });
201
+ it("calls doc(tokenId) to target the correct token", async () => {
202
+ const token = makePasswordResetToken();
203
+ mockDocRef.get.mockResolvedValue(makeSnap(token, "pw-token-abc123"));
204
+ await passwordRepo.markAsUsed("pw-token-abc123");
205
+ expect(mockCollection.doc).toHaveBeenCalledWith("pw-token-abc123");
206
+ });
207
+ });
208
+ // ---------------------------------------------------------------------------
209
+ // PasswordResetTokenRepository.findUnusedForUser
210
+ // ---------------------------------------------------------------------------
211
+ describe("PasswordResetTokenRepository.findUnusedForUser", () => {
212
+ it("returns only tokens where used == false for the user", async () => {
213
+ const unused = makePasswordResetToken({ used: false });
214
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: unused.id, data: unused }]));
215
+ const results = await passwordRepo.findUnusedForUser("user-ravi");
216
+ expect(results).toHaveLength(1);
217
+ });
218
+ it("queries userId and used==false filters", async () => {
219
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
220
+ await passwordRepo.findUnusedForUser("user-ravi");
221
+ expect(mockCollection.where).toHaveBeenCalledWith("userId", "==", "user-ravi");
222
+ expect(mockQuery.where).toHaveBeenCalledWith("used", "==", false);
223
+ });
224
+ it("returns empty array when all tokens for user are used", async () => {
225
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
226
+ const results = await passwordRepo.findUnusedForUser("user-ravi");
227
+ expect(results).toEqual([]);
228
+ });
229
+ });
230
+ // ---------------------------------------------------------------------------
231
+ // PasswordResetTokenRepository.isExpired
232
+ // ---------------------------------------------------------------------------
233
+ describe("PasswordResetTokenRepository.isExpired", () => {
234
+ it("returns false when expiresAt is in the future", () => {
235
+ const token = makePasswordResetToken({ expiresAt: new Date(Date.now() + 60000) });
236
+ expect(passwordRepo.isExpired(token)).toBe(false);
237
+ });
238
+ it("returns true when expiresAt is in the past", () => {
239
+ const token = makePasswordResetToken({ expiresAt: new Date(Date.now() - 1000) });
240
+ expect(passwordRepo.isExpired(token)).toBe(true);
241
+ });
242
+ });
243
+ // ---------------------------------------------------------------------------
244
+ // PasswordResetTokenRepository.findByEmail (blind-index lookup)
245
+ // ---------------------------------------------------------------------------
246
+ describe("PasswordResetTokenRepository.findByEmail", () => {
247
+ it("queries by HMAC blind index (not plaintext email)", async () => {
248
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
249
+ await passwordRepo.findByEmail("ravi@example.com");
250
+ expect(mockCollection.where).toHaveBeenCalledWith("emailIndex", "==", "blind:ravi@example.com");
251
+ });
252
+ it("does not expose raw email in the Firestore query field", async () => {
253
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
254
+ await passwordRepo.findByEmail("ravi@example.com");
255
+ // First call is index lookup — must NOT use raw email
256
+ const firstCallArg = mockCollection.where.mock.calls[0][2];
257
+ expect(firstCallArg).not.toBe("ravi@example.com");
258
+ expect(firstCallArg).toMatch(/^blind:/);
259
+ });
260
+ });
@@ -0,0 +1,140 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+ import { makeMockDb, makeSnap, makeQuerySnap } from "../../../../../tests/helpers/mock-firestore";
3
+ const { db, mockDocRef, mockCollection, mockQuery, mockBatch } = makeMockDb();
4
+ vi.mock("../../../../providers/db-firebase", () => ({
5
+ getAdminDb: () => db,
6
+ BaseRepository: class {
7
+ get db() { return db; }
8
+ constructor(col) { this.collection = col; }
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ getCollection() { return db.collection(this.collection); }
11
+ mapDoc(snap) {
12
+ return { id: snap.id, ...snap.data() };
13
+ }
14
+ async findBy(field, value) {
15
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
16
+ const snap = await db.collection(this.collection).where(field, "==", value).get();
17
+ return snap.docs.map((d) => ({ id: d.id, ...d.data() }));
18
+ }
19
+ async findOneBy(field, value) {
20
+ const results = await this.findBy(field, value);
21
+ return results[0] ?? null;
22
+ }
23
+ async findById(id) {
24
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
25
+ const snap = await db.collection(this.collection).doc(id).get();
26
+ return snap.exists ? { id: snap.id, ...snap.data() } : null;
27
+ }
28
+ async findByIdOrFail(id) {
29
+ const doc = await this.findById(id);
30
+ if (!doc)
31
+ throw new Error("Not found");
32
+ return doc;
33
+ }
34
+ async create(data) {
35
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
36
+ const ref = await db.collection(this.collection).add(data);
37
+ return { id: ref.id, ...data };
38
+ }
39
+ },
40
+ }));
41
+ vi.mock("../../../../monitoring", () => ({
42
+ serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
43
+ }));
44
+ vi.mock("../../../../errors/normalize", () => ({
45
+ normalizeError: vi.fn(),
46
+ }));
47
+ vi.mock("../../../../security", () => ({
48
+ TOKEN_PII_FIELDS: ["email"],
49
+ TOKEN_PII_INDEX_MAP: { email: "emailIndex" },
50
+ encryptPiiFields: (data) => data,
51
+ decryptPiiFields: (data) => data,
52
+ addPiiIndices: (data) => data,
53
+ piiBlindIndex: (v) => `idx:${v}`,
54
+ }));
55
+ vi.mock("../../../../utils", () => ({
56
+ resolveDate: (d) => (d instanceof Date ? d : d ? new Date(d) : null),
57
+ }));
58
+ import { EmailVerificationTokenRepository, PasswordResetTokenRepository, } from "../token.repository";
59
+ const emailRepo = new EmailVerificationTokenRepository();
60
+ const passwordRepo = new PasswordResetTokenRepository();
61
+ beforeEach(() => {
62
+ vi.clearAllMocks();
63
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
64
+ mockDocRef.get.mockResolvedValue(makeSnap(null));
65
+ mockDocRef.set.mockResolvedValue(undefined);
66
+ mockDocRef.update.mockResolvedValue(undefined);
67
+ mockDocRef.delete.mockResolvedValue(undefined);
68
+ mockCollection.doc.mockReturnValue(mockDocRef);
69
+ mockCollection.add.mockResolvedValue({ id: "token-id-123" });
70
+ db.collection.mockReturnValue(mockCollection);
71
+ db.batch.mockReturnValue(mockBatch);
72
+ mockBatch.commit.mockResolvedValue(undefined);
73
+ });
74
+ describe("EmailVerificationTokenRepository.findByToken", () => {
75
+ it("token exists → returns document", async () => {
76
+ const tokenData = { userId: "user-1", email: "a@b.com", expiresAt: new Date(Date.now() + 60000) };
77
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "tok-1", data: tokenData }]));
78
+ const result = await emailRepo.findByToken("some-token");
79
+ expect(result).not.toBeNull();
80
+ expect(result?.userId).toBe("user-1");
81
+ });
82
+ it("token does not exist → returns null", async () => {
83
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
84
+ const result = await emailRepo.findByToken("nonexistent-token");
85
+ expect(result).toBeNull();
86
+ });
87
+ });
88
+ describe("EmailVerificationTokenRepository.isExpired", () => {
89
+ it("expiresAt in past → returns true", () => {
90
+ const pastToken = { id: "t1", userId: "u", email: "a@b.com", expiresAt: new Date(Date.now() - 1000) };
91
+ expect(emailRepo.isExpired(pastToken)).toBe(true);
92
+ });
93
+ it("expiresAt in future → returns false", () => {
94
+ const futureToken = { id: "t1", userId: "u", email: "a@b.com", expiresAt: new Date(Date.now() + 60000) };
95
+ expect(emailRepo.isExpired(futureToken)).toBe(false);
96
+ });
97
+ });
98
+ describe("EmailVerificationTokenRepository.deleteAllForUser", () => {
99
+ it("batch-deletes all tokens for the user", async () => {
100
+ mockQuery.get.mockResolvedValue(makeQuerySnap([
101
+ { id: "tok-1", data: { userId: "user-1" } },
102
+ { id: "tok-2", data: { userId: "user-1" } },
103
+ ]));
104
+ await emailRepo.deleteAllForUser("user-1");
105
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
106
+ });
107
+ it("no tokens for user → still calls commit (empty batch)", async () => {
108
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
109
+ await emailRepo.deleteAllForUser("user-no-tokens");
110
+ expect(mockBatch.commit).toHaveBeenCalledOnce();
111
+ });
112
+ });
113
+ describe("EmailVerificationTokenRepository.findByEmail", () => {
114
+ it("uses HMAC blind index for lookup (emailIndex field)", async () => {
115
+ const tokenData = { userId: "u", emailIndex: "idx:a@b.com", expiresAt: new Date() };
116
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "tok-1", data: tokenData }]));
117
+ await emailRepo.findByEmail("a@b.com");
118
+ expect(mockCollection.where).toHaveBeenCalledWith("emailIndex", "==", "idx:a@b.com");
119
+ });
120
+ });
121
+ describe("PasswordResetTokenRepository.markAsUsed", () => {
122
+ it("sets usedAt timestamp and used: true on the document", async () => {
123
+ mockDocRef.get.mockResolvedValue(makeSnap({ userId: "user-1", token: "tok", used: false }));
124
+ await passwordRepo.markAsUsed("tok-1");
125
+ expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ used: true, usedAt: expect.any(Date) }));
126
+ });
127
+ });
128
+ describe("PasswordResetTokenRepository.findUnusedForUser", () => {
129
+ it("returns null when all tokens have used: true", async () => {
130
+ mockQuery.get.mockResolvedValue(makeQuerySnap([]));
131
+ const result = await passwordRepo.findUnusedForUser("user-1");
132
+ expect(result).toEqual([]);
133
+ });
134
+ it("returns unused tokens (used: false)", async () => {
135
+ const tokenData = { userId: "user-1", token: "abc", used: false, expiresAt: new Date() };
136
+ mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: "tok-1", data: tokenData }]));
137
+ const result = await passwordRepo.findUnusedForUser("user-1");
138
+ expect(result).toHaveLength(1);
139
+ });
140
+ });