@mohasinac/appkit 4.53.0 → 4.53.1
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/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/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/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/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 +25 -0
- package/dist/_internal/server/features/stores/tabs.js +122 -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 +2 -1
- 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 +39 -15
- package/dist/constants/api-endpoints.js +28 -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 +4 -11
- 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 +113 -0
- package/dist/features/categories/components/BrandDetailPageView.js +19 -3
- package/dist/features/categories/components/BrandDetailTabs.js +10 -0
- package/dist/features/categories/components/CategoriesIndexListing.js +23 -8
- package/dist/features/categories/components/CategoryDetailPageView.js +141 -14
- package/dist/features/categories/components/CategoryDetailTabs.js +12 -0
- 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 +4 -9
- 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 +4 -9
- 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 +4 -9
- 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 +4 -11
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +1 -1
- package/dist/features/price-index/components/PriceIndexToolView.d.ts +3 -1
- package/dist/features/price-index/components/PriceIndexToolView.js +92 -13
- package/dist/features/products/components/CompareOverlay.d.ts +18 -1
- package/dist/features/products/components/CompareOverlay.js +21 -2
- 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 +91 -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 +74 -15
- 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/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 +98 -0
- package/dist/features/products/repository/products.repository.js +235 -17
- 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 +39 -0
- package/dist/features/products/utils/group-member-image.js +3 -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/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/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 +280 -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 +372 -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 +441 -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 +9 -9
- package/dist/server.js +27 -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 +135 -26
- 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
|
@@ -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,
|
|
@@ -162,10 +162,33 @@ export class CartRepository extends BaseRepository {
|
|
|
162
162
|
* subtotals all compute `price × quantity`, and a grouped line pins
|
|
163
163
|
* `quantity` to 1, so all four stay correct with no changes of their own.
|
|
164
164
|
*/
|
|
165
|
-
deriveLinePrice(members) {
|
|
165
|
+
deriveLinePrice(members, discountPercent) {
|
|
166
166
|
const raw = members.reduce((sum, m) => sum + m.unitPrice * m.quantity, 0);
|
|
167
|
+
/*
|
|
168
|
+
* 🛑 THE GROUP DISCOUNT IS APPLIED HERE AND NOWHERE ELSE.
|
|
169
|
+
*
|
|
170
|
+
* This function is the only writer of `item.price` on a multi-member line,
|
|
171
|
+
* and `item.price` is what `getSubtotal`, the cart page's per-seller
|
|
172
|
+
* subtotal, both lane subtotals, `unitPriceFor` and
|
|
173
|
+
* `expandCartLineToOrderRows`' per-member GST proration (via
|
|
174
|
+
* `allocateAcrossMembers`) all read. So discounting here reaches every one
|
|
175
|
+
* of them with no change of their own — and discounting anywhere else means
|
|
176
|
+
* the cart total and the invoice are computed from two different numbers,
|
|
177
|
+
* which is exactly what Root Cause #75 cost.
|
|
178
|
+
*
|
|
179
|
+
* In particular `allocateAcrossMembers` prorates the LOCKED line price
|
|
180
|
+
* across members for tax, so a discount applied after it would under- or
|
|
181
|
+
* over-report GST against the amount actually charged.
|
|
182
|
+
*
|
|
183
|
+
* Clamped 0–100 rather than trusted: the percent is a snapshot copied off a
|
|
184
|
+
* seller-editable rule, and a negative value would silently INFLATE the
|
|
185
|
+
* line while a value over 100 would make it negative.
|
|
186
|
+
*/
|
|
187
|
+
const pct = Math.min(100, Math.max(0, discountPercent ?? 0));
|
|
188
|
+
// audit-money-units-ok: pct/100 is a PERCENTAGE divisor (0-100 -> 0-1 fraction), not a paise conversion — raw/discounted are decimal rupees throughout.
|
|
189
|
+
const discounted = pct > 0 ? raw * (1 - pct / 100) : raw;
|
|
167
190
|
// audit-money-units-ok: decimal rupees throughout; rounding to 2dp, not paise conversion.
|
|
168
|
-
return Math.round(
|
|
191
|
+
return Math.round(discounted * 100) / 100;
|
|
169
192
|
}
|
|
170
193
|
/**
|
|
171
194
|
* Create a multi-member line. Deliberately never merges into an existing
|
|
@@ -182,7 +205,7 @@ export class CartRepository extends BaseRepository {
|
|
|
182
205
|
productId: input.productId,
|
|
183
206
|
productTitle: input.productTitle,
|
|
184
207
|
productImage: input.productImage,
|
|
185
|
-
price: this.deriveLinePrice(input.groupMembers),
|
|
208
|
+
price: this.deriveLinePrice(input.groupMembers, input.groupDiscountPercent),
|
|
186
209
|
currency: input.currency,
|
|
187
210
|
quantity: input.quantity,
|
|
188
211
|
storeId: input.storeId,
|
|
@@ -264,7 +287,12 @@ export class CartRepository extends BaseRepository {
|
|
|
264
287
|
items[itemIndex] = {
|
|
265
288
|
...target,
|
|
266
289
|
groupMembers: nextMembers,
|
|
267
|
-
|
|
290
|
+
/*
|
|
291
|
+
* The line's OWN stored percent, so changing a member quantity
|
|
292
|
+
* preserves the discount the buyer was quoted. Re-resolving the rule
|
|
293
|
+
* here would silently reprice a line mid-edit.
|
|
294
|
+
*/
|
|
295
|
+
price: this.deriveLinePrice(nextMembers, target.groupDiscountPercent),
|
|
268
296
|
updatedAt: new Date(),
|
|
269
297
|
};
|
|
270
298
|
const updatedCart = { ...cart, items, updatedAt: new Date() };
|
|
@@ -148,6 +148,24 @@ export interface CartItemDocument {
|
|
|
148
148
|
groupSlug?: string;
|
|
149
149
|
/** Display snapshot of the group/bundle name. */
|
|
150
150
|
groupTitle?: string;
|
|
151
|
+
/**
|
|
152
|
+
* The group discount, as a percentage, AS QUOTED WHEN THIS LINE WAS ADDED.
|
|
153
|
+
*
|
|
154
|
+
* 🛑 Stored on the line rather than resolved at read time, and that is not
|
|
155
|
+
* the mirror-drift mistake it resembles (Root Cause #42). It has to be here
|
|
156
|
+
* for a mechanical reason — `updateGroupMembers` recomputes `price` from
|
|
157
|
+
* `groupMembers` and holds only the CART, so with nothing on the line a
|
|
158
|
+
* buyer changing a quantity would silently lose the discount — and for a
|
|
159
|
+
* commercial one: this is the basis of the price the buyer was shown.
|
|
160
|
+
*
|
|
161
|
+
* 🛑 Because it is a snapshot it is RE-VALIDATED at checkout against the
|
|
162
|
+
* group's current rule, and a disagreement is REPORTED, never silently
|
|
163
|
+
* resolved either way. Honouring a stale percent would charge less than the
|
|
164
|
+
* seller now offers; re-deriving silently would change a total the buyer has
|
|
165
|
+
* already read. Same discipline as `revalidateAppliedCoupons`, which drops a
|
|
166
|
+
* lapsed coupon and tells the caller.
|
|
167
|
+
*/
|
|
168
|
+
groupDiscountPercent?: number;
|
|
151
169
|
/**
|
|
152
170
|
* The members of a multi-member line, AUTHORITATIVE over `bundleProductIds`.
|
|
153
171
|
* See `CartLineMember` for the quantity invariant.
|
|
@@ -300,6 +318,8 @@ export type AddToCartInput = {
|
|
|
300
318
|
groupId?: string;
|
|
301
319
|
groupSlug?: string;
|
|
302
320
|
groupTitle?: string;
|
|
321
|
+
/** See `CartItemDocument.groupDiscountPercent` — a quoted-at-add snapshot. */
|
|
322
|
+
groupDiscountPercent?: number;
|
|
303
323
|
groupMembers?: CartLineMember[];
|
|
304
324
|
};
|
|
305
325
|
/**
|
|
@@ -174,8 +174,8 @@ export declare const cartSummarySchema: z.ZodObject<{
|
|
|
174
174
|
sessionId?: string | undefined;
|
|
175
175
|
addedAt?: string | undefined;
|
|
176
176
|
}[];
|
|
177
|
-
itemCount: number;
|
|
178
177
|
subtotal: number;
|
|
178
|
+
itemCount: number;
|
|
179
179
|
}, {
|
|
180
180
|
items: {
|
|
181
181
|
id: string;
|
|
@@ -193,7 +193,7 @@ export declare const cartSummarySchema: z.ZodObject<{
|
|
|
193
193
|
sessionId?: string | undefined;
|
|
194
194
|
addedAt?: string | undefined;
|
|
195
195
|
}[];
|
|
196
|
-
itemCount: number;
|
|
197
196
|
subtotal: number;
|
|
197
|
+
itemCount: number;
|
|
198
198
|
currency?: string | undefined;
|
|
199
199
|
}>;
|
|
@@ -29,5 +29,5 @@ export function PublicCatalogueItemDetailView({ ownerSlug, itemId }) {
|
|
|
29
29
|
}
|
|
30
30
|
const { owner, item, linkedProductHref } = data;
|
|
31
31
|
const images = item.images.length > 0 ? item.images : item.mainImage ? [item.mainImage] : [];
|
|
32
|
-
return (_jsx(Main, { children: _jsxs(Container, { size: "lg", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", gap: "xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: "/", className: "hover:text-primary-600 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: `${ROUTES.PUBLIC.PROFILE(ownerSlug)}/catalogue`, className: "hover:text-primary-600 transition-colors", children: owner.displayName ?? "Catalogue" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: item.title })] }), _jsxs(Grid, { cols: 2, gap: "lg", children: [images.length > 0 ? (_jsx(Div, { className: `relative aspect-square w-full ${__O.hidden}`, rounded: "xl", surface: "muted", children: _jsx(MediaImage, { src: images[0], alt: item.title, size: "hero", objectFit: "contain" }) })) : (_jsx(Div, { className: "relative aspect-square w-full", rounded: "xl", surface: "muted" })), images.length > 1 && (_jsx(Row, { gap: "sm", wrap: true, className: "col-span-2", children: images.slice(1).map((src, i) => (_jsx(Div, { className: `relative h-20 w-20 ${__O.hidden}`, rounded: "lg", surface: "muted", children: _jsx(MediaImage, { src: src, alt: `${item.title} photo ${i + 2}`, size: "thumbnail", objectFit: "cover" }) }, i))) })), _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "bold", color: "primary", children: item.title }), typeof item.price === "number" && item.price > 0 && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { label: "Sign in to see the estimated value", children: formatCurrency(item.price) }) }), _jsx(Text, { size: "xs", color: "muted", children: "Estimated value \u2014 not for direct sale from this page." })] })), _jsxs(Row, { gap: "sm", wrap: true, children: [item.condition && (_jsx(Span, { size: "xs", weight: "medium", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: item.condition })), typeof item.quantity === "number" && (_jsxs(Span, { size: "xs", weight: "medium", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Qty: ", item.quantity] }))] }), item.description && (_jsx(Text, { color: "muted", className: "whitespace-pre-line", children: item.description })), owner.displayName && (_jsxs(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Owned by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: owner.displayName })] })), linkedProductHref && (_jsx(Link, { href: linkedProductHref, className: "inline-flex w-fit items-center rounded-lg bg-primary px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-semibold text-white transition-colors hover:opacity-90", children: "View live listing \u2192" }))] })] })] }) }));
|
|
32
|
+
return (_jsx(Main, { children: _jsxs(Container, { size: "lg", padding: "y-lg", children: [_jsxs(Row, { className: "mb-4", align: "center", gap: "xs", textSize: "xs", color: "muted", children: [_jsx(Link, { href: "/", className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: "Home" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Link, { href: `${ROUTES.PUBLIC.PROFILE(ownerSlug)}/catalogue`, className: "hover:text-primary-600 dark:hover:text-primary-400 transition-colors", children: owner.displayName ?? "Catalogue" }), _jsx(Span, { "aria-hidden": true, children: "/" }), _jsx(Span, { className: "truncate max-w-[200px]", color: "muted", children: item.title })] }), _jsxs(Grid, { cols: 2, gap: "lg", children: [images.length > 0 ? (_jsx(Div, { className: `relative aspect-square w-full ${__O.hidden}`, rounded: "xl", surface: "muted", children: _jsx(MediaImage, { src: images[0], alt: item.title, size: "hero", objectFit: "contain" }) })) : (_jsx(Div, { className: "relative aspect-square w-full", rounded: "xl", surface: "muted" })), images.length > 1 && (_jsx(Row, { gap: "sm", wrap: true, className: "col-span-2", children: images.slice(1).map((src, i) => (_jsx(Div, { className: `relative h-20 w-20 ${__O.hidden}`, rounded: "lg", surface: "muted", children: _jsx(MediaImage, { src: src, alt: `${item.title} photo ${i + 2}`, size: "thumbnail", objectFit: "cover" }) }, i))) })), _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 1, size: "2xl", weight: "bold", color: "primary", children: item.title }), typeof item.price === "number" && item.price > 0 && (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { size: "2xl", weight: "bold", color: "primary", children: _jsx(GatedPrice, { label: "Sign in to see the estimated value", children: formatCurrency(item.price) }) }), _jsx(Text, { size: "xs", color: "muted", children: "Estimated value \u2014 not for direct sale from this page." })] })), _jsxs(Row, { gap: "sm", wrap: true, children: [item.condition && (_jsx(Span, { size: "xs", weight: "medium", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: item.condition })), typeof item.quantity === "number" && (_jsxs(Span, { size: "xs", weight: "medium", padding: "pill-sm", rounded: "full", surface: "subtle", color: "muted", children: ["Qty: ", item.quantity] }))] }), item.description && (_jsx(Text, { color: "muted", className: "whitespace-pre-line", children: item.description })), owner.displayName && (_jsxs(Div, { border: "subtle", rounded: "xl", surface: "muted", padding: "md", children: [_jsx(Text, { className: "text-[10px] tracking-wide mb-0.5", color: "faint", transform: "uppercase", children: "Owned by" }), _jsx(Text, { size: "sm", weight: "semibold", color: "primary", children: owner.displayName })] })), linkedProductHref && (_jsx(Link, { href: linkedProductHref, className: "inline-flex w-fit items-center rounded-lg bg-primary px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-semibold text-white transition-colors hover:opacity-90", children: "View live listing \u2192" }))] })] })] }) }));
|
|
33
33
|
}
|