@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.
- 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/auctions/data.d.ts +2 -4
- package/dist/_internal/server/features/auctions/data.js +3 -9
- 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/classified/adapters.d.ts +2 -15
- package/dist/_internal/server/features/classified/adapters.js +2 -17
- package/dist/_internal/server/features/classified/data.d.ts +1 -6
- package/dist/_internal/server/features/classified/data.js +2 -10
- package/dist/_internal/server/features/digital-code/adapters.d.ts +2 -15
- package/dist/_internal/server/features/digital-code/adapters.js +2 -17
- package/dist/_internal/server/features/digital-code/data.d.ts +1 -6
- package/dist/_internal/server/features/digital-code/data.js +2 -10
- package/dist/_internal/server/features/live/adapters.d.ts +2 -15
- package/dist/_internal/server/features/live/adapters.js +2 -17
- package/dist/_internal/server/features/live/data.d.ts +1 -6
- package/dist/_internal/server/features/live/data.js +2 -10
- 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/pre-orders/data.d.ts +2 -4
- package/dist/_internal/server/features/pre-orders/data.js +3 -9
- package/dist/_internal/server/features/prize-draws/actions.d.ts +2 -0
- package/dist/_internal/server/features/prize-draws/actions.js +22 -0
- package/dist/_internal/server/features/prize-draws/data.d.ts +1 -17
- package/dist/_internal/server/features/prize-draws/data.js +2 -20
- package/dist/_internal/server/features/prize-draws/index.d.ts +3 -1
- package/dist/_internal/server/features/prize-draws/index.js +2 -0
- package/dist/_internal/server/features/prize-draws/service.d.ts +3 -0
- package/dist/_internal/server/features/prize-draws/service.js +19 -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/features/shared/listing-adapters.d.ts +19 -0
- package/dist/_internal/server/features/shared/listing-adapters.js +20 -0
- package/dist/_internal/server/features/shared/listing-data-factory.d.ts +9 -0
- package/dist/_internal/server/features/shared/listing-data-factory.js +33 -0
- package/dist/_internal/server/features/stores/data.d.ts +3 -14
- package/dist/_internal/server/features/stores/data.js +6 -44
- 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/cart/schema.d.ts +8 -8
- package/dist/_internal/shared/features/cart/schema.js +3 -0
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/_internal/shared/listing-types/_registry.js +2 -0
- package/dist/_internal/shared/listing-types/auction/schema.d.ts +3 -1
- package/dist/_internal/shared/listing-types/auction/schema.js +7 -1
- package/dist/_internal/shared/listing-types/classified/schema.d.ts +2 -1
- package/dist/_internal/shared/listing-types/classified/schema.js +3 -1
- package/dist/_internal/shared/listing-types/digital-code/schema.d.ts +3 -1
- package/dist/_internal/shared/listing-types/digital-code/schema.js +7 -1
- package/dist/_internal/shared/listing-types/live/schema.d.ts +2 -1
- package/dist/_internal/shared/listing-types/live/schema.js +3 -1
- package/dist/_internal/shared/listing-types/pre-order/schema.d.ts +3 -1
- package/dist/_internal/shared/listing-types/pre-order/schema.js +7 -1
- package/dist/_internal/shared/listing-types/prize-draw/schema.d.ts +3 -1
- package/dist/_internal/shared/listing-types/prize-draw/schema.js +13 -1
- package/dist/_internal/shared/listing-types/standard/schema.d.ts +2 -1
- package/dist/_internal/shared/listing-types/standard/schema.js +4 -1
- package/dist/_internal/shared/types/base-document.d.ts +15 -0
- package/dist/_internal/shared/types/base-document.js +1 -0
- package/dist/_internal/shared/types/index.d.ts +2 -0
- package/dist/_internal/shared/types/index.js +1 -0
- package/dist/client.d.ts +13 -4
- package/dist/client.js +11 -2
- package/dist/constants/api-endpoints.d.ts +12 -0
- package/dist/constants/api-endpoints.js +4 -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/addresses/schemas/firestore.d.ts +2 -4
- 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/components/AdminFulfillmentView.d.ts +6 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +107 -0
- package/dist/features/admin/components/AdminProductEditorView.js +21 -9
- package/dist/features/admin/components/AdminProductsView.js +3 -0
- package/dist/features/admin/components/DataListingView.d.ts +1 -0
- package/dist/features/admin/components/index.d.ts +2 -0
- package/dist/features/admin/components/index.js +1 -0
- 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/admin/schemas/firestore.d.ts +3 -8
- 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/auctions/schemas/firestore.d.ts +2 -4
- package/dist/features/auctions/schemas/index.d.ts +2 -2
- 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/before-after/schemas/index.d.ts +2 -2
- package/dist/features/blog/schemas/firestore.d.ts +2 -4
- package/dist/features/blog/schemas/index.d.ts +4 -4
- 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/cart/schemas/firestore.d.ts +2 -4
- 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/categories/schemas/firestore.d.ts +2 -4
- package/dist/features/categories/schemas/index.d.ts +4 -4
- 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 +5 -4
- package/dist/features/events/schemas/index.d.ts +836 -16
- 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/faq/schemas/firestore.d.ts +2 -4
- package/dist/features/faq/schemas/index.d.ts +2 -2
- package/dist/features/grouped/schemas/index.d.ts +2 -2
- 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/homepage/schemas/firestore.d.ts +4 -12
- package/dist/features/homepage/schemas/index.d.ts +2 -2
- 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/messages/schemas/firestore.d.ts +2 -4
- package/dist/features/messages/schemas/index.d.ts +2 -2
- 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/orders/repository/orders.repository.d.ts +5 -0
- package/dist/features/orders/repository/orders.repository.js +26 -0
- package/dist/features/orders/schemas/firestore.d.ts +9 -5
- package/dist/features/orders/schemas/firestore.js +3 -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/payments/schemas/firestore.d.ts +2 -4
- package/dist/features/pre-orders/schemas/index.d.ts +2 -2
- 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/constants/action-defs.d.ts +16 -0
- package/dist/features/products/constants/action-defs.js +16 -0
- package/dist/features/products/constants/listing-tabs.d.ts +48 -0
- package/dist/features/products/constants/listing-tabs.js +12 -0
- 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/repository/products.repository.d.ts +37 -0
- package/dist/features/products/repository/products.repository.js +28 -1
- package/dist/features/products/schemas/firestore.d.ts +23 -7
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +18 -18
- package/dist/features/products/schemas/index.js +4 -2
- package/dist/features/products/types/index.d.ts +3 -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/promotions/schemas/firestore.d.ts +2 -4
- package/dist/features/promotions/schemas/index.d.ts +4 -4
- 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/reviews/schemas/firestore.d.ts +2 -4
- package/dist/features/reviews/schemas/index.d.ts +2 -2
- 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/scams/schemas/firestore.d.ts +2 -4
- package/dist/features/scams/schemas/index.d.ts +4 -4
- 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/search/schemas/index.d.ts +2 -2
- 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/components/BarcodeField.d.ts +17 -0
- package/dist/features/seller/components/BarcodeField.js +30 -0
- package/dist/features/seller/components/FulfillmentView.d.ts +5 -0
- package/dist/features/seller/components/FulfillmentView.js +120 -0
- package/dist/features/seller/components/PrintCenterView.d.ts +3 -1
- package/dist/features/seller/components/PrintCenterView.js +52 -4
- package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
- package/dist/features/seller/components/SellerProductShell.js +2 -1
- package/dist/features/seller/components/index.d.ts +4 -0
- package/dist/features/seller/components/index.js +2 -0
- 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/seller/schemas/index.d.ts +2 -2
- package/dist/features/store-extensions/schemas/firestore.d.ts +12 -44
- package/dist/features/stores/components/StoreClassifiedsListing.d.ts +5 -0
- package/dist/features/stores/components/StoreClassifiedsListing.js +48 -0
- package/dist/features/stores/components/StoreClassifiedsPageView.d.ts +4 -0
- package/dist/features/stores/components/StoreClassifiedsPageView.js +20 -0
- package/dist/features/stores/components/StoreDetailLayoutView.js +23 -2
- package/dist/features/stores/components/StoreDigitalCodesListing.d.ts +5 -0
- package/dist/features/stores/components/StoreDigitalCodesListing.js +48 -0
- package/dist/features/stores/components/StoreDigitalCodesPageView.d.ts +4 -0
- package/dist/features/stores/components/StoreDigitalCodesPageView.js +20 -0
- package/dist/features/stores/components/StoreLiveItemsListing.d.ts +5 -0
- package/dist/features/stores/components/StoreLiveItemsListing.js +48 -0
- package/dist/features/stores/components/StoreLiveItemsPageView.d.ts +4 -0
- package/dist/features/stores/components/StoreLiveItemsPageView.js +20 -0
- package/dist/features/stores/components/index.d.ts +9 -0
- package/dist/features/stores/components/index.js +3 -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/stores/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/schemas/index.d.ts +4 -4
- 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/support/schemas/firestore.d.ts +2 -4
- package/dist/features/support/schemas/index.d.ts +2 -2
- 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 +24 -0
- package/dist/next/routing/route-map.js +11 -0
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +2 -0
- package/dist/schemas/registry.d.ts +24 -24
- 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 +5 -2
- 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 +5 -2
- package/dist/seed/products-standard-seed-data.js +8 -0
- package/dist/server-entry.d.ts +3 -0
- package/dist/server-entry.js +3 -0
- package/dist/server.d.ts +10 -4
- package/dist/server.js +6 -0
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/utils/id-generators.d.ts +8 -0
- package/dist/utils/id-generators.js +14 -0
- package/package.json +2 -1
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { makeMockDb, makeSnap } from "../../../../../tests/helpers/mock-firestore";
|
|
3
|
+
const { db, mockDocRef, mockTxn } = makeMockDb();
|
|
4
|
+
vi.mock("../../../../providers/db-firebase", () => ({
|
|
5
|
+
getAdminDb: () => db,
|
|
6
|
+
}));
|
|
7
|
+
vi.mock("../../../../monitoring", () => ({
|
|
8
|
+
serverLogger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
9
|
+
}));
|
|
10
|
+
import { UserHistoryRepository } from "../user-history.repository";
|
|
11
|
+
import { HISTORY_MAX } from "../../../../constants/limits";
|
|
12
|
+
const repo = new UserHistoryRepository();
|
|
13
|
+
function makeHistoryItems(count, baseDate = new Date("2026-01-01")) {
|
|
14
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
15
|
+
productId: `product-${i}`,
|
|
16
|
+
productType: "product",
|
|
17
|
+
viewedAt: new Date(baseDate.getTime() - i * 1000),
|
|
18
|
+
}));
|
|
19
|
+
}
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
db.runTransaction.mockImplementation(async (fn) => fn(mockTxn));
|
|
23
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
24
|
+
mockTxn.get.mockResolvedValue(makeSnap(null));
|
|
25
|
+
mockTxn.set.mockReturnValue(undefined);
|
|
26
|
+
});
|
|
27
|
+
describe("UserHistoryRepository.track", () => {
|
|
28
|
+
it("empty history → items has 1 entry, returns 1", async () => {
|
|
29
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items: [], updatedAt: new Date() }));
|
|
30
|
+
const count = await repo.track("u", { productId: "product-x", productType: "product" });
|
|
31
|
+
expect(count).toBe(1);
|
|
32
|
+
expect(mockTxn.set).toHaveBeenCalledOnce();
|
|
33
|
+
});
|
|
34
|
+
it("49 items + new productId → 50 items, new at position 0", async () => {
|
|
35
|
+
const items = makeHistoryItems(49);
|
|
36
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items, updatedAt: new Date() }));
|
|
37
|
+
const count = await repo.track("u", { productId: "product-new", productType: "product" });
|
|
38
|
+
expect(count).toBe(50);
|
|
39
|
+
const setCall = mockTxn.set.mock.calls[0][1];
|
|
40
|
+
expect(setCall.items[0].productId).toBe("product-new");
|
|
41
|
+
});
|
|
42
|
+
it("50 items + new productId → evicts last item, stays at 50 (FIFO)", async () => {
|
|
43
|
+
const items = makeHistoryItems(HISTORY_MAX);
|
|
44
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items, updatedAt: new Date() }));
|
|
45
|
+
const count = await repo.track("u", { productId: "product-new", productType: "product" });
|
|
46
|
+
expect(count).toBe(HISTORY_MAX);
|
|
47
|
+
const setCall = mockTxn.set.mock.calls[0][1];
|
|
48
|
+
expect(setCall.items).toHaveLength(HISTORY_MAX);
|
|
49
|
+
expect(setCall.items[0].productId).toBe("product-new");
|
|
50
|
+
// Last item from original list is evicted
|
|
51
|
+
expect(setCall.items[HISTORY_MAX - 1].productId).not.toBe(`product-${HISTORY_MAX - 1}`);
|
|
52
|
+
});
|
|
53
|
+
it("revisit existing productId → removes old, inserts at position 0, length unchanged", async () => {
|
|
54
|
+
const items = [
|
|
55
|
+
{ productId: "product-A", productType: "product", viewedAt: new Date("2026-01-03") },
|
|
56
|
+
{ productId: "product-B", productType: "product", viewedAt: new Date("2026-01-02") },
|
|
57
|
+
{ productId: "product-C", productType: "product", viewedAt: new Date("2026-01-01") },
|
|
58
|
+
];
|
|
59
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items, updatedAt: new Date() }));
|
|
60
|
+
const count = await repo.track("u", { productId: "product-C", productType: "product" });
|
|
61
|
+
expect(count).toBe(3);
|
|
62
|
+
const setCall = mockTxn.set.mock.calls[0][1];
|
|
63
|
+
expect(setCall.items[0].productId).toBe("product-C");
|
|
64
|
+
expect(setCall.items.filter((i) => i.productId === "product-C")).toHaveLength(1);
|
|
65
|
+
});
|
|
66
|
+
it("uses db.runTransaction", async () => {
|
|
67
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items: [], updatedAt: new Date() }));
|
|
68
|
+
await repo.track("u", { productId: "product-x", productType: "product" });
|
|
69
|
+
expect(db.runTransaction).toHaveBeenCalledOnce();
|
|
70
|
+
});
|
|
71
|
+
it("stores viewedAt as current timestamp when not provided", async () => {
|
|
72
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items: [], updatedAt: new Date() }));
|
|
73
|
+
const before = Date.now();
|
|
74
|
+
await repo.track("u", { productId: "product-x", productType: "product" });
|
|
75
|
+
const after = Date.now();
|
|
76
|
+
const setCall = mockTxn.set.mock.calls[0][1];
|
|
77
|
+
const ts = setCall.items[0].viewedAt.getTime();
|
|
78
|
+
expect(ts).toBeGreaterThanOrEqual(before);
|
|
79
|
+
expect(ts).toBeLessThanOrEqual(after);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
describe("UserHistoryRepository.merge", () => {
|
|
83
|
+
it("guest items merged into empty history → all items present", async () => {
|
|
84
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items: [], updatedAt: new Date() }));
|
|
85
|
+
const incoming = [
|
|
86
|
+
{ productId: "product-A", productType: "product" },
|
|
87
|
+
{ productId: "product-B", productType: "product" },
|
|
88
|
+
];
|
|
89
|
+
const count = await repo.merge("u", incoming);
|
|
90
|
+
expect(count).toBe(2);
|
|
91
|
+
});
|
|
92
|
+
it("duplicate productId: keeps newer viewedAt, discards older", async () => {
|
|
93
|
+
const older = new Date("2026-01-01");
|
|
94
|
+
const newer = new Date("2026-01-10");
|
|
95
|
+
const existingItems = [{ productId: "product-A", productType: "product", viewedAt: older }];
|
|
96
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items: existingItems, updatedAt: new Date() }));
|
|
97
|
+
await repo.merge("u", [{ productId: "product-A", productType: "product", viewedAt: newer }]);
|
|
98
|
+
const setCall = mockTxn.set.mock.calls[0][1];
|
|
99
|
+
expect(setCall.items).toHaveLength(1);
|
|
100
|
+
expect(setCall.items[0].viewedAt.getTime()).toBe(newer.getTime());
|
|
101
|
+
});
|
|
102
|
+
it("result trimmed to HISTORY_MAX (50 items)", async () => {
|
|
103
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items: [], updatedAt: new Date() }));
|
|
104
|
+
const incoming = Array.from({ length: 60 }, (_, i) => ({
|
|
105
|
+
productId: `product-${i}`,
|
|
106
|
+
productType: "product",
|
|
107
|
+
viewedAt: new Date(Date.now() - i * 1000),
|
|
108
|
+
}));
|
|
109
|
+
const count = await repo.merge("u", incoming);
|
|
110
|
+
expect(count).toBe(HISTORY_MAX);
|
|
111
|
+
});
|
|
112
|
+
it("items sorted descending by viewedAt after merge", async () => {
|
|
113
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items: [], updatedAt: new Date() }));
|
|
114
|
+
const incoming = [
|
|
115
|
+
{ productId: "product-old", productType: "product", viewedAt: new Date("2026-01-01") },
|
|
116
|
+
{ productId: "product-new", productType: "product", viewedAt: new Date("2026-01-10") },
|
|
117
|
+
];
|
|
118
|
+
await repo.merge("u", incoming);
|
|
119
|
+
const setCall = mockTxn.set.mock.calls[0][1];
|
|
120
|
+
expect(setCall.items[0].productId).toBe("product-new");
|
|
121
|
+
expect(setCall.items[1].productId).toBe("product-old");
|
|
122
|
+
});
|
|
123
|
+
it("uses db.runTransaction", async () => {
|
|
124
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items: [], updatedAt: new Date() }));
|
|
125
|
+
await repo.merge("u", []);
|
|
126
|
+
expect(db.runTransaction).toHaveBeenCalledOnce();
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
describe("UserHistoryRepository.removeOne", () => {
|
|
130
|
+
it("removes specified productId from items", async () => {
|
|
131
|
+
const items = [
|
|
132
|
+
{ productId: "product-A", productType: "product", viewedAt: new Date() },
|
|
133
|
+
{ productId: "product-B", productType: "product", viewedAt: new Date() },
|
|
134
|
+
];
|
|
135
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items, updatedAt: new Date() }));
|
|
136
|
+
await repo.removeOne("u", "product-A");
|
|
137
|
+
const setCall = mockTxn.set.mock.calls[0][1];
|
|
138
|
+
expect(setCall.items).toHaveLength(1);
|
|
139
|
+
expect(setCall.items[0].productId).toBe("product-B");
|
|
140
|
+
});
|
|
141
|
+
it("non-existent productId → no-op, set not called", async () => {
|
|
142
|
+
const items = [{ productId: "product-A", productType: "product", viewedAt: new Date() }];
|
|
143
|
+
mockTxn.get.mockResolvedValue(makeSnap({ userId: "u", items, updatedAt: new Date() }));
|
|
144
|
+
await repo.removeOne("u", "product-xxx");
|
|
145
|
+
expect(mockTxn.set).not.toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
describe("UserHistoryRepository.clearForUser", () => {
|
|
149
|
+
it("sets items: [] without deleting the document", async () => {
|
|
150
|
+
await repo.clearForUser("u");
|
|
151
|
+
expect(mockDocRef.set).toHaveBeenCalledWith(expect.objectContaining({ items: [] }));
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
describe("UserHistoryRepository.getHistory", () => {
|
|
155
|
+
it("returns items array", async () => {
|
|
156
|
+
const items = [{ productId: "product-A", productType: "product", viewedAt: new Date() }];
|
|
157
|
+
mockDocRef.get.mockResolvedValue(makeSnap({ userId: "u", items, updatedAt: new Date() }));
|
|
158
|
+
const result = await repo.getHistory("u");
|
|
159
|
+
expect(result).toHaveLength(1);
|
|
160
|
+
expect(result[0].productId).toBe("product-A");
|
|
161
|
+
});
|
|
162
|
+
it("returns empty array for user with no history document", async () => {
|
|
163
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
164
|
+
const result = await repo.getHistory("u");
|
|
165
|
+
expect(result).toEqual([]);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
describe("UserHistoryRepository.countByUser", () => {
|
|
169
|
+
it("returns items.length", async () => {
|
|
170
|
+
const items = makeHistoryItems(5);
|
|
171
|
+
mockDocRef.get.mockResolvedValue(makeSnap({ userId: "u", items, updatedAt: new Date() }));
|
|
172
|
+
const count = await repo.countByUser("u");
|
|
173
|
+
expect(count).toBe(5);
|
|
174
|
+
});
|
|
175
|
+
it("returns 0 for missing document", async () => {
|
|
176
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
177
|
+
const count = await repo.countByUser("u");
|
|
178
|
+
expect(count).toBe(0);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -15,9 +15,9 @@ export declare function GET(_request: Request, context: {
|
|
|
15
15
|
}>;
|
|
16
16
|
}): Promise<NextResponse>;
|
|
17
17
|
export { GET as homepageSectionItemGET };
|
|
18
|
-
export declare const homepageSectionItemPATCH: (request: Request, context
|
|
18
|
+
export declare const homepageSectionItemPATCH: (request: Request, context?: {
|
|
19
19
|
params: Promise<Record<string, string>>;
|
|
20
|
-
}) => Promise<Response>;
|
|
21
|
-
export declare const homepageSectionItemDELETE: (request: Request, context
|
|
20
|
+
} | undefined) => Promise<Response>;
|
|
21
|
+
export declare const homepageSectionItemDELETE: (request: Request, context?: {
|
|
22
22
|
params: Promise<Record<string, string>>;
|
|
23
|
-
}) => Promise<Response>;
|
|
23
|
+
} | undefined) => Promise<Response>;
|
|
@@ -15,9 +15,9 @@ export declare function GET(_request: Request, context: {
|
|
|
15
15
|
}>;
|
|
16
16
|
}): Promise<NextResponse>;
|
|
17
17
|
export { GET as carouselItemGET };
|
|
18
|
-
export declare const carouselItemPATCH: (request: Request, context
|
|
18
|
+
export declare const carouselItemPATCH: (request: Request, context?: {
|
|
19
19
|
params: Promise<Record<string, string>>;
|
|
20
|
-
}) => Promise<Response>;
|
|
21
|
-
export declare const carouselItemDELETE: (request: Request, context
|
|
20
|
+
} | undefined) => Promise<Response>;
|
|
21
|
+
export declare const carouselItemDELETE: (request: Request, context?: {
|
|
22
22
|
params: Promise<Record<string, string>>;
|
|
23
|
-
}) => Promise<Response>;
|
|
23
|
+
} | undefined) => Promise<Response>;
|
|
@@ -10,6 +10,6 @@
|
|
|
10
10
|
import { NextResponse } from "next/server.js";
|
|
11
11
|
export declare function GET(request: Request): Promise<NextResponse>;
|
|
12
12
|
export { GET as carouselGET };
|
|
13
|
-
export declare const carouselPOST: (request: Request, context
|
|
13
|
+
export declare const carouselPOST: (request: Request, context?: {
|
|
14
14
|
params: Promise<Record<string, string>>;
|
|
15
|
-
}) => Promise<Response>;
|
|
15
|
+
} | undefined) => Promise<Response>;
|
|
@@ -9,6 +9,6 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { NextResponse } from "next/server.js";
|
|
11
11
|
export declare function GET(request: Request): Promise<NextResponse>;
|
|
12
|
-
export declare const POST: (request: Request, context
|
|
12
|
+
export declare const POST: (request: Request, context?: {
|
|
13
13
|
params: Promise<Record<string, string>>;
|
|
14
|
-
}) => Promise<Response>;
|
|
14
|
+
} | undefined) => Promise<Response>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { makeMockDb, makeSnap, makeQuerySnap } from "../../../../../tests/helpers/mock-firestore";
|
|
3
|
+
import { DatabaseError } from "../../../../errors";
|
|
4
|
+
const { db, mockDocRef, mockCollection, mockQuery, mockBatch } = makeMockDb();
|
|
5
|
+
const { mockGetFirestoreCount } = vi.hoisted(() => ({
|
|
6
|
+
mockGetFirestoreCount: vi.fn().mockResolvedValue(0),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("../../../../providers/db-firebase/admin", () => ({
|
|
9
|
+
getAdminDb: () => db,
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal();
|
|
13
|
+
return {
|
|
14
|
+
...actual,
|
|
15
|
+
prepareForFirestore: (d) => d,
|
|
16
|
+
getFirestoreCount: mockGetFirestoreCount,
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
vi.mock("../../../../errors/normalize", () => ({
|
|
20
|
+
normalizeError: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
import { CarouselRepository } from "../carousel.repository";
|
|
23
|
+
import { MAX_ACTIVE_SLIDES } from "../../schemas";
|
|
24
|
+
const repo = new CarouselRepository();
|
|
25
|
+
function makeSlideDoc(overrides = {}) {
|
|
26
|
+
return {
|
|
27
|
+
id: "slide-hero",
|
|
28
|
+
title: "Hero Slide",
|
|
29
|
+
active: true,
|
|
30
|
+
order: 0,
|
|
31
|
+
cards: [],
|
|
32
|
+
background: { type: "image", url: "https://example.com/img.jpg" },
|
|
33
|
+
createdBy: "admin-1",
|
|
34
|
+
createdAt: new Date(),
|
|
35
|
+
updatedAt: new Date(),
|
|
36
|
+
...overrides,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function makeActiveSlides(count) {
|
|
40
|
+
return Array.from({ length: count }, (_, i) => ({
|
|
41
|
+
id: `slide-${i}`,
|
|
42
|
+
data: makeSlideDoc({ id: `slide-${i}`, active: true, order: i }),
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
vi.clearAllMocks();
|
|
47
|
+
db.collection.mockReturnValue(mockCollection);
|
|
48
|
+
mockCollection.doc.mockReturnValue(mockDocRef);
|
|
49
|
+
mockCollection.where.mockReturnValue(mockQuery);
|
|
50
|
+
mockQuery.where.mockReturnValue(mockQuery);
|
|
51
|
+
mockQuery.orderBy.mockReturnValue(mockQuery);
|
|
52
|
+
mockQuery.limit.mockReturnValue(mockQuery);
|
|
53
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
54
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
55
|
+
mockDocRef.set.mockResolvedValue(undefined);
|
|
56
|
+
mockDocRef.update.mockResolvedValue(undefined);
|
|
57
|
+
db.batch.mockReturnValue(mockBatch);
|
|
58
|
+
mockGetFirestoreCount.mockResolvedValue(0);
|
|
59
|
+
});
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// activateSlide
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
describe("CarouselRepository.activateSlide", () => {
|
|
64
|
+
it("activates when active count < MAX_ACTIVE_SLIDES", async () => {
|
|
65
|
+
// getActiveSlides returns fewer than max
|
|
66
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(makeActiveSlides(MAX_ACTIVE_SLIDES - 1)));
|
|
67
|
+
const slide = makeSlideDoc({ active: true });
|
|
68
|
+
mockDocRef.get.mockResolvedValue(makeSnap(slide, "slide-hero"));
|
|
69
|
+
await repo.activateSlide("slide-hero");
|
|
70
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ active: true }));
|
|
71
|
+
});
|
|
72
|
+
it("throws DatabaseError when active count = MAX_ACTIVE_SLIDES", async () => {
|
|
73
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(makeActiveSlides(MAX_ACTIVE_SLIDES)));
|
|
74
|
+
await expect(repo.activateSlide("slide-7")).rejects.toThrow(DatabaseError);
|
|
75
|
+
});
|
|
76
|
+
it("throws DatabaseError when active count > MAX_ACTIVE_SLIDES (defensive)", async () => {
|
|
77
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap(makeActiveSlides(MAX_ACTIVE_SLIDES + 1)));
|
|
78
|
+
await expect(repo.activateSlide("slide-7")).rejects.toThrow(DatabaseError);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// deactivateSlide
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
describe("CarouselRepository.deactivateSlide", () => {
|
|
85
|
+
it("sets active: false on the slide", async () => {
|
|
86
|
+
const slide = makeSlideDoc({ active: false });
|
|
87
|
+
mockDocRef.get.mockResolvedValue(makeSnap(slide, "slide-hero"));
|
|
88
|
+
await repo.deactivateSlide("slide-hero");
|
|
89
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ active: false }));
|
|
90
|
+
});
|
|
91
|
+
it("sets updatedAt timestamp", async () => {
|
|
92
|
+
const slide = makeSlideDoc();
|
|
93
|
+
mockDocRef.get.mockResolvedValue(makeSnap(slide, "slide-hero"));
|
|
94
|
+
await repo.deactivateSlide("slide-hero");
|
|
95
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// canActivateMore
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
describe("CarouselRepository.canActivateMore", () => {
|
|
102
|
+
it("returns true when active count < MAX_ACTIVE_SLIDES", async () => {
|
|
103
|
+
mockGetFirestoreCount.mockResolvedValue(MAX_ACTIVE_SLIDES - 1);
|
|
104
|
+
const result = await repo.canActivateMore();
|
|
105
|
+
expect(result).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
it("returns false when active count = MAX_ACTIVE_SLIDES", async () => {
|
|
108
|
+
mockGetFirestoreCount.mockResolvedValue(MAX_ACTIVE_SLIDES);
|
|
109
|
+
const result = await repo.canActivateMore();
|
|
110
|
+
expect(result).toBe(false);
|
|
111
|
+
});
|
|
112
|
+
it("returns false when active count > MAX_ACTIVE_SLIDES", async () => {
|
|
113
|
+
mockGetFirestoreCount.mockResolvedValue(MAX_ACTIVE_SLIDES + 2);
|
|
114
|
+
const result = await repo.canActivateMore();
|
|
115
|
+
expect(result).toBe(false);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
// reorderSlides
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
describe("CarouselRepository.reorderSlides", () => {
|
|
122
|
+
it("batch-updates order field on each slide", async () => {
|
|
123
|
+
await repo.reorderSlides([
|
|
124
|
+
{ id: "slide-1", order: 0 },
|
|
125
|
+
{ id: "slide-2", order: 1 },
|
|
126
|
+
{ id: "slide-3", order: 2 },
|
|
127
|
+
]);
|
|
128
|
+
expect(mockBatch.update).toHaveBeenCalledTimes(3);
|
|
129
|
+
});
|
|
130
|
+
it("commits the batch", async () => {
|
|
131
|
+
await repo.reorderSlides([{ id: "slide-1", order: 0 }]);
|
|
132
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
133
|
+
});
|
|
134
|
+
it("each update carries the correct order value", async () => {
|
|
135
|
+
await repo.reorderSlides([
|
|
136
|
+
{ id: "slide-a", order: 2 },
|
|
137
|
+
{ id: "slide-b", order: 5 },
|
|
138
|
+
]);
|
|
139
|
+
const updateCalls = mockBatch.update.mock.calls;
|
|
140
|
+
const orders = updateCalls.map((c) => c[1].order);
|
|
141
|
+
expect(orders).toContain(2);
|
|
142
|
+
expect(orders).toContain(5);
|
|
143
|
+
});
|
|
144
|
+
it("empty list → no batch update called, commits empty batch", async () => {
|
|
145
|
+
await repo.reorderSlides([]);
|
|
146
|
+
expect(mockBatch.update).not.toHaveBeenCalled();
|
|
147
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// incrementViews
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
describe("CarouselRepository.incrementViews", () => {
|
|
154
|
+
it("increments stats.views by 1", async () => {
|
|
155
|
+
await repo.incrementViews("slide-hero");
|
|
156
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ "stats.views": expect.anything() }));
|
|
157
|
+
});
|
|
158
|
+
it("updates stats.lastViewed timestamp", async () => {
|
|
159
|
+
await repo.incrementViews("slide-hero");
|
|
160
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ "stats.lastViewed": expect.any(Date) }));
|
|
161
|
+
});
|
|
162
|
+
it("swallows errors — analytics must not break callers", async () => {
|
|
163
|
+
mockDocRef.update.mockRejectedValue(new Error("Firestore error"));
|
|
164
|
+
await expect(repo.incrementViews("slide-hero")).resolves.toBeUndefined();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// getActiveSlides
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
describe("CarouselRepository.getActiveSlides", () => {
|
|
171
|
+
it("queries active=true, ordered by order ascending", async () => {
|
|
172
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
173
|
+
await repo.getActiveSlides();
|
|
174
|
+
expect(mockCollection.where).toHaveBeenCalledWith("active", "==", true);
|
|
175
|
+
expect(mockQuery.orderBy).toHaveBeenCalledWith("order", "asc");
|
|
176
|
+
});
|
|
177
|
+
it("limits results to MAX_ACTIVE_SLIDES", async () => {
|
|
178
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
179
|
+
await repo.getActiveSlides();
|
|
180
|
+
expect(mockQuery.limit).toHaveBeenCalledWith(MAX_ACTIVE_SLIDES);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { makeMockDb, makeSnap, makeQuerySnap } from "../../../../../tests/helpers/mock-firestore";
|
|
3
|
+
const { db, mockDocRef, mockCollection, mockQuery, mockBatch } = makeMockDb();
|
|
4
|
+
vi.mock("../../../../providers/db-firebase/admin", () => ({
|
|
5
|
+
getAdminDb: () => db,
|
|
6
|
+
}));
|
|
7
|
+
vi.mock("../../../../providers/db-firebase", async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal();
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
prepareForFirestore: (d) => d,
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
vi.mock("../../../../errors/normalize", () => ({
|
|
15
|
+
normalizeError: vi.fn(),
|
|
16
|
+
}));
|
|
17
|
+
import { HomepageSectionsRepository } from "../homepage-sections.repository";
|
|
18
|
+
const repo = new HomepageSectionsRepository();
|
|
19
|
+
function makeSectionDoc(overrides = {}) {
|
|
20
|
+
return {
|
|
21
|
+
id: "section-featured-products",
|
|
22
|
+
type: "featured-products",
|
|
23
|
+
enabled: true,
|
|
24
|
+
order: 0,
|
|
25
|
+
config: {},
|
|
26
|
+
createdAt: new Date(),
|
|
27
|
+
updatedAt: new Date(),
|
|
28
|
+
...overrides,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
db.collection.mockReturnValue(mockCollection);
|
|
34
|
+
mockCollection.doc.mockReturnValue(mockDocRef);
|
|
35
|
+
mockCollection.where.mockReturnValue(mockQuery);
|
|
36
|
+
mockQuery.where.mockReturnValue(mockQuery);
|
|
37
|
+
mockQuery.orderBy.mockReturnValue(mockQuery);
|
|
38
|
+
mockQuery.limit.mockReturnValue(mockQuery);
|
|
39
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
40
|
+
mockDocRef.get.mockResolvedValue(makeSnap(null));
|
|
41
|
+
mockDocRef.set.mockResolvedValue(undefined);
|
|
42
|
+
mockDocRef.update.mockResolvedValue(undefined);
|
|
43
|
+
db.batch.mockReturnValue(mockBatch);
|
|
44
|
+
});
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// getEnabledSections
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
describe("HomepageSectionsRepository.getEnabledSections", () => {
|
|
49
|
+
it("queries where enabled=true, ordered by order ascending", async () => {
|
|
50
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
51
|
+
await repo.getEnabledSections();
|
|
52
|
+
expect(mockCollection.where).toHaveBeenCalledWith("enabled", "==", true);
|
|
53
|
+
expect(mockQuery.orderBy).toHaveBeenCalledWith("order", "asc");
|
|
54
|
+
});
|
|
55
|
+
it("returns only enabled sections", async () => {
|
|
56
|
+
const section = makeSectionDoc({ enabled: true });
|
|
57
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([{ id: section.id, data: section }]));
|
|
58
|
+
const results = await repo.getEnabledSections();
|
|
59
|
+
expect(results).toHaveLength(1);
|
|
60
|
+
});
|
|
61
|
+
it("returns empty array when no enabled sections", async () => {
|
|
62
|
+
mockQuery.get.mockResolvedValue(makeQuerySnap([]));
|
|
63
|
+
const results = await repo.getEnabledSections();
|
|
64
|
+
expect(results).toHaveLength(0);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// enableSection
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
describe("HomepageSectionsRepository.enableSection", () => {
|
|
71
|
+
it("sets enabled: true on the section", async () => {
|
|
72
|
+
const section = makeSectionDoc({ enabled: false });
|
|
73
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
74
|
+
await repo.enableSection("section-1");
|
|
75
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ enabled: true }));
|
|
76
|
+
});
|
|
77
|
+
it("sets updatedAt timestamp", async () => {
|
|
78
|
+
const section = makeSectionDoc();
|
|
79
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
80
|
+
await repo.enableSection("section-1");
|
|
81
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// disableSection
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
describe("HomepageSectionsRepository.disableSection", () => {
|
|
88
|
+
it("sets enabled: false on the section", async () => {
|
|
89
|
+
const section = makeSectionDoc({ enabled: true });
|
|
90
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
91
|
+
await repo.disableSection("section-1");
|
|
92
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ enabled: false }));
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
// toggleSection
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
describe("HomepageSectionsRepository.toggleSection", () => {
|
|
99
|
+
it("enabled:true → sets enabled:false", async () => {
|
|
100
|
+
const section = makeSectionDoc({ enabled: true });
|
|
101
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
102
|
+
await repo.toggleSection("section-1");
|
|
103
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ enabled: false }));
|
|
104
|
+
});
|
|
105
|
+
it("enabled:false → sets enabled:true", async () => {
|
|
106
|
+
const section = makeSectionDoc({ enabled: false });
|
|
107
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
108
|
+
await repo.toggleSection("section-1");
|
|
109
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ enabled: true }));
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
// ---------------------------------------------------------------------------
|
|
113
|
+
// reorderSections
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
describe("HomepageSectionsRepository.reorderSections", () => {
|
|
116
|
+
it("batch-updates order field for each section", async () => {
|
|
117
|
+
await repo.reorderSections([
|
|
118
|
+
{ id: "section-1", order: 0 },
|
|
119
|
+
{ id: "section-2", order: 1 },
|
|
120
|
+
{ id: "section-3", order: 2 },
|
|
121
|
+
]);
|
|
122
|
+
expect(mockBatch.update).toHaveBeenCalledTimes(3);
|
|
123
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
124
|
+
});
|
|
125
|
+
it("each update sets the correct order value", async () => {
|
|
126
|
+
await repo.reorderSections([
|
|
127
|
+
{ id: "section-a", order: 5 },
|
|
128
|
+
{ id: "section-b", order: 10 },
|
|
129
|
+
]);
|
|
130
|
+
const calls = mockBatch.update.mock.calls;
|
|
131
|
+
expect(calls.some((c) => c[1].order === 5)).toBe(true);
|
|
132
|
+
expect(calls.some((c) => c[1].order === 10)).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
it("empty list → no batch update, commits empty batch", async () => {
|
|
135
|
+
await repo.reorderSections([]);
|
|
136
|
+
expect(mockBatch.update).not.toHaveBeenCalled();
|
|
137
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// batchToggleSections
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
describe("HomepageSectionsRepository.batchToggleSections", () => {
|
|
144
|
+
it("sets enabled:true on all provided sections in one batch", async () => {
|
|
145
|
+
await repo.batchToggleSections(["section-1", "section-2", "section-3"], true);
|
|
146
|
+
expect(mockBatch.update).toHaveBeenCalledTimes(3);
|
|
147
|
+
const calls = mockBatch.update.mock.calls;
|
|
148
|
+
expect(calls.every((c) => c[1].enabled === true)).toBe(true);
|
|
149
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
150
|
+
});
|
|
151
|
+
it("sets enabled:false on all provided sections", async () => {
|
|
152
|
+
await repo.batchToggleSections(["section-x", "section-y"], false);
|
|
153
|
+
const calls = mockBatch.update.mock.calls;
|
|
154
|
+
expect(calls.every((c) => c[1].enabled === false)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
it("empty list → no updates, commits empty batch", async () => {
|
|
157
|
+
await repo.batchToggleSections([], true);
|
|
158
|
+
expect(mockBatch.update).not.toHaveBeenCalled();
|
|
159
|
+
expect(mockBatch.commit).toHaveBeenCalledOnce();
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
// ---------------------------------------------------------------------------
|
|
163
|
+
// resetSectionToDefault
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
describe("HomepageSectionsRepository.resetSectionToDefault", () => {
|
|
166
|
+
it("overwrites config with the provided default config", async () => {
|
|
167
|
+
const section = makeSectionDoc({ config: { customKey: "old-value" } });
|
|
168
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
169
|
+
const defaultCfg = { title: "Default Title", itemCount: 6 };
|
|
170
|
+
await repo.resetSectionToDefault("section-1", defaultCfg);
|
|
171
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ config: defaultCfg }));
|
|
172
|
+
});
|
|
173
|
+
it("does NOT delete the section document", async () => {
|
|
174
|
+
const section = makeSectionDoc();
|
|
175
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
176
|
+
await repo.resetSectionToDefault("section-1", {});
|
|
177
|
+
expect(mockDocRef.delete).not.toHaveBeenCalled();
|
|
178
|
+
});
|
|
179
|
+
it("sets updatedAt timestamp on reset", async () => {
|
|
180
|
+
const section = makeSectionDoc();
|
|
181
|
+
mockDocRef.get.mockResolvedValue(makeSnap(section, "section-1"));
|
|
182
|
+
await repo.resetSectionToDefault("section-1", {});
|
|
183
|
+
expect(mockDocRef.update).toHaveBeenCalledWith(expect.objectContaining({ updatedAt: expect.any(Date) }));
|
|
184
|
+
});
|
|
185
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Homepage Feature Firestore Document Types & Constants
|
|
3
3
|
* Covers: carousel slides, homepage sections
|
|
4
4
|
*/
|
|
5
|
+
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
5
6
|
/** Unified background for a slide or card — supports image, video, solid color, or gradient. */
|
|
6
7
|
export interface CarouselBackground {
|
|
7
8
|
type: "image" | "video" | "color" | "gradient";
|
|
@@ -63,14 +64,11 @@ export interface CarouselCard {
|
|
|
63
64
|
}
|
|
64
65
|
export type GridCardCreateInput = Omit<CarouselCard, "id">;
|
|
65
66
|
/** A named carousel that holds up to MAX_SLIDES_PER_CAROUSEL slide references. */
|
|
66
|
-
export interface CarouselDocument {
|
|
67
|
-
id: string;
|
|
67
|
+
export interface CarouselDocument extends BaseDocument {
|
|
68
68
|
name: string;
|
|
69
69
|
status: "active" | "draft";
|
|
70
70
|
/** Ordered slide IDs (max MAX_SLIDES_PER_CAROUSEL). */
|
|
71
71
|
slideIds: string[];
|
|
72
|
-
createdAt: Date;
|
|
73
|
-
updatedAt: Date;
|
|
74
72
|
createdBy: string;
|
|
75
73
|
}
|
|
76
74
|
export type CarouselCreateInput = Omit<CarouselDocument, "id" | "createdAt" | "updatedAt">;
|
|
@@ -78,8 +76,7 @@ export type CarouselUpdateInput = Partial<Pick<CarouselDocument, "name" | "statu
|
|
|
78
76
|
export declare class TooManySlidesError extends Error {
|
|
79
77
|
constructor();
|
|
80
78
|
}
|
|
81
|
-
export interface CarouselSlideDocument {
|
|
82
|
-
id: string;
|
|
79
|
+
export interface CarouselSlideDocument extends BaseDocument {
|
|
83
80
|
/**
|
|
84
81
|
* Reference to a named CarouselDocument (EX2).
|
|
85
82
|
* null = default hero carousel (backward-compat with pre-EX2 slides).
|
|
@@ -131,8 +128,6 @@ export interface CarouselSlideDocument {
|
|
|
131
128
|
views: number;
|
|
132
129
|
lastViewed?: Date;
|
|
133
130
|
};
|
|
134
|
-
createdAt: Date;
|
|
135
|
-
updatedAt: Date;
|
|
136
131
|
createdBy: string;
|
|
137
132
|
}
|
|
138
133
|
export declare const CAROUSEL_SLIDES_COLLECTION: "carouselSlides";
|
|
@@ -552,14 +547,11 @@ export interface GoogleReviewsSectionConfig {
|
|
|
552
547
|
}
|
|
553
548
|
export type SectionType = "welcome" | "carousel" | "stats" | "trust-indicators" | "categories" | "brands" | "products" | "pre-orders" | "auctions" | "banner" | "features" | "reviews" | "whatsapp-community" | "faq" | "blog-articles" | "newsletter" | "stores" | "events" | "social-feed" | "custom-cards" | "google-reviews" | "featured-bundles" | "prize-draws" | "event-raffles" | "collection-cards";
|
|
554
549
|
export type SectionConfig = CarouselSectionConfig | WelcomeSectionConfig | StatsSectionConfig | TrustIndicatorsSectionConfig | CategoriesSectionConfig | BrandsSectionConfig | ProductsSectionConfig | PreOrdersSectionConfig | AuctionsSectionConfig | BannerSectionConfig | FeaturesSectionConfig | ReviewsSectionConfig | WhatsAppCommunitySectionConfig | FAQSectionConfig | BlogArticlesSectionConfig | NewsletterSectionConfig | StoresSectionConfig | EventsSectionConfig | SocialFeedSectionConfig | CustomCardsSectionConfig | GoogleReviewsSectionConfig | FeaturedBundlesSectionConfig | PrizeDrawsSectionConfig | EventRafflesSectionConfig | CollectionCardsSectionConfig;
|
|
555
|
-
export interface HomepageSectionDocument {
|
|
556
|
-
id: string;
|
|
550
|
+
export interface HomepageSectionDocument extends BaseDocument {
|
|
557
551
|
type: SectionType;
|
|
558
552
|
order: number;
|
|
559
553
|
enabled: boolean;
|
|
560
554
|
config: SectionConfig;
|
|
561
|
-
createdAt: Date;
|
|
562
|
-
updatedAt: Date;
|
|
563
555
|
}
|
|
564
556
|
export declare const HOMEPAGE_SECTIONS_COLLECTION: "homepageSections";
|
|
565
557
|
export declare const HOMEPAGE_SECTIONS_INDEXED_FIELDS: readonly ["enabled", "order", "type"];
|