@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,148 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
const { mockSendNotification, mockRtdbPush, mockDecryptPii, } = vi.hoisted(() => ({
|
|
3
|
+
mockSendNotification: vi.fn().mockResolvedValue(undefined),
|
|
4
|
+
mockRtdbPush: vi.fn().mockResolvedValue(undefined),
|
|
5
|
+
mockDecryptPii: vi.fn((v) => v),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock("../../../../../features/admin/actions/notification-actions", () => ({
|
|
8
|
+
sendNotification: mockSendNotification,
|
|
9
|
+
}));
|
|
10
|
+
vi.mock("../../../../../providers/db-firebase", () => ({
|
|
11
|
+
getAdminRealtimeDb: vi.fn(() => ({
|
|
12
|
+
ref: vi.fn(() => ({ push: mockRtdbPush })),
|
|
13
|
+
})),
|
|
14
|
+
}));
|
|
15
|
+
vi.mock("../../../../../security/index", () => ({
|
|
16
|
+
decryptPii: mockDecryptPii,
|
|
17
|
+
}));
|
|
18
|
+
vi.mock("../../../../../errors/normalize", () => ({
|
|
19
|
+
normalizeError: vi.fn(),
|
|
20
|
+
}));
|
|
21
|
+
import { handleOrderStatusChange } from "../onOrderStatusChange";
|
|
22
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
23
|
+
function makeCtx() {
|
|
24
|
+
return {
|
|
25
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function makeInput(beforeStatus, afterStatus, extraAfter = {}) {
|
|
29
|
+
return {
|
|
30
|
+
orderId: "order-1",
|
|
31
|
+
before: { status: beforeStatus },
|
|
32
|
+
after: {
|
|
33
|
+
status: afterStatus,
|
|
34
|
+
userId: "user-ravi",
|
|
35
|
+
userEmail: "ravi@example.com",
|
|
36
|
+
productTitle: "Pokemon Charizard",
|
|
37
|
+
trackingNumber: "TRK123",
|
|
38
|
+
...extraAfter,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
vi.clearAllMocks();
|
|
44
|
+
mockSendNotification.mockResolvedValue(undefined);
|
|
45
|
+
mockRtdbPush.mockResolvedValue(undefined);
|
|
46
|
+
mockDecryptPii.mockImplementation((v) => v);
|
|
47
|
+
});
|
|
48
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
49
|
+
describe("handleOrderStatusChange — early exits", () => {
|
|
50
|
+
it("returns immediately when before is null", async () => {
|
|
51
|
+
const ctx = makeCtx();
|
|
52
|
+
await handleOrderStatusChange({ orderId: "o1", before: null, after: null }, ctx);
|
|
53
|
+
expect(mockSendNotification).not.toHaveBeenCalled();
|
|
54
|
+
});
|
|
55
|
+
it("returns immediately when after is null", async () => {
|
|
56
|
+
const ctx = makeCtx();
|
|
57
|
+
await handleOrderStatusChange({ orderId: "o1", before: { status: "pending" }, after: null }, ctx);
|
|
58
|
+
expect(mockSendNotification).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
it("returns when status has not changed", async () => {
|
|
61
|
+
const ctx = makeCtx();
|
|
62
|
+
await handleOrderStatusChange(makeInput("confirmed", "confirmed"), ctx);
|
|
63
|
+
expect(mockSendNotification).not.toHaveBeenCalled();
|
|
64
|
+
});
|
|
65
|
+
it("logs and returns when new status has no handler config", async () => {
|
|
66
|
+
const ctx = makeCtx();
|
|
67
|
+
await handleOrderStatusChange(makeInput("pending", "returned"), ctx);
|
|
68
|
+
expect(mockSendNotification).not.toHaveBeenCalled();
|
|
69
|
+
expect(ctx.logger.info).toHaveBeenCalledWith(expect.stringMatching(/No handler for status transition/i), expect.any(Object));
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe("handleOrderStatusChange — confirmed", () => {
|
|
73
|
+
it("sends order_confirmed notification to buyer", async () => {
|
|
74
|
+
await handleOrderStatusChange(makeInput("pending", "confirmed"), makeCtx());
|
|
75
|
+
expect(mockSendNotification).toHaveBeenCalledWith(expect.objectContaining({ userId: "user-ravi", type: "order_confirmed" }));
|
|
76
|
+
});
|
|
77
|
+
it("includes decrypted userEmail in notification payload", async () => {
|
|
78
|
+
mockDecryptPii.mockReturnValue("decrypted@example.com");
|
|
79
|
+
await handleOrderStatusChange(makeInput("pending", "confirmed"), makeCtx());
|
|
80
|
+
expect(mockSendNotification).toHaveBeenCalledWith(expect.objectContaining({ userEmail: "decrypted@example.com" }));
|
|
81
|
+
});
|
|
82
|
+
it("pushes to RTDB for real-time notification", async () => {
|
|
83
|
+
await handleOrderStatusChange(makeInput("pending", "confirmed"), makeCtx());
|
|
84
|
+
expect(mockRtdbPush).toHaveBeenCalledWith(expect.objectContaining({ type: "order_confirmed" }));
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
describe("handleOrderStatusChange — shipped", () => {
|
|
88
|
+
it("sends order_shipped notification with high priority", async () => {
|
|
89
|
+
await handleOrderStatusChange(makeInput("confirmed", "shipped"), makeCtx());
|
|
90
|
+
expect(mockSendNotification).toHaveBeenCalledWith(expect.objectContaining({ type: "order_shipped", priority: "high" }));
|
|
91
|
+
});
|
|
92
|
+
it("notification message includes tracking number", async () => {
|
|
93
|
+
await handleOrderStatusChange(makeInput("confirmed", "shipped", { trackingNumber: "AWB123" }), makeCtx());
|
|
94
|
+
const notifCall = mockSendNotification.mock.calls[0][0];
|
|
95
|
+
expect(notifCall.message).toMatch(/AWB123/);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
describe("handleOrderStatusChange — delivered", () => {
|
|
99
|
+
it("sends order_delivered notification", async () => {
|
|
100
|
+
await handleOrderStatusChange(makeInput("shipped", "delivered"), makeCtx());
|
|
101
|
+
expect(mockSendNotification).toHaveBeenCalledWith(expect.objectContaining({ type: "order_delivered" }));
|
|
102
|
+
});
|
|
103
|
+
it("notification relatedId = orderId, relatedType = 'order'", async () => {
|
|
104
|
+
await handleOrderStatusChange(makeInput("shipped", "delivered"), makeCtx());
|
|
105
|
+
expect(mockSendNotification).toHaveBeenCalledWith(expect.objectContaining({ relatedId: "order-1", relatedType: "order" }));
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
describe("handleOrderStatusChange — cancelled", () => {
|
|
109
|
+
it("sends order_cancelled notification", async () => {
|
|
110
|
+
await handleOrderStatusChange(makeInput("pending", "cancelled"), makeCtx());
|
|
111
|
+
expect(mockSendNotification).toHaveBeenCalledWith(expect.objectContaining({ type: "order_cancelled" }));
|
|
112
|
+
});
|
|
113
|
+
it("cancelled message mentions the product title", async () => {
|
|
114
|
+
await handleOrderStatusChange(makeInput("pending", "cancelled", { productTitle: "Hot Wheels Redline" }), makeCtx());
|
|
115
|
+
const notifCall = mockSendNotification.mock.calls[0][0];
|
|
116
|
+
expect(notifCall.message).toMatch(/Hot Wheels Redline/);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
describe("handleOrderStatusChange — RTDB failure is non-fatal", () => {
|
|
120
|
+
it("RTDB push failure → logs but does NOT re-throw", async () => {
|
|
121
|
+
mockRtdbPush.mockRejectedValue(new Error("RTDB down"));
|
|
122
|
+
const ctx = makeCtx();
|
|
123
|
+
await expect(handleOrderStatusChange(makeInput("pending", "confirmed"), ctx)).resolves.toBeUndefined();
|
|
124
|
+
expect(ctx.logger.error).toHaveBeenCalledWith(expect.stringMatching(/Realtime DB/i), expect.any(Error));
|
|
125
|
+
});
|
|
126
|
+
it("Firestore notification still sent even when RTDB fails", async () => {
|
|
127
|
+
mockRtdbPush.mockRejectedValue(new Error("RTDB down"));
|
|
128
|
+
await handleOrderStatusChange(makeInput("pending", "confirmed"), makeCtx());
|
|
129
|
+
expect(mockSendNotification).toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
describe("handleOrderStatusChange — error propagation", () => {
|
|
133
|
+
it("sendNotification failure → re-throws", async () => {
|
|
134
|
+
mockSendNotification.mockRejectedValue(new Error("Notification service down"));
|
|
135
|
+
await expect(handleOrderStatusChange(makeInput("pending", "confirmed"), makeCtx())).rejects.toThrow("Notification service down");
|
|
136
|
+
});
|
|
137
|
+
it("logs error context before re-throwing", async () => {
|
|
138
|
+
mockSendNotification.mockRejectedValue(new Error("Notification service down"));
|
|
139
|
+
const ctx = makeCtx();
|
|
140
|
+
try {
|
|
141
|
+
await handleOrderStatusChange(makeInput("pending", "confirmed"), ctx);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
// expected
|
|
145
|
+
}
|
|
146
|
+
expect(ctx.logger.error).toHaveBeenCalledWith(expect.any(String), expect.any(Error), expect.objectContaining({ orderId: "order-1" }));
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
// onProductStockChange uses ctx.db directly, no repository imports needed.
|
|
3
|
+
vi.mock("../../../../../errors/normalize", () => ({
|
|
4
|
+
normalizeError: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
import { handleProductStockChange, isAvailable, syncBundlesForProduct, } from "../onProductStockChange";
|
|
7
|
+
import { PRODUCT_FIELDS } from "../../../../../constants/field-names";
|
|
8
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
9
|
+
function makeCtx(dbQuery) {
|
|
10
|
+
const mockQuery = dbQuery ?? vi.fn().mockReturnThis();
|
|
11
|
+
const queryChain = { where: vi.fn().mockReturnThis(), get: vi.fn() };
|
|
12
|
+
return {
|
|
13
|
+
db: {
|
|
14
|
+
collection: vi.fn().mockReturnValue(queryChain),
|
|
15
|
+
},
|
|
16
|
+
queryChain,
|
|
17
|
+
now: new Date(),
|
|
18
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const PUBLISHED = PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED;
|
|
22
|
+
// ── isAvailable (pure function) ───────────────────────────────────────────────
|
|
23
|
+
describe("isAvailable — pure function", () => {
|
|
24
|
+
it("null doc → false", () => {
|
|
25
|
+
expect(isAvailable(null)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
it("isSold=true → false", () => {
|
|
28
|
+
expect(isAvailable({ isSold: true, availableQuantity: 1, status: PUBLISHED })).toBe(false);
|
|
29
|
+
});
|
|
30
|
+
it("availableQuantity=0 → false", () => {
|
|
31
|
+
expect(isAvailable({ isSold: false, availableQuantity: 0, status: PUBLISHED })).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
it("status != published → false", () => {
|
|
34
|
+
expect(isAvailable({ isSold: false, availableQuantity: 5, status: "draft" })).toBe(false);
|
|
35
|
+
});
|
|
36
|
+
it("status=published, qty>0, not sold → true", () => {
|
|
37
|
+
expect(isAvailable({ isSold: false, availableQuantity: 1, status: PUBLISHED })).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
it("qty undefined (no quantity field) → true (quantity not tracked = always available)", () => {
|
|
40
|
+
// Intentional design: the check is `typeof qty === "number" && qty <= 0`.
|
|
41
|
+
// If qty is undefined, the guard is skipped → published product is available.
|
|
42
|
+
expect(isAvailable({ isSold: false, availableQuantity: undefined, status: PUBLISHED })).toBe(true);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
// ── handleProductStockChange ──────────────────────────────────────────────────
|
|
46
|
+
const INPUT_BASE = {
|
|
47
|
+
productId: "product-charizard",
|
|
48
|
+
isDelete: false,
|
|
49
|
+
};
|
|
50
|
+
function makeAvailableSnap() {
|
|
51
|
+
return { isSold: false, availableQuantity: 1, status: PUBLISHED };
|
|
52
|
+
}
|
|
53
|
+
function makeUnavailableSnap() {
|
|
54
|
+
return { isSold: false, availableQuantity: 0, status: PUBLISHED };
|
|
55
|
+
}
|
|
56
|
+
describe("handleProductStockChange — early exit: no state flip", () => {
|
|
57
|
+
it("skips when available → available (no flip)", async () => {
|
|
58
|
+
const ctx = makeCtx();
|
|
59
|
+
await handleProductStockChange({
|
|
60
|
+
...INPUT_BASE,
|
|
61
|
+
before: makeAvailableSnap(),
|
|
62
|
+
after: makeAvailableSnap(),
|
|
63
|
+
}, ctx);
|
|
64
|
+
expect(ctx.db.collection).not.toHaveBeenCalled();
|
|
65
|
+
});
|
|
66
|
+
it("skips when unavailable → unavailable (no flip)", async () => {
|
|
67
|
+
const ctx = makeCtx();
|
|
68
|
+
await handleProductStockChange({
|
|
69
|
+
...INPUT_BASE,
|
|
70
|
+
before: makeUnavailableSnap(),
|
|
71
|
+
after: makeUnavailableSnap(),
|
|
72
|
+
}, ctx);
|
|
73
|
+
expect(ctx.db.collection).not.toHaveBeenCalled();
|
|
74
|
+
});
|
|
75
|
+
it("processes when isDelete=true even if state appears unchanged", async () => {
|
|
76
|
+
const ctx = makeCtx();
|
|
77
|
+
ctx.db.collection = vi.fn().mockReturnValue({
|
|
78
|
+
where: vi.fn().mockReturnThis(),
|
|
79
|
+
get: vi.fn().mockResolvedValue({ docs: [], size: 0 }),
|
|
80
|
+
});
|
|
81
|
+
await handleProductStockChange({
|
|
82
|
+
...INPUT_BASE,
|
|
83
|
+
isDelete: true,
|
|
84
|
+
before: makeAvailableSnap(),
|
|
85
|
+
after: makeAvailableSnap(),
|
|
86
|
+
}, ctx);
|
|
87
|
+
expect(ctx.db.collection).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
describe("handleProductStockChange — state flip triggers sync", () => {
|
|
91
|
+
it("available → unavailable triggers sync", async () => {
|
|
92
|
+
const ctx = makeCtx();
|
|
93
|
+
ctx.db.collection = vi.fn().mockReturnValue({
|
|
94
|
+
where: vi.fn().mockReturnThis(),
|
|
95
|
+
get: vi.fn().mockResolvedValue({ docs: [], size: 0 }),
|
|
96
|
+
});
|
|
97
|
+
await handleProductStockChange({
|
|
98
|
+
...INPUT_BASE,
|
|
99
|
+
before: makeAvailableSnap(),
|
|
100
|
+
after: makeUnavailableSnap(),
|
|
101
|
+
}, ctx);
|
|
102
|
+
expect(ctx.db.collection).toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
it("unavailable → available triggers sync", async () => {
|
|
105
|
+
const ctx = makeCtx();
|
|
106
|
+
ctx.db.collection = vi.fn().mockReturnValue({
|
|
107
|
+
where: vi.fn().mockReturnThis(),
|
|
108
|
+
get: vi.fn().mockResolvedValue({ docs: [], size: 0 }),
|
|
109
|
+
});
|
|
110
|
+
await handleProductStockChange({
|
|
111
|
+
...INPUT_BASE,
|
|
112
|
+
before: makeUnavailableSnap(),
|
|
113
|
+
after: makeAvailableSnap(),
|
|
114
|
+
}, ctx);
|
|
115
|
+
expect(ctx.db.collection).toHaveBeenCalled();
|
|
116
|
+
});
|
|
117
|
+
it("logs synced bundles and groups count", async () => {
|
|
118
|
+
const ctx = makeCtx();
|
|
119
|
+
ctx.db.collection = vi.fn().mockReturnValue({
|
|
120
|
+
where: vi.fn().mockReturnThis(),
|
|
121
|
+
get: vi.fn().mockResolvedValue({ docs: [], size: 0 }),
|
|
122
|
+
});
|
|
123
|
+
await handleProductStockChange({
|
|
124
|
+
...INPUT_BASE,
|
|
125
|
+
before: makeAvailableSnap(),
|
|
126
|
+
after: makeUnavailableSnap(),
|
|
127
|
+
}, ctx);
|
|
128
|
+
expect(ctx.logger.info).toHaveBeenCalledWith("onProductStockChange synced", expect.objectContaining({ productId: "product-charizard" }));
|
|
129
|
+
});
|
|
130
|
+
it("sync failure is non-fatal — logs error but does not throw", async () => {
|
|
131
|
+
const ctx = makeCtx();
|
|
132
|
+
ctx.db.collection = vi.fn().mockImplementation(() => {
|
|
133
|
+
throw new Error("Firestore unavailable");
|
|
134
|
+
});
|
|
135
|
+
await expect(handleProductStockChange({
|
|
136
|
+
...INPUT_BASE,
|
|
137
|
+
before: makeAvailableSnap(),
|
|
138
|
+
after: makeUnavailableSnap(),
|
|
139
|
+
}, ctx)).resolves.toBeUndefined();
|
|
140
|
+
expect(ctx.logger.error).toHaveBeenCalled();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe("syncBundlesForProduct — bundle recompute", () => {
|
|
144
|
+
it("does nothing when no bundles reference the product", async () => {
|
|
145
|
+
const ctx = makeCtx();
|
|
146
|
+
const mockQuery = { where: vi.fn().mockReturnThis(), get: vi.fn().mockResolvedValue({ docs: [], size: 0 }) };
|
|
147
|
+
ctx.db.collection = vi.fn().mockReturnValue(mockQuery);
|
|
148
|
+
const updated = await syncBundlesForProduct("product-x", ctx);
|
|
149
|
+
expect(updated).toBe(0);
|
|
150
|
+
});
|
|
151
|
+
it("updates bundle when stockStatus changes", async () => {
|
|
152
|
+
// Bundle has 1 product: "product-x" (now unavailable)
|
|
153
|
+
const bundleRef = { update: vi.fn().mockResolvedValue(undefined) };
|
|
154
|
+
const bundleDoc = {
|
|
155
|
+
ref: bundleRef,
|
|
156
|
+
data: () => ({
|
|
157
|
+
bundleProductIds: ["product-x"],
|
|
158
|
+
bundleStockStatus: "in_stock", // currently in_stock...
|
|
159
|
+
}),
|
|
160
|
+
};
|
|
161
|
+
const memberSnap = {
|
|
162
|
+
docs: [{ data: () => ({ isSold: false, availableQuantity: 0, status: PUBLISHED }) }],
|
|
163
|
+
size: 1,
|
|
164
|
+
};
|
|
165
|
+
const ctx = makeCtx();
|
|
166
|
+
let callIdx = 0;
|
|
167
|
+
ctx.db.collection = vi.fn().mockReturnValue({
|
|
168
|
+
where: vi.fn().mockReturnThis(),
|
|
169
|
+
get: vi.fn().mockImplementation(async () => {
|
|
170
|
+
callIdx++;
|
|
171
|
+
// First call = bundles query; subsequent calls = member product queries
|
|
172
|
+
if (callIdx === 1)
|
|
173
|
+
return { docs: [bundleDoc], size: 1 };
|
|
174
|
+
return memberSnap;
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
const updated = await syncBundlesForProduct("product-x", ctx);
|
|
178
|
+
expect(updated).toBe(1);
|
|
179
|
+
expect(bundleRef.update).toHaveBeenCalledWith(expect.objectContaining({ bundleStockStatus: "out_of_stock" }));
|
|
180
|
+
});
|
|
181
|
+
it("does NOT update bundle when stockStatus has not changed", async () => {
|
|
182
|
+
const bundleRef = { update: vi.fn().mockResolvedValue(undefined) };
|
|
183
|
+
const bundleDoc = {
|
|
184
|
+
ref: bundleRef,
|
|
185
|
+
data: () => ({
|
|
186
|
+
bundleProductIds: ["product-x"],
|
|
187
|
+
bundleStockStatus: "in_stock", // already in_stock
|
|
188
|
+
}),
|
|
189
|
+
};
|
|
190
|
+
// Member is available → status stays in_stock → no update needed
|
|
191
|
+
const memberSnap = {
|
|
192
|
+
docs: [{ data: () => ({ isSold: false, availableQuantity: 5, status: PUBLISHED }) }],
|
|
193
|
+
size: 1,
|
|
194
|
+
};
|
|
195
|
+
const ctx = makeCtx();
|
|
196
|
+
let callIdx = 0;
|
|
197
|
+
ctx.db.collection = vi.fn().mockReturnValue({
|
|
198
|
+
where: vi.fn().mockReturnThis(),
|
|
199
|
+
get: vi.fn().mockImplementation(async () => {
|
|
200
|
+
callIdx++;
|
|
201
|
+
if (callIdx === 1)
|
|
202
|
+
return { docs: [bundleDoc], size: 1 };
|
|
203
|
+
return memberSnap;
|
|
204
|
+
}),
|
|
205
|
+
});
|
|
206
|
+
const updated = await syncBundlesForProduct("product-x", ctx);
|
|
207
|
+
expect(updated).toBe(0);
|
|
208
|
+
expect(bundleRef.update).not.toHaveBeenCalled();
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
const { mockGetRatingAggregate, mockGetRatingAggregateByStore, mockProductUpdate, mockUpdateReviewStats, } = vi.hoisted(() => ({
|
|
3
|
+
mockGetRatingAggregate: vi.fn(),
|
|
4
|
+
mockGetRatingAggregateByStore: vi.fn(),
|
|
5
|
+
mockProductUpdate: vi.fn().mockResolvedValue(undefined),
|
|
6
|
+
mockUpdateReviewStats: vi.fn().mockResolvedValue(undefined),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("../../../../../repositories", () => ({
|
|
9
|
+
reviewRepository: {
|
|
10
|
+
getApprovedRatingAggregate: mockGetRatingAggregate,
|
|
11
|
+
getApprovedRatingAggregateByStore: mockGetRatingAggregateByStore,
|
|
12
|
+
},
|
|
13
|
+
storeRepository: {
|
|
14
|
+
updateReviewStats: mockUpdateReviewStats,
|
|
15
|
+
},
|
|
16
|
+
}));
|
|
17
|
+
vi.mock("../../../../../features/reviews/schemas/firestore", () => ({
|
|
18
|
+
ReviewStatusValues: {
|
|
19
|
+
APPROVED: "approved",
|
|
20
|
+
PENDING: "pending",
|
|
21
|
+
REJECTED: "rejected",
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
vi.mock("../../../../../features/products/schemas/firestore", () => ({
|
|
25
|
+
PRODUCT_COLLECTION: "products",
|
|
26
|
+
}));
|
|
27
|
+
import { handleReviewWrite } from "../onReviewWrite";
|
|
28
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
29
|
+
function makeCtx() {
|
|
30
|
+
return {
|
|
31
|
+
db: {
|
|
32
|
+
collection: vi.fn().mockReturnValue({
|
|
33
|
+
doc: vi.fn().mockReturnValue({
|
|
34
|
+
update: mockProductUpdate,
|
|
35
|
+
}),
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function makeReviewData(overrides = {}) {
|
|
42
|
+
return {
|
|
43
|
+
productId: "product-charizard",
|
|
44
|
+
storeId: "store-A",
|
|
45
|
+
buyerId: "user-ravi",
|
|
46
|
+
rating: 5,
|
|
47
|
+
status: "approved",
|
|
48
|
+
...overrides,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
beforeEach(() => {
|
|
52
|
+
vi.clearAllMocks();
|
|
53
|
+
mockProductUpdate.mockResolvedValue(undefined);
|
|
54
|
+
mockUpdateReviewStats.mockResolvedValue(undefined);
|
|
55
|
+
mockGetRatingAggregate.mockResolvedValue({ count: 10, avgRating: 4.5 });
|
|
56
|
+
mockGetRatingAggregateByStore.mockResolvedValue({ count: 5, avgRating: 4.2 });
|
|
57
|
+
});
|
|
58
|
+
// ── Tests ────────────────────────────────────────────────────────────────────
|
|
59
|
+
describe("handleReviewWrite — early exits", () => {
|
|
60
|
+
it("skips if neither before nor after is approved", async () => {
|
|
61
|
+
const ctx = makeCtx();
|
|
62
|
+
await handleReviewWrite({
|
|
63
|
+
reviewId: "r1",
|
|
64
|
+
before: makeReviewData({ status: "pending" }),
|
|
65
|
+
after: makeReviewData({ status: "rejected" }),
|
|
66
|
+
}, ctx);
|
|
67
|
+
expect(mockGetRatingAggregate).not.toHaveBeenCalled();
|
|
68
|
+
});
|
|
69
|
+
it("skips if both before and after are non-approved status", async () => {
|
|
70
|
+
const ctx = makeCtx();
|
|
71
|
+
await handleReviewWrite({
|
|
72
|
+
reviewId: "r1",
|
|
73
|
+
before: makeReviewData({ status: "rejected" }),
|
|
74
|
+
after: makeReviewData({ status: "pending" }),
|
|
75
|
+
}, ctx);
|
|
76
|
+
expect(mockProductUpdate).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
it("skips if no data (both before and after are null)", async () => {
|
|
79
|
+
const ctx = makeCtx();
|
|
80
|
+
await handleReviewWrite({ reviewId: "r1", before: null, after: null }, ctx);
|
|
81
|
+
expect(mockGetRatingAggregate).not.toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
it("logs and skips if review has no productId", async () => {
|
|
84
|
+
const ctx = makeCtx();
|
|
85
|
+
await handleReviewWrite({
|
|
86
|
+
reviewId: "r1",
|
|
87
|
+
before: null,
|
|
88
|
+
after: makeReviewData({ status: "approved", productId: undefined }),
|
|
89
|
+
}, ctx);
|
|
90
|
+
expect(ctx.logger.error).toHaveBeenCalledWith(expect.stringMatching(/no productId/i), null, expect.any(Object));
|
|
91
|
+
expect(mockGetRatingAggregate).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
describe("handleReviewWrite — review approved (new approval)", () => {
|
|
95
|
+
it("recalculates rating aggregate for the product", async () => {
|
|
96
|
+
const ctx = makeCtx();
|
|
97
|
+
await handleReviewWrite({
|
|
98
|
+
reviewId: "r1",
|
|
99
|
+
before: makeReviewData({ status: "pending" }),
|
|
100
|
+
after: makeReviewData({ status: "approved" }),
|
|
101
|
+
}, ctx);
|
|
102
|
+
expect(mockGetRatingAggregate).toHaveBeenCalledWith("product-charizard");
|
|
103
|
+
});
|
|
104
|
+
it("updates product avgRating and reviewCount", async () => {
|
|
105
|
+
mockGetRatingAggregate.mockResolvedValue({ count: 7, avgRating: 4.2 });
|
|
106
|
+
const ctx = makeCtx();
|
|
107
|
+
await handleReviewWrite({
|
|
108
|
+
reviewId: "r1",
|
|
109
|
+
before: makeReviewData({ status: "pending" }),
|
|
110
|
+
after: makeReviewData({ status: "approved" }),
|
|
111
|
+
}, ctx);
|
|
112
|
+
expect(mockProductUpdate).toHaveBeenCalledWith(expect.objectContaining({ avgRating: 4.2, reviewCount: 7 }));
|
|
113
|
+
});
|
|
114
|
+
it("updates store review stats when storeId is present", async () => {
|
|
115
|
+
const ctx = makeCtx();
|
|
116
|
+
await handleReviewWrite({
|
|
117
|
+
reviewId: "r1",
|
|
118
|
+
before: makeReviewData({ status: "pending" }),
|
|
119
|
+
after: makeReviewData({ status: "approved", storeId: "store-A" }),
|
|
120
|
+
}, ctx);
|
|
121
|
+
expect(mockGetRatingAggregateByStore).toHaveBeenCalledWith("store-A");
|
|
122
|
+
expect(mockUpdateReviewStats).toHaveBeenCalledWith("store-A", 5, 4.2);
|
|
123
|
+
});
|
|
124
|
+
it("skips store stats update when storeId is absent", async () => {
|
|
125
|
+
const ctx = makeCtx();
|
|
126
|
+
await handleReviewWrite({
|
|
127
|
+
reviewId: "r1",
|
|
128
|
+
before: makeReviewData({ status: "pending" }),
|
|
129
|
+
after: makeReviewData({ status: "approved", storeId: undefined }),
|
|
130
|
+
}, ctx);
|
|
131
|
+
expect(mockGetRatingAggregateByStore).not.toHaveBeenCalled();
|
|
132
|
+
expect(mockUpdateReviewStats).not.toHaveBeenCalled();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe("handleReviewWrite — review rejected (was approved)", () => {
|
|
136
|
+
it("recalculates rating when a review transitions from approved to rejected", async () => {
|
|
137
|
+
const ctx = makeCtx();
|
|
138
|
+
await handleReviewWrite({
|
|
139
|
+
reviewId: "r1",
|
|
140
|
+
before: makeReviewData({ status: "approved" }),
|
|
141
|
+
after: makeReviewData({ status: "rejected" }),
|
|
142
|
+
}, ctx);
|
|
143
|
+
expect(mockGetRatingAggregate).toHaveBeenCalled();
|
|
144
|
+
expect(mockProductUpdate).toHaveBeenCalled();
|
|
145
|
+
});
|
|
146
|
+
it("uses afterData for productId when available", async () => {
|
|
147
|
+
const ctx = makeCtx();
|
|
148
|
+
await handleReviewWrite({
|
|
149
|
+
reviewId: "r1",
|
|
150
|
+
before: makeReviewData({ status: "approved", productId: "old-product" }),
|
|
151
|
+
after: makeReviewData({ status: "rejected", productId: "product-charizard" }),
|
|
152
|
+
}, ctx);
|
|
153
|
+
expect(mockGetRatingAggregate).toHaveBeenCalledWith("product-charizard");
|
|
154
|
+
});
|
|
155
|
+
it("falls back to beforeData for productId when after is null (delete)", async () => {
|
|
156
|
+
const ctx = makeCtx();
|
|
157
|
+
await handleReviewWrite({
|
|
158
|
+
reviewId: "r1",
|
|
159
|
+
before: makeReviewData({ status: "approved", productId: "product-abc" }),
|
|
160
|
+
after: null,
|
|
161
|
+
}, ctx);
|
|
162
|
+
expect(mockGetRatingAggregate).toHaveBeenCalledWith("product-abc");
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe("handleReviewWrite — error propagation", () => {
|
|
166
|
+
it("aggregate calculation failure → re-throws", async () => {
|
|
167
|
+
mockGetRatingAggregate.mockRejectedValue(new Error("Firestore error"));
|
|
168
|
+
const ctx = makeCtx();
|
|
169
|
+
await expect(handleReviewWrite({
|
|
170
|
+
reviewId: "r1",
|
|
171
|
+
before: null,
|
|
172
|
+
after: makeReviewData({ status: "approved" }),
|
|
173
|
+
}, ctx)).rejects.toThrow("Firestore error");
|
|
174
|
+
});
|
|
175
|
+
it("logs error before re-throwing", async () => {
|
|
176
|
+
mockGetRatingAggregate.mockRejectedValue(new Error("Firestore error"));
|
|
177
|
+
const ctx = makeCtx();
|
|
178
|
+
try {
|
|
179
|
+
await handleReviewWrite({
|
|
180
|
+
reviewId: "r1",
|
|
181
|
+
before: null,
|
|
182
|
+
after: makeReviewData({ status: "approved" }),
|
|
183
|
+
}, ctx);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// expected
|
|
187
|
+
}
|
|
188
|
+
expect(ctx.logger.error).toHaveBeenCalledWith(expect.stringMatching(/rating stats/i), expect.any(Error), expect.any(Object));
|
|
189
|
+
});
|
|
190
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|