@mohasinac/appkit 4.53.0 → 4.53.2
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/.build-stamp.json +3 -3
- package/dist/_internal/client/scaffolds/AppShell.js +1 -1
- package/dist/_internal/client/theme/ThemeProvider.js +15 -1
- package/dist/_internal/server/features/auctions/og.d.ts +3 -1
- package/dist/_internal/server/features/auctions/og.js +3 -1
- package/dist/_internal/server/features/bundles/actions.js +1 -1
- package/dist/_internal/server/features/business-documents/download.d.ts +41 -0
- package/dist/_internal/server/features/business-documents/download.js +91 -0
- package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
- package/dist/_internal/server/features/categories/adapters.js +10 -0
- package/dist/_internal/server/features/categories/price-guidance-resolve.d.ts +66 -0
- package/dist/_internal/server/features/categories/price-guidance-resolve.js +138 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +14 -0
- package/dist/_internal/server/features/checkout/actions.js +106 -12
- package/dist/_internal/server/features/checkout/bundle-expansion.js +17 -0
- package/dist/_internal/server/features/checkout/data.d.ts +22 -0
- package/dist/_internal/server/features/checkout/group-rule-revalidation.d.ts +34 -0
- package/dist/_internal/server/features/checkout/group-rule-revalidation.js +124 -0
- package/dist/_internal/server/features/checkout/supplier-identity.d.ts +55 -0
- package/dist/_internal/server/features/checkout/supplier-identity.js +81 -0
- package/dist/_internal/server/features/classified/metadata.js +1 -1
- package/dist/_internal/server/features/classified/og.d.ts +3 -1
- package/dist/_internal/server/features/classified/og.js +3 -1
- package/dist/_internal/server/features/digital-code/metadata.js +1 -1
- package/dist/_internal/server/features/digital-code/og.d.ts +3 -1
- package/dist/_internal/server/features/digital-code/og.js +3 -1
- package/dist/_internal/server/features/digital-code/pool.d.ts +19 -4
- package/dist/_internal/server/features/digital-code/pool.js +19 -4
- package/dist/_internal/server/features/live/metadata.js +1 -1
- package/dist/_internal/server/features/live/og.d.ts +3 -1
- package/dist/_internal/server/features/live/og.js +3 -1
- package/dist/_internal/server/features/messages/actions.js +1 -1
- package/dist/_internal/server/features/parts/og.js +1 -1
- package/dist/_internal/server/features/pre-orders/og.d.ts +3 -1
- package/dist/_internal/server/features/pre-orders/og.js +3 -1
- package/dist/_internal/server/features/price-index/market.d.ts +57 -0
- package/dist/_internal/server/features/price-index/market.js +120 -0
- package/dist/_internal/server/features/price-index/search.d.ts +18 -0
- package/dist/_internal/server/features/price-index/search.js +47 -7
- package/dist/_internal/server/features/prize-draws/og.d.ts +3 -1
- package/dist/_internal/server/features/prize-draws/og.js +3 -1
- package/dist/_internal/server/features/products/group-members.d.ts +26 -0
- package/dist/_internal/server/features/products/group-members.js +7 -1
- package/dist/_internal/server/features/products/list-public.d.ts +12 -0
- package/dist/_internal/server/features/products/list-public.js +10 -1
- package/dist/_internal/server/features/products/listing-tab-counts.d.ts +25 -1
- package/dist/_internal/server/features/products/listing-tab-counts.js +128 -10
- package/dist/_internal/server/features/products/og.d.ts +3 -1
- package/dist/_internal/server/features/products/og.js +3 -1
- package/dist/_internal/server/features/search/scoped-sources.d.ts +26 -0
- package/dist/_internal/server/features/search/scoped-sources.js +252 -0
- package/dist/_internal/server/features/search/search-everything.d.ts +57 -0
- package/dist/_internal/server/features/search/search-everything.js +70 -20
- package/dist/_internal/server/features/seo/sitemap.js +6 -0
- package/dist/_internal/server/features/shared/listing-adapters.js +4 -2
- package/dist/_internal/server/features/sticker-trace/adapters.d.ts +11 -0
- package/dist/_internal/server/features/sticker-trace/adapters.js +1 -0
- package/dist/_internal/server/features/sticker-trace/promote-to-listings.d.ts +26 -0
- package/dist/_internal/server/features/sticker-trace/promote-to-listings.js +264 -0
- package/dist/_internal/server/features/sticker-trace/sheet-actions.d.ts +3 -1
- package/dist/_internal/server/features/sticker-trace/sheet-actions.js +32 -0
- package/dist/_internal/server/features/sticker-trace/storage.d.ts +20 -1
- package/dist/_internal/server/features/sticker-trace/storage.js +20 -1
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +16 -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/features/stores/tabs.d.ts +30 -0
- package/dist/_internal/server/features/stores/tabs.js +124 -0
- package/dist/_internal/shared/checkout/rules/_defaults.js +0 -2
- package/dist/_internal/shared/checkout/rules/_limits.d.ts +14 -1
- package/dist/_internal/shared/checkout/rules/_limits.js +14 -1
- package/dist/_internal/shared/checkout/rules/_registry.js +2 -0
- package/dist/_internal/shared/checkout/rules/auction.rule.js +9 -1
- package/dist/_internal/shared/checkout/rules/bundle.rule.js +13 -0
- package/dist/_internal/shared/checkout/rules/index.d.ts +2 -0
- package/dist/_internal/shared/checkout/rules/index.js +9 -0
- package/dist/_internal/shared/checkout/rules/offer.rule.js +0 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +26 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +133 -0
- package/dist/_internal/shared/checkout/rules/prize-draw.rule.js +29 -8
- package/dist/_internal/shared/checkout/rules/spares.rule.d.ts +16 -0
- package/dist/_internal/shared/checkout/rules/spares.rule.js +5 -0
- package/dist/_internal/shared/checkout/rules/types.d.ts +26 -2
- package/dist/_internal/shared/config/schema.d.ts +122 -0
- package/dist/_internal/shared/features/cart/schema.js +3 -2
- package/dist/_internal/shared/features/products/group-rules.d.ts +75 -0
- package/dist/_internal/shared/features/products/group-rules.js +142 -0
- package/dist/_internal/shared/listing-types/_registry.d.ts +24 -0
- package/dist/_internal/shared/listing-types/_registry.js +39 -1
- package/dist/_internal/shared/listing-types/art/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/art/config.js +2 -0
- package/dist/_internal/shared/listing-types/auction/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/auction/config.js +2 -0
- package/dist/_internal/shared/listing-types/availability.js +48 -0
- package/dist/_internal/shared/listing-types/capabilities.js +19 -0
- package/dist/_internal/shared/listing-types/classified/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/classified/config.js +2 -0
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/digital-code/config.js +2 -0
- package/dist/_internal/shared/listing-types/feature-flags.js +1 -0
- package/dist/_internal/shared/listing-types/live/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/live/config.js +2 -0
- package/dist/_internal/shared/listing-types/pre-order/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/pre-order/config.js +2 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/prize-draw/config.js +2 -0
- package/dist/_internal/shared/listing-types/spares/config.d.ts +74 -0
- package/dist/_internal/shared/listing-types/spares/config.js +60 -0
- package/dist/_internal/shared/listing-types/spares/ctas.d.ts +1 -0
- package/dist/_internal/shared/listing-types/spares/ctas.js +4 -0
- package/dist/_internal/shared/listing-types/spares/og.js +1 -0
- package/dist/_internal/shared/listing-types/spares/schema.d.ts +2 -0
- package/dist/_internal/shared/listing-types/spares/schema.js +3 -0
- package/dist/_internal/shared/listing-types/spares/seed-factory.d.ts +1 -0
- package/dist/_internal/shared/listing-types/spares/seed-factory.js +1 -0
- package/dist/_internal/shared/listing-types/standard/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/standard/config.js +2 -0
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +1 -0
- package/dist/_internal/shared/listing-types/stickers/config.js +2 -0
- package/dist/_internal/shared/search/match.d.ts +11 -0
- package/dist/_internal/shared/search/match.js +30 -3
- package/dist/_internal/shared/search/to-groups.d.ts +52 -0
- package/dist/_internal/shared/search/to-groups.js +141 -0
- package/dist/client.d.ts +8 -2
- package/dist/client.js +4 -1
- package/dist/constants/api-endpoints.d.ts +46 -15
- package/dist/constants/api-endpoints.js +35 -5
- package/dist/constants/cache-invalidation.js +3 -1
- package/dist/constants/field-names.d.ts +6 -0
- package/dist/constants/field-names.js +6 -0
- package/dist/features/about/components/PolicyPageView.d.ts +22 -2
- package/dist/features/about/components/PolicyPageView.js +14 -2
- package/dist/features/about/components/PublicProfileView.js +2 -1
- package/dist/features/about/components/ShippingPolicyView.js +42 -2
- package/dist/features/account/components/NotificationBell.js +1 -1
- package/dist/features/account/hooks/useProfileStats.js +5 -1
- package/dist/features/addresses/repository/addresses.repository.d.ts +16 -0
- package/dist/features/addresses/repository/addresses.repository.js +21 -0
- package/dist/features/admin/actions/admin-actions.js +16 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +19 -2
- package/dist/features/admin/components/AdminNavigationView.js +1 -1
- package/dist/features/admin/components/AdminPageHeader.js +1 -1
- package/dist/features/admin/components/AdminProductEditorView.js +17 -2
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/admin/components/AdminSiteSettingsView.js +39 -8
- package/dist/features/admin/components/AdminSparesView.d.ts +12 -0
- package/dist/features/admin/components/AdminSparesView.js +30 -0
- package/dist/features/admin/components/AdminUsersView.js +9 -7
- package/dist/features/admin/components/index.d.ts +2 -0
- package/dist/features/admin/components/index.js +1 -0
- package/dist/features/admin/components/sections/adminSectionsTypes.js +7 -0
- package/dist/features/admin/constants/filter-tabs.d.ts +3 -0
- package/dist/features/admin/constants/filter-tabs.js +1 -0
- package/dist/features/admin/schemas/firestore.d.ts +26 -2
- package/dist/features/auctions/components/AuctionDetailPageView.js +12 -18
- package/dist/features/auth/repository/user.repository.d.ts +13 -0
- package/dist/features/auth/repository/user.repository.js +28 -0
- package/dist/features/auth/schemas/firestore.d.ts +2 -0
- package/dist/features/auth/schemas/firestore.js +2 -0
- package/dist/features/business-documents/repository/business-documents.repository.d.ts +58 -0
- package/dist/features/business-documents/repository/business-documents.repository.js +54 -0
- package/dist/features/business-documents/schemas/firestore.d.ts +105 -0
- package/dist/features/business-documents/schemas/firestore.js +75 -0
- package/dist/features/cart/actions/cart-actions.js +83 -6
- package/dist/features/cart/repository/cart.repository.js +32 -4
- package/dist/features/cart/schemas/firestore.d.ts +20 -0
- package/dist/features/cart/schemas/index.d.ts +2 -2
- package/dist/features/catalogue/components/PublicCatalogueItemDetailView.js +1 -1
- package/dist/features/categories/api/[id]/price-guidance/route.d.ts +42 -0
- package/dist/features/categories/api/[id]/price-guidance/route.js +90 -0
- package/dist/features/categories/components/BrandDetailPageView.js +32 -8
- package/dist/features/categories/components/BrandDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/BrandDetailTabs.js +28 -3
- package/dist/features/categories/components/BundleCollage.js +3 -2
- package/dist/features/categories/components/BundleItemsPicker.js +3 -4
- package/dist/features/categories/components/CategoriesIndexListing.js +23 -8
- package/dist/features/categories/components/CategoryDetailPageView.js +157 -20
- package/dist/features/categories/components/CategoryDetailTabs.d.ts +7 -1
- package/dist/features/categories/components/CategoryDetailTabs.js +41 -7
- package/dist/features/categories/components/CategoryGrid.js +10 -1
- package/dist/features/categories/components/PriceGuidanceHint.d.ts +29 -0
- package/dist/features/categories/components/PriceGuidanceHint.js +61 -0
- package/dist/features/categories/components/SubcategoryCardRow.d.ts +27 -0
- package/dist/features/categories/components/SubcategoryCardRow.js +98 -0
- package/dist/features/categories/constants/category-tabs.d.ts +20 -25
- package/dist/features/categories/constants/category-tabs.js +22 -39
- package/dist/features/categories/repository/categories.repository.d.ts +12 -0
- package/dist/features/categories/repository/categories.repository.js +20 -0
- package/dist/features/categories/schemas/firestore.d.ts +22 -1
- package/dist/features/categories/schemas/firestore.js +10 -0
- package/dist/features/categories/schemas/index.d.ts +6 -0
- package/dist/features/categories/schemas/index.js +2 -0
- package/dist/features/categories/types/index.d.ts +8 -0
- package/dist/features/categories/utils/taxonomy-visual.d.ts +16 -3
- package/dist/features/categories/utils/taxonomy-visual.js +26 -10
- package/dist/features/classified/components/ClassifiedDetailPageView.js +14 -10
- package/dist/features/contact/components/ContactPageView.d.ts +33 -1
- package/dist/features/contact/components/ContactPageView.js +58 -28
- package/dist/features/digital-codes/components/DigitalCodeDetailPageView.js +14 -10
- package/dist/features/grouped/components/GroupedListingDetailView.js +9 -1
- package/dist/features/grouped/schemas/firestore.d.ts +5 -0
- package/dist/features/homepage/actions/homepage-section-actions.d.ts +3 -3
- package/dist/features/homepage/components/CollectionCardsSection.d.ts +28 -6
- package/dist/features/homepage/components/CollectionCardsSection.js +43 -8
- package/dist/features/homepage/components/HowItWorksSection.d.ts +23 -6
- package/dist/features/homepage/components/HowItWorksSection.js +4 -4
- package/dist/features/homepage/components/PromoGrid.d.ts +31 -2
- package/dist/features/homepage/components/PromoGrid.js +12 -3
- package/dist/features/homepage/components/TestimonialsCarousel.d.ts +34 -2
- package/dist/features/homepage/components/TestimonialsCarousel.js +1 -1
- package/dist/features/homepage/components/index.d.ts +0 -20
- package/dist/features/homepage/components/index.js +0 -11
- package/dist/features/homepage/index.d.ts +0 -2
- package/dist/features/homepage/index.js +0 -2
- package/dist/features/homepage/lib/section-renderer.d.ts +9 -0
- package/dist/features/homepage/lib/section-renderer.js +202 -58
- package/dist/features/homepage/repository/homepage-sections.repository.d.ts +35 -3
- package/dist/features/homepage/repository/homepage-sections.repository.js +58 -8
- package/dist/features/homepage/schemas/firestore.d.ts +106 -3
- package/dist/features/homepage/schemas/firestore.js +32 -2
- package/dist/features/homepage/schemas/index.d.ts +36 -7
- package/dist/features/homepage/schemas/index.js +39 -27
- package/dist/features/homepage/server.d.ts +0 -3
- package/dist/features/homepage/server.js +0 -3
- package/dist/features/homepage/types/index.d.ts +0 -47
- package/dist/features/homepage/types/index.js +49 -0
- package/dist/features/layout/AppLayoutShell.js +1 -1
- package/dist/features/layout/BottomNavItem.js +1 -1
- package/dist/features/layout/BottomSheet.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +1 -1
- package/dist/features/live/components/LiveItemDetailPageView.js +14 -10
- package/dist/features/media/hooks/useMedia.js +17 -0
- package/dist/features/media/repository/media-assets.repository.d.ts +46 -0
- package/dist/features/media/repository/media-assets.repository.js +31 -0
- package/dist/features/media/trust-signals.d.ts +85 -0
- package/dist/features/media/trust-signals.js +126 -0
- package/dist/features/parts/components/PartDetailPageView.d.ts +14 -1
- package/dist/features/parts/components/PartDetailPageView.js +2 -2
- package/dist/features/parts/components/PartsIndexPageView.d.ts +11 -2
- package/dist/features/parts/components/PartsIndexPageView.js +2 -2
- package/dist/features/parts/components/PartsSearchBox.d.ts +9 -0
- package/dist/features/parts/components/PartsSearchBox.js +57 -0
- package/dist/features/parts/schemas/firestore.d.ts +149 -3
- package/dist/features/parts/schemas/firestore.js +184 -0
- package/dist/features/payments/repository/payout.repository.d.ts +15 -0
- package/dist/features/payments/repository/payout.repository.js +21 -0
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +12 -18
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +1 -1
- package/dist/features/price-index/components/PriceIndexToolView.d.ts +24 -1
- package/dist/features/price-index/components/PriceIndexToolView.js +299 -17
- package/dist/features/price-index/lookup-target.d.ts +48 -0
- package/dist/features/price-index/lookup-target.js +107 -0
- package/dist/features/products/components/AvailabilityScopeHint.d.ts +22 -0
- package/dist/features/products/components/AvailabilityScopeHint.js +27 -0
- package/dist/features/products/components/CompareOverlay.d.ts +20 -2
- package/dist/features/products/components/CompareOverlay.js +23 -3
- package/dist/features/products/components/CustomSectionTabContent.d.ts +31 -0
- package/dist/features/products/components/CustomSectionTabContent.js +29 -3
- package/dist/features/products/components/GroupMemberPicker.d.ts +19 -1
- package/dist/features/products/components/GroupMemberPicker.js +46 -8
- package/dist/features/products/components/GroupSettingsPanel.d.ts +11 -1
- package/dist/features/products/components/GroupSettingsPanel.js +92 -3
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.d.ts +15 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +70 -8
- package/dist/features/products/components/ProductDetailPageView.js +81 -17
- package/dist/features/products/components/ProductForm.d.ts +13 -1
- package/dist/features/products/components/ProductForm.js +16 -2
- package/dist/features/products/components/ProductsIndexListing.d.ts +16 -1
- package/dist/features/products/components/ProductsIndexListing.js +3 -3
- package/dist/features/products/components/ShowGroupSection.js +15 -2
- package/dist/features/products/components/SparesListView.d.ts +7 -0
- package/dist/features/products/components/SparesListView.js +39 -0
- package/dist/features/products/components/SublistingCarouselSection.js +2 -1
- package/dist/features/products/constants/action-defs.d.ts +1 -0
- package/dist/features/products/constants/action-defs.js +1 -0
- package/dist/features/products/repository/products.repository.d.ts +99 -0
- package/dist/features/products/repository/products.repository.js +240 -18
- package/dist/features/products/schemas/firestore.d.ts +97 -4
- package/dist/features/products/schemas/firestore.js +104 -0
- package/dist/features/products/schemas/index.d.ts +10 -19
- package/dist/features/products/schemas/index.js +23 -6
- package/dist/features/products/types/index.d.ts +45 -1
- package/dist/features/products/utils/group-member-image.d.ts +46 -0
- package/dist/features/products/utils/group-member-image.js +4 -0
- package/dist/features/products/utils/listing-badge-variant.js +4 -0
- package/dist/features/reviews/components/ReviewsList.js +1 -1
- package/dist/features/scams/components/ScamProfileView.js +1 -1
- package/dist/features/search/components/EntityTypeahead.d.ts +44 -0
- package/dist/features/search/components/EntityTypeahead.js +92 -0
- package/dist/features/search/components/Search.d.ts +12 -1
- package/dist/features/search/components/Search.js +47 -3
- package/dist/features/search/components/SearchFacetRail.d.ts +11 -0
- package/dist/features/search/components/SearchFacetRail.js +92 -0
- package/dist/features/search/components/SearchResultsClient.d.ts +33 -0
- package/dist/features/search/components/SearchResultsClient.js +103 -0
- package/dist/features/search/components/SearchResultsPageView.d.ts +15 -1
- package/dist/features/search/components/SearchResultsPageView.js +2 -2
- package/dist/features/search/components/index.d.ts +0 -6
- package/dist/features/search/components/index.js +0 -3
- package/dist/features/search/hooks/useNavSuggestions.d.ts +13 -0
- package/dist/features/search/hooks/useScopedSearch.d.ts +34 -0
- package/dist/features/search/hooks/useScopedSearch.js +114 -0
- package/dist/features/search/hooks/useSiteSearch.d.ts +26 -0
- package/dist/features/search/hooks/useSiteSearch.js +149 -0
- package/dist/features/search/index.d.ts +0 -4
- package/dist/features/search/index.js +0 -4
- package/dist/features/search/manifest.d.ts +12 -0
- package/dist/features/search/manifest.js +17 -2
- package/dist/features/search/server.d.ts +0 -3
- package/dist/features/search/server.js +0 -3
- package/dist/features/seller/components/SellerListingTypeView.d.ts +1 -1
- package/dist/features/seller/components/SellerListingTypeView.js +20 -0
- package/dist/features/seller/components/SellerPayoutSettingsView.js +35 -14
- package/dist/features/seller/components/SellerProductShell.d.ts +1 -1
- package/dist/features/seller/components/SellerProductShell.js +2 -1
- package/dist/features/seller/components/SellerProductsView.js +3 -2
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/seller/components/SellerSparesView.d.ts +17 -0
- package/dist/features/seller/components/SellerSparesView.js +6 -0
- package/dist/features/seller/components/index.d.ts +2 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/shell/CommandPalette.js +1 -1
- package/dist/features/sticker-trace/schemas/firestore.d.ts +19 -0
- package/dist/features/stores/components/StoreAuctionsListing.js +2 -1
- package/dist/features/stores/components/StoreDetailLayoutView.d.ts +1 -1
- package/dist/features/stores/components/StoreDetailLayoutView.js +7 -82
- package/dist/features/stores/components/StoreHeader.js +1 -2
- package/dist/features/stores/components/StoreSparesPageView.d.ts +27 -0
- package/dist/features/stores/components/StoreSparesPageView.js +48 -0
- package/dist/features/stores/schemas/firestore.d.ts +19 -0
- package/dist/features/tester/seed-data/authored/buying__spares-and-groups.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/buying__spares-and-groups.js +96 -0
- package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__authenticity-checker.js +392 -45
- package/dist/features/tester/seed-data/authored/content-discovery__category-tabs-franchise-bundles.js +54 -32
- package/dist/features/tester/seed-data/authored/content-discovery__parts-database.js +3 -2
- package/dist/features/tester/seed-data/authored/content-discovery__price-index-and-compare.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__price-index-and-compare.js +73 -0
- package/dist/features/tester/seed-data/authored/content-discovery__site-wide-index.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/content-discovery__site-wide-index.js +340 -0
- package/dist/features/tester/seed-data/authored/index.js +16 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__header-search.d.ts +1 -0
- package/dist/features/tester/seed-data/authored/search-and-nav__header-search.js +57 -0
- package/dist/features/tester/seed-data/authored/selling__spares-and-group-editing.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/selling__spares-and-group-editing.js +77 -0
- package/dist/features/tester/seed-data/authored/tools__sticker-promote-to-listings.d.ts +2 -0
- package/dist/features/tester/seed-data/authored/tools__sticker-promote-to-listings.js +58 -0
- package/dist/features/tester/seed-data/authored/tools__sticker-tracer.js +5 -4
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +384 -18
- package/dist/index.d.ts +17 -57
- package/dist/index.js +35 -96
- package/dist/next/routing/route-map.d.ts +42 -1
- package/dist/next/routing/route-map.js +45 -0
- package/dist/providers/db-firebase/base.repository.d.ts +22 -0
- package/dist/providers/db-firebase/base.repository.js +36 -0
- package/dist/schemas/registry.d.ts +15 -6
- package/dist/security/blind-index-tokens.d.ts +54 -0
- package/dist/security/blind-index-tokens.js +152 -0
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/settings-encryption.d.ts +35 -0
- package/dist/security/settings-encryption.js +91 -0
- package/dist/seed/_helpers/brand-rows.js +38 -14
- package/dist/seed/_helpers/category-forest.js +73 -7
- package/dist/seed/_helpers/category-images-generated.d.ts +25 -6
- package/dist/seed/_helpers/category-images-generated.js +81 -6
- package/dist/seed/_helpers/category-models.d.ts +1 -1
- package/dist/seed/_helpers/category-models.js +7 -1
- package/dist/seed/_helpers/category-parts-glossary-map.js +8 -0
- package/dist/seed/_helpers/category-parts.d.ts +1 -1
- package/dist/seed/_helpers/category-parts.js +50 -2
- package/dist/seed/_helpers/character-features.d.ts +1 -1
- package/dist/seed/_helpers/character-features.js +2 -1
- package/dist/seed/_helpers/franchise-rows.js +10 -2
- package/dist/seed/_helpers/media.d.ts +47 -0
- package/dist/seed/_helpers/media.js +34 -0
- package/dist/seed/authenticity-knowledge-seed-data.d.ts +49 -5
- package/dist/seed/authenticity-knowledge-seed-data.js +10001 -549
- package/dist/seed/categories-seed-data.js +124 -20
- package/dist/seed/grouped-listings-seed-data.js +16 -0
- package/dist/seed/homepage-sections-seed-data.js +94 -5
- package/dist/seed/index.d.ts +2 -3
- package/dist/seed/index.js +2 -2
- package/dist/seed/manifest.js +3 -0
- package/dist/seed/media-assets-seed-data.d.ts +1 -1
- package/dist/seed/media-assets-seed-data.js +746 -196
- package/dist/seed/model-composition-seed-data.js +122 -6
- package/dist/seed/part-glossary-seed-data.d.ts +1 -1
- package/dist/seed/part-glossary-seed-data.js +37219 -463
- package/dist/seed/products-art-seed-data.js +7 -18
- package/dist/seed/products-auctions-seed-data.js +56 -47
- package/dist/seed/products-beyblade-x-seed-data.d.ts +2 -0
- package/dist/seed/products-beyblade-x-seed-data.js +476 -0
- package/dist/seed/products-catalogue-seed-data.d.ts +5 -5
- package/dist/seed/products-catalogue-seed-data.js +1500 -1240
- package/dist/seed/products-classifieds-seed-data.js +9 -27
- package/dist/seed/products-digital-codes-seed-data.js +9 -18
- package/dist/seed/products-live-items-seed-data.js +5 -13
- package/dist/seed/products-preorders-seed-data.js +67 -33
- package/dist/seed/products-prize-draws-seed-data.js +8 -17
- package/dist/seed/products-spares-seed-data.d.ts +2 -0
- package/dist/seed/products-spares-seed-data.js +368 -0
- package/dist/seed/products-standard-seed-data.js +39 -126
- package/dist/seed/products-stickers-seed-data.js +59 -15
- package/dist/seed/site-settings-seed-data.js +8 -0
- package/dist/seo/json-ld.d.ts +25 -0
- package/dist/seo/json-ld.js +28 -0
- package/dist/server-entry.d.ts +3 -1
- package/dist/server-entry.js +2 -2
- package/dist/server.d.ts +11 -9
- package/dist/server.js +35 -25
- package/dist/styles.css +202 -1
- package/dist/tailwind-input.css +31 -17
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Accordion.js +1 -1
- package/dist/ui/components/Breadcrumb.js +1 -1
- package/dist/ui/components/DetailPageGallery.js +0 -2
- package/dist/ui/components/DetailPageHero.js +0 -1
- package/dist/ui/components/DetailPageTabs.js +1 -1
- package/dist/ui/components/Divider.js +3 -3
- package/dist/ui/components/GatedPrice.js +5 -1
- package/dist/ui/components/HorizontalScroller.js +1 -1
- package/dist/ui/components/ListingLayout.js +3 -3
- package/dist/ui/components/MediaTabs.style.css +81 -0
- package/dist/ui/components/Pagination.js +1 -1
- package/dist/ui/components/PendingValue.d.ts +45 -0
- package/dist/ui/components/PendingValue.js +7 -0
- package/dist/ui/components/Radio.style.css +119 -0
- package/dist/ui/components/RatingDisplay.js +2 -2
- package/dist/ui/components/SettingsSection.js +0 -2
- package/dist/ui/components/SiteLogo.js +3 -0
- package/dist/ui/components/SiteMark.js +3 -0
- package/dist/ui/components/SortDropdown.js +1 -1
- package/dist/ui/components/SpecTable.d.ts +58 -0
- package/dist/ui/components/SpecTable.js +8 -0
- package/dist/ui/components/StarRating.js +1 -3
- package/dist/ui/components/SummaryCard.js +1 -1
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/icons/icon-registry.d.ts +1 -0
- package/dist/ui/icons/icon-registry.js +9 -1
- package/dist/ui/index.d.ts +4 -0
- package/dist/ui/index.js +2 -0
- package/dist/utils/search-txt-query.d.ts +12 -0
- package/dist/utils/search-txt-query.js +12 -0
- package/package.json +1 -1
- package/scripts/dedupe-peer-deps.mjs +18 -0
- package/scripts/seed-cli.mjs +250 -35
- package/scripts/verify-css-build.mjs +118 -37
- package/dist/_internal/server/features/search/actions.d.ts +0 -7
- package/dist/_internal/server/features/search/actions.js +0 -21
- package/dist/_internal/server/features/search/data.d.ts +0 -15
- package/dist/_internal/server/features/search/data.js +0 -18
- package/dist/_internal/server/features/search/index.d.ts +0 -3
- package/dist/_internal/server/features/search/index.js +0 -2
- package/dist/features/homepage/api/[id]/route.d.ts +0 -23
- package/dist/features/homepage/api/[id]/route.js +0 -103
- package/dist/features/homepage/api/route.d.ts +0 -14
- package/dist/features/homepage/api/route.js +0 -140
- package/dist/features/homepage/api/sections/[id]/route.d.ts +0 -26
- package/dist/features/homepage/api/sections/[id]/route.js +0 -95
- package/dist/features/homepage/api/sections/route.d.ts +0 -13
- package/dist/features/homepage/api/sections/route.js +0 -82
- package/dist/features/homepage/components/AdvertisementBanner.d.ts +0 -14
- package/dist/features/homepage/components/AdvertisementBanner.js +0 -35
- package/dist/features/homepage/components/BeforeAfterCard.d.ts +0 -9
- package/dist/features/homepage/components/BeforeAfterCard.js +0 -73
- package/dist/features/homepage/components/FeaturedResultsSection.d.ts +0 -17
- package/dist/features/homepage/components/FeaturedResultsSection.js +0 -17
- package/dist/features/homepage/components/HeroBanner.d.ts +0 -7
- package/dist/features/homepage/components/HeroBanner.js +0 -43
- package/dist/features/homepage/components/HeroSection.d.ts +0 -7
- package/dist/features/homepage/components/HeroSection.js +0 -6
- package/dist/features/homepage/components/HomepageSkeleton.d.ts +0 -2
- package/dist/features/homepage/components/HomepageSkeleton.js +0 -21
- package/dist/features/homepage/components/HomepageView.d.ts +0 -25
- package/dist/features/homepage/components/HomepageView.js +0 -5
- package/dist/features/homepage/components/NewsletterBanner.d.ts +0 -23
- package/dist/features/homepage/components/NewsletterBanner.js +0 -37
- package/dist/features/homepage/components/SiteFeaturesSection.d.ts +0 -14
- package/dist/features/homepage/components/SiteFeaturesSection.js +0 -11
- package/dist/features/homepage/components/TrustBadges.d.ts +0 -6
- package/dist/features/homepage/components/TrustBadges.js +0 -46
- package/dist/features/homepage/components/TrustIndicatorsSection.d.ts +0 -12
- package/dist/features/homepage/components/TrustIndicatorsSection.js +0 -11
- package/dist/features/homepage/hooks/useHomepage.d.ts +0 -11
- package/dist/features/homepage/hooks/useHomepage.js +0 -17
- package/dist/features/homepage/hooks/useHomepageSections.d.ts +0 -2
- package/dist/features/homepage/hooks/useHomepageSections.js +0 -10
- package/dist/features/homepage/repository/homepage.repository.d.ts +0 -11
- package/dist/features/homepage/repository/homepage.repository.js +0 -25
- package/dist/features/search/actions/index.d.ts +0 -1
- package/dist/features/search/actions/index.js +0 -1
- package/dist/features/search/actions/search-actions.d.ts +0 -18
- package/dist/features/search/actions/search-actions.js +0 -43
- package/dist/features/search/api/route.d.ts +0 -22
- package/dist/features/search/api/route.js +0 -128
- package/dist/features/search/columns/index.d.ts +0 -17
- package/dist/features/search/columns/index.js +0 -49
- package/dist/features/search/components/SearchFiltersRow.d.ts +0 -22
- package/dist/features/search/components/SearchFiltersRow.js +0 -21
- package/dist/features/search/components/SearchResultsSection.d.ts +0 -53
- package/dist/features/search/components/SearchResultsSection.js +0 -35
- package/dist/features/search/components/SearchView.d.ts +0 -27
- package/dist/features/search/components/SearchView.js +0 -8
- package/dist/features/search/hooks/useSearch.d.ts +0 -21
- package/dist/features/search/hooks/useSearch.js +0 -33
- package/dist/features/search/repository/search.repository.d.ts +0 -14
- package/dist/features/search/repository/search.repository.js +0 -59
- package/dist/features/search/schemas/index.d.ts +0 -70
- package/dist/features/search/schemas/index.js +0 -36
- package/dist/features/search/types/index.d.ts +0 -48
- package/dist/features/stores/components/StoreScopedSearch.d.ts +0 -17
- package/dist/features/stores/components/StoreScopedSearch.js +0 -27
- package/dist/seed/defaults/homepage-sections.d.ts +0 -2
- package/dist/seed/defaults/homepage-sections.js +0 -60
- package/dist/seed/factories/homepage-section.factory.d.ts +0 -10
- package/dist/seed/factories/homepage-section.factory.js +0 -13
- /package/dist/{features/search/types/index.js → _internal/shared/listing-types/spares/og.d.ts} +0 -0
|
@@ -8,7 +8,7 @@ const __P = {
|
|
|
8
8
|
p4: "p-[var(--appkit-space-4)]",
|
|
9
9
|
p5: "p-[var(--appkit-space-5)]",
|
|
10
10
|
};
|
|
11
|
-
const CLS_BREADCRUMB_LINK = "hover:text-primary-600 transition-colors";
|
|
11
|
+
const CLS_BREADCRUMB_LINK = "hover:text-primary-600 dark:hover:text-primary-400 transition-colors";
|
|
12
12
|
const CLS_LIVE_BADGE = "inline-block rounded-full bg-warning-surface dark:bg-warning-surface px-[var(--appkit-space-2-5)] py-[var(--appkit-space-0-5)] text-warning dark:text-warning";
|
|
13
13
|
import { ROUTES } from "../../../next/routing/route-map.js";
|
|
14
14
|
import { getDefaultCurrency } from "../../../core/baseline-resolver.js";
|
|
@@ -17,16 +17,17 @@ import { normalizeRichTextHtml } from "../../../utils/string.formatter.js";
|
|
|
17
17
|
import { safeDisplayName } from "../../../security/index.js";
|
|
18
18
|
import { getSiteSettingsGlobal } from "../../admin/utils/getSiteSettingsGlobal.js";
|
|
19
19
|
import { isBuyNowAvailable, resolveMinBid, resolveMinBidIncrement, } from "../../../_internal/shared/features/auctions/config.js";
|
|
20
|
-
import { Button, Container, CountdownDisplay, Div, GatedPrice, PricesOnly, Heading, Input, Main, Nav, RichText, Row, Section, Span, Stack, Text, Ul, Li,
|
|
20
|
+
import { Button, Container, CountdownDisplay, Div, GatedPrice, PricesOnly, Heading, Input, Main, Nav, RichText, Row, Section, Span, Stack, Text, Ul, Li, SpecTable, } from "../../../ui/index.js";
|
|
21
21
|
import { AuctionDetailView } from "../../products/components/AuctionDetailView.js";
|
|
22
22
|
import { ProductTabsShell } from "../../products/components/ProductTabsShell.js";
|
|
23
|
-
import {
|
|
23
|
+
import { toCustomSectionTabs } from "../../products/components/CustomSectionTabContent.js";
|
|
24
24
|
import { AuctionBottomActions } from "./AuctionBottomActions.js";
|
|
25
25
|
import { AuctionLiveStatusBadge } from "./AuctionLiveStatusBadge.js";
|
|
26
26
|
import { RelatedProducts } from "../../products/components/RelatedProducts.js";
|
|
27
27
|
import { ProductGalleryClient } from "../../products/components/ProductGalleryClient.js";
|
|
28
28
|
import { ProductFeatureBadges } from "../../products/components/ProductFeatureBadges.js";
|
|
29
29
|
import { FeatureBadgeList } from "../../products/components/FeatureBadge.js";
|
|
30
|
+
import { productImageUrls } from "../../products/schemas/firestore.js";
|
|
30
31
|
import { HistoryTracker } from "../../history/components/HistoryTracker.js";
|
|
31
32
|
import { ShareButton } from "../../products/components/ShareButton.js";
|
|
32
33
|
import { MarketplaceAuctionGrid } from "./MarketplaceAuctionGrid.js";
|
|
@@ -149,8 +150,12 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
149
150
|
? p.price
|
|
150
151
|
: 0;
|
|
151
152
|
const minBidIncrementOverride = typeof p.minBidIncrement === "number" ? p.minBidIncrement : undefined;
|
|
152
|
-
|
|
153
|
-
|
|
153
|
+
// `images` is `ProductImage[]` since A6 — `productImageUrls` is the one
|
|
154
|
+
// normaliser (it also accepts the legacy `string[]`). Casting it to
|
|
155
|
+
// `string[]` handed the gallery objects and rendered a placeholder.
|
|
156
|
+
const galleryUrls = productImageUrls(p.images);
|
|
157
|
+
const images = galleryUrls.length > 0
|
|
158
|
+
? galleryUrls
|
|
154
159
|
: typeof p.mainImage === "string"
|
|
155
160
|
? [p.mainImage]
|
|
156
161
|
: [];
|
|
@@ -184,9 +189,6 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
184
189
|
const specs = Array.isArray(p.specifications)
|
|
185
190
|
? p.specifications
|
|
186
191
|
: [];
|
|
187
|
-
const customSections = Array.isArray(p.customSections)
|
|
188
|
-
? p.customSections
|
|
189
|
-
: [];
|
|
190
192
|
const descriptionHtml = toDescriptionHtml(p.description);
|
|
191
193
|
const sublistingCategoryId = typeof p.sublistingCategoryId === "string" ? p.sublistingCategoryId : null;
|
|
192
194
|
const relatedDocs = (await safeRead(() => productRepository.findByCategory(String(p.category ?? "")), {
|
|
@@ -209,11 +211,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
209
211
|
});
|
|
210
212
|
return (_jsxs(Main, { children: [_jsx(HistoryTracker, { productId: String(p.id ?? p.slug ?? ""), productType: "auction", snapshot: {
|
|
211
213
|
title: typeof p.title === "string" ? p.title : undefined,
|
|
212
|
-
thumb: typeof p.mainImage === "string"
|
|
213
|
-
? p.mainImage
|
|
214
|
-
: Array.isArray(p.images) && typeof p.images[0] === "string"
|
|
215
|
-
? p.images[0]
|
|
216
|
-
: undefined,
|
|
214
|
+
thumb: typeof p.mainImage === "string" ? p.mainImage : images[0],
|
|
217
215
|
price: typeof p.price === "number" ? p.price : undefined,
|
|
218
216
|
storeId: typeof p.storeId === "string" ? p.storeId : undefined,
|
|
219
217
|
storeName: typeof p.storeName === "string" ? p.storeName : undefined,
|
|
@@ -230,11 +228,7 @@ export async function AuctionDetailPageView({ id, initialAuction, onPlaceBid, on
|
|
|
230
228
|
// prompt here would be the same affordance twice.
|
|
231
229
|
_jsx(PricesOnly, { children: _jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Buy Now available at ", formatCurrency(buyNowPrice, currency)] }) }))] }), tags.length > 0 && (_jsx(Div, { border: "default", className: "border-t", padding: "t-md", children: _jsx(Row, { wrap: true, gap: "xs", children: tags.map((tag) => (_jsx(Span, { size: "xs", rounded: "full", padding: "pill-sm-tall", surface: "subtle", color: "muted", children: tag }, tag))) }) }))] })), renderMobileBidForm: () => !isEnded && onPlaceBid ? (_jsxs(Stack, { className: `lg:hidden ${__P.p4}`, border: "subtle", gap: "3", rounded: "xl", surface: "muted", children: [_jsx(LiveBidPrice, { productId: String(product.id), currentBid: currentBid, bidCount: bidCount, currency: currency, isEnded: isEnded, priceSize: "base", badgeSize: "xs" }), endDate && (_jsxs(Text, { align: "center", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(PlaceBidModalButton, { productId: String(product.id), currentBid: currentBid, startingBid: startingBid, tiers: bidIncrementTiers, minBidIncrementOverride: minBidIncrementOverride, currency: currency, isEnded: isEnded, auctionEndDate: endDate, buyNowPrice: buyNowPrice, bidCount: bidCount, tags: tags, onPlaceBid: onPlaceBid, onBuyNow: onBuyNow, triggerClassName: "w-full" })] })) : !isEnded ? (_jsxs(Div, { className: `${__P.p4} lg:hidden`, border: "subtle", rounded: "xl", surface: "muted", children: [_jsxs(Row, { align: "center", gap: "sm", className: "mb-3", children: [_jsx(Span, { size: "base", weight: "bold", className: "text-primary-600 dark:text-primary-400", children: _jsx(GatedPrice, { label: "Sign in to see the current bid", children: formatCurrency(currentBid, currency) }) }), _jsxs(Span, { size: "xs", color: "muted", children: [bidCount, " bids"] })] }), endDate && (_jsxs(Text, { align: "center", className: "mb-3", size: "xs", color: "muted", children: ["Ends in ", _jsx(CountdownDisplay, { targetDate: endDate, format: "auto", expiredLabel: "Ended" })] })), _jsx(Button, { variant: "primary", size: "md", className: "w-full", children: "Place Bid" })] })) : null, renderSublistingSection: sublistingCategoryId
|
|
232
230
|
? () => (_jsx(SublistingCarouselSection, { sublistingCategoryId: sublistingCategoryId, currentListingId: String(product.id) }))
|
|
233
|
-
: undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(
|
|
234
|
-
id: s.id,
|
|
235
|
-
label: s.title,
|
|
236
|
-
content: _jsx(CustomSectionTabContent, { section: s }),
|
|
237
|
-
})) })), renderBidHistory: () => {
|
|
231
|
+
: undefined, renderTabs: () => (_jsx(ProductTabsShell, { descriptionContent: descriptionHtml ? (_jsx(RichText, { html: descriptionHtml, proseClass: "prose prose-sm sm:prose max-w-none dark:prose-invert", className: "text-[var(--appkit-color-text-muted)]" })) : undefined, specsContent: specs.length > 0 ? (_jsx(SpecTable, { rows: specs })) : undefined, customTabs: toCustomSectionTabs(p.customSections) })), renderBidHistory: () => {
|
|
238
232
|
if (!bidsResult)
|
|
239
233
|
return null;
|
|
240
234
|
return (_jsx(CollapsibleBidHistory, { productId: String(product.id), initialData: bidsResult, currency: currency }));
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { JsonValue } from "../../../schemas/types";
|
|
1
2
|
import { BaseRepository, type FirebaseSieveFields, type FirebaseSieveResult, type SieveModel } from "../../../providers/db-firebase";
|
|
2
3
|
import type { UserRole } from "../types";
|
|
3
4
|
import { type UserDocument, type UserPlain } from "../schemas";
|
|
@@ -46,6 +47,18 @@ export declare class UserRepository extends BaseRepository<UserDocument> {
|
|
|
46
47
|
*/
|
|
47
48
|
protected piiFields: readonly ["email", "phoneNumber", "googleLinkedEmail"];
|
|
48
49
|
protected piiIndexMap: Record<string, string>;
|
|
50
|
+
/**
|
|
51
|
+
* Admin search by first/last name or email local-part, without decryption.
|
|
52
|
+
* `displayName` and `email` only — `phoneNumber` is excluded on purpose: a
|
|
53
|
+
* phone number has no word boundaries, so tokenising it yields one token
|
|
54
|
+
* that is the whole number, an exact lookup `phoneIndex` already covers.
|
|
55
|
+
*
|
|
56
|
+
* Running through `applyWriteHooks` is what makes a `users` row that
|
|
57
|
+
* SURVIVES a tester cascade (it is PRESERVE-tier and never wiped) keep a
|
|
58
|
+
* correct index going forward, and what makes a NEWLY signed-up real user
|
|
59
|
+
* searchable without anyone having to remember to re-run the backfill.
|
|
60
|
+
*/
|
|
61
|
+
protected buildTokensIndexFor(data: Record<string, JsonValue>): Record<string, string[]> | null;
|
|
49
62
|
/**
|
|
50
63
|
* The base hook, plus the nested objects it cannot see.
|
|
51
64
|
*
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
2
|
+
import { buildTokensIndex } from "../../../security/blind-index-tokens.js";
|
|
2
3
|
import { DatabaseError } from "../../../errors/index.js";
|
|
3
4
|
import { BaseRepository, getFirestoreCount, prepareForFirestore, parseSieveDateValue, } from "../../../providers/db-firebase/index.js";
|
|
4
5
|
import { USER_PII_FIELDS, USER_PII_INDEX_MAP, decryptPayoutDetails, maskPii, revealPii, decryptShippingConfig, encryptPayoutDetails, encryptShippingConfig, piiBlindIndex, } from "../../../security/index.js";
|
|
@@ -89,6 +90,24 @@ export class UserRepository extends BaseRepository {
|
|
|
89
90
|
.filter(([, v]) => typeof v === "string")
|
|
90
91
|
.map(([k]) => k));
|
|
91
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Admin search by first/last name or email local-part, without decryption.
|
|
95
|
+
* `displayName` and `email` only — `phoneNumber` is excluded on purpose: a
|
|
96
|
+
* phone number has no word boundaries, so tokenising it yields one token
|
|
97
|
+
* that is the whole number, an exact lookup `phoneIndex` already covers.
|
|
98
|
+
*
|
|
99
|
+
* Running through `applyWriteHooks` is what makes a `users` row that
|
|
100
|
+
* SURVIVES a tester cascade (it is PRESERVE-tier and never wiped) keep a
|
|
101
|
+
* correct index going forward, and what makes a NEWLY signed-up real user
|
|
102
|
+
* searchable without anyone having to remember to re-run the backfill.
|
|
103
|
+
*/
|
|
104
|
+
buildTokensIndexFor(data) {
|
|
105
|
+
const displayName = typeof data.displayName === "string" ? data.displayName : undefined;
|
|
106
|
+
const email = typeof data.email === "string" ? data.email : undefined;
|
|
107
|
+
if (!displayName && !email)
|
|
108
|
+
return null;
|
|
109
|
+
return { nameTokensIndex: buildTokensIndex(displayName, email) };
|
|
110
|
+
}
|
|
92
111
|
/**
|
|
93
112
|
* The base hook, plus the nested objects it cannot see.
|
|
94
113
|
*
|
|
@@ -381,6 +400,15 @@ UserRepository.SIEVE_FIELDS = {
|
|
|
381
400
|
// filter matched every employee. The composite indexes it needs are already
|
|
382
401
|
// declared and deployed — (permissionGroup, role, createdAt|displayName).
|
|
383
402
|
permissionGroup: { canFilter: true, canSort: false },
|
|
403
|
+
/*
|
|
404
|
+
* Whole-word admin name search. `canSort: false` — an array field has no
|
|
405
|
+
* sort order Firestore can express. Without this entry, `@=` (Firestore
|
|
406
|
+
* `array-contains`) on `nameTokensIndex` is dropped SILENTLY by Sieve
|
|
407
|
+
* (`throwExceptions: false`), and the clause after it goes with it —
|
|
408
|
+
* which here would be nothing, but is the exact mechanism Root Cause #62
|
|
409
|
+
* documents for every other field this has happened to.
|
|
410
|
+
*/
|
|
411
|
+
nameTokensIndex: { canFilter: true, canSort: false },
|
|
384
412
|
createdAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
|
|
385
413
|
updatedAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
|
|
386
414
|
};
|
|
@@ -271,6 +271,8 @@ export declare const USER_FIELDS: {
|
|
|
271
271
|
readonly DISPLAY_NAME: "displayName";
|
|
272
272
|
readonly EMAIL_INDEX: "emailIndex";
|
|
273
273
|
readonly PHONE_INDEX: "phoneIndex";
|
|
274
|
+
/** Whole-word HMAC digests of `displayName` + `email`. See `blind-index-tokens.ts`. */
|
|
275
|
+
readonly NAME_TOKENS_INDEX: "nameTokensIndex";
|
|
274
276
|
readonly PHOTO_URL: "photoURL";
|
|
275
277
|
readonly AVATAR_METADATA: "avatarMetadata";
|
|
276
278
|
readonly ROLE: "role";
|
|
@@ -102,6 +102,8 @@ export const USER_FIELDS = {
|
|
|
102
102
|
DISPLAY_NAME: "displayName",
|
|
103
103
|
EMAIL_INDEX: "emailIndex",
|
|
104
104
|
PHONE_INDEX: "phoneIndex",
|
|
105
|
+
/** Whole-word HMAC digests of `displayName` + `email`. See `blind-index-tokens.ts`. */
|
|
106
|
+
NAME_TOKENS_INDEX: "nameTokensIndex",
|
|
105
107
|
PHOTO_URL: "photoURL",
|
|
106
108
|
AVATAR_METADATA: "avatarMetadata",
|
|
107
109
|
ROLE: "role",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BusinessDocumentsRepository — thin wrapper over the `businessDocuments`
|
|
3
|
+
* collection. See `../schemas/firestore.ts` for the doc shape and for why the
|
|
4
|
+
* bytes are not in Firestore.
|
|
5
|
+
*/
|
|
6
|
+
import { BaseRepository, parseSieveDateValue, type SieveModel, type FirebaseSieveResult } from "../../../providers/db-firebase";
|
|
7
|
+
import { type BusinessDocumentDocument, type BusinessDocumentKind } from "../schemas/firestore";
|
|
8
|
+
export declare class BusinessDocumentsRepository extends BaseRepository<BusinessDocumentDocument> {
|
|
9
|
+
constructor();
|
|
10
|
+
static readonly SIEVE_FIELDS: {
|
|
11
|
+
id: {
|
|
12
|
+
canFilter: boolean;
|
|
13
|
+
canSort: boolean;
|
|
14
|
+
};
|
|
15
|
+
kind: {
|
|
16
|
+
canFilter: boolean;
|
|
17
|
+
canSort: boolean;
|
|
18
|
+
};
|
|
19
|
+
fileName: {
|
|
20
|
+
canFilter: boolean;
|
|
21
|
+
canSort: boolean;
|
|
22
|
+
};
|
|
23
|
+
uploadedBy: {
|
|
24
|
+
canFilter: boolean;
|
|
25
|
+
canSort: boolean;
|
|
26
|
+
};
|
|
27
|
+
uploadedAt: {
|
|
28
|
+
canFilter: boolean;
|
|
29
|
+
canSort: boolean;
|
|
30
|
+
parseValue: typeof parseSieveDateValue;
|
|
31
|
+
};
|
|
32
|
+
createdAt: {
|
|
33
|
+
canFilter: boolean;
|
|
34
|
+
canSort: boolean;
|
|
35
|
+
parseValue: typeof parseSieveDateValue;
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
list(model: SieveModel): Promise<FirebaseSieveResult<BusinessDocumentDocument>>;
|
|
39
|
+
/**
|
|
40
|
+
* The current document of a given kind — newest `uploadedAt` wins.
|
|
41
|
+
*
|
|
42
|
+
* Re-uploading a re-issued certificate is normal and both copies are kept:
|
|
43
|
+
* the superseded one is the evidence of what was true at the time, which is
|
|
44
|
+
* the entire point of keeping a registration document. So this resolves the
|
|
45
|
+
* current one rather than assuming a single row per kind.
|
|
46
|
+
*
|
|
47
|
+
* 🛑 Ordered by `createdAt`, NOT `uploadedAt`, even though the latter reads
|
|
48
|
+
* more naturally here. `BaseRepository.create()` stamps `createdAt` on every
|
|
49
|
+
* document it writes, so it is guaranteed present; `uploadedAt` is set by the
|
|
50
|
+
* caller and a single write path that forgot it would make that row vanish
|
|
51
|
+
* from this query — a Firestore `orderBy` on a field a document lacks is a
|
|
52
|
+
* silent `WHERE field IS NOT NULL` (Root Cause #100). The two are written in
|
|
53
|
+
* the same operation and agree, so there is nothing to gain by taking the
|
|
54
|
+
* field that can be missed.
|
|
55
|
+
*/
|
|
56
|
+
findCurrentByKind(kind: BusinessDocumentKind): Promise<BusinessDocumentDocument | null>;
|
|
57
|
+
}
|
|
58
|
+
export declare const businessDocumentsRepository: BusinessDocumentsRepository;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BusinessDocumentsRepository — thin wrapper over the `businessDocuments`
|
|
3
|
+
* collection. See `../schemas/firestore.ts` for the doc shape and for why the
|
|
4
|
+
* bytes are not in Firestore.
|
|
5
|
+
*/
|
|
6
|
+
import { BaseRepository, parseSieveDateValue, } from "../../../providers/db-firebase/index.js";
|
|
7
|
+
import { BUSINESS_DOCUMENTS_COLLECTION, } from "../schemas/firestore.js";
|
|
8
|
+
export class BusinessDocumentsRepository extends BaseRepository {
|
|
9
|
+
constructor() {
|
|
10
|
+
super(BUSINESS_DOCUMENTS_COLLECTION);
|
|
11
|
+
}
|
|
12
|
+
async list(model) {
|
|
13
|
+
return this.sieveQuery(model, BusinessDocumentsRepository.SIEVE_FIELDS, { defaultPageSize: 25, maxPageSize: 100 });
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The current document of a given kind — newest `uploadedAt` wins.
|
|
17
|
+
*
|
|
18
|
+
* Re-uploading a re-issued certificate is normal and both copies are kept:
|
|
19
|
+
* the superseded one is the evidence of what was true at the time, which is
|
|
20
|
+
* the entire point of keeping a registration document. So this resolves the
|
|
21
|
+
* current one rather than assuming a single row per kind.
|
|
22
|
+
*
|
|
23
|
+
* 🛑 Ordered by `createdAt`, NOT `uploadedAt`, even though the latter reads
|
|
24
|
+
* more naturally here. `BaseRepository.create()` stamps `createdAt` on every
|
|
25
|
+
* document it writes, so it is guaranteed present; `uploadedAt` is set by the
|
|
26
|
+
* caller and a single write path that forgot it would make that row vanish
|
|
27
|
+
* from this query — a Firestore `orderBy` on a field a document lacks is a
|
|
28
|
+
* silent `WHERE field IS NOT NULL` (Root Cause #100). The two are written in
|
|
29
|
+
* the same operation and agree, so there is nothing to gain by taking the
|
|
30
|
+
* field that can be missed.
|
|
31
|
+
*/
|
|
32
|
+
async findCurrentByKind(kind) {
|
|
33
|
+
const result = await this.list({
|
|
34
|
+
filters: `kind==${kind}`,
|
|
35
|
+
sorts: "-createdAt",
|
|
36
|
+
page: 1,
|
|
37
|
+
pageSize: 1,
|
|
38
|
+
});
|
|
39
|
+
return result.items[0] ?? null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
BusinessDocumentsRepository.SIEVE_FIELDS = {
|
|
43
|
+
id: { canFilter: true, canSort: false },
|
|
44
|
+
kind: { canFilter: true, canSort: true },
|
|
45
|
+
fileName: { canFilter: true, canSort: true },
|
|
46
|
+
uploadedBy: { canFilter: true, canSort: false },
|
|
47
|
+
// Timestamp fields that are filterable MUST carry parseValue, or an ISO
|
|
48
|
+
// string reaches Firestore as a string, the type does not match the stored
|
|
49
|
+
// Timestamp, and the query matches ZERO documents with no error anywhere
|
|
50
|
+
// (Root Cause #47).
|
|
51
|
+
uploadedAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
|
|
52
|
+
createdAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
|
|
53
|
+
};
|
|
54
|
+
export const businessDocumentsRepository = new BusinessDocumentsRepository();
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Business Documents Firestore Document Types & Constants
|
|
3
|
+
*
|
|
4
|
+
* Collection: businessDocuments
|
|
5
|
+
* Document ID: Firestore auto-ID (no slug prefix — same convention as
|
|
6
|
+
* `adminAuditLog` / `notifications` / `sessions` / `jobs`; see CLAUDE.md's
|
|
7
|
+
* "True Firestore auto-IDs" list).
|
|
8
|
+
*
|
|
9
|
+
* The statutory registration documents of the business itself — GST
|
|
10
|
+
* certificate, Udyam registration, bank passbook. These are NOT product media
|
|
11
|
+
* and NOT user uploads; there is exactly one current document per `kind`.
|
|
12
|
+
*
|
|
13
|
+
* ## 🛑 Metadata only. The bytes are never in Firestore.
|
|
14
|
+
*
|
|
15
|
+
* A Firestore document is capped at 1 MiB and the Udyam certificate alone is
|
|
16
|
+
* 680 KB, which base64-encodes to ~907 KB — so an "encrypt the PDF into a
|
|
17
|
+
* string field" design is one slightly larger certificate away from an
|
|
18
|
+
* unrecoverable write failure, at upload time, after the user has handed over
|
|
19
|
+
* the document. The ciphertext lives in Storage; this collection records where.
|
|
20
|
+
*
|
|
21
|
+
* ## 🛑 Why `storagePath` must never be served through /api/media
|
|
22
|
+
*
|
|
23
|
+
* `GET /api/media/[...slug]` applies NO authentication — deliberately, because
|
|
24
|
+
* its job is serving public product photography through a watermarker — and
|
|
25
|
+
* `storage.rules` is `allow read: if true` for every object in the bucket. So
|
|
26
|
+
* the only thing standing between these documents and the public internet is
|
|
27
|
+
* that nothing routes them there, and that they carry no `mediaAssets` row (the
|
|
28
|
+
* row IS the public `/media/<shortId>` slug, so no row means no public URL).
|
|
29
|
+
*
|
|
30
|
+
* Read them through `GET /api/admin/business-documents/[id]` and nothing else.
|
|
31
|
+
*/
|
|
32
|
+
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
33
|
+
export declare const BUSINESS_DOCUMENTS_COLLECTION = "businessDocuments";
|
|
34
|
+
export declare const BusinessDocumentKindValues: {
|
|
35
|
+
readonly GST_CERTIFICATE: "gst_certificate";
|
|
36
|
+
readonly UDYAM_CERTIFICATE: "udyam_certificate";
|
|
37
|
+
readonly BANK_PASSBOOK: "bank_passbook";
|
|
38
|
+
};
|
|
39
|
+
export type BusinessDocumentKind = (typeof BusinessDocumentKindValues)[keyof typeof BusinessDocumentKindValues];
|
|
40
|
+
export declare const ALL_BUSINESS_DOCUMENT_KINDS: readonly BusinessDocumentKind[];
|
|
41
|
+
/**
|
|
42
|
+
* A `Record<BusinessDocumentKind, string>` rather than a lookup with a
|
|
43
|
+
* fallback, so adding a fourth kind is a COMPILE error here instead of
|
|
44
|
+
* rendering as a raw snake_case id in the admin list.
|
|
45
|
+
*/
|
|
46
|
+
export declare const BUSINESS_DOCUMENT_KIND_LABELS: Record<BusinessDocumentKind, string>;
|
|
47
|
+
export interface BusinessDocumentDocument extends BaseDocument {
|
|
48
|
+
kind: BusinessDocumentKind;
|
|
49
|
+
/**
|
|
50
|
+
* Path inside the Storage bucket, e.g.
|
|
51
|
+
* `private/business-documents/3f2a…-gst-certificate.pdf`.
|
|
52
|
+
*
|
|
53
|
+
* 🛑 Never returned by any API, to anyone. The download route resolves it
|
|
54
|
+
* server-side and streams the decrypted bytes; handing the path to a client
|
|
55
|
+
* would let it be fetched straight from Storage, where the rules permit any
|
|
56
|
+
* read.
|
|
57
|
+
*/
|
|
58
|
+
storagePath: string;
|
|
59
|
+
/** Original filename as uploaded — shown in the admin list and used for Content-Disposition. */
|
|
60
|
+
fileName: string;
|
|
61
|
+
/** Verified against the file's magic bytes at upload; never trusted from the client. */
|
|
62
|
+
contentType: string;
|
|
63
|
+
/** Size of the PLAINTEXT, before the envelope. What an admin expects to see downloaded. */
|
|
64
|
+
sizeBytes: number;
|
|
65
|
+
/**
|
|
66
|
+
* Whether the stored object carries the `enc:v1:` binary envelope.
|
|
67
|
+
*
|
|
68
|
+
* Always `true` for anything this codebase writes — `encryptBuffer` throws
|
|
69
|
+
* rather than degrading when `SETTINGS_ENCRYPTION_KEY` is unset, so there is
|
|
70
|
+
* no path that produces a plaintext object. It is recorded anyway because
|
|
71
|
+
* the download route must not GUESS: a future hand-uploaded object would
|
|
72
|
+
* otherwise be fed to `decryptBuffer` and throw an envelope error that
|
|
73
|
+
* describes the crypto rather than the upload.
|
|
74
|
+
*/
|
|
75
|
+
encrypted: boolean;
|
|
76
|
+
/** Auth UID of the admin who uploaded it. */
|
|
77
|
+
uploadedBy: string;
|
|
78
|
+
uploadedAt: Date;
|
|
79
|
+
}
|
|
80
|
+
export declare const BUSINESS_DOCUMENT_FIELDS: {
|
|
81
|
+
readonly KIND: "kind";
|
|
82
|
+
readonly STORAGE_PATH: "storagePath";
|
|
83
|
+
readonly FILE_NAME: "fileName";
|
|
84
|
+
readonly CONTENT_TYPE: "contentType";
|
|
85
|
+
readonly SIZE_BYTES: "sizeBytes";
|
|
86
|
+
readonly ENCRYPTED: "encrypted";
|
|
87
|
+
readonly UPLOADED_BY: "uploadedBy";
|
|
88
|
+
readonly UPLOADED_AT: "uploadedAt";
|
|
89
|
+
readonly CREATED_AT: "createdAt";
|
|
90
|
+
readonly KIND_VALUES: {
|
|
91
|
+
readonly GST_CERTIFICATE: "gst_certificate";
|
|
92
|
+
readonly UDYAM_CERTIFICATE: "udyam_certificate";
|
|
93
|
+
readonly BANK_PASSBOOK: "bank_passbook";
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
/** Storage prefix. Mirrors `private/digital-content/` — outside any public read path. */
|
|
97
|
+
export declare const BUSINESS_DOCUMENTS_STORAGE_PREFIX = "private/business-documents";
|
|
98
|
+
/**
|
|
99
|
+
* Accepted upload types. PDFs and scans only.
|
|
100
|
+
*
|
|
101
|
+
* Checked against the file's MAGIC BYTES, not the declared Content-Type: a
|
|
102
|
+
* declared type is a claim by the uploader, and an HTML document served from
|
|
103
|
+
* this origin would be stored XSS against a signed-in admin.
|
|
104
|
+
*/
|
|
105
|
+
export declare const BUSINESS_DOCUMENT_ALLOWED_TYPES: readonly ["application/pdf", "image/jpeg", "image/png", "image/webp"];
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Business Documents Firestore Document Types & Constants
|
|
3
|
+
*
|
|
4
|
+
* Collection: businessDocuments
|
|
5
|
+
* Document ID: Firestore auto-ID (no slug prefix — same convention as
|
|
6
|
+
* `adminAuditLog` / `notifications` / `sessions` / `jobs`; see CLAUDE.md's
|
|
7
|
+
* "True Firestore auto-IDs" list).
|
|
8
|
+
*
|
|
9
|
+
* The statutory registration documents of the business itself — GST
|
|
10
|
+
* certificate, Udyam registration, bank passbook. These are NOT product media
|
|
11
|
+
* and NOT user uploads; there is exactly one current document per `kind`.
|
|
12
|
+
*
|
|
13
|
+
* ## 🛑 Metadata only. The bytes are never in Firestore.
|
|
14
|
+
*
|
|
15
|
+
* A Firestore document is capped at 1 MiB and the Udyam certificate alone is
|
|
16
|
+
* 680 KB, which base64-encodes to ~907 KB — so an "encrypt the PDF into a
|
|
17
|
+
* string field" design is one slightly larger certificate away from an
|
|
18
|
+
* unrecoverable write failure, at upload time, after the user has handed over
|
|
19
|
+
* the document. The ciphertext lives in Storage; this collection records where.
|
|
20
|
+
*
|
|
21
|
+
* ## 🛑 Why `storagePath` must never be served through /api/media
|
|
22
|
+
*
|
|
23
|
+
* `GET /api/media/[...slug]` applies NO authentication — deliberately, because
|
|
24
|
+
* its job is serving public product photography through a watermarker — and
|
|
25
|
+
* `storage.rules` is `allow read: if true` for every object in the bucket. So
|
|
26
|
+
* the only thing standing between these documents and the public internet is
|
|
27
|
+
* that nothing routes them there, and that they carry no `mediaAssets` row (the
|
|
28
|
+
* row IS the public `/media/<shortId>` slug, so no row means no public URL).
|
|
29
|
+
*
|
|
30
|
+
* Read them through `GET /api/admin/business-documents/[id]` and nothing else.
|
|
31
|
+
*/
|
|
32
|
+
export const BUSINESS_DOCUMENTS_COLLECTION = "businessDocuments";
|
|
33
|
+
export const BusinessDocumentKindValues = {
|
|
34
|
+
GST_CERTIFICATE: "gst_certificate",
|
|
35
|
+
UDYAM_CERTIFICATE: "udyam_certificate",
|
|
36
|
+
BANK_PASSBOOK: "bank_passbook",
|
|
37
|
+
};
|
|
38
|
+
export const ALL_BUSINESS_DOCUMENT_KINDS = Object.values(BusinessDocumentKindValues);
|
|
39
|
+
/**
|
|
40
|
+
* A `Record<BusinessDocumentKind, string>` rather than a lookup with a
|
|
41
|
+
* fallback, so adding a fourth kind is a COMPILE error here instead of
|
|
42
|
+
* rendering as a raw snake_case id in the admin list.
|
|
43
|
+
*/
|
|
44
|
+
export const BUSINESS_DOCUMENT_KIND_LABELS = {
|
|
45
|
+
[BusinessDocumentKindValues.GST_CERTIFICATE]: "GST Certificate",
|
|
46
|
+
[BusinessDocumentKindValues.UDYAM_CERTIFICATE]: "Udyam Registration Certificate",
|
|
47
|
+
[BusinessDocumentKindValues.BANK_PASSBOOK]: "Bank Passbook",
|
|
48
|
+
};
|
|
49
|
+
export const BUSINESS_DOCUMENT_FIELDS = {
|
|
50
|
+
KIND: "kind",
|
|
51
|
+
STORAGE_PATH: "storagePath",
|
|
52
|
+
FILE_NAME: "fileName",
|
|
53
|
+
CONTENT_TYPE: "contentType",
|
|
54
|
+
SIZE_BYTES: "sizeBytes",
|
|
55
|
+
ENCRYPTED: "encrypted",
|
|
56
|
+
UPLOADED_BY: "uploadedBy",
|
|
57
|
+
UPLOADED_AT: "uploadedAt",
|
|
58
|
+
CREATED_AT: "createdAt",
|
|
59
|
+
KIND_VALUES: BusinessDocumentKindValues,
|
|
60
|
+
};
|
|
61
|
+
/** Storage prefix. Mirrors `private/digital-content/` — outside any public read path. */
|
|
62
|
+
export const BUSINESS_DOCUMENTS_STORAGE_PREFIX = "private/business-documents";
|
|
63
|
+
/**
|
|
64
|
+
* Accepted upload types. PDFs and scans only.
|
|
65
|
+
*
|
|
66
|
+
* Checked against the file's MAGIC BYTES, not the declared Content-Type: a
|
|
67
|
+
* declared type is a claim by the uploader, and an HTML document served from
|
|
68
|
+
* this origin would be stored XSS against a signed-in admin.
|
|
69
|
+
*/
|
|
70
|
+
export const BUSINESS_DOCUMENT_ALLOWED_TYPES = [
|
|
71
|
+
"application/pdf",
|
|
72
|
+
"image/jpeg",
|
|
73
|
+
"image/png",
|
|
74
|
+
"image/webp",
|
|
75
|
+
];
|
|
@@ -15,6 +15,7 @@ import { CART_LANE, activeLane, canAddNewItems, } from "../../../_internal/share
|
|
|
15
15
|
import { CART_MAX_ITEMS } from "../../../constants/limits.js";
|
|
16
16
|
import { getExpandedDecrements, validateCartItemStock, } from "../../../_internal/server/features/checkout/bundle-expansion.js";
|
|
17
17
|
import { safeRead } from "../../../errors/safe-read.js";
|
|
18
|
+
import { checkGroupRules, readGroupRules, } from "../../../_internal/shared/features/products/group-rules.js";
|
|
18
19
|
/**
|
|
19
20
|
* Product documents don't always carry a denormalized `storeName` — the
|
|
20
21
|
* create routes only set it when the caller explicitly passes one, so
|
|
@@ -133,7 +134,13 @@ export async function addGroupLineToCart(userId, input) {
|
|
|
133
134
|
if (!rule.cartEligible) {
|
|
134
135
|
throw new ValidationError(`"${product.title}" can't be added to the cart. ${rule.cartIneligibleHint ?? ""}`.trim());
|
|
135
136
|
}
|
|
136
|
-
|
|
137
|
+
/*
|
|
138
|
+
* 🛑 Made to order: no stock CHECK for this member. Nothing is held in
|
|
139
|
+
* reserve — it is printed or built when the order arrives — so the only
|
|
140
|
+
* honest answer to "how many are left" is "as many as you want". The
|
|
141
|
+
* decrement still happens at checkout; see `validateCartItemStock`.
|
|
142
|
+
*/
|
|
143
|
+
if (product.madeToOrder !== true && (product.availableQuantity ?? 0) < m.quantity) {
|
|
137
144
|
throw new ValidationError(`Only ${product.availableQuantity ?? 0} left of "${product.title}".`);
|
|
138
145
|
}
|
|
139
146
|
resolved.push({
|
|
@@ -141,7 +148,7 @@ export async function addGroupLineToCart(userId, input) {
|
|
|
141
148
|
quantity: m.quantity,
|
|
142
149
|
unitPrice: product.price,
|
|
143
150
|
title: product.title,
|
|
144
|
-
image: product.images?.[0] ?? product.mainImage,
|
|
151
|
+
image: product.images?.[0]?.url ?? product.mainImage,
|
|
145
152
|
gstRate: product.gstRate,
|
|
146
153
|
hsnCode: product.hsnCode,
|
|
147
154
|
listingType,
|
|
@@ -164,7 +171,7 @@ export async function addGroupLineToCart(userId, input) {
|
|
|
164
171
|
return addItemToCart(userId, {
|
|
165
172
|
productId: first.id,
|
|
166
173
|
productTitle: first.title,
|
|
167
|
-
productImage: first.images?.[0] ?? first.mainImage,
|
|
174
|
+
productImage: first.images?.[0]?.url ?? first.mainImage,
|
|
168
175
|
price: first.price,
|
|
169
176
|
currency: first.currency,
|
|
170
177
|
quantity: resolved[0].quantity,
|
|
@@ -192,6 +199,25 @@ export async function addGroupLineToCart(userId, input) {
|
|
|
192
199
|
throw new ValidationError(`Only ${shortfall.availableQty} left of "${short?.title ?? shortfall.productId}" once your cart is counted.`);
|
|
193
200
|
}
|
|
194
201
|
const groupTitle = await resolveGroupTitle(groupId, groupSource, first ?? null);
|
|
202
|
+
/*
|
|
203
|
+
* 🛑 SERVER-SIDE ENFORCEMENT OF THE GROUP'S OWN RULES.
|
|
204
|
+
*
|
|
205
|
+
* `GroupMemberPicker` mirrors this so a buyer is not told "no" only after
|
|
206
|
+
* submitting — but the picker is a convenience and this is the boundary. The
|
|
207
|
+
* two share `checkGroupRules`, so they cannot disagree about what a valid
|
|
208
|
+
* selection is; only about WHEN the buyer finds out.
|
|
209
|
+
*
|
|
210
|
+
* It throws on the FIRST violation while the picker shows all of them
|
|
211
|
+
* together, which is each surface doing the right thing for its own job: a
|
|
212
|
+
* form lists everything to fix, an API answers one reason.
|
|
213
|
+
*/
|
|
214
|
+
const rules = await resolveGroupRules(groupId, groupSource, first ?? null);
|
|
215
|
+
const violations = checkGroupRules(rules, resolved.map((r) => ({ productId: r.productId, quantity: r.quantity, title: r.title })));
|
|
216
|
+
if (violations.length > 0) {
|
|
217
|
+
throw Object.assign(new ValidationError(violations[0].message), {
|
|
218
|
+
code: violations[0].code,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
195
221
|
return cartRepository.addGroupLine(userId, {
|
|
196
222
|
productId: groupId,
|
|
197
223
|
productTitle: groupTitle,
|
|
@@ -208,9 +234,51 @@ export async function addGroupLineToCart(userId, input) {
|
|
|
208
234
|
groupId,
|
|
209
235
|
groupSlug: groupSource === "product-group" ? groupId : undefined,
|
|
210
236
|
groupTitle,
|
|
237
|
+
/*
|
|
238
|
+
* Snapshotted at add time — see `CartItemDocument.groupDiscountPercent`.
|
|
239
|
+
* Omitted entirely when there is no discount, so a line with no group
|
|
240
|
+
* pricing carries no field rather than a zero that reads as "0% off".
|
|
241
|
+
*/
|
|
242
|
+
...(rules.groupDiscountPercent !== undefined && rules.groupDiscountPercent > 0
|
|
243
|
+
? { groupDiscountPercent: rules.groupDiscountPercent }
|
|
244
|
+
: {}),
|
|
211
245
|
groupMembers: resolved,
|
|
212
246
|
});
|
|
213
247
|
}
|
|
248
|
+
/**
|
|
249
|
+
* The group's selection rules, from whichever of the two homes owns them.
|
|
250
|
+
*
|
|
251
|
+
* 🛑 ONE resolver, because there are TWO homes: a product group keeps them on
|
|
252
|
+
* its PARENT PRODUCT (identified by the parent's slug, which IS the groupId),
|
|
253
|
+
* while a `groupedListing` keeps them on its own document. Reading them at each
|
|
254
|
+
* call site is how the picker and the server come to disagree.
|
|
255
|
+
*
|
|
256
|
+
* Fails OPEN — an unreadable group yields no rules rather than refusing the
|
|
257
|
+
* add. A transient Firestore error must not look like "your selection is
|
|
258
|
+
* invalid", which is a statement about the buyer's choice and would send them
|
|
259
|
+
* to change something that was already correct.
|
|
260
|
+
*/
|
|
261
|
+
async function resolveGroupRules(groupId, groupSource, sample) {
|
|
262
|
+
if (groupSource === "grouped-listing") {
|
|
263
|
+
const { groupedListingsRepository } = await import("../../grouped/repository/grouped-listings.repository.js");
|
|
264
|
+
const group = await safeRead(() => groupedListingsRepository.findById(groupId), {
|
|
265
|
+
route: "/cart", key: "groupedListings.rules", fallback: null,
|
|
266
|
+
});
|
|
267
|
+
return readGroupRules(group);
|
|
268
|
+
}
|
|
269
|
+
/*
|
|
270
|
+
* A product group's rules live on the PARENT, and `sample` is an arbitrary
|
|
271
|
+
* member — so the parent has to be fetched unless the sample happens to BE
|
|
272
|
+
* it. `groupId` is the parent's slug and products are pure slugs
|
|
273
|
+
* (`id === slug`), so `findById` resolves it directly.
|
|
274
|
+
*/
|
|
275
|
+
if (sample?.isGroupParent && sample.id === groupId)
|
|
276
|
+
return readGroupRules(sample);
|
|
277
|
+
const parent = await safeRead(() => productRepository.findById(groupId), {
|
|
278
|
+
route: "/cart", key: "products.groupParentRules", fallback: null,
|
|
279
|
+
});
|
|
280
|
+
return readGroupRules(parent);
|
|
281
|
+
}
|
|
214
282
|
/** Is this product listed on that `groupedListings` document? */
|
|
215
283
|
async function isGroupedListingMember(groupId, productId) {
|
|
216
284
|
const { groupedListingsRepository } = await import("../../grouped/repository/grouped-listings.repository.js");
|
|
@@ -263,14 +331,23 @@ export async function mergeGuestCart(userId, items) {
|
|
|
263
331
|
const product = await productRepository.findById(item.productId);
|
|
264
332
|
if (!product || product.status !== "published")
|
|
265
333
|
continue;
|
|
266
|
-
|
|
334
|
+
/*
|
|
335
|
+
* A made-to-order row survives the guest-cart merge at its full requested
|
|
336
|
+
* quantity. Dropping it (or clamping it to 0) would silently delete the
|
|
337
|
+
* line a signed-out visitor had chosen, which reads as the cart losing
|
|
338
|
+
* items on login — and there is no stock level for the clamp to clamp to.
|
|
339
|
+
*/
|
|
340
|
+
const unlimited = product.madeToOrder === true;
|
|
341
|
+
if (!unlimited && product.availableQuantity < 1)
|
|
267
342
|
continue;
|
|
268
|
-
const safeQty =
|
|
343
|
+
const safeQty = unlimited
|
|
344
|
+
? item.quantity
|
|
345
|
+
: Math.min(item.quantity, product.availableQuantity);
|
|
269
346
|
const storeName = await resolveStoreName(product.storeId, product.storeName);
|
|
270
347
|
await cartRepository.addItem(userId, {
|
|
271
348
|
productId: product.id,
|
|
272
349
|
productTitle: product.title,
|
|
273
|
-
productImage: product.images?.[0] ?? "",
|
|
350
|
+
productImage: product.images?.[0]?.url ?? "",
|
|
274
351
|
price: product.price,
|
|
275
352
|
currency: product.currency ?? getDefaultCurrency(),
|
|
276
353
|
quantity: safeQty,
|