@mohasinac/appkit 3.5.6 → 3.5.7

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.
Files changed (57) hide show
  1. package/dist/_internal/client/features/tour/TourProvider.d.ts +11 -3
  2. package/dist/_internal/client/features/tour/TourProvider.js +93 -4
  3. package/dist/_internal/server/features/checkout/actions.js +65 -11
  4. package/dist/_internal/server/features/orders/adapters.js +3 -0
  5. package/dist/_internal/shared/actions/action-registry.js +20 -0
  6. package/dist/_internal/shared/checkout/order-math.d.ts +21 -0
  7. package/dist/_internal/shared/checkout/order-math.js +24 -0
  8. package/dist/_internal/shared/features/orders/config.d.ts +1 -1
  9. package/dist/_internal/shared/features/orders/config.js +1 -1
  10. package/dist/_internal/shared/fees/calculator.d.ts +13 -0
  11. package/dist/_internal/shared/fees/calculator.js +8 -0
  12. package/dist/client.d.ts +8 -0
  13. package/dist/client.js +8 -0
  14. package/dist/constants/api-endpoints.d.ts +9 -0
  15. package/dist/constants/api-endpoints.js +3 -0
  16. package/dist/features/admin/components/AdminBundleEditorView.d.ts +8 -1
  17. package/dist/features/admin/components/AdminBundleEditorView.js +15 -11
  18. package/dist/features/admin/components/AdminPayoutsView.js +41 -4
  19. package/dist/features/admin/components/AdminSiteSettingsView.js +49 -1
  20. package/dist/features/admin/schemas/firestore.d.ts +8 -0
  21. package/dist/features/admin/schemas/firestore.js +6 -0
  22. package/dist/features/layout/TitleBarLayout.js +4 -4
  23. package/dist/features/messages/hooks/useConversations.d.ts +7 -1
  24. package/dist/features/messages/hooks/useConversations.js +7 -6
  25. package/dist/features/orders/actions/order-actions.d.ts +10 -0
  26. package/dist/features/orders/actions/order-actions.js +57 -4
  27. package/dist/features/orders/schemas/firestore.d.ts +16 -0
  28. package/dist/features/orders/types/index.d.ts +2 -0
  29. package/dist/features/products/schemas/firestore.d.ts +4 -0
  30. package/dist/features/scams/actions/scam-actions.d.ts +12 -0
  31. package/dist/features/scams/actions/scam-actions.js +31 -0
  32. package/dist/features/scams/components/SellerTrustBadge.d.ts +16 -0
  33. package/dist/features/scams/components/SellerTrustBadge.js +21 -0
  34. package/dist/features/scams/components/index.d.ts +2 -0
  35. package/dist/features/scams/components/index.js +1 -0
  36. package/dist/features/seller/components/SellerBundlesView.d.ts +5 -5
  37. package/dist/features/seller/components/SellerBundlesView.js +81 -84
  38. package/dist/features/seller/components/SellerProductShell.d.ts +2 -0
  39. package/dist/features/seller/components/SellerProductShell.js +7 -1
  40. package/dist/features/stores/components/StoreDetailLayoutView.d.ts +8 -1
  41. package/dist/features/stores/components/StoreDetailLayoutView.js +9 -3
  42. package/dist/features/stores/components/StoreHeader.d.ts +4 -1
  43. package/dist/features/stores/components/StoreHeader.js +3 -2
  44. package/dist/index.d.ts +7 -3
  45. package/dist/index.js +6 -2
  46. package/dist/jobs.d.ts +2 -0
  47. package/dist/jobs.js +6 -0
  48. package/dist/next/routing/route-map.d.ts +3 -0
  49. package/dist/next/routing/route-map.js +1 -0
  50. package/dist/security/rate-limit.d.ts +5 -0
  51. package/dist/security/rate-limit.js +2 -0
  52. package/dist/seed/categories-seed-data.js +180 -1
  53. package/dist/seed/grouped-listings-seed-data.d.ts +2 -0
  54. package/dist/seed/products-preorders-seed-data.d.ts +2 -0
  55. package/dist/server.d.ts +4 -0
  56. package/dist/server.js +5 -0
  57. package/package.json +2 -1
@@ -8,20 +8,73 @@ import { NotFoundError, AuthorizationError, ValidationError, } from "../../../er
8
8
  import { serverLogger } from "../../../monitoring";
9
9
  import { getProviders } from "../../../contracts";
10
10
  import { orderRepository } from "../repository/orders.repository";
11
+ import { ORDER_CANCELLABLE_STATUSES } from "../../../_internal/shared/features/orders/config";
11
12
  const ERR_ORDER_NOT_FOUND = "Order not found";
12
- const CANCELLABLE_STATUSES = ["pending", "confirmed"];
13
+ function assertCancellable(order) {
14
+ if (!ORDER_CANCELLABLE_STATUSES.includes(order.status)) {
15
+ throw new ValidationError("Only pending or confirmed orders can be cancelled");
16
+ }
17
+ }
13
18
  export async function cancelOrderForUser(userId, orderId, reason) {
14
19
  const order = await orderRepository.findById(orderId);
15
20
  if (!order)
16
21
  throw new NotFoundError(ERR_ORDER_NOT_FOUND);
17
22
  if (order.userId !== userId)
18
23
  throw new AuthorizationError("You are not authorised to cancel this order");
19
- if (!CANCELLABLE_STATUSES.includes(order.status)) {
20
- throw new ValidationError("Only pending or confirmed orders can be cancelled");
21
- }
24
+ assertCancellable(order);
22
25
  await orderRepository.cancelOrder(orderId, reason);
23
26
  serverLogger.info("Order cancelled by user", { userId, orderId, reason });
24
27
  }
28
+ /**
29
+ * Cancels a subset of line items on an already-placed order and lets the rest
30
+ * proceed. Matches today's whole-order cancellation behavior: marks the items
31
+ * cancelled + sets `refundPending: true`, leaves the actual refund to the
32
+ * existing admin-initiated flow (no auto-refund call here). If every item on
33
+ * the order ends up cancelled, falls through to the same whole-order
34
+ * `cancelOrder` path `cancelOrderForUser` uses, rather than leaving a
35
+ * zero-item live order.
36
+ */
37
+ export async function cancelOrderItemsForUser(userId, orderId, itemIds, reason) {
38
+ if (itemIds.length === 0) {
39
+ return cancelOrderForUser(userId, orderId, reason);
40
+ }
41
+ const order = await orderRepository.findById(orderId);
42
+ if (!order)
43
+ throw new NotFoundError(ERR_ORDER_NOT_FOUND);
44
+ if (order.userId !== userId)
45
+ throw new AuthorizationError("You are not authorised to cancel this order");
46
+ assertCancellable(order);
47
+ const items = order.items ?? [];
48
+ const targetIds = new Set(itemIds);
49
+ const matchedIds = new Set(items.filter((i) => targetIds.has(i.productId)).map((i) => i.productId));
50
+ if (matchedIds.size === 0) {
51
+ throw new ValidationError("None of the selected items belong to this order");
52
+ }
53
+ const allCancelled = items.every((i) => matchedIds.has(i.productId) || i.cancelledQuantity != null);
54
+ if (allCancelled) {
55
+ return cancelOrderForUser(userId, orderId, reason);
56
+ }
57
+ const cancelledAt = new Date();
58
+ const updatedItems = items.map((item) => matchedIds.has(item.productId)
59
+ ? {
60
+ ...item,
61
+ cancelledQuantity: item.quantity,
62
+ cancelledAt,
63
+ cancelledReason: reason,
64
+ }
65
+ : item);
66
+ await orderRepository.update(orderId, {
67
+ items: updatedItems,
68
+ refundPending: true,
69
+ updatedAt: cancelledAt,
70
+ });
71
+ serverLogger.info("Order items cancelled by user", {
72
+ userId,
73
+ orderId,
74
+ itemIds: [...matchedIds],
75
+ reason,
76
+ });
77
+ }
25
78
  export async function listOrdersForUser(userId) {
26
79
  return orderRepository.findByUser(userId);
27
80
  }
@@ -96,6 +96,13 @@ export interface OrderDocumentItem {
96
96
  prizeRevealDeadline?: string;
97
97
  /** SB8-F â€" set after the reveal API picks a winner. */
98
98
  revealedItemNumber?: number;
99
+ /** Set when the buyer cancels this specific line item post-order (partial cancellation). */
100
+ cancelledQuantity?: number;
101
+ cancelledAt?: Date;
102
+ cancelledReason?: string;
103
+ /** P-8 GST — snapshotted from the product at order time so the invoice stays accurate even if the product's HSN/rate later changes. */
104
+ hsnCode?: string;
105
+ gstRate?: 0 | 5 | 12 | 18 | 28;
99
106
  }
100
107
  /**
101
108
  * One refund event on an order. Multiple partial refund events are allowed
@@ -173,6 +180,15 @@ export interface OrderDocument extends BaseDocument {
173
180
  codRemainingAmount?: number;
174
181
  /** COD handling fee charged to the buyer: max(codHandlingFeeMinInPaise, subtotal × codHandlingFeePercent / 100). Only set when paymentMethod === "cod". */
175
182
  codHandlingFee?: number;
183
+ /** Order subtotal before GST — the amount GST is computed on. */
184
+ taxableAmount?: number;
185
+ /** Total GST charged (cgst + sgst, or igst — never both pairs at once). */
186
+ gstAmount?: number;
187
+ /** Intra-state half: buyer and seller/store pickup address share the same state. */
188
+ cgst?: number;
189
+ sgst?: number;
190
+ /** Inter-state: buyer and seller/store pickup address are in different states. */
191
+ igst?: number;
176
192
  emiEnabled?: boolean;
177
193
  /** Number of monthly installments the buyer chose (2–6). */
178
194
  emiTenureMonths?: number;
@@ -36,6 +36,8 @@ export interface OrderItem {
36
36
  prizeRevealDeadline?: string;
37
37
  /** Set after the reveal endpoint picks a winner — the prize item index. */
38
38
  revealedItemNumber?: number;
39
+ /** Set when the buyer cancels this specific line item post-order (partial cancellation). */
40
+ cancelledQuantity?: number;
39
41
  /**
40
42
  * SB-UNI-4 2026-05-13 — bundle identifier when this order line represents a
41
43
  * bundle purchase. `productId` then points at the bundle category id and
@@ -195,6 +195,10 @@ export interface ProductDocument extends BaseDocument {
195
195
  isSold?: boolean;
196
196
  /** When true, an EMI order for this product ships as soon as it's confirmed instead of waiting for every installment to be paid. Default false. */
197
197
  allowShipBeforeEmiComplete?: boolean;
198
+ /** P-8 GST — buyer-facing tax rate on this product (%). Unset/0 = exempt. */
199
+ gstRate?: 0 | 5 | 12 | 18 | 28;
200
+ /** P-8 GST — Harmonized System of Nomenclature code for GST-compliant invoices. */
201
+ hsnCode?: string;
198
202
  /** Print-specific metadata for "art" / "stickers" listings — printed-only physical goods. Optional on every listing type; only populated for art/stickers. */
199
203
  printMeta?: ProductPrintMeta;
200
204
  promotionEndDate?: Date;
@@ -27,3 +27,15 @@ export declare function getPublicScammerById(id: string): Promise<ScammerDocumen
27
27
  * Returns null if not found or not verified.
28
28
  */
29
29
  export declare function getScammerProfilePageData(id: string): Promise<ScammerProfilePageData | null>;
30
+ export type SellerTrustStatus = "clear" | "flagged";
31
+ export interface SellerTrustResult {
32
+ status: SellerTrustStatus;
33
+ /** seoSlug of matched verified scammer profiles — links to /scams/[slug]. Empty when clear. */
34
+ matchedProfileSlugs: string[];
35
+ }
36
+ /**
37
+ * P-12 — resolves a store owner's phone/email against the verified (admin-published)
38
+ * scammer registry only. Never surfaces "under review" reports to buyers — a report
39
+ * that hasn't been verified must not damage a seller's storefront reputation.
40
+ */
41
+ export declare function getSellerTrustStatus(storeId: string): Promise<SellerTrustResult>;
@@ -1,6 +1,8 @@
1
1
  "use server";
2
2
  import { sieveFilter, SIEVE_OP } from "@mohasinac/appkit";
3
3
  import { scammerRepository } from "../repository/scammer.repository";
4
+ import { userRepository } from "../../auth/repository/user.repository";
5
+ import { storeRepository } from "../../stores/repository/store.repository";
4
6
  import { safeFireAndForget } from "../../../utils/safe-fire-forget";
5
7
  /**
6
8
  * List verified scammer profiles for the public registry page.
@@ -62,3 +64,32 @@ export async function getScammerProfilePageData(id) {
62
64
  ]);
63
65
  return { scammer, incidents, comments, relatedScammers };
64
66
  }
67
+ const TRUST_STATUS_CLEAR = { status: "clear", matchedProfileSlugs: [] };
68
+ /**
69
+ * P-12 — resolves a store owner's phone/email against the verified (admin-published)
70
+ * scammer registry only. Never surfaces "under review" reports to buyers — a report
71
+ * that hasn't been verified must not damage a seller's storefront reputation.
72
+ */
73
+ export async function getSellerTrustStatus(storeId) {
74
+ const store = await storeRepository.findById(storeId).catch(() => null);
75
+ if (!store?.ownerId)
76
+ return TRUST_STATUS_CLEAR;
77
+ const owner = await userRepository.findById(store.ownerId).catch(() => null);
78
+ if (!owner)
79
+ return TRUST_STATUS_CLEAR;
80
+ const lookups = [];
81
+ if (owner.phoneNumber)
82
+ lookups.push(scammerRepository.findByContactField("phones", owner.phoneNumber));
83
+ if (owner.email)
84
+ lookups.push(scammerRepository.findByContactField("emails", owner.email));
85
+ if (lookups.length === 0)
86
+ return TRUST_STATUS_CLEAR;
87
+ const results = await Promise.all(lookups.map((p) => p.catch(() => [])));
88
+ const verified = results.flat().filter((s) => s.status === "verified");
89
+ if (verified.length === 0)
90
+ return TRUST_STATUS_CLEAR;
91
+ return {
92
+ status: "flagged",
93
+ matchedProfileSlugs: [...new Set(verified.map((s) => s.seoSlug))],
94
+ };
95
+ }
@@ -0,0 +1,16 @@
1
+ import type { SellerTrustResult } from "../actions/scam-actions";
2
+ export interface SellerTrustBadgeProps {
3
+ trust: SellerTrustResult;
4
+ className?: string;
5
+ }
6
+ /**
7
+ * P-12 — renders next to a seller's storefront name. Only ever reflects
8
+ * admin-verified scammer profiles (see getSellerTrustStatus) — unverified
9
+ * reports never reach this component, so "clear" here means "no verified
10
+ * match," not "no reports filed."
11
+ *
12
+ * Named SellerTrustBadge (not TrustBadge) to avoid colliding with the
13
+ * unrelated homepage-section `TrustBadge` config type in
14
+ * features/homepage/index.ts (marketing trust-badge icons row).
15
+ */
16
+ export declare function SellerTrustBadge({ trust, className }: SellerTrustBadgeProps): import("react").JSX.Element;
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { Badge } from "../../../ui/components/Badge";
3
+ import { TextLink } from "../../../ui/components/TextLink";
4
+ import { ROUTES } from "../../../next";
5
+ /**
6
+ * P-12 — renders next to a seller's storefront name. Only ever reflects
7
+ * admin-verified scammer profiles (see getSellerTrustStatus) — unverified
8
+ * reports never reach this component, so "clear" here means "no verified
9
+ * match," not "no reports filed."
10
+ *
11
+ * Named SellerTrustBadge (not TrustBadge) to avoid colliding with the
12
+ * unrelated homepage-section `TrustBadge` config type in
13
+ * features/homepage/index.ts (marketing trust-badge icons row).
14
+ */
15
+ export function SellerTrustBadge({ trust, className = "" }) {
16
+ if (trust.status === "clear") {
17
+ return (_jsx(Badge, { variant: "success", className: className, children: "\u2713 Verified Safe" }));
18
+ }
19
+ const firstSlug = trust.matchedProfileSlugs[0];
20
+ return (_jsx(Badge, { variant: "danger", className: className, children: firstSlug ? (_jsx(TextLink, { href: String(ROUTES.PUBLIC.SCAM_DETAIL(firstSlug)), children: "\u26A0 Flagged in Scam Registry" })) : ("⚠ Flagged in Scam Registry") }));
21
+ }
@@ -4,3 +4,5 @@ export { ScamProfileView } from "./ScamProfileView";
4
4
  export type { ScamProfileViewProps } from "./ScamProfileView";
5
5
  export { ScamAwarenessModal } from "./ScamAwarenessModal";
6
6
  export type { ScamAwarenessModalProps } from "./ScamAwarenessModal";
7
+ export { SellerTrustBadge } from "./SellerTrustBadge";
8
+ export type { SellerTrustBadgeProps } from "./SellerTrustBadge";
@@ -1,3 +1,4 @@
1
1
  export { ScamRegistryView } from "./ScamRegistryView";
2
2
  export { ScamProfileView } from "./ScamProfileView";
3
3
  export { ScamAwarenessModal } from "./ScamAwarenessModal";
4
+ export { SellerTrustBadge } from "./SellerTrustBadge";
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  export interface SellerBundlesViewProps {
3
- onCreateClick?: () => void;
4
- onEditClick?: (id: string) => void;
5
- onDelete?: (id: string) => Promise<void>;
6
- onBulkDelete?: (ids: string[]) => Promise<void>;
3
+ getEditHref: (row: {
4
+ id: string;
5
+ }) => string;
6
+ newHref: string;
7
7
  }
8
- export declare function SellerBundlesView({ onCreateClick, onEditClick, onDelete, onBulkDelete, }: SellerBundlesViewProps): React.JSX.Element;
8
+ export declare function SellerBundlesView({ getEditHref, newHref }: SellerBundlesViewProps): React.JSX.Element;
@@ -1,111 +1,108 @@
1
1
  "use client";
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { Badge, Span, sortBy } from "@mohasinac/appkit";
4
- import { useState, useCallback } from "react";
5
- import { useEntityDelete } from "../../../react/hooks/useEntityDelete";
6
- import { ConfirmDeleteModal, RowActionMenu, Text } from "../../../ui";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { SIEVE_OP, sieveFilter, sortBy } from "@mohasinac/appkit";
4
+ import { Badge, Button, FilterChipGroup, Stack, Text, TextLink } from "../../../ui";
7
5
  import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints";
8
- import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
9
- import { ROUTES } from "../../..";
10
- import { SELLER_BULK_ACTIONS, ROW_ACTION_META } from "../../products/constants/action-defs";
11
- const CLS_ITEMS_PILL = "inline-flex items-center rounded-full px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-medium bg-violet-100 text-violet-700 dark:bg-violet-900/40 dark:text-violet-300 tabular-nums";
12
- import { toRecordArray, toRelativeDate, toRupees, toStringValue, } from "../../admin/hooks/useAdminListingData";
6
+ import { ROW_ACTION_META, ROW_ACTION_ID } from "../../products/constants/action-defs";
7
+ import { SELLER_BULK_ACTIONS } from "../../products/constants/action-defs";
8
+ import { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "../../../_internal/shared/features/categories/bundle-copy";
9
+ import { toRecordArray, toRelativeDate, toStringValue, } from "../../admin/hooks/useAdminListingData";
13
10
  import { DataListingView } from "../../admin/components/DataListingView";
11
+ function formatPrice(paise) {
12
+ if (typeof paise !== "number" || paise <= 0)
13
+ return "—";
14
+ return `₹${Math.round(paise / 100).toLocaleString("en-IN")}`;
15
+ }
14
16
  const COLUMNS = [
15
- { key: "title", header: "Bundle", render: (row) => _jsx(Text, { size: "sm", weight: "medium", children: row.title }) },
16
- { key: "price", header: "Price", render: (row) => _jsx(Text, { className: "tabular-nums", size: "sm", children: row.price }) },
17
17
  {
18
- key: "itemCount",
19
- header: "Items",
20
- render: (row) => (_jsx(Span, { className: CLS_ITEMS_PILL, children: row.itemCount })),
18
+ key: "primary",
19
+ header: "Name",
20
+ render: (row) => (_jsxs(Stack, { gap: "xs", children: [_jsx(Text, { weight: "medium", color: "primary", children: row.primary }), _jsx(Text, { size: "xs", color: "muted", children: row.secondary })] })),
21
21
  },
22
+ { key: "price", header: "Price", className: "w-28" },
23
+ { key: "members", header: "Members", className: "w-24" },
22
24
  {
23
- key: "status",
24
- header: "Status",
25
- render: (row) => (_jsx(Badge, { variant: row.status === "active" ? "active" : "inactive", size: "xs", className: "capitalize", children: row.status })),
25
+ key: "stockStatus",
26
+ header: "Stock",
27
+ className: "w-28",
28
+ render: (row) => (_jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[row.stockStatus] ?? "default", children: row.stockStatus === "in_stock"
29
+ ? BUNDLE_COPY.stockBadge.listVariantInStock
30
+ : BUNDLE_COPY.stockBadge.listVariantOutOfStock })),
26
31
  },
27
32
  {
28
- key: "createdAt",
29
- header: "Created",
30
- render: (row) => (_jsx(Text, { className: "text-[var(--appkit-color-text-muted)]", size: "sm", children: row.createdAt })),
33
+ key: "status",
34
+ header: "Status",
35
+ className: "w-24",
36
+ render: (row) => (_jsx(Badge, { variant: row.isActive ? "success" : "default", children: row.status })),
31
37
  },
38
+ { key: "updatedAt", header: "Updated", className: "w-28" },
32
39
  ];
33
- export function SellerBundlesView({ onCreateClick, onEditClick, onDelete, onBulkDelete, }) {
34
- const [deleteTargetId, setDeleteTargetId] = useState(null);
35
- const { deletingId, handleDelete: performDelete } = useEntityDelete({
36
- endpoint: SELLER_ENDPOINTS.PRODUCT_BY_ID,
37
- deleteFn: onDelete,
38
- successMessage: "Bundle deleted.",
39
- fetchOptions: { credentials: "include" },
40
- });
41
- const handleDelete = useCallback(async (id) => {
42
- await performDelete(id);
43
- setDeleteTargetId(null);
44
- }, [performDelete]);
45
- const handleEdit = useCallback((id) => {
46
- if (onEditClick)
47
- onEditClick(id);
48
- else
49
- window.location.href = String(ROUTES.STORE.PRODUCTS_EDIT(id));
50
- }, [onEditClick]);
51
- const handleCreate = useCallback(() => {
52
- if (onCreateClick)
53
- onCreateClick();
54
- else
55
- window.location.href = String(ROUTES.STORE.PRODUCTS_NEW);
56
- }, [onCreateClick]);
40
+ export function SellerBundlesView({ getEditHref, newHref }) {
57
41
  const config = {
58
42
  portal: "seller",
59
43
  title: "Bundles",
60
- searchPlaceholder: "Search bundles...",
61
- emptyLabel: "No bundles yet — create a bundle to group multiple products together",
62
- filterKeys: [],
63
- defaultSort: sortBy("createdAt", "DESC"),
64
- queryKey: ["seller", "bundles"],
65
- endpoint: SELLER_ENDPOINTS.PRODUCTS,
44
+ searchPlaceholder: "Search bundles by name or slug…",
45
+ emptyLabel: "No bundles yet — create a bundle to group multiple of your products together",
46
+ filterKeys: ["isActive", "bundleStockStatus"],
47
+ defaultSort: sortBy("name", "ASC"),
48
+ queryKey: ["seller", "bundles", "listing"],
49
+ endpoint: SELLER_ENDPOINTS.BUNDLES,
66
50
  sortOptions: [
51
+ { value: sortBy("name", "ASC"), label: "Name A–Z" },
52
+ { value: sortBy("name", "DESC"), label: "Name Z–A" },
53
+ { value: sortBy("bundlePriceInPaise", "DESC"), label: "Price high→low" },
54
+ { value: "bundlePriceInPaise", label: "Price low→high" },
67
55
  { value: sortBy("createdAt", "DESC"), label: "Newest" },
68
56
  { value: sortBy("createdAt", "ASC"), label: "Oldest" },
69
- { value: "productTitle", label: "Name A–Z" },
70
- { value: sortBy("productTitle", "DESC"), label: "Name Z–A" },
71
- { value: sortBy("price", "ASC"), label: "Price: Low–High" },
72
- { value: sortBy("price", "DESC"), label: "Price: High–Low" },
73
57
  ],
74
58
  columns: COLUMNS,
75
- mapRows: (response) => toRecordArray(response.products).map((item, index) => ({
59
+ mapRows: (response) => toRecordArray(response.items).map((item, index) => ({
76
60
  id: toStringValue(item.id, `bundle-${index}`),
77
- title: toStringValue(item.productTitle ?? item.title, "Untitled bundle"),
78
- price: toRupees(item.price),
79
- itemCount: Array.isArray(item.bundleProductIds)
80
- ? item.bundleProductIds.length
81
- : Number(item.bundleItemCount ?? 0),
82
- status: toStringValue(item.status, "draft"),
83
- createdAt: toRelativeDate(item.createdAt),
61
+ primary: toStringValue(item.name, "Untitled bundle"),
62
+ secondary: toStringValue(item.slug, "no-slug"),
63
+ price: formatPrice(item.bundlePriceInPaise),
64
+ members: String(Array.isArray(item.bundleProductIds) ? item.bundleProductIds.length : 0),
65
+ stockStatus: toStringValue(item.bundleStockStatus, "in_stock"),
66
+ isActive: item.isActive === true,
67
+ status: item.isActive === true
68
+ ? BUNDLE_COPY.adminList.activeBadge
69
+ : BUNDLE_COPY.adminList.inactiveBadge,
70
+ updatedAt: toRelativeDate(item.updatedAt ?? item.createdAt),
84
71
  })),
85
- getTotal: (response, mappedRows) => typeof response.meta?.total === "number" ? response.meta.total : mappedRows.length,
86
- buildFilters: () => "listingType==bundle",
87
- primaryAction: { label: "New Bundle", onClick: () => handleCreate() },
72
+ getTotal: (response, mappedRows) => typeof response.total === "number" ? response.total : mappedRows.length,
73
+ buildFilters: (filterState) => {
74
+ const parts = [];
75
+ if (filterState.isActive)
76
+ parts.push(sieveFilter("isActive", SIEVE_OP.EQ, filterState.isActive));
77
+ if (filterState.bundleStockStatus)
78
+ parts.push(sieveFilter("bundleStockStatus", SIEVE_OP.EQ, filterState.bundleStockStatus));
79
+ return parts.join(",") || undefined;
80
+ },
81
+ getRowHref: getEditHref,
82
+ toolbarExtra: (_jsx(Button, { asChild: true, size: "sm", variant: "primary", children: _jsx(TextLink, { href: newHref, layout: "flex", align: "center", gap: "xs", children: "+ New Bundle" }) })),
88
83
  // Rule #7: bulk-action array sourced from the SELLER_BULK_ACTIONS preset.
89
- buildBulkActions: onBulkDelete
90
- ? (selection) => SELLER_BULK_ACTIONS.bundles.map((id) => ({
84
+ buildBulkActions: (selection) => {
85
+ const handlers = {
86
+ [ROW_ACTION_ID.DELETE]: async () => {
87
+ await Promise.all(selection.selectedIds.map((id) => fetch(SELLER_ENDPOINTS.BUNDLE_BY_ID(id), { method: "DELETE" })));
88
+ selection.clearSelection();
89
+ },
90
+ };
91
+ return SELLER_BULK_ACTIONS.bundles.map((id) => ({
91
92
  id,
92
93
  label: ROW_ACTION_META[id].label,
93
94
  destructive: ROW_ACTION_META[id].destructive,
94
- onClick: async () => {
95
- await onBulkDelete(selection.selectedIds);
96
- selection.clearSelection();
97
- },
98
- }))
99
- : undefined,
100
- renderRowActions: (row) => (_jsx(RowActionMenu, { actions: [
101
- { label: ACTIONS.STORE["edit-listing"].label, onClick: () => handleEdit(row.id) },
102
- {
103
- label: ACTIONS.STORE["delete-listing"].label,
104
- destructive: true,
105
- onClick: () => setDeleteTargetId(row.id),
106
- disabled: deletingId === row.id,
107
- },
108
- ] })),
95
+ onClick: handlers[id],
96
+ }));
97
+ },
98
+ renderFilterPanel: ({ pendingFilters, setPendingFilters }) => (_jsxs(_Fragment, { children: [_jsx(FilterChipGroup, { label: "Status", tabs: [
99
+ { id: "All", label: "All" },
100
+ { id: "true", label: "Active" },
101
+ { id: "false", label: "Inactive" },
102
+ ], value: pendingFilters.isActive || "", onChange: (v) => setPendingFilters((p) => ({ ...p, isActive: v })) }), _jsx(FilterChipGroup, { label: "Stock", tabs: [
103
+ { id: "All", label: "All" },
104
+ { id: "out_of_stock", label: "Sold out" },
105
+ ], value: pendingFilters.bundleStockStatus || "", onChange: (v) => setPendingFilters((p) => ({ ...p, bundleStockStatus: v })) })] })),
109
106
  };
110
- return (_jsxs(_Fragment, { children: [_jsx(DataListingView, { config: config }), deleteTargetId && (_jsx(ConfirmDeleteModal, { isOpen: true, title: "Delete Bundle", message: "Are you sure you want to delete this bundle? This cannot be undone.", onConfirm: () => handleDelete(deleteTargetId), onClose: () => setDeleteTargetId(null), isDeleting: deletingId === deleteTargetId }))] }));
107
+ return _jsx(DataListingView, { config: config });
111
108
  }
@@ -23,6 +23,8 @@ export interface SellerProductDraft {
23
23
  pickupAddressId?: string;
24
24
  insurance?: boolean;
25
25
  insuranceCost?: number;
26
+ gstRate?: 0 | 5 | 12 | 18 | 28;
27
+ hsnCode?: string;
26
28
  status?: "draft" | "published";
27
29
  seoTitle?: string;
28
30
  seoDescription?: string;
@@ -190,7 +190,13 @@ function StepShipping({ values, onChange, renderAddressSelector, }) {
190
190
  ] }), renderAddressSelector ? (_jsxs(_Fragment, { children: [_jsx(Text, { className: "text-[var(--appkit-color-text)] mb-1", size: "sm", weight: "medium", children: "Pickup Address (optional)" }), renderAddressSelector({
191
191
  value: values.pickupAddressId ?? "",
192
192
  onChange: (v) => onChange({ pickupAddressId: v }),
193
- })] })) : (_jsx(StoreAddressSelectorCreate, { label: "Pickup Address (optional)", value: values.pickupAddressId ?? "", onChange: (id) => onChange({ pickupAddressId: id }) })), _jsx(Toggle, { checked: !!values.insurance, onChange: (checked) => onChange({ insurance: checked, insuranceCost: checked ? values.insuranceCost ?? 0 : undefined }), label: "Offer shipping insurance" }), values.insurance && (_jsx(FormField, { name: "insuranceCost", label: "Insurance Cost (\u20B9)", type: "number", value: toRupees(values.insuranceCost), onChange: (v) => onChange({ insuranceCost: toPaise(v) }), placeholder: "0" }))] }));
193
+ })] })) : (_jsx(StoreAddressSelectorCreate, { label: "Pickup Address (optional)", value: values.pickupAddressId ?? "", onChange: (id) => onChange({ pickupAddressId: id }) })), _jsx(Toggle, { checked: !!values.insurance, onChange: (checked) => onChange({ insurance: checked, insuranceCost: checked ? values.insuranceCost ?? 0 : undefined }), label: "Offer shipping insurance" }), values.insurance && (_jsx(FormField, { name: "insuranceCost", label: "Insurance Cost (\u20B9)", type: "number", value: toRupees(values.insuranceCost), onChange: (v) => onChange({ insuranceCost: toPaise(v) }), placeholder: "0" })), _jsx(FormField, { name: "gstRate", label: "GST Rate", type: "select", value: String(values.gstRate ?? 0), onChange: (v) => onChange({ gstRate: Number(v) }), options: [
194
+ { value: "0", label: "Exempt (0%)" },
195
+ { value: "5", label: "5%" },
196
+ { value: "12", label: "12%" },
197
+ { value: "18", label: "18%" },
198
+ { value: "28", label: "28%" },
199
+ ] }), _jsx(FormField, { name: "hsnCode", label: "HSN Code", value: values.hsnCode ?? "", onChange: (v) => onChange({ hsnCode: v }), placeholder: "e.g. 9503" })] }));
194
200
  }
195
201
  // ── Step: Publish / SEO ───────────────────────────────────────────────────
196
202
  function StepPublish({ values, onChange, }) {
@@ -6,5 +6,12 @@ export interface StoreDetailLayoutViewProps {
6
6
  /** The current active tab value: "products" | "auctions" | "reviews" | "about" */
7
7
  activeTab: string;
8
8
  children: ReactNode;
9
+ /**
10
+ * P-12 — gates the trust badge behind the same FEATURE_SCAM_REGISTRY flag
11
+ * that gates the registry itself (pending legal sign-off). Consumer passes
12
+ * `getFlag("SCAM_REGISTRY")`; appkit has no access to that env reader.
13
+ * Defaults to `false` — off unless the consumer explicitly opts in.
14
+ */
15
+ scamRegistryEnabled?: boolean;
9
16
  }
10
- export declare function StoreDetailLayoutView({ storeSlug, activeTab, children, }: StoreDetailLayoutViewProps): Promise<React.JSX.Element>;
17
+ export declare function StoreDetailLayoutView({ storeSlug, activeTab, children, scamRegistryEnabled, }: StoreDetailLayoutViewProps): Promise<React.JSX.Element>;
@@ -6,6 +6,7 @@ import { ROUTES } from "../../../next";
6
6
  import { Container, Main, Section, Text } from "../../../ui";
7
7
  import { STORE_PAGE_TABS } from "../../products/constants/listing-tabs";
8
8
  import { isListingTypeEnabled, isCategoryTypeEnabled } from "../../../_internal/shared/listing-types/feature-flags";
9
+ import { getSellerTrustStatus } from "../../scams/actions/scam-actions";
9
10
  import { StoreHeader } from "./StoreHeader";
10
11
  import { StoreNavTabs } from "./StoreNavTabs";
11
12
  const STORE_LISTING_HREF = {
@@ -28,13 +29,18 @@ function tabLabel(base, count) {
28
29
  return base;
29
30
  return `${base} (${count.toLocaleString()})`;
30
31
  }
31
- export async function StoreDetailLayoutView({ storeSlug, activeTab, children, }) {
32
+ export async function StoreDetailLayoutView({ storeSlug, activeTab, children, scamRegistryEnabled = false, }) {
32
33
  const store = await getStoreBySlug(storeSlug);
33
34
  if (!store) {
34
35
  return (_jsx(Main, { children: _jsx(Section, { padding: "y-5xl", children: _jsx(Container, { size: "md", children: _jsx(Text, { align: "start", color: "muted", children: "Store not found." }) }) }) }));
35
36
  }
36
37
  const storeId = store?.id;
37
- const settings = await siteSettingsRepository.findById("global").catch(() => null);
38
+ const [settings, trust] = await Promise.all([
39
+ siteSettingsRepository.findById("global").catch(() => null),
40
+ scamRegistryEnabled && storeId
41
+ ? getSellerTrustStatus(storeId).catch(() => undefined)
42
+ : Promise.resolve(undefined),
43
+ ]);
38
44
  const [productsCount, auctionsCount, preOrdersCount, prizeDrawsCount, bundlesCount, classifiedsCount, digitalCodesCount, liveCount, artCount, stickersCount, couponsCount, reviewsCount] = storeId
39
45
  ? await Promise.all([
40
46
  productRepository
@@ -128,5 +134,5 @@ export async function StoreDetailLayoutView({ storeSlug, activeTab, children, })
128
134
  { value: "reviews", label: tabLabel("Reviews", reviewsCount), href: String(ROUTES.PUBLIC.STORE_REVIEWS(storeSlug)) },
129
135
  { value: "about", label: "About", href: String(ROUTES.PUBLIC.STORE_ABOUT(storeSlug)) },
130
136
  ];
131
- return (_jsxs(Main, { children: [_jsx(StoreHeader, { store: store }), _jsxs(Container, { size: "xl", className: "mt-6", children: [_jsx(StoreNavTabs, { tabs: tabs, activeValue: activeTab }), _jsx(Section, { padding: "t-lg", children: children })] })] }));
137
+ return (_jsxs(Main, { children: [_jsx(StoreHeader, { store: store, trust: trust }), _jsxs(Container, { size: "xl", className: "mt-6", children: [_jsx(StoreNavTabs, { tabs: tabs, activeValue: activeTab }), _jsx(Section, { padding: "t-lg", children: children })] })] }));
132
138
  }
@@ -1,3 +1,4 @@
1
+ import type { SellerTrustResult } from "../../scams/actions/scam-actions";
1
2
  import type { StoreDetail } from "../types";
2
3
  interface StoreHeaderProps {
3
4
  store: StoreDetail;
@@ -10,6 +11,8 @@ interface StoreHeaderProps {
10
11
  };
11
12
  onFollow?: (storeSlug: string) => void;
12
13
  className?: string;
14
+ /** P-12 — omitted (undefined) when the scam registry feature is off; no badge renders. */
15
+ trust?: SellerTrustResult;
13
16
  }
14
- export declare function StoreHeader({ store, labels, onFollow, className, }: StoreHeaderProps): import("react").JSX.Element;
17
+ export declare function StoreHeader({ store, labels, onFollow, className, trust, }: StoreHeaderProps): import("react").JSX.Element;
15
18
  export {};
@@ -4,6 +4,7 @@ import { MediaImage } from "../../media/MediaImage";
4
4
  import { normalizeRichTextHtml } from "../../../utils/string.formatter";
5
5
  import { ShareButton } from "../../products/components/ShareButton";
6
6
  import { StoreScopedSearch } from "./StoreScopedSearch";
7
+ import { SellerTrustBadge } from "../../scams/components/SellerTrustBadge";
7
8
  const __O = {
8
9
  hidden: "overflow-hidden",
9
10
  };
@@ -11,8 +12,8 @@ const CLS_AVATAR = "-mt-8 h-16 w-16 rounded-xl border-2 border-white border-[var
11
12
  const CLS_STARS = "inline-flex items-center gap-[var(--appkit-space-1)] text-warning";
12
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";
13
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";
14
- export function StoreHeader({ store, labels = {}, onFollow, className = "", }) {
15
- return (_jsxs(Section, { surface: "default", border: "bottom", className: className, children: [store.storeBannerURL && (_jsx(Div, { className: `relative h-40 md:h-56 ${__O.hidden} bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]`, children: _jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner" }) })), _jsxs(Div, { paddingX: "x-page", className: "max-w-7xl mx-auto", padding: "y-md", children: [_jsxs(Row, { align: "end", gap: "md", children: [store.storeLogoURL ? (_jsx(Div, { className: "relative -mt-8 h-16 w-16 border-2 border-white", rounded: "xl", shadow: "sm", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "avatar" }) })) : (_jsx(Div, { className: CLS_AVATAR, children: store.storeName[0]?.toUpperCase() })), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "xl", weight: "bold", children: store.storeName }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Span, { size: "sm", weight: "medium", className: CLS_STARS, children: ["\u2605 ", store.averageRating.toFixed(1)] }))] }), _jsxs(Row, { textSize: "xs", className: "text-[var(--appkit-color-text-muted)] mb-0.5", gap: "3", children: [store.category && _jsx(Span, { transform: "capitalize", children: store.category }), store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { children: [store.totalProducts, " ", labels.products ?? "products"] })), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { children: [store.totalReviews, " ", labels.reviews ?? "reviews"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { children: [store.itemsSold, " ", labels.sold ?? "sold"] }))] }), store.storeDescription && (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), copyableCode: true, className: "mt-0.5" }))] }), _jsxs(Row, { gap: "sm", align: "center", className: "shrink-0", children: [_jsx(ShareButton, { title: store.storeName }), onFollow && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onFollow(store.storeSlug), className: CLS_FOLLOW_BTN, children: labels.follow ?? "Follow" }))] })] }), store.isVacationMode && (_jsx(Text, { className: CLS_WARN_BANNER, children: store.vacationMessage ??
15
+ export function StoreHeader({ store, labels = {}, onFollow, className = "", trust, }) {
16
+ return (_jsxs(Section, { surface: "default", border: "bottom", className: className, children: [store.storeBannerURL && (_jsx(Div, { className: `relative h-40 md:h-56 ${__O.hidden} bg-[var(--appkit-color-surface)] bg-[var(--appkit-color-surface-elevated)]`, children: _jsx(MediaImage, { src: store.storeBannerURL, alt: `${store.storeName} banner`, size: "banner" }) })), _jsxs(Div, { paddingX: "x-page", className: "max-w-7xl mx-auto", padding: "y-md", children: [_jsxs(Row, { align: "end", gap: "md", children: [store.storeLogoURL ? (_jsx(Div, { className: "relative -mt-8 h-16 w-16 border-2 border-white", rounded: "xl", shadow: "sm", overflow: "hidden", children: _jsx(MediaImage, { src: store.storeLogoURL, alt: store.storeName, size: "avatar" }) })) : (_jsx(Div, { className: CLS_AVATAR, children: store.storeName[0]?.toUpperCase() })), _jsxs(Div, { className: "flex-1 min-w-0", children: [_jsxs(Row, { className: "mb-0.5", align: "center", gap: "sm", wrap: true, children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "xl", weight: "bold", children: store.storeName }), store.averageRating != null && store.averageRating > 0 && (_jsxs(Span, { size: "sm", weight: "medium", className: CLS_STARS, children: ["\u2605 ", store.averageRating.toFixed(1)] })), trust && _jsx(SellerTrustBadge, { trust: trust })] }), _jsxs(Row, { textSize: "xs", className: "text-[var(--appkit-color-text-muted)] mb-0.5", gap: "3", children: [store.category && _jsx(Span, { transform: "capitalize", children: store.category }), store.totalProducts != null && store.totalProducts > 0 && (_jsxs(Span, { children: [store.totalProducts, " ", labels.products ?? "products"] })), store.totalReviews != null && store.totalReviews > 0 && (_jsxs(Span, { children: [store.totalReviews, " ", labels.reviews ?? "reviews"] })), store.itemsSold != null && store.itemsSold > 0 && (_jsxs(Span, { children: [store.itemsSold, " ", labels.sold ?? "sold"] }))] }), store.storeDescription && (_jsx(RichText, { html: normalizeRichTextHtml(store.storeDescription), copyableCode: true, className: "mt-0.5" }))] }), _jsxs(Row, { gap: "sm", align: "center", className: "shrink-0", children: [_jsx(ShareButton, { title: store.storeName }), onFollow && (_jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: () => onFollow(store.storeSlug), className: CLS_FOLLOW_BTN, children: labels.follow ?? "Follow" }))] })] }), store.isVacationMode && (_jsx(Text, { className: CLS_WARN_BANNER, children: store.vacationMessage ??
16
17
  labels.vacationMode ??
17
18
  "Store is on vacation mode" })), _jsx(Div, { className: "mt-3", children: _jsx(StoreScopedSearch, { storeId: store.storeSlug, storeName: store.storeName }) })] })] }));
18
19
  }
package/dist/index.d.ts CHANGED
@@ -2279,6 +2279,7 @@ export type { PaymentGateway } from "./features/orders/index";
2279
2279
  export type { RefundStatus } from "./features/orders/index";
2280
2280
  export type { ShippingMethod } from "./features/orders/index";
2281
2281
  export { cancelOrderForUser } from "./features/orders/server";
2282
+ export { cancelOrderItemsForUser } from "./features/orders/server";
2282
2283
  export { getOrderByIdForUser } from "./features/orders/server";
2283
2284
  export { getOrderTrackingHandler } from "./features/orders/server";
2284
2285
  export { getTrackingInfo } from "./features/orders/server";
@@ -2450,8 +2451,8 @@ export { actionTracker, setActionTrackerSink, resetActionTrackerSink, type Actio
2450
2451
  export { cartRequiresShipping, cartIsDigitalOnly, cartIsChatOnly, } from "./_internal/shared/listing-types/cart-shipping";
2451
2452
  export { ACTIONS, action, act, canPerformAction, actionsForListingType, actionLabel, type ActionDef, type ActionKind, type ActionResource, type ActionTree, type ActionConfirmation, } from "./_internal/shared/actions/action-registry";
2452
2453
  export { buildBulkAction } from "./_internal/shared/actions/bulk-helpers";
2453
- export { computeCheckoutFees, computePayoutDeduction, computeCodHandlingFee, } from "./_internal/shared/fees/calculator";
2454
- export type { FeeCommissionRates, CheckoutFees, PayoutDeduction, CodHandlingFeeRates, } from "./_internal/shared/fees/calculator";
2454
+ export { computeCheckoutFees, computePayoutDeduction, computeCodHandlingFee, calculateGst, } from "./_internal/shared/fees/calculator";
2455
+ export type { FeeCommissionRates, CheckoutFees, PayoutDeduction, CodHandlingFeeRates, GstBreakdown, } from "./_internal/shared/fees/calculator";
2455
2456
  export { checkEmiEligibility, computeEmiSchedule } from "./_internal/shared/features/emi/schedule";
2456
2457
  export type { EmiSettings, EmiIneligibleReason, EmiEligibility, EmiInstallmentPlan, EmiScheduleResult, } from "./_internal/shared/features/emi/schedule";
2457
2458
  export { isAdminUser, isSellerUser, isModeratorUser, isEmployeeUser, isBuyerUser, } from "./features/auth/role-predicates";
@@ -3142,9 +3143,12 @@ export { ScamProfileView } from "./features/scams/components/ScamProfileView";
3142
3143
  export type { ScamProfileViewProps } from "./features/scams/components/ScamProfileView";
3143
3144
  export { ScamAwarenessModal } from "./features/scams/components/ScamAwarenessModal";
3144
3145
  export type { ScamAwarenessModalProps } from "./features/scams/components/ScamAwarenessModal";
3145
- export { listVerifiedScammers, getPublicScammerById, getScammerProfilePageData, } from "./features/scams/actions/scam-actions";
3146
+ export { SellerTrustBadge } from "./features/scams/components/SellerTrustBadge";
3147
+ export type { SellerTrustBadgeProps } from "./features/scams/components/SellerTrustBadge";
3148
+ export { listVerifiedScammers, getPublicScammerById, getScammerProfilePageData, getSellerTrustStatus, } from "./features/scams/actions/scam-actions";
3146
3149
  export type { ScammerProfilePageData } from "./features/scams/actions/scam-actions";
3147
3150
  export type { ScammerListResult } from "./features/scams/actions/scam-actions";
3151
+ export type { SellerTrustResult, SellerTrustStatus } from "./features/scams/actions/scam-actions";
3148
3152
  export { SellerProductShell } from "./features/seller/components/SellerProductShell";
3149
3153
  export type { SellerProductShellProps, SellerProductDraft, ProductListingMode } from "./features/seller/components/SellerProductShell";
3150
3154
  export { CategoryInlineSelect } from "./features/seller/components/CategoryInlineSelect";