@mohasinac/appkit 4.13.1 → 4.14.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/layout/DashboardLayoutClient.js +14 -5
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +3 -0
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +6 -0
- package/dist/_internal/client/features/lottery/LotteryDetailView.d.ts +8 -0
- package/dist/_internal/client/features/lottery/LotteryDetailView.js +4 -1
- package/dist/_internal/client/features/lottery/LotteryListView.d.ts +7 -0
- package/dist/_internal/client/features/lottery/LotteryListView.js +1 -1
- package/dist/_internal/client/features/lottery/PrizeDrawLotteryDetailView.js +4 -1
- package/dist/_internal/client/features/lottery/slot-collage-items.d.ts +15 -0
- package/dist/_internal/client/features/lottery/slot-collage-items.js +13 -0
- package/dist/_internal/server/features/auctions/actions.js +6 -0
- package/dist/_internal/server/features/bundles/data.d.ts +16 -0
- package/dist/_internal/server/features/bundles/data.js +26 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.js +6 -0
- package/dist/_internal/server/features/checkout/locked-lines.d.ts +38 -0
- package/dist/_internal/server/features/checkout/locked-lines.js +171 -0
- package/dist/_internal/server/features/homepage/index.d.ts +0 -1
- package/dist/_internal/server/features/homepage/index.js +7 -1
- package/dist/_internal/server/features/lottery/adapters.js +4 -0
- package/dist/_internal/server/features/products/data.js +15 -37
- package/dist/_internal/server/features/products/index.d.ts +1 -1
- package/dist/_internal/server/features/products/index.js +1 -1
- package/dist/_internal/server/features/products/list-public.d.ts +46 -20
- package/dist/_internal/server/features/products/list-public.js +266 -90
- package/dist/_internal/server/features/products/listing-tab-counts.d.ts +44 -0
- package/dist/_internal/server/features/products/listing-tab-counts.js +97 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +51 -0
- package/dist/_internal/server/features/site-settings/adapters.js +75 -0
- package/dist/_internal/server/features/site-settings/index.d.ts +1 -0
- package/dist/_internal/server/features/site-settings/index.js +1 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +56 -0
- package/dist/_internal/server/features/stores/adapters.js +91 -0
- package/dist/_internal/server/features/stores/index.d.ts +1 -0
- package/dist/_internal/server/features/stores/index.js +1 -0
- package/dist/_internal/server/jobs/core/auctionSettlement.js +56 -2
- package/dist/_internal/server/jobs/core/offerExpiry.js +45 -9
- package/dist/_internal/server/jobs/core/paymentReviewAutoApprove.d.ts +5 -0
- package/dist/_internal/server/jobs/core/paymentReviewAutoApprove.js +33 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +7 -0
- package/dist/_internal/server/jobs/handlers/messages.js +10 -0
- package/dist/_internal/shared/actions/action-registry.js +15 -1
- package/dist/_internal/shared/checkout/lanes.d.ts +25 -0
- package/dist/_internal/shared/checkout/lanes.js +26 -0
- package/dist/_internal/shared/features/auctions/config.d.ts +27 -0
- package/dist/_internal/shared/features/auctions/config.js +27 -0
- package/dist/_internal/shared/features/blog/schema.d.ts +2 -2
- package/dist/_internal/shared/features/cart/schema.d.ts +12 -12
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +32 -32
- package/dist/_internal/shared/features/emi/schedule.d.ts +24 -3
- package/dist/_internal/shared/features/emi/schedule.js +20 -5
- package/dist/_internal/shared/features/events/schema.d.ts +2 -2
- package/dist/_internal/shared/features/homepage/section-gate.d.ts +14 -0
- package/dist/_internal/shared/features/homepage/section-gate.js +51 -0
- package/dist/_internal/shared/features/offers/config.d.ts +30 -0
- package/dist/_internal/shared/features/offers/config.js +59 -0
- package/dist/_internal/shared/features/orders/schema.d.ts +8 -8
- package/dist/_internal/shared/features/products/schema.d.ts +32 -32
- package/dist/_internal/shared/features/products/to-product-item.js +6 -0
- package/dist/_internal/shared/features/reviews/schema.d.ts +2 -2
- package/dist/_internal/shared/features/site-settings/fees.d.ts +60 -0
- package/dist/_internal/shared/features/site-settings/fees.js +39 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +40 -0
- package/dist/_internal/shared/features/site-settings/types.js +1 -0
- package/dist/_internal/shared/history/history.d.ts +66 -0
- package/dist/_internal/shared/history/history.js +158 -0
- package/dist/_internal/shared/history/index.d.ts +4 -0
- package/dist/_internal/shared/history/index.js +2 -0
- package/dist/_internal/shared/history/types.d.ts +61 -0
- package/dist/_internal/shared/history/types.js +6 -0
- package/dist/_internal/shared/listing-types/_registry.d.ts +54 -1
- package/dist/_internal/shared/listing-types/_registry.js +67 -0
- package/dist/_internal/shared/listing-types/art/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/art/config.js +5 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +13 -0
- package/dist/_internal/shared/listing-types/auction/config.js +21 -0
- package/dist/_internal/shared/listing-types/availability.d.ts +79 -0
- package/dist/_internal/shared/listing-types/availability.js +104 -0
- package/dist/_internal/shared/listing-types/capabilities.d.ts +15 -0
- package/dist/_internal/shared/listing-types/capabilities.js +12 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/classified/config.js +5 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +11 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +22 -2
- package/dist/_internal/shared/listing-types/live/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/live/config.js +5 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +19 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +11 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +15 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/standard/config.js +5 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +3 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +5 -0
- package/dist/client.d.ts +7 -5
- package/dist/client.js +2 -2
- package/dist/constants/api-endpoints.d.ts +6 -0
- package/dist/constants/api-endpoints.js +2 -0
- package/dist/constants/field-names.d.ts +49 -2
- package/dist/constants/field-names.js +53 -2
- package/dist/constants/table-keys.d.ts +17 -0
- package/dist/constants/table-keys.js +17 -0
- package/dist/errors/error-codes.d.ts +13 -1
- package/dist/errors/error-codes.js +13 -1
- package/dist/errors/error-display-map.js +12 -3
- package/dist/errors/messages.d.ts +10 -2
- package/dist/errors/messages.js +10 -2
- package/dist/features/about/components/AboutView.d.ts +3 -0
- package/dist/features/about/components/AboutView.js +3 -2
- package/dist/features/about/components/PolicyPageView.d.ts +2 -1
- package/dist/features/about/components/PolicyPageView.js +61 -23
- package/dist/features/about/components/PublicProfileView.js +5 -0
- package/dist/features/about/schemas/firestore.d.ts +3 -0
- package/dist/features/account/components/UserOrdersView.js +7 -0
- package/dist/features/account/components/index.d.ts +0 -2
- package/dist/features/account/components/index.js +0 -1
- package/dist/features/account/schemas/index.d.ts +2 -2
- package/dist/features/account/types/index.d.ts +21 -2
- package/dist/features/addresses/schemas/index.d.ts +4 -4
- package/dist/features/admin/components/AdminArtView.js +8 -3
- package/dist/features/admin/components/AdminClassifiedView.js +8 -3
- package/dist/features/admin/components/AdminDigitalCodesView.js +8 -3
- package/dist/features/admin/components/AdminFeatureFlagsView.js +1 -0
- package/dist/features/admin/components/AdminLiveView.js +8 -3
- package/dist/features/admin/components/AdminOffersView.d.ts +4 -0
- package/dist/features/admin/components/AdminOffersView.js +186 -0
- package/dist/features/admin/components/AdminOrdersView.js +10 -1
- package/dist/features/admin/components/AdminPrizeDrawsView.js +5 -0
- package/dist/features/admin/components/AdminProductsView.js +16 -29
- package/dist/features/admin/components/AdminSiteSettingsView.js +31 -13
- package/dist/features/admin/components/AdminSiteView.js +6 -2
- package/dist/features/admin/components/AdminStickersView.js +8 -3
- package/dist/features/admin/components/index.d.ts +2 -0
- package/dist/features/admin/components/index.js +1 -0
- package/dist/features/admin/constants/filter-tabs.d.ts +86 -0
- package/dist/features/admin/constants/filter-tabs.js +55 -0
- package/dist/features/admin/schemas/firestore.d.ts +122 -3
- package/dist/features/admin/schemas/firestore.js +1 -0
- package/dist/features/auctions/actions/bid-actions.d.ts +20 -5
- package/dist/features/auctions/actions/bid-actions.js +93 -20
- package/dist/features/auctions/components/AuctionDetailPageView.js +11 -6
- package/dist/features/auctions/components/AuctionsListView.js +5 -6
- package/dist/features/auctions/components/MarketplaceAuctionCard.d.ts +7 -0
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +19 -2
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +8 -4
- package/dist/features/auctions/repository/bid.repository.d.ts +13 -0
- package/dist/features/auctions/repository/bid.repository.js +23 -0
- package/dist/features/auctions/schemas/firestore.d.ts +17 -0
- package/dist/features/auctions/schemas/index.d.ts +12 -12
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +10 -1
- package/dist/features/blog/actions/blog-actions.d.ts +7 -7
- package/dist/features/blog/schemas/index.d.ts +6 -6
- package/dist/features/cart/repository/cart.repository.js +1 -0
- package/dist/features/cart/schemas/firestore.d.ts +15 -0
- package/dist/features/catalogue/schemas/validation.d.ts +4 -4
- package/dist/features/categories/components/BrandDetailPageView.js +23 -40
- package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -7
- package/dist/features/categories/components/BrandDetailTabs.js +8 -12
- package/dist/features/categories/components/BundleCollage.js +1 -1
- package/dist/features/categories/components/CategoryDetailPageView.js +53 -57
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +10 -8
- package/dist/features/categories/components/CategoryDetailTabs.js +11 -16
- package/dist/features/categories/components/CategoryProductsListing.js +11 -1
- package/dist/features/categories/components/ConcernCard.js +7 -1
- package/dist/features/categories/schemas/firestore.d.ts +13 -0
- package/dist/features/categories/schemas/index.d.ts +26 -26
- package/dist/features/classified/components/ClassifiedDetailPageView.d.ts +16 -1
- package/dist/features/classified/components/ClassifiedDetailPageView.js +23 -3
- package/dist/features/events/components/AdminEventsView.js +15 -11
- package/dist/features/events/components/EventFilters.d.ts +9 -1
- package/dist/features/events/components/EventFilters.js +45 -21
- package/dist/features/events/components/EventsIndexListing.d.ts +6 -1
- package/dist/features/events/components/EventsIndexListing.js +2 -2
- package/dist/features/events/components/EventsListPageView.js +13 -4
- package/dist/features/events/repository/events.repository.d.ts +17 -0
- package/dist/features/events/repository/events.repository.js +27 -0
- package/dist/features/events/schemas/firestore.d.ts +12 -0
- package/dist/features/events/schemas/firestore.js +12 -0
- package/dist/features/events/schemas/index.d.ts +50 -50
- package/dist/features/events/types/index.d.ts +11 -0
- package/dist/features/events/types/index.js +57 -1
- package/dist/features/faq/actions/faq-actions.d.ts +12 -12
- package/dist/features/faq/schemas/index.d.ts +8 -8
- package/dist/features/filters/FilterFacetSection.d.ts +7 -1
- package/dist/features/filters/FilterFacetSection.js +11 -3
- package/dist/features/grouped/schemas/index.d.ts +4 -4
- package/dist/features/homepage/components/AnnouncementBar.d.ts +7 -1
- package/dist/features/homepage/components/AnnouncementBar.js +3 -3
- package/dist/features/homepage/components/MarketplaceHomepageView.js +20 -6
- package/dist/features/homepage/hooks/homepage-query.d.ts +16 -0
- package/dist/features/homepage/hooks/homepage-query.js +45 -0
- package/dist/features/homepage/hooks/useFeaturedAuctions.d.ts +10 -0
- package/dist/features/homepage/hooks/useFeaturedAuctions.js +28 -8
- package/dist/features/homepage/hooks/useFeaturedPreOrders.js +17 -8
- package/dist/features/homepage/hooks/useFeaturedProducts.js +15 -8
- package/dist/features/homepage/schemas/index.d.ts +2 -2
- package/dist/features/layout/AppLayoutShell.d.ts +11 -1
- package/dist/features/layout/AppLayoutShell.js +15 -14
- package/dist/features/layout/BackToTop.d.ts +9 -0
- package/dist/features/layout/BackToTop.js +10 -1
- package/dist/features/layout/BottomActions.js +28 -25
- package/dist/features/layout/BottomChrome.d.ts +34 -0
- package/dist/features/layout/BottomChrome.js +55 -0
- package/dist/features/layout/BottomNavLayout.d.ts +6 -0
- package/dist/features/layout/BottomNavLayout.js +7 -1
- package/dist/features/layout/BottomNavbar.js +15 -2
- package/dist/features/layout/FooterLayout.js +11 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.d.ts +7 -0
- package/dist/features/layout/TitleBarLayout.js +28 -5
- package/dist/features/layout/index.d.ts +2 -0
- package/dist/features/layout/index.js +1 -0
- package/dist/features/lottery/types/index.d.ts +3 -0
- package/dist/features/media/MediaPickerModal.js +5 -1
- package/dist/features/messages/schemas/index.d.ts +2 -2
- package/dist/features/orders/components/OrderScopeTabs.d.ts +20 -0
- package/dist/features/orders/components/OrderScopeTabs.js +31 -0
- package/dist/features/orders/constants/order-scope.d.ts +48 -0
- package/dist/features/orders/constants/order-scope.js +111 -0
- package/dist/features/orders/repository/orders.repository.d.ts +54 -7
- package/dist/features/orders/repository/orders.repository.js +116 -22
- package/dist/features/orders/schemas/firestore.d.ts +117 -0
- package/dist/features/orders/schemas/firestore.js +30 -0
- package/dist/features/orders/schemas/index.d.ts +18 -18
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +14 -9
- package/dist/features/pre-orders/components/PreOrdersListView.js +6 -5
- package/dist/features/pre-orders/schemas/index.d.ts +1 -1
- package/dist/features/products/actions/product-actions.d.ts +14 -0
- package/dist/features/products/actions/product-actions.js +49 -11
- package/dist/features/products/api/route.d.ts +12 -0
- package/dist/features/products/api/route.js +42 -100
- package/dist/features/products/components/ArtStickersListView.js +2 -2
- package/dist/features/products/components/AuctionsIndexListing.js +13 -11
- package/dist/features/products/components/AvailabilityTabs.d.ts +11 -0
- package/dist/features/products/components/AvailabilityTabs.js +47 -0
- package/dist/features/products/components/MakeOfferButton.js +48 -49
- package/dist/features/products/components/PrizeDrawCollage.d.ts +20 -4
- package/dist/features/products/components/PrizeDrawCollage.js +9 -1
- package/dist/features/products/components/PrizeDrawsIndexListing.js +14 -12
- package/dist/features/products/components/PrizeDrawsListingView.js +2 -2
- package/dist/features/products/components/PrizeDrawsSection.d.ts +7 -1
- package/dist/features/products/components/PrizeDrawsSection.js +23 -18
- package/dist/features/products/components/ProductDetailPageView.d.ts +4 -1
- package/dist/features/products/components/ProductDetailPageView.js +18 -4
- package/dist/features/products/components/ProductForm.js +5 -2
- package/dist/features/products/components/ProductGrid.js +6 -1
- package/dist/features/products/components/ProductsIndexListing.js +22 -32
- package/dist/features/products/components/ProductsIndexPageView.js +6 -9
- package/dist/features/products/components/index.d.ts +2 -2
- package/dist/features/products/components/index.js +1 -1
- package/dist/features/products/config/listing-type-listing-config.d.ts +20 -0
- package/dist/features/products/config/listing-type-listing-config.js +22 -10
- package/dist/features/products/constants/action-defs.d.ts +2 -0
- package/dist/features/products/constants/action-defs.js +6 -0
- package/dist/features/products/hooks/useAvailabilityScope.d.ts +13 -0
- package/dist/features/products/hooks/useAvailabilityScope.js +19 -0
- package/dist/features/products/hooks/useProducts.d.ts +5 -0
- package/dist/features/products/hooks/useProducts.js +13 -2
- package/dist/features/products/repository/products.repository.d.ts +31 -2
- package/dist/features/products/repository/products.repository.js +53 -21
- package/dist/features/products/schemas/firestore.d.ts +17 -3
- package/dist/features/products/schemas/firestore.js +7 -2
- package/dist/features/products/schemas/index.d.ts +66 -66
- package/dist/features/products/schemas/product-features.validators.d.ts +6 -6
- package/dist/features/products/types/index.d.ts +16 -1
- package/dist/features/promotions/schemas/index.d.ts +2 -2
- package/dist/features/reviews/schemas/index.d.ts +16 -16
- package/dist/features/search/schemas/index.d.ts +4 -4
- package/dist/features/seller/actions/offer-actions.js +25 -2
- package/dist/features/seller/components/SellerArtView.js +5 -0
- package/dist/features/seller/components/SellerAuctionsView.js +13 -15
- package/dist/features/seller/components/SellerClassifiedView.js +5 -0
- package/dist/features/seller/components/SellerDigitalCodesView.js +5 -0
- package/dist/features/seller/components/SellerLiveView.js +5 -0
- package/dist/features/seller/components/SellerOrdersView.js +31 -9
- package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -18
- package/dist/features/seller/components/SellerPreOrdersView.js +5 -0
- package/dist/features/seller/components/SellerPrizeDrawsView.js +14 -16
- package/dist/features/seller/components/SellerProductShell.js +7 -1
- package/dist/features/seller/components/SellerProductsView.js +11 -8
- package/dist/features/seller/components/SellerStickersView.js +5 -0
- package/dist/features/seller/components/index.d.ts +0 -2
- package/dist/features/seller/components/index.js +0 -1
- package/dist/features/seller/repository/offer.repository.d.ts +73 -16
- package/dist/features/seller/repository/offer.repository.js +38 -3
- package/dist/features/seller/schemas/index.d.ts +2 -2
- package/dist/features/seller/schemas/offer-forms.d.ts +25 -0
- package/dist/features/seller/schemas/offer-forms.js +21 -0
- package/dist/features/seller/server.d.ts +0 -1
- package/dist/features/seller/server.js +0 -1
- package/dist/features/shell/SectionForm.d.ts +99 -0
- package/dist/features/shell/SectionForm.js +128 -0
- package/dist/features/shell/entity-form.d.ts +109 -0
- package/dist/features/shell/entity-form.js +96 -0
- package/dist/features/shell/index.d.ts +4 -0
- package/dist/features/shell/index.js +2 -0
- package/dist/features/stores/api/[storeSlug]/route.js +2 -25
- package/dist/features/stores/components/StoreAuctionsListing.js +15 -1
- package/dist/features/stores/components/StoreClassifiedsListing.js +12 -2
- package/dist/features/stores/components/StoreDetailLayoutView.js +24 -71
- package/dist/features/stores/components/StoreDigitalCodesListing.js +12 -2
- package/dist/features/stores/components/StoreLiveItemsListing.js +12 -2
- package/dist/features/stores/components/StorePreOrdersListing.js +12 -1
- package/dist/features/stores/components/StoreProductsListing.js +11 -1
- package/dist/features/stores/repository/store.repository.js +6 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/schemas/index.d.ts +4 -4
- package/dist/features/tester/actions/checklist-item-actions.d.ts +4 -4
- package/dist/features/tester/seed-data/offers-tester-seed-data.js +119 -3
- package/dist/features/tester/seed-data/products-tester-seed-data.js +131 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +105 -1
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +13 -10
- package/dist/index.js +14 -9
- package/dist/next/routing/route-map.d.ts +7 -1
- package/dist/next/routing/route-map.js +5 -0
- package/dist/providers/db-firebase/base.repository.d.ts +30 -0
- package/dist/providers/db-firebase/base.repository.js +50 -1
- package/dist/providers/db-firebase/index.d.ts +1 -1
- package/dist/providers/db-firebase/index.js +1 -1
- package/dist/providers/db-firebase/sieve.d.ts +25 -0
- package/dist/providers/db-firebase/sieve.js +52 -18
- package/dist/react/hooks/useUrlTable.js +10 -1
- package/dist/schemas/registry.d.ts +68 -68
- package/dist/seed/_helpers/category-forest.d.ts +2 -0
- package/dist/seed/_helpers/category-forest.js +309 -0
- package/dist/seed/_helpers/category-tree.d.ts +27 -0
- package/dist/seed/_helpers/category-tree.js +103 -0
- package/dist/seed/catalogue-seed-data.js +12 -12
- package/dist/seed/categories-seed-data.js +177 -162
- package/dist/seed/events-seed-data.js +61 -10
- package/dist/seed/homepage-sections-seed-data.js +5 -1
- package/dist/seed/offers-seed-data.js +32 -27
- package/dist/seed/products-art-seed-data.js +50 -12
- package/dist/seed/products-auctions-seed-data.js +97 -18
- package/dist/seed/products-classifieds-seed-data.js +80 -16
- package/dist/seed/products-digital-codes-seed-data.js +66 -18
- package/dist/seed/products-live-items-seed-data.js +64 -13
- package/dist/seed/products-preorders-seed-data.d.ts +0 -1
- package/dist/seed/products-preorders-seed-data.js +52 -13
- package/dist/seed/products-prize-draws-seed-data.js +63 -14
- package/dist/seed/products-standard-seed-data.js +99 -26
- package/dist/seed/products-stickers-seed-data.js +47 -12
- package/dist/seed/reviews-seed-data.js +6 -1
- package/dist/seed/site-settings-seed-data.d.ts +1 -38
- package/dist/seed/site-settings-seed-data.js +38 -76
- package/dist/seed/stores-seed-data.js +71 -1
- package/dist/seed/users-seed-data.js +15 -5
- package/dist/server-entry.d.ts +1 -2
- package/dist/server-entry.js +4 -3
- package/dist/server.d.ts +4 -3
- package/dist/server.js +8 -4
- package/dist/styles.css +78 -12
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.style.css +19 -4
- package/dist/ui/components/CollapsibleSection.d.ts +17 -1
- package/dist/ui/components/CollapsibleSection.js +6 -2
- package/dist/ui/components/FilterChipGroup.d.ts +25 -1
- package/dist/ui/components/FilterChipGroup.js +21 -4
- package/dist/ui/components/HandMode.style.css +47 -0
- package/dist/ui/components/Layout.d.ts +9 -2
- package/dist/ui/components/Layout.js +1 -1
- package/dist/ui/components/ListingLayout.js +8 -8
- package/dist/ui/components/ListingLayout.style.css +8 -7
- package/dist/ui/components/StickyToolbar.d.ts +3 -1
- package/dist/ui/components/StickyToolbar.js +62 -20
- package/dist/ui/components/bottom-chrome-slot.d.ts +21 -0
- package/dist/ui/components/bottom-chrome-slot.js +29 -0
- package/dist/ui/components/index.style.css +3 -0
- package/dist/ui/forms/FormShell.d.ts +43 -13
- package/dist/ui/forms/FormShell.js +22 -11
- package/dist/ui/forms/index.d.ts +1 -1
- package/dist/ui/index.d.ts +1 -1
- package/package.json +1 -1
|
@@ -34,9 +34,9 @@ import { filterNavItems } from "./filterNavItems";
|
|
|
34
34
|
import { useSiteSettings } from "../../../../core/hooks/useSiteSettings";
|
|
35
35
|
import { useTheme } from "../../theme";
|
|
36
36
|
import { useHandMode } from "../../hand-mode";
|
|
37
|
-
import { BackgroundRenderer, Div, Nav, Span } from "../../../../ui";
|
|
37
|
+
import { BackgroundRenderer, Div, Li, Nav, Span } from "../../../../ui";
|
|
38
|
+
import { BottomNavLayout } from "../../../../features/layout/BottomNavLayout";
|
|
38
39
|
import { useToast } from "../../../../ui/components/Toast";
|
|
39
|
-
import { useVisualViewportInset } from "../../../../react/hooks/useVisualViewportInset";
|
|
40
40
|
import { useSession } from "../../../../react/contexts/SessionContext";
|
|
41
41
|
import { ROUTES } from "../../../../next/routing/route-map";
|
|
42
42
|
/**
|
|
@@ -140,14 +140,23 @@ const MAX_BOTTOM_NAV_ITEMS = 5;
|
|
|
140
140
|
* behind a hamburger toggle). Pattern lifted from the unused
|
|
141
141
|
* `DashboardScaffold` scaffold, which has since been retired in favour of
|
|
142
142
|
* wiring it directly here where the sidebars already live.
|
|
143
|
+
*
|
|
144
|
+
* Renders through the shared `<BottomNavLayout>` rather than hand-rolling its
|
|
145
|
+
* own `fixed bottom-0` chrome. That is not cosmetic: `BottomNavLayout` is what
|
|
146
|
+
* publishes `--bottom-nav-height`, which every bottom-anchored surface above it
|
|
147
|
+
* (the CTA tier, the pagination bar, BackToTop, the footer's clearance) offsets
|
|
148
|
+
* against. The hand-rolled version published nothing, so on dashboard routes
|
|
149
|
+
* that whole stack was positioned against the PUBLIC nav's height instead —
|
|
150
|
+
* which was itself a bug, since both bars were rendering on the same pixels.
|
|
151
|
+
* `AppLayoutShell` is now told to suppress the public one here via
|
|
152
|
+
* `showBottomNav={!isDashboard}`; exactly one nav must be mounted at a time.
|
|
143
153
|
*/
|
|
144
154
|
function DashboardBottomNav({ items, activeHref }) {
|
|
145
|
-
const isKeyboardOpen = useVisualViewportInset();
|
|
146
155
|
if (items.length === 0)
|
|
147
156
|
return null;
|
|
148
|
-
return (_jsx(
|
|
157
|
+
return (_jsx(BottomNavLayout, { id: "dashboard-bottom-navbar", ariaLabel: "Dashboard bottom navigation", children: items.slice(0, MAX_BOTTOM_NAV_ITEMS).map((item) => {
|
|
149
158
|
const isActive = activeHref === item.href || activeHref.startsWith(`${item.href}/`);
|
|
150
|
-
return (_jsxs(Link, { href: item.href, className: `flex
|
|
159
|
+
return (_jsx(Li, { className: "flex-1", children: _jsxs(Link, { href: item.href, className: `flex h-full flex-col items-center justify-center gap-[var(--appkit-space-0-5)] py-[var(--appkit-space-2)] ${isActive ? "text-[var(--appkit-color-primary)]" : "text-[var(--appkit-color-text-muted)]"}`, "aria-current": isActive ? "page" : undefined, children: [item.icon && _jsx(Span, { size: "base", children: item.icon }), _jsx(Span, { size: "xs", className: "max-w-full truncate", children: item.label })] }) }, item.href));
|
|
151
160
|
}) }));
|
|
152
161
|
}
|
|
153
162
|
// Asymmetric padding clears the persistent sidebar rail — right-heavy by
|
|
@@ -3,6 +3,8 @@ interface LotterySlotRow {
|
|
|
3
3
|
slotNumber: number;
|
|
4
4
|
name: string;
|
|
5
5
|
price: number;
|
|
6
|
+
/** Prize photo — rendered publicly in the prize collage on the detail page. */
|
|
7
|
+
image?: string;
|
|
6
8
|
}
|
|
7
9
|
interface LotteryEventFormData {
|
|
8
10
|
title: string;
|
|
@@ -12,6 +14,7 @@ interface LotteryEventFormData {
|
|
|
12
14
|
slots: Array<{
|
|
13
15
|
slotNumber: number;
|
|
14
16
|
name: string;
|
|
17
|
+
image?: string;
|
|
15
18
|
price: number;
|
|
16
19
|
weight: number;
|
|
17
20
|
isBooked: boolean;
|
|
@@ -37,6 +37,7 @@ export function LotteryAdminEditView({ eventId, initialData, onSubmit, }) {
|
|
|
37
37
|
slots: slots.map((s) => ({
|
|
38
38
|
slotNumber: s.slotNumber,
|
|
39
39
|
name: s.name,
|
|
40
|
+
image: s.image?.trim() || undefined,
|
|
40
41
|
price: s.price,
|
|
41
42
|
weight: 0,
|
|
42
43
|
isBooked: false,
|
|
@@ -68,6 +69,11 @@ export function LotteryAdminEditView({ eventId, initialData, onSubmit, }) {
|
|
|
68
69
|
header: "Prize Name",
|
|
69
70
|
render: (s) => (_jsx(Input, { type: "text", className: "w-full", placeholder: `Prize for slot ${s.slotNumber}`, value: s.name, onChange: (e) => updateSlot(s._idx, "name", e.target.value), "aria-label": `Slot ${s.slotNumber} prize name` })),
|
|
70
71
|
},
|
|
72
|
+
{
|
|
73
|
+
key: "image",
|
|
74
|
+
header: "Image",
|
|
75
|
+
render: (s) => (_jsx(Input, { type: "text", className: "w-full", placeholder: "/media/\u2026 or https://\u2026", value: s.image ?? "", onChange: (e) => updateSlot(s._idx, "image", e.target.value), "aria-label": `Slot ${s.slotNumber} prize image URL` })),
|
|
76
|
+
},
|
|
71
77
|
...(pricingMode === "variable"
|
|
72
78
|
? [
|
|
73
79
|
{
|
|
@@ -6,7 +6,15 @@ interface ClientLotteryEvent {
|
|
|
6
6
|
description?: string;
|
|
7
7
|
status: string;
|
|
8
8
|
endsAt?: string | Date | null;
|
|
9
|
+
/**
|
|
10
|
+
* Two spellings exist on EventDocument and only `coverImage.url` is ever
|
|
11
|
+
* populated — by seed data or the admin editor. Reading `coverImageUrl`
|
|
12
|
+
* alone is why this page showed the 🎰 placeholder for every lottery.
|
|
13
|
+
*/
|
|
9
14
|
coverImageUrl?: string;
|
|
15
|
+
coverImage?: {
|
|
16
|
+
url?: string;
|
|
17
|
+
} | null;
|
|
10
18
|
lotteryConfig?: ClientLotteryConfig;
|
|
11
19
|
}
|
|
12
20
|
interface LotteryDetailViewProps {
|
|
@@ -6,6 +6,8 @@ import { MediaImage } from "../../../../features/media/MediaImage";
|
|
|
6
6
|
import { ROUTES } from "../../../../next/routing/route-map";
|
|
7
7
|
import { LotterySlotGrid } from "./LotterySlotGrid";
|
|
8
8
|
import { LotteryPullForm } from "./LotteryPullForm";
|
|
9
|
+
import { PrizeDrawCollage } from "../../../../features/products/components/PrizeDrawCollage";
|
|
10
|
+
import { slotsToCollageItems } from "./slot-collage-items";
|
|
9
11
|
/**
|
|
10
12
|
* Full lottery detail page view.
|
|
11
13
|
* Shows cover + description + slot grid + pull form (if active + logged in).
|
|
@@ -13,7 +15,8 @@ import { LotteryPullForm } from "./LotteryPullForm";
|
|
|
13
15
|
export function LotteryDetailView({ event, user, currentEntry }) {
|
|
14
16
|
const [pulledEntry, setPulledEntry] = useState(currentEntry?.assignedPrizeSlotNumber != null ? currentEntry : null);
|
|
15
17
|
const config = event.lotteryConfig;
|
|
18
|
+
const prizeItems = slotsToCollageItems(config?.slots ?? []);
|
|
16
19
|
const isActive = event.status === "active";
|
|
17
20
|
const isEnded = event.status === "ended" || event.status === "cancelled";
|
|
18
|
-
return (_jsx(Container, { children: _jsx(Section, { children: _jsxs(Stack, { gap: "xl", children: [event.coverImageUrl ? (_jsx(Div, { rounded: "2xl", className: "relative overflow-hidden", style: { aspectRatio: "16/9", maxHeight: "16rem" }, children: _jsx(MediaImage, { src: event.coverImageUrl, alt: event.title, size: "hero" }) })) : (_jsx(Div, { rounded: "2xl", surface: "subtle", padding: "xl", align: "center", justify: "center", children: _jsx(Text, { size: "5xl", children: "\uD83C\uDFB0" }) })), _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, children: [_jsx(Badge, { variant: isActive ? "success" : isEnded ? "danger" : "warning", children: event.status }), config && (_jsxs(Badge, { variant: "info", children: [config.slots.filter((s) => s.isBooked).length, "/", config.totalSlots, " slots claimed"] }))] }), _jsx(Heading, { level: 1, size: "3xl", weight: "bold", children: event.title }), event.description ? (_jsx(Text, { color: "muted", size: "sm", children: event.description.replace(/<[^>]+>/g, "").slice(0, 200) })) : null, event.endsAt ? (_jsxs(Text, { size: "sm", color: "muted", children: ["Ends: ", new Date(event.endsAt).toLocaleDateString("en-IN", { dateStyle: "long" })] })) : null] }), config && config.slots.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", children: "Slots" }), _jsx(LotterySlotGrid, { slots: config.slots, totalSlots: config.totalSlots })] })) : null, isActive && config && (_jsx(Div, { padding: "md", rounded: "2xl", surface: "card", className: "border border-[var(--appkit-color-border)]", children: !user ? (_jsxs(Stack, { gap: "md", className: "text-center", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", children: "Want to Participate?" }), _jsx(Text, { color: "muted", children: "Log in to submit your lottery entry." }), _jsx(Anchor, { href: String(ROUTES.AUTH.LOGIN), tone: "brand", children: "Log In to Enter" })] })) : pulledEntry ? (_jsxs(Stack, { gap: "sm", surface: "success-surface", padding: "md", rounded: "xl", className: "border border-success/20", children: [_jsxs(Heading, { level: 2, size: "lg", weight: "bold", className: "text-success", children: ["You're Lottery #", pulledEntry.userLotteryNumber, "!"] }), _jsxs(Text, { className: "text-success", size: "sm", children: ["Assigned to Slot #", pulledEntry.assignedPrizeSlotNumber, pulledEntry.slotName ? ` — ${pulledEntry.slotName}` : "", "."] })] })) : (_jsx(LotteryPullForm, { sourceType: "event", eventId: event.id, totalSlots: config.totalSlots, maxPullsPerUser: config.maxPullsPerUser, onSuccess: (res) => setPulledEntry(res) })) })), isEnded && !pulledEntry && (_jsx(Stack, { gap: "sm", surface: "muted", padding: "md", rounded: "xl", children: _jsx(Text, { color: "muted", weight: "semibold", children: "This lottery has ended." }) }))] }) }) }));
|
|
21
|
+
return (_jsx(Container, { children: _jsx(Section, { children: _jsxs(Stack, { gap: "xl", children: [event.coverImageUrl || event.coverImage?.url ? (_jsx(Div, { rounded: "2xl", className: "relative overflow-hidden", style: { aspectRatio: "16/9", maxHeight: "16rem" }, children: _jsx(MediaImage, { src: event.coverImageUrl || event.coverImage?.url || "", alt: event.title, size: "hero" }) })) : (_jsx(Div, { rounded: "2xl", surface: "subtle", padding: "xl", align: "center", justify: "center", children: _jsx(Text, { size: "5xl", children: "\uD83C\uDFB0" }) })), _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, children: [_jsx(Badge, { variant: isActive ? "success" : isEnded ? "danger" : "warning", children: event.status }), config && (_jsxs(Badge, { variant: "info", children: [config.slots.filter((s) => s.isBooked).length, "/", config.totalSlots, " slots claimed"] }))] }), _jsx(Heading, { level: 1, size: "3xl", weight: "bold", children: event.title }), event.description ? (_jsx(Text, { color: "muted", size: "sm", children: event.description.replace(/<[^>]+>/g, "").slice(0, 200) })) : null, event.endsAt ? (_jsxs(Text, { size: "sm", color: "muted", children: ["Ends: ", new Date(event.endsAt).toLocaleDateString("en-IN", { dateStyle: "long" })] })) : null] }), prizeItems.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", children: "Prizes" }), _jsx(PrizeDrawCollage, { items: prizeItems, wonLabel: "Claimed" })] })) : null, config && config.slots.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", children: "Slots" }), _jsx(LotterySlotGrid, { slots: config.slots, totalSlots: config.totalSlots })] })) : null, isActive && config && (_jsx(Div, { padding: "md", rounded: "2xl", surface: "card", className: "border border-[var(--appkit-color-border)]", children: !user ? (_jsxs(Stack, { gap: "md", className: "text-center", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", children: "Want to Participate?" }), _jsx(Text, { color: "muted", children: "Log in to submit your lottery entry." }), _jsx(Anchor, { href: String(ROUTES.AUTH.LOGIN), tone: "brand", children: "Log In to Enter" })] })) : pulledEntry ? (_jsxs(Stack, { gap: "sm", surface: "success-surface", padding: "md", rounded: "xl", className: "border border-success/20", children: [_jsxs(Heading, { level: 2, size: "lg", weight: "bold", className: "text-success", children: ["You're Lottery #", pulledEntry.userLotteryNumber, "!"] }), _jsxs(Text, { className: "text-success", size: "sm", children: ["Assigned to Slot #", pulledEntry.assignedPrizeSlotNumber, pulledEntry.slotName ? ` — ${pulledEntry.slotName}` : "", "."] })] })) : (_jsx(LotteryPullForm, { sourceType: "event", eventId: event.id, totalSlots: config.totalSlots, maxPullsPerUser: config.maxPullsPerUser, onSuccess: (res) => setPulledEntry(res) })) })), isEnded && !pulledEntry && (_jsx(Stack, { gap: "sm", surface: "muted", padding: "md", rounded: "xl", children: _jsx(Text, { color: "muted", weight: "semibold", children: "This lottery has ended." }) }))] }) }) }));
|
|
19
22
|
}
|
|
@@ -7,7 +7,14 @@ interface ClientLotteryEvent {
|
|
|
7
7
|
status: string;
|
|
8
8
|
startsAt?: string | Date;
|
|
9
9
|
endsAt?: string | Date;
|
|
10
|
+
/**
|
|
11
|
+
* Only `coverImage.url` is ever populated on EventDocument — reading
|
|
12
|
+
* `coverImageUrl` alone showed the 🎰 placeholder on every card.
|
|
13
|
+
*/
|
|
10
14
|
coverImageUrl?: string;
|
|
15
|
+
coverImage?: {
|
|
16
|
+
url?: string;
|
|
17
|
+
} | null;
|
|
11
18
|
lotteryConfig?: ClientLotteryConfig;
|
|
12
19
|
}
|
|
13
20
|
interface LotteryListViewProps {
|
|
@@ -17,7 +17,7 @@ export function LotteryListView({ items, adminMode = false }) {
|
|
|
17
17
|
const claimed = event.lotteryConfig?.slots.filter((s) => s.isBooked).length ?? 0;
|
|
18
18
|
const total = event.lotteryConfig?.totalSlots ?? 0;
|
|
19
19
|
const slotsLeft = total - claimed;
|
|
20
|
-
return (_jsx(Anchor, { href: `/lottery/${event.id}`, tone: "brand", rounded: "2xl", className: "block focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", "aria-label": `${event.title} lottery`, children: _jsxs(Card, { variant: "default", padding: "none", animate: "hoverLift", className: "overflow-hidden", children: [event.coverImageUrl ? (_jsx(Div, { className: "relative aspect-video overflow-hidden", children: _jsx(MediaImage, { src: event.coverImageUrl, alt: event.title, size: "card" }) })) : (_jsx(Div, { className: "aspect-video", surface: "muted", align: "center", justify: "center", children: _jsx(Text, { size: "3xl", children: "\uD83C\uDFB0" }) })), _jsxs(Stack, { gap: "xs", padding: "md", children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, children: [_jsx(Badge, { variant: event.status === "active"
|
|
20
|
+
return (_jsx(Anchor, { href: `/lottery/${event.id}`, tone: "brand", rounded: "2xl", className: "block focus:outline-none focus-visible:ring-2 focus-visible:ring-primary", "aria-label": `${event.title} lottery`, children: _jsxs(Card, { variant: "default", padding: "none", animate: "hoverLift", className: "overflow-hidden", children: [event.coverImageUrl || event.coverImage?.url ? (_jsx(Div, { className: "relative aspect-video overflow-hidden", children: _jsx(MediaImage, { src: event.coverImageUrl || event.coverImage?.url || "", alt: event.title, size: "card" }) })) : (_jsx(Div, { className: "aspect-video", surface: "muted", align: "center", justify: "center", children: _jsx(Text, { size: "3xl", children: "\uD83C\uDFB0" }) })), _jsxs(Stack, { gap: "xs", padding: "md", children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, children: [_jsx(Badge, { variant: event.status === "active"
|
|
21
21
|
? "success"
|
|
22
22
|
: event.status === "ended"
|
|
23
23
|
? "danger"
|
|
@@ -6,6 +6,8 @@ import { MediaImage } from "../../../../features/media/MediaImage";
|
|
|
6
6
|
import { ROUTES } from "../../../../next/routing/route-map";
|
|
7
7
|
import { LotterySlotGrid } from "./LotterySlotGrid";
|
|
8
8
|
import { LotteryPullForm } from "./LotteryPullForm";
|
|
9
|
+
import { PrizeDrawCollage } from "../../../../features/products/components/PrizeDrawCollage";
|
|
10
|
+
import { slotsToCollageItems } from "./slot-collage-items";
|
|
9
11
|
/**
|
|
10
12
|
* Prize-draw product detail view for lottery mode.
|
|
11
13
|
* Same flow as LotteryDetailView but sourced from a product.
|
|
@@ -15,6 +17,7 @@ export function PrizeDrawLotteryDetailView({ product, user, currentEntry, }) {
|
|
|
15
17
|
? currentEntry
|
|
16
18
|
: null);
|
|
17
19
|
const config = product.lotteryConfig;
|
|
20
|
+
const prizeItems = slotsToCollageItems(config?.slots ?? []);
|
|
18
21
|
const isActive = product.status === "published";
|
|
19
|
-
return (_jsx(Container, { children: _jsx(Section, { children: _jsxs(Stack, { gap: "xl", children: [product.mainImage ? (_jsx(Div, { rounded: "2xl", className: "relative overflow-hidden", style: { aspectRatio: "16/9", maxHeight: "16rem" }, children: _jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "hero" }) })) : (_jsx(Div, { rounded: "2xl", surface: "muted", padding: "xl", align: "center", justify: "center", children: _jsx(Text, { size: "5xl", children: "\uD83C\uDF81" }) })), _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, children: [_jsx(Badge, { variant: "info", children: "Lottery Prize Draw" }), config && (_jsxs(Badge, { variant: isActive ? "success" : "secondary", children: [config.slots.filter((s) => s.isBooked).length, "/", config.totalSlots, " claimed"] }))] }), _jsx(Heading, { level: 1, size: "3xl", weight: "bold", children: product.title }), product.description ? (_jsx(Text, { color: "muted", children: product.description })) : null] }), config && config.slots.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", children: "Slots" }), _jsx(LotterySlotGrid, { slots: config.slots, totalSlots: config.totalSlots })] })) : null, isActive && config && (_jsx(Div, { padding: "md", rounded: "2xl", surface: "card", className: "border border-[var(--appkit-color-border)]", children: !user ? (_jsxs(Stack, { gap: "md", className: "text-center", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", children: "Want to Enter?" }), _jsx(Text, { color: "muted", children: "Log in to submit your entry." }), _jsx(Anchor, { href: String(ROUTES.AUTH.LOGIN), tone: "brand", children: "Log In to Enter" })] })) : pulledEntry ? (_jsxs(Stack, { gap: "sm", surface: "success-surface", padding: "md", rounded: "xl", className: "border border-success/20", children: [_jsxs(Heading, { level: 2, size: "lg", weight: "bold", className: "text-success", children: ["You're Entry #", pulledEntry.userLotteryNumber, "!"] }), _jsxs(Text, { className: "text-success", size: "sm", children: ["Assigned to Slot #", pulledEntry.assignedPrizeSlotNumber, pulledEntry.slotName ? ` — ${pulledEntry.slotName}` : "", "."] })] })) : (_jsx(LotteryPullForm, { sourceType: "product", productId: product.id, totalSlots: config.totalSlots, maxPullsPerUser: config.maxPullsPerUser, onSuccess: (res) => setPulledEntry(res) })) }))] }) }) }));
|
|
22
|
+
return (_jsx(Container, { children: _jsx(Section, { children: _jsxs(Stack, { gap: "xl", children: [product.mainImage ? (_jsx(Div, { rounded: "2xl", className: "relative overflow-hidden", style: { aspectRatio: "16/9", maxHeight: "16rem" }, children: _jsx(MediaImage, { src: product.mainImage, alt: product.title, size: "hero" }) })) : (_jsx(Div, { rounded: "2xl", surface: "muted", padding: "xl", align: "center", justify: "center", children: _jsx(Text, { size: "5xl", children: "\uD83C\uDF81" }) })), _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, children: [_jsx(Badge, { variant: "info", children: "Lottery Prize Draw" }), config && (_jsxs(Badge, { variant: isActive ? "success" : "secondary", children: [config.slots.filter((s) => s.isBooked).length, "/", config.totalSlots, " claimed"] }))] }), _jsx(Heading, { level: 1, size: "3xl", weight: "bold", children: product.title }), product.description ? (_jsx(Text, { color: "muted", children: product.description })) : null] }), prizeItems.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", children: "Prizes" }), _jsx(PrizeDrawCollage, { items: prizeItems, wonLabel: "Claimed" })] })) : null, config && config.slots.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", children: "Slots" }), _jsx(LotterySlotGrid, { slots: config.slots, totalSlots: config.totalSlots })] })) : null, isActive && config && (_jsx(Div, { padding: "md", rounded: "2xl", surface: "card", className: "border border-[var(--appkit-color-border)]", children: !user ? (_jsxs(Stack, { gap: "md", className: "text-center", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", children: "Want to Enter?" }), _jsx(Text, { color: "muted", children: "Log in to submit your entry." }), _jsx(Anchor, { href: String(ROUTES.AUTH.LOGIN), tone: "brand", children: "Log In to Enter" })] })) : pulledEntry ? (_jsxs(Stack, { gap: "sm", surface: "success-surface", padding: "md", rounded: "xl", className: "border border-success/20", children: [_jsxs(Heading, { level: 2, size: "lg", weight: "bold", className: "text-success", children: ["You're Entry #", pulledEntry.userLotteryNumber, "!"] }), _jsxs(Text, { className: "text-success", size: "sm", children: ["Assigned to Slot #", pulledEntry.assignedPrizeSlotNumber, pulledEntry.slotName ? ` — ${pulledEntry.slotName}` : "", "."] })] })) : (_jsx(LotteryPullForm, { sourceType: "product", productId: product.id, totalSlots: config.totalSlots, maxPullsPerUser: config.maxPullsPerUser, onSuccess: (res) => setPulledEntry(res) })) }))] }) }) }));
|
|
20
23
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map lottery slots onto the shape `PrizeDrawCollage` renders.
|
|
3
|
+
*
|
|
4
|
+
* Shared by all three lottery surfaces (event lottery detail, prize-draw-mode
|
|
5
|
+
* lottery detail, and any future one) so the "which slots get a tile" rule
|
|
6
|
+
* cannot drift between them.
|
|
7
|
+
*
|
|
8
|
+
* Slots with no image are dropped rather than rendered as empty tiles: a
|
|
9
|
+
* lottery may have 200 slots and only a handful of photographed prizes, and a
|
|
10
|
+
* collage of placeholders communicates nothing. The numbered `LotterySlotGrid`
|
|
11
|
+
* below it is still the complete, authoritative slot map.
|
|
12
|
+
*/
|
|
13
|
+
import type { ClientLotterySlot } from "../../../../features/lottery/types";
|
|
14
|
+
import type { CollagePrizeItem } from "../../../../features/products/components/PrizeDrawCollage";
|
|
15
|
+
export declare function slotsToCollageItems(slots: readonly ClientLotterySlot[]): CollagePrizeItem[];
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function slotsToCollageItems(slots) {
|
|
2
|
+
return slots
|
|
3
|
+
.filter((slot) => Boolean(slot.image))
|
|
4
|
+
.map((slot) => ({
|
|
5
|
+
itemNumber: slot.slotNumber,
|
|
6
|
+
title: slot.name,
|
|
7
|
+
images: slot.image ? [slot.image] : [],
|
|
8
|
+
// `isBooked` is the lottery's equivalent of a prize being taken. The
|
|
9
|
+
// collage's own label prop renames the stamp to "Claimed" at the call
|
|
10
|
+
// site — price and weight never reach the client at all.
|
|
11
|
+
isWon: slot.isBooked,
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
@@ -32,9 +32,15 @@ export async function placeBidAction(input) {
|
|
|
32
32
|
});
|
|
33
33
|
// Now set this new bid as the winning bid
|
|
34
34
|
await bidRepository.setWinningBid(bid.id, auctionId);
|
|
35
|
+
// `bidsHaveStarted` and `leadingBidderId` are set by the primary bid path
|
|
36
|
+
// (`features/auctions/actions/bid-actions.ts`) but were missing here, so a
|
|
37
|
+
// bid placed through this legacy entrypoint left both mirrors stale —
|
|
38
|
+
// CLAUDE.md Root Cause #42.
|
|
35
39
|
const updates = {
|
|
36
40
|
currentBid: amount,
|
|
37
41
|
bidCount: (product.bidCount ?? 0) + 1,
|
|
42
|
+
leadingBidderId: user.uid,
|
|
43
|
+
bidsHaveStarted: true,
|
|
38
44
|
};
|
|
39
45
|
if (shouldAutoExtend(product)) {
|
|
40
46
|
updates.auctionEndDate = computeExtendedEndDate(product);
|
|
@@ -40,6 +40,22 @@ export declare const listBundleMembers: (bundle: Pick<CategoryDocument, "bundleP
|
|
|
40
40
|
* displayed discount.
|
|
41
41
|
*/
|
|
42
42
|
export declare function resolveBundleOriginalTotal(productIds: string[]): Promise<number | undefined>;
|
|
43
|
+
/**
|
|
44
|
+
* Union of the member products' `categorySlugs` — the `bundleCategorySlugs`
|
|
45
|
+
* mirror that lets a category page scope its Bundles tab.
|
|
46
|
+
*
|
|
47
|
+
* A bundle row has no category field of its own, which is why the category page
|
|
48
|
+
* counted every active bundle site-wide before this existed. Maintained at the
|
|
49
|
+
* same call sites as `resolveBundleOriginalTotal` so the two mirrors cannot
|
|
50
|
+
* drift apart.
|
|
51
|
+
*
|
|
52
|
+
* Unlike the price total, a partially-resolvable member set still yields a
|
|
53
|
+
* useful answer: over-inclusion (a bundle appearing under one category too many)
|
|
54
|
+
* is a far smaller error than a bundle vanishing from the category it belongs
|
|
55
|
+
* to, and readers already treat an ABSENT mirror as "unscoped" rather than
|
|
56
|
+
* "belongs nowhere" (Root Cause #42).
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveBundleCategorySlugs(productIds: string[]): Promise<string[]>;
|
|
43
59
|
/**
|
|
44
60
|
* List the most recent active bundles for homepage placement. Bounded to
|
|
45
61
|
* `limit` (default 8) and filtered to `isActive: true` server-side.
|
|
@@ -68,6 +68,32 @@ export async function resolveBundleOriginalTotal(productIds) {
|
|
|
68
68
|
const total = prices.reduce((sum, p) => sum + p, 0);
|
|
69
69
|
return Math.round(total * 100) / 100;
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* Union of the member products' `categorySlugs` — the `bundleCategorySlugs`
|
|
73
|
+
* mirror that lets a category page scope its Bundles tab.
|
|
74
|
+
*
|
|
75
|
+
* A bundle row has no category field of its own, which is why the category page
|
|
76
|
+
* counted every active bundle site-wide before this existed. Maintained at the
|
|
77
|
+
* same call sites as `resolveBundleOriginalTotal` so the two mirrors cannot
|
|
78
|
+
* drift apart.
|
|
79
|
+
*
|
|
80
|
+
* Unlike the price total, a partially-resolvable member set still yields a
|
|
81
|
+
* useful answer: over-inclusion (a bundle appearing under one category too many)
|
|
82
|
+
* is a far smaller error than a bundle vanishing from the category it belongs
|
|
83
|
+
* to, and readers already treat an ABSENT mirror as "unscoped" rather than
|
|
84
|
+
* "belongs nowhere" (Root Cause #42).
|
|
85
|
+
*/
|
|
86
|
+
export async function resolveBundleCategorySlugs(productIds) {
|
|
87
|
+
if (productIds.length === 0)
|
|
88
|
+
return [];
|
|
89
|
+
const results = await Promise.all(productIds.map((id) => productRepository.findById(id).catch(() => null)));
|
|
90
|
+
const slugs = new Set();
|
|
91
|
+
for (const p of results) {
|
|
92
|
+
for (const slug of p?.categorySlugs ?? [])
|
|
93
|
+
slugs.add(slug);
|
|
94
|
+
}
|
|
95
|
+
return [...slugs];
|
|
96
|
+
}
|
|
71
97
|
/**
|
|
72
98
|
* List the most recent active bundles for homepage placement. Bounded to
|
|
73
99
|
* `limit` (default 8) and filtered to `isActive: true` server-side.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, type BundleDataOptions, } from "./data";
|
|
1
|
+
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, resolveBundleCategorySlugs, type BundleDataOptions, } from "./data";
|
|
2
2
|
export { buildBundleMetadata, type BundleMetadataOptions } from "./metadata";
|
|
3
3
|
export { renderBundleOg, renderBundleOgImage, type BundleOgData, } from "./og";
|
|
4
4
|
export { addBundleToCartAction } from "./actions";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, } from "./data";
|
|
1
|
+
export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, resolveBundleCategorySlugs, } from "./data";
|
|
2
2
|
export { buildBundleMetadata } from "./metadata";
|
|
3
3
|
// S-SBUNI-4 2026-05-13 — bundle OG renderer.
|
|
4
4
|
export { renderBundleOg, renderBundleOgImage, } from "./og";
|
|
@@ -503,6 +503,9 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
503
503
|
items: orderItems,
|
|
504
504
|
orderType,
|
|
505
505
|
offerId: firstItem.offerId ?? undefined,
|
|
506
|
+
// Distinguishes a Buy Now purchase from a settled auction win. Both are
|
|
507
|
+
// orderType "auction"; only this one gets the 24h admin-review window.
|
|
508
|
+
isBuyout: firstItem.isBuyout ? true : undefined,
|
|
506
509
|
status: adminBypass ? OrderStatusValues.PROCESSING : OrderStatusValues.PENDING,
|
|
507
510
|
paymentStatus: adminBypass ? PaymentStatusValues.PAID : PaymentStatusValues.PENDING,
|
|
508
511
|
paymentMethod: adminBypass ? PaymentMethodValues.ADMIN_BYPASS : paymentMethod,
|
|
@@ -1216,6 +1219,9 @@ async function createRazorpayGroupOrder(group, orderType, ctx) {
|
|
|
1216
1219
|
items: orderItems,
|
|
1217
1220
|
orderType,
|
|
1218
1221
|
offerId: firstItem.offerId ?? undefined,
|
|
1222
|
+
// Distinguishes a Buy Now purchase from a settled auction win. Both are
|
|
1223
|
+
// orderType "auction"; only this one gets the 24h admin-review window.
|
|
1224
|
+
isBuyout: firstItem.isBuyout ? true : undefined,
|
|
1219
1225
|
status: OrderStatusValues.CONFIRMED,
|
|
1220
1226
|
paymentStatus: PaymentStatusValues.PAID,
|
|
1221
1227
|
paymentMethod: PaymentMethodValues.ONLINE,
|
|
@@ -11,6 +11,38 @@ import type { CartItemDocument } from "../../../../features/cart/schemas/firesto
|
|
|
11
11
|
export declare function assertCheckoutLane(allCartItems: readonly CartItemDocument[], selectedItems: readonly CartItemDocument[]): void;
|
|
12
12
|
/** Error code the client maps to a lane-aware message. */
|
|
13
13
|
export declare const CHECKOUT_LANE_BLOCKED = "CHECKOUT_LANE_BLOCKED";
|
|
14
|
+
/** Why a buyout claim could not be converted into a sale. */
|
|
15
|
+
type ClaimFailure = "NOT_FOUND" | "ALREADY_SOLD" | "AUCTION_ENDED" | "LAPSED" | "OUTBID" | "WRONG_BUYER";
|
|
16
|
+
/**
|
|
17
|
+
* Convert a pending Buy Now claim into an actual sale, atomically.
|
|
18
|
+
*
|
|
19
|
+
* This is where a buyout is decided, and it is the ONLY place the auction is
|
|
20
|
+
* closed. `buyNowAuction` deliberately leaves the listing untouched: the auction
|
|
21
|
+
* keeps running, keeps taking bids, and keeps accepting other buyers' buyouts.
|
|
22
|
+
* That means several people can be holding a claim on the same auction at once,
|
|
23
|
+
* which is intentional — whoever reaches this transaction first gets the item,
|
|
24
|
+
* and everyone else is refused HERE, before their order is written and before
|
|
25
|
+
* any payment is captured.
|
|
26
|
+
*
|
|
27
|
+
* Four ways to lose, all of them checked inside the transaction against freshly
|
|
28
|
+
* read documents rather than against the caller's stale cart line:
|
|
29
|
+
* - the auction is already sold (another buyout committed first);
|
|
30
|
+
* - its clock ran out (settlement will award it to a real bidder — the user's
|
|
31
|
+
* explicit rule: an unfinished buyout at auction end fails);
|
|
32
|
+
* - the claim's own bid is no longer `active` (the deadline sweep cancelled it);
|
|
33
|
+
* - bidding passed the buyout price, so buying out is no longer the better deal.
|
|
34
|
+
*
|
|
35
|
+
* Returns a discriminated result rather than throwing out of the transaction
|
|
36
|
+
* body, so each of these precise, buyer-readable refusals survives as itself
|
|
37
|
+
* instead of being reshaped by the transaction wrapper. The caller throws.
|
|
38
|
+
*/
|
|
39
|
+
export declare function claimAuctionForCheckout(item: CartItemDocument, buyerUid: string): Promise<{
|
|
40
|
+
ok: true;
|
|
41
|
+
bidAmount: number;
|
|
42
|
+
} | {
|
|
43
|
+
ok: false;
|
|
44
|
+
reason: ClaimFailure;
|
|
45
|
+
}>;
|
|
14
46
|
/**
|
|
15
47
|
* Re-validate every locked line immediately before the order is written.
|
|
16
48
|
*
|
|
@@ -18,6 +50,11 @@ export declare const CHECKOUT_LANE_BLOCKED = "CHECKOUT_LANE_BLOCKED";
|
|
|
18
50
|
* to the cart, but nothing re-checked at order time — so a buyer could add an
|
|
19
51
|
* accepted offer, wait out the 48h window, and still be charged the locked
|
|
20
52
|
* price. Throws on the first invalid line; the caller's transaction aborts.
|
|
53
|
+
*
|
|
54
|
+
* Auction lines were skipped entirely until 2026-08-24 (`if (!item.offerId)
|
|
55
|
+
* continue`), so a won auction's own `checkoutDeadline` had never been checked
|
|
56
|
+
* at order time at all — and a buyout claim, which is decided here, had nowhere
|
|
57
|
+
* to be decided.
|
|
21
58
|
*/
|
|
22
59
|
export declare function assertLockedLinesStillValid(items: readonly CartItemDocument[], buyerUid: string): Promise<void>;
|
|
23
60
|
/**
|
|
@@ -28,3 +65,4 @@ export declare function assertLockedLinesStillValid(items: readonly CartItemDocu
|
|
|
28
65
|
* bookkeeping problem, refusing a completed order is a customer-facing one.
|
|
29
66
|
*/
|
|
30
67
|
export declare function finalizeLockedLines(items: readonly CartItemDocument[], orderId: string): Promise<void>;
|
|
68
|
+
export {};
|
|
@@ -24,9 +24,15 @@
|
|
|
24
24
|
import { offerRepository } from "../../../../features/seller/repository/offer.repository";
|
|
25
25
|
import { bidRepository } from "../../../../repositories";
|
|
26
26
|
import { OfferStatusValues } from "../../../../features/seller/schemas";
|
|
27
|
+
import { BID_COLLECTION } from "../../../../features/auctions/schemas/firestore";
|
|
28
|
+
import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firestore";
|
|
27
29
|
import { ValidationError } from "../../../../errors";
|
|
30
|
+
import { BID_ERROR_CODES } from "../../../../errors/error-codes";
|
|
31
|
+
import { ERROR_MESSAGES } from "../../../../errors";
|
|
32
|
+
import { getAdminDb } from "../../../../providers/db-firebase";
|
|
28
33
|
import { serverLogger } from "../../../../monitoring/server-logger";
|
|
29
34
|
import { normalizeError } from "../../../../errors/normalize";
|
|
35
|
+
import { resolveDate } from "../../../../utils/date.formatter";
|
|
30
36
|
import { activeLane, laneOf, laneBlockReason } from "../../../shared/checkout/lanes";
|
|
31
37
|
/**
|
|
32
38
|
* Refuse a checkout that isn't the cart's active lane.
|
|
@@ -49,6 +55,108 @@ export function assertCheckoutLane(allCartItems, selectedItems) {
|
|
|
49
55
|
}
|
|
50
56
|
/** Error code the client maps to a lane-aware message. */
|
|
51
57
|
export const CHECKOUT_LANE_BLOCKED = "CHECKOUT_LANE_BLOCKED";
|
|
58
|
+
const CLAIM_FAILURE_ERRORS = {
|
|
59
|
+
NOT_FOUND: {
|
|
60
|
+
message: ERROR_MESSAGES.BID.AUCTION_NOT_FOUND,
|
|
61
|
+
code: BID_ERROR_CODES.PRODUCT_NOT_FOUND,
|
|
62
|
+
},
|
|
63
|
+
ALREADY_SOLD: {
|
|
64
|
+
message: ERROR_MESSAGES.BID.AUCTION_ALREADY_SOLD,
|
|
65
|
+
code: BID_ERROR_CODES.AUCTION_ALREADY_SOLD,
|
|
66
|
+
},
|
|
67
|
+
AUCTION_ENDED: {
|
|
68
|
+
message: ERROR_MESSAGES.BID.AUCTION_ENDED_BEFORE_CHECKOUT,
|
|
69
|
+
code: BID_ERROR_CODES.AUCTION_ENDED_BEFORE_CHECKOUT,
|
|
70
|
+
},
|
|
71
|
+
LAPSED: {
|
|
72
|
+
message: ERROR_MESSAGES.BID.BUYOUT_LAPSED,
|
|
73
|
+
code: BID_ERROR_CODES.BUYOUT_LAPSED,
|
|
74
|
+
},
|
|
75
|
+
OUTBID: {
|
|
76
|
+
message: ERROR_MESSAGES.BID.BUYOUT_OUTBID,
|
|
77
|
+
code: BID_ERROR_CODES.BUYOUT_OUTBID,
|
|
78
|
+
},
|
|
79
|
+
WRONG_BUYER: {
|
|
80
|
+
message: ERROR_MESSAGES.BID.BUYOUT_LAPSED,
|
|
81
|
+
code: BID_ERROR_CODES.BUYOUT_LAPSED,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* Convert a pending Buy Now claim into an actual sale, atomically.
|
|
86
|
+
*
|
|
87
|
+
* This is where a buyout is decided, and it is the ONLY place the auction is
|
|
88
|
+
* closed. `buyNowAuction` deliberately leaves the listing untouched: the auction
|
|
89
|
+
* keeps running, keeps taking bids, and keeps accepting other buyers' buyouts.
|
|
90
|
+
* That means several people can be holding a claim on the same auction at once,
|
|
91
|
+
* which is intentional — whoever reaches this transaction first gets the item,
|
|
92
|
+
* and everyone else is refused HERE, before their order is written and before
|
|
93
|
+
* any payment is captured.
|
|
94
|
+
*
|
|
95
|
+
* Four ways to lose, all of them checked inside the transaction against freshly
|
|
96
|
+
* read documents rather than against the caller's stale cart line:
|
|
97
|
+
* - the auction is already sold (another buyout committed first);
|
|
98
|
+
* - its clock ran out (settlement will award it to a real bidder — the user's
|
|
99
|
+
* explicit rule: an unfinished buyout at auction end fails);
|
|
100
|
+
* - the claim's own bid is no longer `active` (the deadline sweep cancelled it);
|
|
101
|
+
* - bidding passed the buyout price, so buying out is no longer the better deal.
|
|
102
|
+
*
|
|
103
|
+
* Returns a discriminated result rather than throwing out of the transaction
|
|
104
|
+
* body, so each of these precise, buyer-readable refusals survives as itself
|
|
105
|
+
* instead of being reshaped by the transaction wrapper. The caller throws.
|
|
106
|
+
*/
|
|
107
|
+
export async function claimAuctionForCheckout(item, buyerUid) {
|
|
108
|
+
const productId = item.auctionId ?? item.productId;
|
|
109
|
+
const bidId = item.bidId;
|
|
110
|
+
if (!bidId)
|
|
111
|
+
return { ok: false, reason: "NOT_FOUND" };
|
|
112
|
+
const db = getAdminDb();
|
|
113
|
+
return db.runTransaction(async (tx) => {
|
|
114
|
+
const productRef = db.collection(PRODUCT_COLLECTION).doc(productId);
|
|
115
|
+
const bidRef = db.collection(BID_COLLECTION).doc(bidId);
|
|
116
|
+
const [productSnap, bidSnap] = await tx.getAll(productRef, bidRef);
|
|
117
|
+
if (!productSnap.exists || !bidSnap.exists) {
|
|
118
|
+
return { ok: false, reason: "NOT_FOUND" };
|
|
119
|
+
}
|
|
120
|
+
const product = productSnap.data();
|
|
121
|
+
const bid = bidSnap.data();
|
|
122
|
+
if (bid.userId !== buyerUid)
|
|
123
|
+
return { ok: false, reason: "WRONG_BUYER" };
|
|
124
|
+
if (bid.status !== "active")
|
|
125
|
+
return { ok: false, reason: "LAPSED" };
|
|
126
|
+
if (product.isSold === true)
|
|
127
|
+
return { ok: false, reason: "ALREADY_SOLD" };
|
|
128
|
+
const endDate = product.auctionEndDate ? resolveDate(product.auctionEndDate) : null;
|
|
129
|
+
if (endDate && endDate.getTime() <= Date.now()) {
|
|
130
|
+
return { ok: false, reason: "AUCTION_ENDED" };
|
|
131
|
+
}
|
|
132
|
+
const bidAmount = bid.bidAmount;
|
|
133
|
+
const standingBid = typeof product.currentBid === "number" ? product.currentBid : 0;
|
|
134
|
+
// `currentBid` never reflects a pending buyout (see BidDocument.isBuyout),
|
|
135
|
+
// so this compares against real bidding only — exactly the check the buyer
|
|
136
|
+
// passed when they started, re-run against the price as it is right now.
|
|
137
|
+
if (standingBid >= bidAmount) {
|
|
138
|
+
return { ok: false, reason: "OUTBID" };
|
|
139
|
+
}
|
|
140
|
+
tx.update(productRef, {
|
|
141
|
+
isSold: true,
|
|
142
|
+
availableQuantity: 0,
|
|
143
|
+
// Ends the auction NOW. Everything above has already established that
|
|
144
|
+
// nobody outbid the buyout price, so no bidder loses anything they would
|
|
145
|
+
// otherwise have won.
|
|
146
|
+
auctionEndDate: new Date(),
|
|
147
|
+
currentBid: bidAmount,
|
|
148
|
+
leadingBidderId: bid.userId,
|
|
149
|
+
bidsHaveStarted: true,
|
|
150
|
+
updatedAt: new Date(),
|
|
151
|
+
});
|
|
152
|
+
tx.update(bidRef, {
|
|
153
|
+
status: "won",
|
|
154
|
+
isWinning: true,
|
|
155
|
+
updatedAt: new Date(),
|
|
156
|
+
});
|
|
157
|
+
return { ok: true, bidAmount };
|
|
158
|
+
});
|
|
159
|
+
}
|
|
52
160
|
/**
|
|
53
161
|
* Re-validate every locked line immediately before the order is written.
|
|
54
162
|
*
|
|
@@ -56,10 +164,37 @@ export const CHECKOUT_LANE_BLOCKED = "CHECKOUT_LANE_BLOCKED";
|
|
|
56
164
|
* to the cart, but nothing re-checked at order time — so a buyer could add an
|
|
57
165
|
* accepted offer, wait out the 48h window, and still be charged the locked
|
|
58
166
|
* price. Throws on the first invalid line; the caller's transaction aborts.
|
|
167
|
+
*
|
|
168
|
+
* Auction lines were skipped entirely until 2026-08-24 (`if (!item.offerId)
|
|
169
|
+
* continue`), so a won auction's own `checkoutDeadline` had never been checked
|
|
170
|
+
* at order time at all — and a buyout claim, which is decided here, had nowhere
|
|
171
|
+
* to be decided.
|
|
59
172
|
*/
|
|
60
173
|
export async function assertLockedLinesStillValid(items, buyerUid) {
|
|
61
174
|
const now = new Date();
|
|
62
175
|
for (const item of items) {
|
|
176
|
+
// --- Auction lines: deadline for every one, plus the claim for a buyout ---
|
|
177
|
+
if (item.bidId || item.isAuctionWin) {
|
|
178
|
+
if (item.checkoutDeadline && now > resolveDate(item.checkoutDeadline)) {
|
|
179
|
+
throw new ValidationError(item.isBuyout
|
|
180
|
+
? ERROR_MESSAGES.BID.BUYOUT_LAPSED
|
|
181
|
+
: "The payment window for this auction win has expired.", { code: item.isBuyout ? BID_ERROR_CODES.BUYOUT_LAPSED : undefined });
|
|
182
|
+
}
|
|
183
|
+
if (item.isBuyout) {
|
|
184
|
+
const claim = await claimAuctionForCheckout(item, buyerUid);
|
|
185
|
+
if (!claim.ok) {
|
|
186
|
+
const { message, code } = CLAIM_FAILURE_ERRORS[claim.reason];
|
|
187
|
+
serverLogger.info("claimAuctionForCheckout refused", {
|
|
188
|
+
productId: item.auctionId ?? item.productId,
|
|
189
|
+
bidId: item.bidId,
|
|
190
|
+
buyerUid,
|
|
191
|
+
reason: claim.reason,
|
|
192
|
+
});
|
|
193
|
+
throw new ValidationError(message, { code });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
63
198
|
if (!item.offerId)
|
|
64
199
|
continue;
|
|
65
200
|
const offer = await offerRepository.findById(item.offerId);
|
|
@@ -112,5 +247,41 @@ export async function finalizeLockedLines(items, orderId) {
|
|
|
112
247
|
});
|
|
113
248
|
}
|
|
114
249
|
}
|
|
250
|
+
if (item.isBuyout && item.bidId) {
|
|
251
|
+
await closeOutBuyoutLosers(item.auctionId ?? item.productId, item.bidId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* A buyout ended the auction, so everyone still bidding on it lost.
|
|
257
|
+
*
|
|
258
|
+
* Deliberately AFTER the claim transaction rather than inside it: the number of
|
|
259
|
+
* active bids on a hot auction is unbounded, and a transaction that touches all
|
|
260
|
+
* of them would either blow the 500-write limit or contend with every incoming
|
|
261
|
+
* bid. The claim only has to be atomic about who gets the ITEM; tidying up the
|
|
262
|
+
* losers is bookkeeping, so it is bounded, best-effort and logged.
|
|
263
|
+
*
|
|
264
|
+
* Their bids are `lost`, not `outbid` — the auction is over, which is the same
|
|
265
|
+
* distinction `auctionSettlement` draws.
|
|
266
|
+
*/
|
|
267
|
+
async function closeOutBuyoutLosers(productId, winningBidId) {
|
|
268
|
+
try {
|
|
269
|
+
const active = await bidRepository.getActiveByProduct(productId);
|
|
270
|
+
const losers = active.filter((e) => e.id !== winningBidId);
|
|
271
|
+
if (losers.length === 0)
|
|
272
|
+
return;
|
|
273
|
+
await bidRepository.markManyLost(losers.map((e) => e.ref));
|
|
274
|
+
serverLogger.info("finalizeLockedLines: buyout closed out losing bids", {
|
|
275
|
+
productId,
|
|
276
|
+
winningBidId,
|
|
277
|
+
losers: losers.length,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
void normalizeError(err);
|
|
282
|
+
serverLogger.error("finalizeLockedLines: buyout loser close-out failed", {
|
|
283
|
+
productId,
|
|
284
|
+
winningBidId,
|
|
285
|
+
});
|
|
115
286
|
}
|
|
116
287
|
}
|
|
@@ -1,2 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
// `getHomepageInitial` / `getHomepageSections` / `getHeroCarouselSlides` lived
|
|
2
|
+
// in ./data and were deleted 2026-08-24. All three had ZERO call sites — the
|
|
3
|
+
// live homepage is `MarketplaceHomepageView`, which reads its repositories
|
|
4
|
+
// directly — and the only part worth keeping, the feature-flag gate, moved to
|
|
5
|
+
// `_internal/shared/features/homepage/section-gate.ts` and is now actually
|
|
6
|
+
// applied. The rest was a Rule #6 liability: `findActiveAuctions()` /
|
|
7
|
+
// `findActivePreOrders()` were unbounded `.get()` collection scans.
|
|
2
8
|
export { HOMEPAGE_FEATURED_REVIEWS_LIMIT, HOMEPAGE_RECENT_BLOG_POSTS_LIMIT, HOMEPAGE_MAX_SECTIONS, } from "../../../shared/features/homepage/config";
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
/** Strip price and weight from a single slot for client consumption. */
|
|
2
2
|
export function toClientLotterySlot(slot) {
|
|
3
|
+
// Allow-list, not a spread: every field named here is a deliberate public
|
|
4
|
+
// exposure with a known reader (§ Public Data Projections). `image` is read
|
|
5
|
+
// by the prize collage on the lottery detail page.
|
|
3
6
|
return {
|
|
4
7
|
slotNumber: slot.slotNumber,
|
|
5
8
|
name: slot.name,
|
|
9
|
+
image: slot.image,
|
|
6
10
|
isBooked: slot.isBooked,
|
|
7
11
|
bookedByUserLotteryNumber: slot.bookedByUserLotteryNumber,
|
|
8
12
|
bookedByDisplayName: slot.bookedByDisplayName,
|