@mohasinac/appkit 4.27.0 → 4.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.d.ts +16 -16
- package/dist/_internal/client/features/lottery/LotteryAdminEditView.js +29 -6
- package/dist/_internal/client/features/lottery/LotteryListView.js +13 -1
- package/dist/_internal/server/features/checkout/actions.js +6 -4
- package/dist/_internal/server/features/classified/metadata.js +3 -1
- package/dist/_internal/server/features/digital-code/metadata.js +3 -1
- package/dist/_internal/server/features/live/metadata.js +3 -1
- package/dist/_internal/server/features/refunds/actions.js +3 -2
- package/dist/_internal/server/features/reviews/actions.js +3 -2
- package/dist/_internal/server/features/seo/sitemap.js +189 -40
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +5 -0
- package/dist/_internal/server/jobs/core/listingProcessor.d.ts +0 -1
- package/dist/_internal/server/jobs/core/listingProcessor.js +19 -1
- package/dist/_internal/shared/actions/action-registry.js +14 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +25 -3
- package/dist/_internal/shared/features/checkout/config.d.ts +3 -3
- package/dist/_internal/shared/features/checkout/config.js +6 -3
- package/dist/client.d.ts +32 -2
- package/dist/client.js +37 -2
- package/dist/constants/api-endpoints.d.ts +21 -3
- package/dist/constants/api-endpoints.js +14 -1
- package/dist/constants/field-names.d.ts +2 -2
- package/dist/constants/field-names.js +2 -2
- package/dist/core/newsletter.repository.js +14 -1
- package/dist/features/about/components/FAQPageView.js +1 -1
- package/dist/features/about/components/FeesView.js +1 -1
- package/dist/features/account/components/AddressForm.d.ts +2 -0
- package/dist/features/account/components/AddressForm.js +5 -1
- package/dist/features/account/components/UserBidsView.js +1 -1
- package/dist/features/account/components/UserSidebar.js +1 -1
- package/dist/features/account/components/UserSupportView.js +24 -2
- package/dist/features/account/hooks/useAddresses.d.ts +29 -1
- package/dist/features/account/hooks/useAddresses.js +4 -1
- package/dist/features/account/hooks/useProfile.d.ts +1 -1
- package/dist/features/account/hooks/useProfile.js +1 -1
- package/dist/features/addresses/repository/addresses.repository.d.ts +13 -0
- package/dist/features/addresses/repository/addresses.repository.js +19 -0
- package/dist/features/admin/actions/chat-actions.js +5 -1
- package/dist/features/admin/components/AdminAdsView.js +1 -1
- package/dist/features/admin/components/AdminBidsView.js +6 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.d.ts +9 -1
- package/dist/features/admin/components/AdminCarouselGroupEditorView.js +42 -10
- package/dist/features/admin/components/AdminCarouselView.js +1 -1
- package/dist/features/admin/components/AdminContentGuideView.js +1 -1
- package/dist/features/admin/components/AdminFeatureEditorView.js +1 -1
- package/dist/features/admin/components/AdminGroupedListingsView.js +14 -2
- package/dist/features/admin/components/AdminSidebar.js +1 -1
- package/dist/features/admin/components/AdminViewCards.js +2 -2
- package/dist/features/admin/components/DataTable.js +2 -2
- package/dist/features/admin/schemas/firestore.js +9 -3
- package/dist/features/auctions/actions/bid-actions.js +45 -4
- package/dist/features/auctions/components/AuctionBottomActions.d.ts +21 -6
- package/dist/features/auctions/components/AuctionBottomActions.js +42 -11
- package/dist/features/auctions/components/AuctionDetailPageView.d.ts +8 -3
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -2
- package/dist/features/auctions/components/MarketplaceAuctionCard.js +4 -5
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +27 -2
- package/dist/features/auctions/components/PlaceBidFormClient.js +42 -20
- package/dist/features/auth/components/SocialAuthButtons.js +1 -1
- package/dist/features/auth/hooks/useAuth.d.ts +23 -1
- package/dist/features/auth/hooks/useAuth.js +27 -1
- package/dist/features/auth/permissions/constants.d.ts +1 -1
- package/dist/features/auth/permissions/constants.js +3 -0
- package/dist/features/auth/repository/session.repository.d.ts +16 -1
- package/dist/features/auth/repository/session.repository.js +23 -0
- package/dist/features/auth/repository/user.repository.js +13 -2
- package/dist/features/blog/components/BlogListView.js +2 -2
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +3 -1
- package/dist/features/blog/schemas/firestore.js +1 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +11 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +3 -3
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +9 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +15 -5
- package/dist/features/cart/components/index.d.ts +1 -1
- package/dist/features/cart/components/index.js +1 -1
- package/dist/features/cart/hooks/useGuestCartMerge.d.ts +8 -1
- package/dist/features/cart/hooks/useGuestCartMerge.js +2 -1
- package/dist/features/cart/utils/line-total.d.ts +25 -0
- package/dist/features/cart/utils/line-total.js +26 -0
- package/dist/features/catalogue/repository/catalogue.repository.d.ts +12 -0
- package/dist/features/catalogue/repository/catalogue.repository.js +15 -0
- package/dist/features/categories/components/CategoryTree.js +2 -2
- package/dist/features/contact/components/ContactForm.js +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.d.ts +1 -1
- package/dist/features/copilot/hooks/useCopilotFeedback.js +1 -1
- package/dist/features/events/components/AdminEventEditorView.js +16 -9
- package/dist/features/events/components/AdminEventEntriesView.js +7 -13
- package/dist/features/events/components/EventRafflesSection.js +14 -2
- package/dist/features/events/components/EventStatusBadge.js +1 -1
- package/dist/features/events/components/event-entry-detail-fields.d.ts +24 -0
- package/dist/features/events/components/event-entry-detail-fields.js +13 -0
- package/dist/features/events/repository/events.repository.js +1 -0
- package/dist/features/events/schemas/firestore.d.ts +3 -1
- package/dist/features/events/schemas/firestore.js +1 -0
- package/dist/features/faq/components/ContactCTA.js +1 -1
- package/dist/features/faq/components/FAQHelpfulButtons.js +2 -2
- package/dist/features/faq/repository/faqs.repository.d.ts +16 -1
- package/dist/features/faq/repository/faqs.repository.js +64 -54
- package/dist/features/faq/schemas/firestore.d.ts +3 -2
- package/dist/features/faq/schemas/firestore.js +2 -2
- package/dist/features/faq/schemas/index.d.ts +3 -3
- package/dist/features/faq/schemas/index.js +1 -1
- package/dist/features/filters/FilterFacetSection.js +2 -2
- package/dist/features/forms/Toggle.js +1 -1
- package/dist/features/grouped/components/GroupedListingEditorView.d.ts +18 -0
- package/dist/features/grouped/components/GroupedListingEditorView.js +192 -0
- package/dist/features/homepage/components/AdvertisementBanner.js +2 -2
- package/dist/features/homepage/components/BrandsSection.js +1 -1
- package/dist/features/homepage/components/CTABannerSection.js +1 -1
- package/dist/features/homepage/components/HomepageCustomerReviewsSection.js +1 -1
- package/dist/features/homepage/components/HomepageSectionBoundary.d.ts +39 -0
- package/dist/features/homepage/components/HomepageSectionBoundary.js +49 -0
- package/dist/features/homepage/components/ShopByCategorySection.js +1 -1
- package/dist/features/homepage/components/WelcomeSection.js +1 -1
- package/dist/features/homepage/lib/section-renderer.js +2 -1
- package/dist/features/layout/AppLayoutShell.js +5 -5
- package/dist/features/layout/BottomActions.js +5 -5
- package/dist/features/layout/BottomSheet.js +1 -1
- package/dist/features/layout/MainNavbar.js +1 -1
- package/dist/features/layout/NavbarLayout.js +1 -1
- package/dist/features/layout/TitleBarLayout.js +3 -3
- package/dist/features/layout/hooks/useFormBottomActions.d.ts +11 -1
- package/dist/features/layout/hooks/useFormBottomActions.js +16 -2
- package/dist/features/lottery/schemas/config-write.d.ts +141 -0
- package/dist/features/lottery/schemas/config-write.js +139 -0
- package/dist/features/media/MediaPickerModal.js +3 -1
- package/dist/features/media/MediaVideo.js +4 -2
- package/dist/features/media/components/MediaSlider.js +1 -1
- package/dist/features/messages/repository/conversations.repository.js +15 -5
- package/dist/features/orders/components/OrdersList.js +2 -2
- package/dist/features/payments/repository/payout.repository.d.ts +11 -1
- package/dist/features/payments/repository/payout.repository.js +14 -6
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +3 -2
- package/dist/features/pre-orders/components/PreOrderBadge.js +1 -1
- package/dist/features/pre-orders/components/PreOrderDetailPageView.js +1 -1
- package/dist/features/products/columns/productTableColumns.js +3 -3
- package/dist/features/products/components/CompareOverlay.js +1 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +1 -1
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -2
- package/dist/features/products/components/PrizeDrawsSection.js +5 -1
- package/dist/features/products/components/ProductDetailActions.js +11 -7
- package/dist/features/products/components/ProductDetailPageView.js +3 -3
- package/dist/features/products/components/ProductFeatureBadges.js +2 -2
- package/dist/features/products/components/ProductFeaturesSelector.js +1 -1
- package/dist/features/products/components/ProductFilters.js +15 -8
- package/dist/features/products/components/ProductGrid.js +8 -17
- package/dist/features/products/components/ProductTabs.js +7 -4
- package/dist/features/products/components/ProductTabsShell.js +2 -7
- package/dist/features/products/constants/action-defs.d.ts +21 -4
- package/dist/features/products/constants/action-defs.js +7 -3
- package/dist/features/products/hooks/useRelatedProducts.js +2 -1
- package/dist/features/products/repository/products.repository.d.ts +1 -1
- package/dist/features/products/repository/products.repository.js +38 -16
- package/dist/features/products/schemas/firestore.d.ts +3 -2
- package/dist/features/products/schemas/firestore.js +1 -1
- package/dist/features/reviews/actions/review-actions.d.ts +8 -1
- package/dist/features/reviews/actions/review-actions.js +14 -3
- package/dist/features/reviews/components/ReviewDetailShell.js +10 -4
- package/dist/features/reviews/components/ReviewModal.js +1 -1
- package/dist/features/reviews/repository/reviews.repository.d.ts +29 -1
- package/dist/features/reviews/repository/reviews.repository.js +54 -13
- package/dist/features/reviews/schemas/firestore.d.ts +9 -1
- package/dist/features/reviews/schemas/firestore.js +1 -0
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/components/Search.js +3 -3
- package/dist/features/search/components/SearchResultsSection.js +1 -1
- package/dist/features/seller/components/SellerAddressesView.js +3 -1
- package/dist/features/seller/components/SellerBidsView.js +6 -1
- package/dist/features/seller/components/SellerSidebar.js +1 -1
- package/dist/features/shell/SectionForm.d.ts +21 -1
- package/dist/features/shell/SectionForm.js +15 -3
- package/dist/features/shell/build-sections.js +10 -1
- package/dist/features/shell/field-ui-meta.d.ts +21 -0
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +23 -6
- package/dist/features/site-settings/components/ActionPermissionsManager.js +1 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +21 -4
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +24 -4
- package/dist/features/store-extensions/schemas/shipping-config-form.js +13 -0
- package/dist/features/store-extensions/schemas/store-category-form.js +1 -1
- package/dist/features/stores/actions/store-query-actions.js +25 -1
- package/dist/features/stores/components/InteractiveStoreCard.js +1 -1
- package/dist/features/stores/components/StoreHeader.js +1 -1
- package/dist/features/stores/components/StoreNavTabs.js +20 -8
- package/dist/features/stores/repository/store.repository.js +1 -0
- package/dist/features/stores/schemas/firestore.d.ts +2 -0
- package/dist/features/support/repository/support.repository.d.ts +12 -0
- package/dist/features/support/repository/support.repository.js +15 -0
- package/dist/features/support/schemas/ticket-create-form.d.ts +33 -0
- package/dist/features/support/schemas/ticket-create-form.js +82 -0
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +0 -1
- package/dist/features/tester/repository/tester-checklist-item.repository.js +6 -15
- package/dist/features/tester/schemas/firestore.d.ts +5 -4
- package/dist/features/tester/schemas/firestore.js +3 -3
- package/dist/features/tester/seed-data/products-tester-seed-data.js +2 -2
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +138 -1
- package/dist/features/wishlist/components/WishlistHeartButton.d.ts +55 -0
- package/dist/features/wishlist/components/WishlistHeartButton.js +52 -0
- package/dist/features/wishlist/components/index.d.ts +2 -1
- package/dist/features/wishlist/components/index.js +1 -1
- package/dist/index.d.ts +10 -3
- package/dist/index.js +19 -4
- package/dist/monitoring/error-tracking.js +39 -5
- package/dist/monitoring/server-logger.js +70 -32
- package/dist/next/components/ErrorView.js +1 -1
- package/dist/next/routing/route-map.d.ts +39 -7
- package/dist/next/routing/route-map.js +34 -4
- package/dist/providers/db-firebase/base.repository.d.ts +30 -0
- package/dist/providers/db-firebase/base.repository.js +56 -9
- package/dist/react/ErrorBoundary.js +9 -1
- package/dist/react/contexts/SessionContext.js +6 -2
- package/dist/react/hooks/useAuthGate.d.ts +22 -0
- package/dist/react/hooks/useAuthGate.js +86 -7
- package/dist/schemas/registry.d.ts +3 -3
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +1 -1
- package/dist/security/pii-encrypt.d.ts +13 -5
- package/dist/security/pii-encrypt.js +132 -6
- package/dist/security/pii-schemas.d.ts +32 -4
- package/dist/security/pii-schemas.js +38 -5
- package/dist/seed/_helpers/product-search-txt.d.ts +18 -0
- package/dist/seed/_helpers/product-search-txt.js +33 -0
- package/dist/seed/_helpers/search-txt-wrappers.d.ts +20 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +73 -0
- package/dist/seed/blog-posts-seed-data.d.ts +4 -0
- package/dist/seed/blog-posts-seed-data.js +7 -1
- package/dist/seed/events-seed-data.d.ts +5 -1
- package/dist/seed/events-seed-data.js +7 -1
- package/dist/seed/faq-seed-data.d.ts +16 -0
- package/dist/seed/faq-seed-data.js +22 -1
- package/dist/seed/products-art-seed-data.js +49 -7
- package/dist/seed/products-auctions-seed-data.js +12 -4
- package/dist/seed/products-classifieds-seed-data.d.ts +5 -0
- package/dist/seed/products-classifieds-seed-data.js +65 -9
- package/dist/seed/products-digital-codes-seed-data.d.ts +5 -0
- package/dist/seed/products-digital-codes-seed-data.js +65 -9
- package/dist/seed/products-live-items-seed-data.d.ts +5 -0
- package/dist/seed/products-live-items-seed-data.js +33 -5
- package/dist/seed/products-preorders-seed-data.d.ts +1 -1
- package/dist/seed/products-preorders-seed-data.js +6 -2
- package/dist/seed/products-prize-draws-seed-data.d.ts +5 -0
- package/dist/seed/products-prize-draws-seed-data.js +57 -8
- package/dist/seed/products-standard-seed-data.js +6 -2
- package/dist/seed/products-stickers-seed-data.js +49 -7
- package/dist/seed/reviews-seed-data.js +2 -1
- package/dist/seed/site-settings-seed-data.js +5 -3
- package/dist/seed/stores-seed-data.d.ts +4 -0
- package/dist/seed/stores-seed-data.js +7 -1
- package/dist/seo/json-ld.d.ts +44 -2
- package/dist/seo/json-ld.js +57 -11
- package/dist/styles.css +368 -90
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/index.d.ts +1 -1
- package/dist/tokens/index.js +1 -1
- package/dist/tokens/themes/default-dark.js +6 -0
- package/dist/tokens/themes/default-light.js +5 -0
- package/dist/tokens/tokens.css +84 -0
- package/dist/ui/DataTable.js +1 -1
- package/dist/ui/DataTable.style.css +3 -3
- package/dist/ui/components/BulkActionBar.style.css +1 -3
- package/dist/ui/components/Button.js +15 -6
- package/dist/ui/components/Dropdown.style.css +1 -4
- package/dist/ui/components/FilterChipGroup.js +1 -1
- package/dist/ui/components/FilterDrawer.style.css +1 -1
- package/dist/ui/components/Icon.d.ts +53 -0
- package/dist/ui/components/Icon.js +41 -0
- package/dist/ui/components/IconButton.d.ts +13 -0
- package/dist/ui/components/IconButton.js +13 -0
- package/dist/ui/components/IconButton.style.css +18 -10
- package/dist/ui/components/ListingLayout.style.css +2 -10
- package/dist/ui/components/ListingToolbar.js +6 -6
- package/dist/ui/components/Modal.js +1 -1
- package/dist/ui/components/PaginatedSelect.style.css +3 -3
- package/dist/ui/components/Pagination.style.css +1 -6
- package/dist/ui/components/RichTextEditor.js +1 -1
- package/dist/ui/components/RowActionMenu.style.css +1 -1
- package/dist/ui/components/SideDrawer.style.css +1 -1
- package/dist/ui/components/Slider.js +1 -1
- package/dist/ui/components/TabBarShell.d.ts +78 -0
- package/dist/ui/components/TabBarShell.js +95 -0
- package/dist/ui/components/Tabs.js +12 -8
- package/dist/ui/components/Tabs.style.css +249 -41
- package/dist/ui/components/ViewToggle.style.css +1 -5
- package/dist/ui/components/tabs-overflow.d.ts +17 -0
- package/dist/ui/components/tabs-overflow.js +142 -0
- package/dist/ui/icons/icon-registry.d.ts +38 -0
- package/dist/ui/icons/icon-registry.js +37 -0
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/utils/index.d.ts +1 -1
- package/dist/utils/index.js +1 -1
- package/dist/utils/media-url.d.ts +27 -4
- package/dist/utils/media-url.js +37 -6
- package/dist/utils/search-txt.d.ts +71 -0
- package/dist/utils/search-txt.js +118 -0
- package/package.json +1 -1
|
@@ -4,6 +4,8 @@ import React from "react";
|
|
|
4
4
|
import { useApiMutation } from "@mohasinac/appkit/client";
|
|
5
5
|
import { Alert, Badge, Button, Div, FieldCheckbox, FieldInput, Grid, Heading, Modal, Pagination, Skeleton, Stack, Table, Thead, Tbody, Tr, Th, Td, Text, Textarea, } from "../../../ui";
|
|
6
6
|
import { apiClient } from "../../../http";
|
|
7
|
+
import { createShipmentItemSchema } from "../schemas/validation";
|
|
8
|
+
import { ValidationError } from "../../../errors/validation-error";
|
|
7
9
|
import { ADMIN_ENDPOINTS } from "../../../constants/api-endpoints";
|
|
8
10
|
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
9
11
|
import { useShipmentItems } from "../hooks/useShipments";
|
|
@@ -42,12 +44,27 @@ export function AdminShipmentLotItemsView({ shipmentId, lotId }) {
|
|
|
42
44
|
const [linkTarget, setLinkTarget] = React.useState(null);
|
|
43
45
|
const addItemMutation = useApiMutation({
|
|
44
46
|
successMessage: "Item added",
|
|
45
|
-
mutationFn: () =>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
mutationFn: () => {
|
|
48
|
+
/*
|
|
49
|
+
* `createShipmentItemSchema` already validated this route and had no
|
|
50
|
+
* client consumer, so its rule — a projected sale price is required
|
|
51
|
+
* unless the item is for self use — only ever surfaced as a 400 that
|
|
52
|
+
* named no field. `Number(priceRupees)` also yields NaN on a typo,
|
|
53
|
+
* which the schema rejects and the old payload sent.
|
|
54
|
+
*/
|
|
55
|
+
const parsed = createShipmentItemSchema.safeParse({
|
|
56
|
+
title,
|
|
57
|
+
quantity: Number(quantity),
|
|
58
|
+
isForSelfUse,
|
|
59
|
+
price: isForSelfUse ? undefined : Math.round(Number(priceRupees) * 100) / 100,
|
|
60
|
+
});
|
|
61
|
+
if (!parsed.success) {
|
|
62
|
+
// ValidationError carries the issue list; a bare Error flattens it to
|
|
63
|
+
// one string and loses which field failed.
|
|
64
|
+
throw new ValidationError(parsed.error.issues[0]?.message ?? "Check the item details.", parsed.error.issues);
|
|
65
|
+
}
|
|
66
|
+
return apiClient.post(ADMIN_ENDPOINTS.SHIPMENT_LOT_ITEMS(shipmentId, lotId), parsed.data);
|
|
67
|
+
},
|
|
51
68
|
onSuccess: () => {
|
|
52
69
|
setTitle("");
|
|
53
70
|
setQuantity("1");
|
|
@@ -57,7 +57,7 @@ const CATEGORY_COLORS = {
|
|
|
57
57
|
"Messaging": "bg-cyan-100 text-cyan-800 dark:bg-cyan-900/30 dark:text-cyan-300",
|
|
58
58
|
"Seller": "bg-warning-surface text-warning dark:bg-warning-surface dark:text-warning",
|
|
59
59
|
"Orders": "bg-lime-100 text-lime-800 dark:bg-lime-900/30 dark:text-lime-300",
|
|
60
|
-
"Account": "bg-[var(--appkit-color-surface)]
|
|
60
|
+
"Account": "bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)] text-[var(--appkit-color-text-muted)]",
|
|
61
61
|
};
|
|
62
62
|
const ALL_ACTION_IDS = Object.values(ACTION_ID);
|
|
63
63
|
export function ActionPermissionsManager({ initialConfig, onUpdate }) {
|
|
@@ -1,15 +1,32 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* S-STORE Extension Repositories
|
|
3
3
|
*
|
|
4
|
-
* 11 lightweight BaseRepository subclasses.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
4
|
+
* 11 lightweight BaseRepository subclasses.
|
|
5
|
+
*
|
|
6
|
+
* This header used to claim "none of these collections store PII directly" —
|
|
7
|
+
* that was wrong: `PayoutMethodsRepository` holds `upiVpa`, `accountNumber` and
|
|
8
|
+
* `ifscCode`, which sat in cleartext because of it. It now declares `piiFields`
|
|
9
|
+
* and BaseRepository's write hooks encrypt them on every write path, so no
|
|
10
|
+
* per-method override is needed (which is the point of the hooks — Pattern #9
|
|
11
|
+
* described overrides that never existed).
|
|
12
|
+
*
|
|
13
|
+
* The other ten hold no PII and use the BaseRepository public API only.
|
|
8
14
|
*/
|
|
9
15
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
16
|
+
import type { DocumentSnapshot } from "firebase-admin/firestore";
|
|
10
17
|
import { type AnalyticsAlertDocument, type AnalyticsCardDocument, type ItemRequestDocument, type ListingTemplateDocument, type ModerationQueueDocument, type PayoutMethodDocument, type ReportDocument, type ShippingConfigDocument, type StoreCategoryDocument, type StoreGoogleConfigDocument, type StoreWhatsAppConfigDocument } from "../schemas/firestore";
|
|
11
18
|
export declare class PayoutMethodsRepository extends BaseRepository<PayoutMethodDocument> {
|
|
19
|
+
/**
|
|
20
|
+
* Bank/UPI details, encrypted at rest by BaseRepository's write hooks.
|
|
21
|
+
*
|
|
22
|
+
* This repository had no encryption at all. Nothing queries these by equality
|
|
23
|
+
* (`listByStore` filters on `storeId`), so no read path depended on their
|
|
24
|
+
* plaintext.
|
|
25
|
+
*/
|
|
26
|
+
protected piiFields: readonly ["upiVpa", "accountNumber", "ifscCode"];
|
|
12
27
|
constructor();
|
|
28
|
+
/** Decrypt on read so callers see the shape they wrote. */
|
|
29
|
+
protected mapDoc<D = PayoutMethodDocument>(snap: DocumentSnapshot): D;
|
|
13
30
|
listByStore(storeId: string): Promise<{
|
|
14
31
|
items: PayoutMethodDocument[];
|
|
15
32
|
}>;
|
|
@@ -1,16 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* S-STORE Extension Repositories
|
|
3
3
|
*
|
|
4
|
-
* 11 lightweight BaseRepository subclasses.
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `
|
|
4
|
+
* 11 lightweight BaseRepository subclasses.
|
|
5
|
+
*
|
|
6
|
+
* This header used to claim "none of these collections store PII directly" —
|
|
7
|
+
* that was wrong: `PayoutMethodsRepository` holds `upiVpa`, `accountNumber` and
|
|
8
|
+
* `ifscCode`, which sat in cleartext because of it. It now declares `piiFields`
|
|
9
|
+
* and BaseRepository's write hooks encrypt them on every write path, so no
|
|
10
|
+
* per-method override is needed (which is the point of the hooks — Pattern #9
|
|
11
|
+
* described overrides that never existed).
|
|
12
|
+
*
|
|
13
|
+
* The other ten hold no PII and use the BaseRepository public API only.
|
|
8
14
|
*/
|
|
9
15
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
16
|
+
import { decryptPiiFields } from "../../../security/pii-encrypt";
|
|
10
17
|
import { ANALYTICS_ALERTS_COLLECTION, ANALYTICS_CARDS_COLLECTION, ITEM_REQUESTS_COLLECTION, LISTING_TEMPLATES_COLLECTION, MODERATION_QUEUE_COLLECTION, PAYOUT_METHODS_COLLECTION, REPORTS_COLLECTION, SHIPPING_CONFIGS_COLLECTION, STORE_CATEGORIES_COLLECTION, STORE_GOOGLE_CONFIG_COLLECTION, STORE_WHATSAPP_CONFIG_COLLECTION, } from "../schemas/firestore";
|
|
11
18
|
export class PayoutMethodsRepository extends BaseRepository {
|
|
12
19
|
constructor() {
|
|
13
20
|
super(PAYOUT_METHODS_COLLECTION);
|
|
21
|
+
/**
|
|
22
|
+
* Bank/UPI details, encrypted at rest by BaseRepository's write hooks.
|
|
23
|
+
*
|
|
24
|
+
* This repository had no encryption at all. Nothing queries these by equality
|
|
25
|
+
* (`listByStore` filters on `storeId`), so no read path depended on their
|
|
26
|
+
* plaintext.
|
|
27
|
+
*/
|
|
28
|
+
this.piiFields = ["upiVpa", "accountNumber", "ifscCode"];
|
|
29
|
+
}
|
|
30
|
+
/** Decrypt on read so callers see the shape they wrote. */
|
|
31
|
+
mapDoc(snap) {
|
|
32
|
+
const raw = super.mapDoc(snap);
|
|
33
|
+
return decryptPiiFields(raw, [...this.piiFields]);
|
|
14
34
|
}
|
|
15
35
|
async listByStore(storeId) {
|
|
16
36
|
const items = await this.findBy("storeId", storeId);
|
|
@@ -66,17 +66,30 @@ export const shippingConfigFormSchema = z
|
|
|
66
66
|
method: annotate(shippingMethodSchema, {
|
|
67
67
|
section: "basics", quick: true, order: 2, row: "pair",
|
|
68
68
|
}),
|
|
69
|
+
/*
|
|
70
|
+
* Each rate field shows only for the method it belongs to. A flat-rate
|
|
71
|
+
* rule has no price-per-kg, and showing one invites a seller to fill in a
|
|
72
|
+
* number the checkout will never read.
|
|
73
|
+
*
|
|
74
|
+
* These pair with the `superRefine` below, which requires the field under
|
|
75
|
+
* the SAME condition — hiding a required field would otherwise fail
|
|
76
|
+
* validation invisibly.
|
|
77
|
+
*/
|
|
69
78
|
flatRate: annotate(amount("Flat rate").optional(), {
|
|
70
79
|
section: "rates", sectionLabel: "Rates", order: 1, row: "pair",
|
|
80
|
+
when: (v) => v.method === "flat",
|
|
71
81
|
}),
|
|
72
82
|
pricePerKg: annotate(amount("Price per kg").optional(), {
|
|
73
83
|
section: "rates", order: 2, row: "pair",
|
|
84
|
+
when: (v) => v.method === "weight",
|
|
74
85
|
}),
|
|
75
86
|
freeAbove: annotate(amount("Free above").optional(), {
|
|
76
87
|
section: "rates", order: 3, row: "pair",
|
|
88
|
+
when: (v) => v.method === "free",
|
|
77
89
|
}),
|
|
78
90
|
expressSurcharge: annotate(amount("Express surcharge").optional(), {
|
|
79
91
|
section: "rates", order: 4, row: "pair",
|
|
92
|
+
when: (v) => v.method === "express",
|
|
80
93
|
}),
|
|
81
94
|
estimatedDays: annotate(z.coerce.number().int("Estimated days must be a whole number.").min(0).max(365).optional(), { section: "rates", order: 5, row: "pair" }),
|
|
82
95
|
isDefault: annotate(z.boolean(), {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* parses this: `storeId` is set from the session, never from the body, so a
|
|
18
18
|
* caller cannot write a category into someone else's store by adding a field.
|
|
19
19
|
*
|
|
20
|
-
* `productIds`
|
|
20
|
+
* `productIds` is deliberately absent: it is managed after
|
|
21
21
|
* creation (assigning products, toggling visibility), not typed into the
|
|
22
22
|
* create form, and the repository supplies their defaults.
|
|
23
23
|
*
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* Public and seller-facing read functions for store listings, products,
|
|
5
5
|
* auctions and reviews. No auth — callers decide whether auth is needed.
|
|
6
6
|
*/
|
|
7
|
+
import { cache } from "react";
|
|
7
8
|
import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
|
|
8
9
|
import { NotFoundError } from "../../../errors";
|
|
9
10
|
import { filterTestDataForViewer, filterSingleTestData } from "../../../_internal/server/features/tester/visibility";
|
|
@@ -26,8 +27,31 @@ export async function listStores(params = {}, viewer) {
|
|
|
26
27
|
const result = await storeRepository.listStores(model);
|
|
27
28
|
return { ...result, items: filterTestDataForViewer(result.items, viewer) };
|
|
28
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Request-scoped memo of the raw store fetch.
|
|
32
|
+
*
|
|
33
|
+
* CLAUDE.md § SSR Architecture requires `React.cache` on any data-fetch shared
|
|
34
|
+
* by a page and its `generateMetadata`. `stores/[storeSlug]/layout.tsx` awaits
|
|
35
|
+
* `getStoreBySlug` in BOTH — so every store page view was doing two identical
|
|
36
|
+
* Firestore reads.
|
|
37
|
+
*
|
|
38
|
+
* 🛑 The cache wraps the REPOSITORY call, not `getStoreBySlug`, and that is
|
|
39
|
+
* deliberate. `React.cache` memoises per argument list, and the two call sites
|
|
40
|
+
* pass different arguments — `getStoreBySlug(slug)` in `generateMetadata`,
|
|
41
|
+
* `getStoreBySlug(slug, viewer)` in the layout body. Wrapping the exported
|
|
42
|
+
* function would produce two cache entries and dedupe nothing.
|
|
43
|
+
*
|
|
44
|
+
* Making them match by reading the session in `generateMetadata` would be worse
|
|
45
|
+
* still: `getServerSessionUser()` is a dynamic API, and a dynamic API reached
|
|
46
|
+
* from metadata opts the route out of static rendering — the exact defect that
|
|
47
|
+
* made the whole site uncacheable (see `src/app/[locale]/layout.tsx`).
|
|
48
|
+
*
|
|
49
|
+
* So the read is cached on `storeSlug` alone and the viewer-specific
|
|
50
|
+
* `filterSingleTestData` is applied outside it, per caller.
|
|
51
|
+
*/
|
|
52
|
+
const fetchStoreBySlug = cache((storeSlug) => storeRepository.findBySlug(storeSlug));
|
|
29
53
|
export async function getStoreBySlug(storeSlug, viewer) {
|
|
30
|
-
const store = await
|
|
54
|
+
const store = await fetchStoreBySlug(storeSlug);
|
|
31
55
|
return filterSingleTestData(store, viewer);
|
|
32
56
|
}
|
|
33
57
|
export async function getStoreProducts(storeSlug, params = {}, viewer) {
|
|
@@ -25,5 +25,5 @@ export function InteractiveStoreCard({ store, href, selectable, isSelected = fal
|
|
|
25
25
|
].join(" "), onMouseDown: !isSelected ? longPress.onMouseDown : undefined, onMouseUp: !isSelected ? longPress.onMouseUp : undefined, onMouseLeave: !isSelected ? longPress.onMouseLeave : undefined, onTouchStart: !isSelected ? longPress.onTouchStart : undefined, onTouchEnd: !isSelected ? longPress.onTouchEnd : undefined, onTouchCancel: !isSelected ? longPress.onTouchCancel : undefined, children: [onSelect && (_jsx(BaseListingCard.Checkbox, { selected: isSelected, onSelect: (e) => {
|
|
26
26
|
e.preventDefault();
|
|
27
27
|
onSelect(store.id, !isSelected);
|
|
28
|
-
}, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-video w-full ${__O.hidden} flex-shrink-0`, children: [hasBanner ? (_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Row, { className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-20 select-none", size: "5xl", "aria-hidden": "true", children: "\uD83C\uDFEA" }) })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-x-0 bottom-0 h-12" }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Div, { className: CLS_RATING_BADGE, children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Stack, { paddingY: "b-md", className: "flex-1", padding: "x-md", children: [_jsx(Row, { className: "-mt-5 mb-2", align: "end", justify: "between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(Div, { className: "relative h-10 w-10 border-2 border-
|
|
28
|
+
}, label: isSelected ? "Deselect store" : "Select store", position: "top-2 left-2", className: selectable || isSelected ? "opacity-100" : "opacity-0 pointer-events-none group-hover:opacity-100 group-hover:pointer-events-auto transition-opacity" })), _jsxs(Link, { href: href, className: "flex flex-col flex-1 min-h-0", children: [_jsxs(Div, { surface: "muted", className: `relative aspect-video w-full ${__O.hidden} flex-shrink-0`, children: [hasBanner ? (_jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner", className: "h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" })) : (_jsx(Row, { className: "absolute inset-0", align: "center", justify: "center", children: _jsx(Span, { className: "opacity-20 select-none", size: "5xl", "aria-hidden": "true", children: "\uD83C\uDFEA" }) })), _jsx(Scrim, { direction: "bottom-up", intensity: "subtle", className: "absolute inset-x-0 bottom-0 h-12" }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Div, { className: CLS_RATING_BADGE, children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Stack, { paddingY: "b-md", className: "flex-1", padding: "x-md", children: [_jsx(Row, { className: "-mt-5 mb-2", align: "end", justify: "between", children: _jsx(Div, { className: "flex-shrink-0", children: hasLogo ? (_jsx(Div, { className: "relative h-10 w-10 border-2 border-[var(--appkit-color-border-subtle)] bg-[var(--appkit-color-surface)]", rounded: "lg", shadow: "md", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "thumbnail", onError: () => setLogoBroken(true) }) })) : (_jsx(Row, { textWeight: "bold", textSize: "base", className: "h-10 w-10 border-2 border-white bg-primary/10 dark:bg-primary/20 text-primary", align: "center", justify: "center", rounded: "lg", shadow: "md", children: initial })) }) }), _jsx(Heading, { level: 3, className: `group-hover:text-primary transition-colors`, truncate: true, size: "sm", weight: "bold", color: "primary", children: store.storeName }), store.storeDescription ? (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), proseClass: "prose prose-sm max-w-none dark:prose-invert prose-p:my-0", className: "mt-1 line-clamp-2 text-[length:var(--appkit-text-xs)] text-[var(--appkit-color-text-muted)] flex-1" })) : (_jsx(Div, { className: "flex-1" })), _jsxs(Row, { color: "muted", textSize: "xs", gap: "sm", className: "mt-2.5", wrap: true, children: [store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDCE6" }), " ", store.totalProducts, " ", labels.products ?? "products"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { layout: "flex", gap: "2xs", children: [_jsx(Span, { "aria-hidden": "true", children: "\uD83D\uDECD\uFE0F" }), " ", store.itemsSold, " ", labels.sold ?? "sold"] }))] }), _jsxs(Row, { border: "top-subtle", paddingY: "y-xs-tall", className: "mt-3", align: "center", justify: "between", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: "text-primary group-hover:underline transition-colors", children: [labels.visitStore ?? "Visit store", " \u2192"] }), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { size: "xs", color: "muted", children: [store.totalReviews, " ", labels.reviews ?? "reviews"] }))] })] })] })] }));
|
|
29
29
|
}
|
|
@@ -8,7 +8,7 @@ import { SellerTrustBadge } from "../../scams/components/SellerTrustBadge";
|
|
|
8
8
|
const __O = {
|
|
9
9
|
hidden: "overflow-hidden",
|
|
10
10
|
};
|
|
11
|
-
const CLS_AVATAR = "-mt-8 h-16 w-16 rounded-xl border-2 border-
|
|
11
|
+
const CLS_AVATAR = "-mt-8 h-16 w-16 rounded-xl border-2 border-[var(--appkit-color-border-subtle)] bg-warning-surface dark:bg-warning-surface flex items-center justify-center text-warning dark:text-warning font-bold text-[length:var(--appkit-text-2xl)] shadow-sm";
|
|
12
12
|
const CLS_STARS = "inline-flex items-center gap-[var(--appkit-space-1)] text-warning";
|
|
13
13
|
const CLS_FOLLOW_BTN = "rounded-lg border border-warning px-[var(--appkit-space-4)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] font-medium text-warning hover:bg-warning-surface transition-colors";
|
|
14
14
|
const CLS_WARN_BANNER = "mt-3 rounded-lg bg-warning-surface dark:bg-warning-surface border border-warning dark:border-warning px-[var(--appkit-space-3)] py-[var(--appkit-space-2)] text-[length:var(--appkit-text-sm)] text-warning dark:text-warning";
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { Fragment as _Fragment, jsx as _jsx
|
|
2
|
+
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { useRouter } from "next/navigation";
|
|
4
|
-
import {
|
|
5
|
-
const CLS_ACTIVE_TAB = "border-primary text-primary";
|
|
4
|
+
import { TabBarButton, TabBarShell, TabsNavSelect, TextLink } from "../../../ui";
|
|
6
5
|
export function StoreNavTabs({ tabs, dropdownTabs = [], dropdownPlaceholder = "Browse listings", activeValue, onTabChange, renderTabBar, className = "", }) {
|
|
7
6
|
const router = useRouter();
|
|
8
7
|
const handleChange = (v) => onTabChange?.(v);
|
|
@@ -18,9 +17,22 @@ export function StoreNavTabs({ tabs, dropdownTabs = [], dropdownPlaceholder = "B
|
|
|
18
17
|
router.push(tab.href);
|
|
19
18
|
handleChange(tab.value);
|
|
20
19
|
};
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
/*
|
|
21
|
+
* The listing-type picker goes in `leading` — i.e. INSIDE the shell but
|
|
22
|
+
* OUTSIDE the role="tablist" element. It used to be a direct child of the
|
|
23
|
+
* tablist, which is invalid ARIA: a <select> is not a role="tab". Its old
|
|
24
|
+
* `max-w-[12rem] flex-shrink-0` sizing also went onto the inner <select>,
|
|
25
|
+
* the wrong element for sizing utilities (Root Cause #29); those now live
|
|
26
|
+
* on `.appkit-tabs-dropdown` in Tabs.style.css.
|
|
27
|
+
*/
|
|
28
|
+
const leading = dropdownTabs.length > 0 ? (_jsx(TabsNavSelect, { ariaLabel: dropdownPlaceholder, options: dropdownTabs.map((tab) => ({ value: tab.value, label: tab.label })), value: activeDropdownTab?.value ?? "", placeholder: activeDropdownTab ? undefined : dropdownPlaceholder, selected: Boolean(activeDropdownTab), onValueChange: handleDropdownChange })) : undefined;
|
|
29
|
+
/*
|
|
30
|
+
* Triggers carry `.appkit-tabs-trigger` and NOTHING else — no padding /
|
|
31
|
+
* size / weight props, no conditional colour classes. `important: true` is
|
|
32
|
+
* set in both tailwind configs, so any surviving utility would beat the
|
|
33
|
+
* component CSS unconditionally. The old inactive string also contained
|
|
34
|
+
* contradictory duplicates (`hover:text-neutral-800` immediately overridden
|
|
35
|
+
* by `hover:text-…text-muted`), so hover had been a no-op.
|
|
36
|
+
*/
|
|
37
|
+
return (_jsx(TabBarShell, { className: className, ariaLabel: "Store sections", leading: leading, activeKey: activeValue, children: tabs.map((tab) => tab.href ? (_jsx(TextLink, { variant: "bare", href: tab.href, role: "tab", "aria-selected": activeValue === tab.value, className: "appkit-tabs-trigger", children: tab.label }, tab.value)) : (_jsx(TabBarButton, { selected: activeValue === tab.value, onClick: () => handleChange(tab.value), children: tab.label }, tab.value))) }));
|
|
26
38
|
}
|
|
@@ -154,6 +154,7 @@ export class StoreRepository extends BaseRepository {
|
|
|
154
154
|
async listStores(model, activeOnly = true) {
|
|
155
155
|
const sieveFields = {
|
|
156
156
|
[STORE_FIELDS.STORE_NAME]: { canFilter: true, canSort: true },
|
|
157
|
+
searchTxt: { canFilter: true, canSort: false },
|
|
157
158
|
[STORE_FIELDS.STORE_CATEGORY]: { canFilter: true, canSort: false },
|
|
158
159
|
[STORE_FIELDS.STATUS]: { canFilter: true, canSort: false },
|
|
159
160
|
[STORE_FIELDS.IS_PUBLIC]: { canFilter: true, canSort: false },
|
|
@@ -49,6 +49,8 @@ export declare const StoreStatusValues: {
|
|
|
49
49
|
};
|
|
50
50
|
export type StoreStatus = (typeof StoreStatusValues)[keyof typeof StoreStatusValues];
|
|
51
51
|
export interface StoreDocument extends BaseDocument {
|
|
52
|
+
/** Normalized edge-n-gram search field — see appkit/src/utils/search-txt.ts. */
|
|
53
|
+
searchTxt?: string[];
|
|
52
54
|
storeSlug: string;
|
|
53
55
|
ownerId: string;
|
|
54
56
|
/** Tester sandbox flag — set on the shared admin-seeded test store; swept by testerSandboxCleanup. */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
2
2
|
import type { FirebaseSieveFields, FirebaseSieveResult, SieveModel } from "../../../providers/db-firebase";
|
|
3
|
+
import type { DocumentSnapshot } from "firebase-admin/firestore";
|
|
3
4
|
import { type SupportTicketDocument, type SupportTicketCreateInput, type SupportTicketUpdateInput, type TicketMessage, type TicketStatus } from "../schemas/firestore";
|
|
4
5
|
import { type HistoryActor } from "../../../_internal/shared/history/index";
|
|
5
6
|
/** Who/why for a write that lands in `statusHistory`. */
|
|
@@ -11,7 +12,18 @@ export interface TicketWriteContext {
|
|
|
11
12
|
}
|
|
12
13
|
export declare class SupportRepository extends BaseRepository<SupportTicketDocument> {
|
|
13
14
|
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
15
|
+
/**
|
|
16
|
+
* Ticket PII, encrypted by BaseRepository's write hooks.
|
|
17
|
+
*
|
|
18
|
+
* `SUPPORT_TICKET_PII_FIELDS` existed but was only ever used to scrub status
|
|
19
|
+
* history — the documents themselves were never encrypted. Safe to enable:
|
|
20
|
+
* nothing queries these by equality (the admin list searches `subject`), so
|
|
21
|
+
* no read path depends on their plaintext.
|
|
22
|
+
*/
|
|
23
|
+
protected piiFields: readonly ["userEmail", "userDisplayName", "assignedToName"];
|
|
14
24
|
constructor();
|
|
25
|
+
/** Decrypt on read so callers see the shape they wrote. */
|
|
26
|
+
protected mapDoc<D = SupportTicketDocument>(snap: DocumentSnapshot): D;
|
|
15
27
|
createTicket(input: SupportTicketCreateInput): Promise<SupportTicketDocument>;
|
|
16
28
|
getUserTickets(userId: string, page?: number, pageSize?: number): Promise<FirebaseSieveResult<SupportTicketDocument>>;
|
|
17
29
|
getTicketById(ticketId: string): Promise<SupportTicketDocument | null>;
|
|
@@ -3,11 +3,26 @@ import { BaseRepository, prepareForFirestore, parseSieveDateValue, } from "../..
|
|
|
3
3
|
import { normalizeError } from "../../../errors/normalize";
|
|
4
4
|
import { DatabaseError } from "../../../errors";
|
|
5
5
|
import { SUPPORT_TICKET_FIELDS } from "../../../constants/field-names";
|
|
6
|
+
import { decryptPiiFields } from "../../../security/pii-encrypt";
|
|
6
7
|
import { SUPPORT_TICKET_COLLECTION, SUPPORT_TICKET_PII_FIELDS, SUPPORT_TICKET_TRACKED_FIELDS, TicketStatusValues, ACTIVE_TICKET_STATUSES, } from "../schemas/firestore";
|
|
7
8
|
import { withHistory } from "../../../_internal/shared/history/index";
|
|
8
9
|
export class SupportRepository extends BaseRepository {
|
|
9
10
|
constructor() {
|
|
10
11
|
super(SUPPORT_TICKET_COLLECTION);
|
|
12
|
+
/**
|
|
13
|
+
* Ticket PII, encrypted by BaseRepository's write hooks.
|
|
14
|
+
*
|
|
15
|
+
* `SUPPORT_TICKET_PII_FIELDS` existed but was only ever used to scrub status
|
|
16
|
+
* history — the documents themselves were never encrypted. Safe to enable:
|
|
17
|
+
* nothing queries these by equality (the admin list searches `subject`), so
|
|
18
|
+
* no read path depends on their plaintext.
|
|
19
|
+
*/
|
|
20
|
+
this.piiFields = SUPPORT_TICKET_PII_FIELDS;
|
|
21
|
+
}
|
|
22
|
+
/** Decrypt on read so callers see the shape they wrote. */
|
|
23
|
+
mapDoc(snap) {
|
|
24
|
+
const raw = super.mapDoc(snap);
|
|
25
|
+
return decryptPiiFields(raw, [...SUPPORT_TICKET_PII_FIELDS]);
|
|
11
26
|
}
|
|
12
27
|
async createTicket(input) {
|
|
13
28
|
try {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type TicketCategory } from "./firestore";
|
|
3
|
+
/**
|
|
4
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
5
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
6
|
+
*/
|
|
7
|
+
export declare const supportTicketCreateSchema: z.ZodEffects<z.ZodObject<{
|
|
8
|
+
category: z.ZodEnum<[TicketCategory, ...TicketCategory[]]>;
|
|
9
|
+
subject: z.ZodString;
|
|
10
|
+
description: z.ZodString;
|
|
11
|
+
orderId: z.ZodOptional<z.ZodString>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
category: TicketCategory;
|
|
14
|
+
description: string;
|
|
15
|
+
subject: string;
|
|
16
|
+
orderId?: string | undefined;
|
|
17
|
+
}, {
|
|
18
|
+
category: TicketCategory;
|
|
19
|
+
description: string;
|
|
20
|
+
subject: string;
|
|
21
|
+
orderId?: string | undefined;
|
|
22
|
+
}>, {
|
|
23
|
+
category: TicketCategory;
|
|
24
|
+
description: string;
|
|
25
|
+
subject: string;
|
|
26
|
+
orderId?: string | undefined;
|
|
27
|
+
}, {
|
|
28
|
+
category: TicketCategory;
|
|
29
|
+
description: string;
|
|
30
|
+
subject: string;
|
|
31
|
+
orderId?: string | undefined;
|
|
32
|
+
}>;
|
|
33
|
+
export type SupportTicketCreateValues = z.infer<typeof supportTicketCreateSchema>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* WHY: Support tickets had THREE validation rules in three places that did not
|
|
3
|
+
* agree. `/user/support/new` hand-rolled a `canSubmit` boolean;
|
|
4
|
+
* `UserSupportView`'s drawer checked nothing at all; and
|
|
5
|
+
* `POST /api/support/tickets` declared its own inline schema.
|
|
6
|
+
* WHAT: One schema for both surfaces AND the route.
|
|
7
|
+
*
|
|
8
|
+
* ## The page was STRICTER than the route, which is the interesting part
|
|
9
|
+
*
|
|
10
|
+
* `/user/support/new` required an `orderId` when the category is
|
|
11
|
+
* `order_issue` — a real rule, since an order complaint with no order is
|
|
12
|
+
* unactionable and support has to go back and ask. The route never enforced
|
|
13
|
+
* it, and the drawer never enforced anything, so the same ticket was
|
|
14
|
+
* acceptable or not depending on which surface the user happened to open.
|
|
15
|
+
*
|
|
16
|
+
* That rule is a `.superRefine` here, so it now holds on all three. Deleting
|
|
17
|
+
* it to "match the route" would have been the easy read of consolidation and
|
|
18
|
+
* the wrong one: the stricter rule was the correct one.
|
|
19
|
+
*
|
|
20
|
+
* EXPORTS: supportTicketCreateSchema, type SupportTicketCreateValues
|
|
21
|
+
*
|
|
22
|
+
* @tag domain:support
|
|
23
|
+
* @tag layer:schema
|
|
24
|
+
* @tag pattern:none
|
|
25
|
+
* @tag access:isomorphic
|
|
26
|
+
* @tag consumers:/user/support/new,UserSupportView,/api/support/tickets
|
|
27
|
+
* @tag sideEffects:none
|
|
28
|
+
*/
|
|
29
|
+
import { z } from "zod";
|
|
30
|
+
import { annotate } from "../../shell/field-ui-meta";
|
|
31
|
+
import { TicketCategoryValues } from "./firestore";
|
|
32
|
+
/*
|
|
33
|
+
* `Object.values(...)` rather than `z.enum(TicketCategoryValues)`. The object
|
|
34
|
+
* form of `z.enum` is Zod 4 syntax, and appkit compiles against zod 3 while
|
|
35
|
+
* the app is on 4 — the two-zod split this repo carries. The tuple form works
|
|
36
|
+
* in both, and still DERIVES from the one map, so a twelfth category cannot be
|
|
37
|
+
* omitted here (Root Cause #61).
|
|
38
|
+
*/
|
|
39
|
+
const CATEGORY_TUPLE = Object.values(TicketCategoryValues);
|
|
40
|
+
/**
|
|
41
|
+
* 🛑 `annotate()` must be the OUTERMOST call on each field — it keys a WeakMap
|
|
42
|
+
* by schema instance and every zod wrapper returns a new one.
|
|
43
|
+
*/
|
|
44
|
+
export const supportTicketCreateSchema = z
|
|
45
|
+
.object({
|
|
46
|
+
category: annotate(z.enum(CATEGORY_TUPLE), {
|
|
47
|
+
section: "ticket",
|
|
48
|
+
sectionLabel: "What do you need help with?",
|
|
49
|
+
sectionRequired: true,
|
|
50
|
+
quick: true,
|
|
51
|
+
order: 1,
|
|
52
|
+
row: "pair",
|
|
53
|
+
}),
|
|
54
|
+
subject: annotate(z
|
|
55
|
+
.string()
|
|
56
|
+
.min(3, "Give the ticket a subject of at least 3 characters.")
|
|
57
|
+
.max(200, "Keep the subject under 200 characters."), { section: "ticket", quick: true, order: 2, row: "full" }),
|
|
58
|
+
description: annotate(z
|
|
59
|
+
.string()
|
|
60
|
+
.min(10, "Describe the problem in at least 10 characters.")
|
|
61
|
+
.max(5000, "Keep the description under 5000 characters."), { section: "ticket", quick: true, order: 3, row: "full", kind: "textarea" }),
|
|
62
|
+
orderId: annotate(z.string().max(120).optional(), {
|
|
63
|
+
section: "ticket",
|
|
64
|
+
order: 4,
|
|
65
|
+
row: "pair",
|
|
66
|
+
help: "Required when the category is an order issue.",
|
|
67
|
+
}),
|
|
68
|
+
})
|
|
69
|
+
.superRefine((v, ctx) => {
|
|
70
|
+
/*
|
|
71
|
+
* The rule only `/user/support/new` enforced. An order complaint with no
|
|
72
|
+
* order id cannot be actioned — support has to reply asking for it, which
|
|
73
|
+
* is a round trip the form can prevent.
|
|
74
|
+
*/
|
|
75
|
+
if (v.category === TicketCategoryValues.ORDER_ISSUE && !v.orderId?.trim()) {
|
|
76
|
+
ctx.addIssue({
|
|
77
|
+
code: z.ZodIssueCode.custom,
|
|
78
|
+
path: ["orderId"],
|
|
79
|
+
message: "Which order is this about? An order id is required.",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
});
|
|
@@ -4,7 +4,6 @@ import { type TesterChecklistItemDocument, type TesterChecklistItemCreateInput,
|
|
|
4
4
|
export declare class TesterChecklistItemRepository extends BaseRepository<TesterChecklistItemDocument> {
|
|
5
5
|
static readonly SIEVE_FIELDS: FirebaseSieveFields;
|
|
6
6
|
constructor();
|
|
7
|
-
private buildSearchTokens;
|
|
8
7
|
createItem(input: TesterChecklistItemCreateInput): Promise<TesterChecklistItemDocument>;
|
|
9
8
|
update(id: string, data: TesterChecklistItemUpdateInput): Promise<TesterChecklistItemDocument>;
|
|
10
9
|
listActive(): Promise<TesterChecklistItemDocument[]>;
|
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import { BaseRepository, parseSieveDateValue } from "../../../providers/db-firebase";
|
|
2
|
+
import { buildSearchTxt } from "../../../utils/search-txt";
|
|
2
3
|
import { DatabaseError } from "../../../errors";
|
|
3
4
|
import { TESTER_CHECKLIST_ITEM_COLLECTION, TESTER_CHECKLIST_ITEM_FIELDS, createChecklistItemId, } from "../schemas/firestore";
|
|
4
5
|
export class TesterChecklistItemRepository extends BaseRepository {
|
|
5
6
|
constructor() {
|
|
6
7
|
super(TESTER_CHECKLIST_ITEM_COLLECTION);
|
|
7
8
|
}
|
|
8
|
-
buildSearchTokens(input) {
|
|
9
|
-
const rawText = [input.label, input.description, input.groupLabel, input.pageLabel]
|
|
10
|
-
.filter(Boolean)
|
|
11
|
-
.join(" ")
|
|
12
|
-
.toLowerCase();
|
|
13
|
-
return Array.from(new Set(rawText
|
|
14
|
-
.split(/[^a-z0-9]+/i)
|
|
15
|
-
.map((token) => token.trim())
|
|
16
|
-
.filter((token) => token.length >= 2))).slice(0, 50);
|
|
17
|
-
}
|
|
18
9
|
async createItem(input) {
|
|
19
10
|
const id = createChecklistItemId(input.groupKey, input.pageKey, input.label);
|
|
20
|
-
const
|
|
21
|
-
return this.createWithId(id, { ...input,
|
|
11
|
+
const searchTxt = buildSearchTxt([input.label, input.description, input.groupLabel, input.pageLabel]);
|
|
12
|
+
return this.createWithId(id, { ...input, searchTxt });
|
|
22
13
|
}
|
|
23
14
|
async update(id, data) {
|
|
24
15
|
const current = await this.findById(id);
|
|
@@ -28,7 +19,7 @@ export class TesterChecklistItemRepository extends BaseRepository {
|
|
|
28
19
|
const merged = { ...current, ...data };
|
|
29
20
|
return super.update(id, {
|
|
30
21
|
...data,
|
|
31
|
-
|
|
22
|
+
searchTxt: buildSearchTxt([merged.label, merged.description, merged.groupLabel, merged.pageLabel]),
|
|
32
23
|
});
|
|
33
24
|
}
|
|
34
25
|
async listActive() {
|
|
@@ -90,7 +81,7 @@ export class TesterChecklistItemRepository extends BaseRepository {
|
|
|
90
81
|
isActive: true,
|
|
91
82
|
version: nextVersion,
|
|
92
83
|
previousVersionId: old.id,
|
|
93
|
-
|
|
84
|
+
searchTxt: buildSearchTxt([old.label, old.description, old.groupLabel, old.pageLabel]),
|
|
94
85
|
});
|
|
95
86
|
await this.update(old.id, { supersededByItemId: newId });
|
|
96
87
|
return newItem;
|
|
@@ -129,7 +120,7 @@ TesterChecklistItemRepository.SIEVE_FIELDS = {
|
|
|
129
120
|
order: { canFilter: true, canSort: true },
|
|
130
121
|
phase: { canFilter: true, canSort: true },
|
|
131
122
|
isActive: { canFilter: true, canSort: false },
|
|
132
|
-
|
|
123
|
+
searchTxt: { canFilter: true, canSort: false },
|
|
133
124
|
createdAt: { canFilter: true, canSort: true, parseValue: parseSieveDateValue },
|
|
134
125
|
bugConfirmed: { canFilter: true, canSort: false },
|
|
135
126
|
};
|
|
@@ -17,7 +17,8 @@ export interface TesterChecklistItemDocument extends BaseDocument {
|
|
|
17
17
|
order: number;
|
|
18
18
|
phase: number;
|
|
19
19
|
isActive: boolean;
|
|
20
|
-
searchTokens
|
|
20
|
+
/** Normalized edge-n-gram search field. Replaced `searchTokens`. */
|
|
21
|
+
searchTxt: string[];
|
|
21
22
|
adminOnly?: boolean;
|
|
22
23
|
bugConfirmed?: boolean;
|
|
23
24
|
bugHunterId?: string;
|
|
@@ -29,9 +30,9 @@ export interface TesterChecklistItemDocument extends BaseDocument {
|
|
|
29
30
|
}
|
|
30
31
|
export declare const TESTER_CHECKLIST_ITEM_COLLECTION: "testerChecklistItems";
|
|
31
32
|
export declare const TESTER_CHECKLIST_ITEM_ID_PREFIX: "checklist-";
|
|
32
|
-
export declare const TESTER_CHECKLIST_ITEM_INDEXED_FIELDS: readonly ["groupKey", "pageKey", "phase", "order", "isActive", "
|
|
33
|
+
export declare const TESTER_CHECKLIST_ITEM_INDEXED_FIELDS: readonly ["groupKey", "pageKey", "phase", "order", "isActive", "searchTxt", "createdAt"];
|
|
33
34
|
export declare const DEFAULT_TESTER_CHECKLIST_ITEM_DATA: Partial<TesterChecklistItemDocument>;
|
|
34
|
-
export type TesterChecklistItemCreateInput = Omit<TesterChecklistItemDocument, "id" | "createdAt" | "updatedAt" | "
|
|
35
|
+
export type TesterChecklistItemCreateInput = Omit<TesterChecklistItemDocument, "id" | "createdAt" | "updatedAt" | "searchTxt">;
|
|
35
36
|
export type TesterChecklistItemUpdateInput = Partial<Pick<TesterChecklistItemDocument, "groupKey" | "groupLabel" | "pageKey" | "pageLabel" | "label" | "description" | "href" | "order" | "phase" | "isActive" | "adminOnly" | "bugConfirmed" | "bugHunterId" | "bugHunterName" | "bugConfirmedAt" | "version" | "previousVersionId" | "supersededByItemId">>;
|
|
36
37
|
export declare const testerChecklistItemQueryHelpers: {
|
|
37
38
|
readonly byGroup: (groupKey: string) => readonly ["groupKey", "==", string];
|
|
@@ -84,7 +85,7 @@ export declare const TESTER_CHECKLIST_ITEM_FIELDS: {
|
|
|
84
85
|
readonly ORDER: "order";
|
|
85
86
|
readonly PHASE: "phase";
|
|
86
87
|
readonly IS_ACTIVE: "isActive";
|
|
87
|
-
readonly SEARCH_TOKENS: "
|
|
88
|
+
readonly SEARCH_TOKENS: "searchTxt";
|
|
88
89
|
readonly ADMIN_ONLY: "adminOnly";
|
|
89
90
|
readonly BUG_CONFIRMED: "bugConfirmed";
|
|
90
91
|
readonly BUG_HUNTER_ID: "bugHunterId";
|
|
@@ -14,14 +14,14 @@ export const TESTER_CHECKLIST_ITEM_INDEXED_FIELDS = [
|
|
|
14
14
|
"phase",
|
|
15
15
|
"order",
|
|
16
16
|
"isActive",
|
|
17
|
-
"
|
|
17
|
+
"searchTxt",
|
|
18
18
|
"createdAt",
|
|
19
19
|
];
|
|
20
20
|
export const DEFAULT_TESTER_CHECKLIST_ITEM_DATA = {
|
|
21
21
|
order: 0,
|
|
22
22
|
phase: 1,
|
|
23
23
|
isActive: true,
|
|
24
|
-
|
|
24
|
+
searchTxt: [],
|
|
25
25
|
adminOnly: false,
|
|
26
26
|
};
|
|
27
27
|
export const testerChecklistItemQueryHelpers = {
|
|
@@ -80,7 +80,7 @@ export const TESTER_CHECKLIST_ITEM_FIELDS = {
|
|
|
80
80
|
ORDER: "order",
|
|
81
81
|
PHASE: "phase",
|
|
82
82
|
IS_ACTIVE: "isActive",
|
|
83
|
-
SEARCH_TOKENS: "
|
|
83
|
+
SEARCH_TOKENS: "searchTxt",
|
|
84
84
|
ADMIN_ONLY: "adminOnly",
|
|
85
85
|
BUG_CONFIRMED: "bugConfirmed",
|
|
86
86
|
BUG_HUNTER_ID: "bugHunterId",
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* @tag sideEffects:none
|
|
24
24
|
*/
|
|
25
25
|
import { PRODUCT_FIELDS } from "../../../constants/field-names";
|
|
26
|
-
import {
|
|
26
|
+
import { buildSearchTxt } from "../../../utils/search-txt";
|
|
27
27
|
import { seedExtMedia } from "../../../seed/_helpers/media";
|
|
28
28
|
import { testDataExpiresAt } from "./tester-ttl";
|
|
29
29
|
const TESTER_STORE_ID = "store-tester-sandbox";
|
|
@@ -42,7 +42,7 @@ function withTokens(p) {
|
|
|
42
42
|
storeId: TESTER_STORE_ID,
|
|
43
43
|
storeName: TESTER_STORE_NAME,
|
|
44
44
|
...p,
|
|
45
|
-
|
|
45
|
+
searchTxt: buildSearchTxt([p.title, p.description, p.brand, p.brandSlug, p.categoryNames, p.tags]),
|
|
46
46
|
};
|
|
47
47
|
}
|
|
48
48
|
// Staggered, incrementing-id auction fixtures so testers can watch a live
|