@mohasinac/appkit 2.7.17 → 2.7.18

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 (88) hide show
  1. package/dist/_internal/server/features/bundles/actions.d.ts +15 -0
  2. package/dist/_internal/server/features/bundles/actions.js +48 -0
  3. package/dist/_internal/server/features/bundles/index.d.ts +1 -0
  4. package/dist/_internal/server/features/bundles/index.js +1 -0
  5. package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
  6. package/dist/_internal/server/features/checkout/actions.js +23 -12
  7. package/dist/_internal/server/features/payments/data.d.ts +5 -3
  8. package/dist/_internal/server/features/payments/data.js +13 -8
  9. package/dist/_internal/server/features/payments/index.d.ts +1 -1
  10. package/dist/_internal/server/features/payments/index.js +1 -1
  11. package/dist/_internal/shared/features/categories/bundle-config.d.ts +24 -0
  12. package/dist/_internal/shared/features/categories/bundle-config.js +28 -0
  13. package/dist/_internal/shared/features/categories/bundle-copy.d.ts +7 -0
  14. package/dist/_internal/shared/features/categories/bundle-copy.js +9 -0
  15. package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +69 -19
  16. package/dist/_internal/shared/features/categories/bundle-schemas.js +20 -2
  17. package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
  18. package/dist/_internal/shared/features/checkout/config.js +1 -1
  19. package/dist/_internal/shared/features/payments/config.d.ts +2 -1
  20. package/dist/_internal/shared/features/payments/config.js +2 -1
  21. package/dist/_internal/shared/features/products/schema.d.ts +8 -8
  22. package/dist/constants/field-names.d.ts +8 -1
  23. package/dist/constants/field-names.js +9 -2
  24. package/dist/constants/table-keys.d.ts +3 -0
  25. package/dist/constants/table-keys.js +3 -0
  26. package/dist/features/admin/components/AdminSiteSettingsView.js +17 -15
  27. package/dist/features/admin/components/DashboardStats.js +20 -12
  28. package/dist/features/admin/components/analytics/AdminAnalyticsCharts.js +18 -2
  29. package/dist/features/admin/components/analytics/AdminStatCard.d.ts +7 -1
  30. package/dist/features/admin/components/analytics/AdminStatCard.js +13 -2
  31. package/dist/features/admin/schemas/firestore.d.ts +18 -4
  32. package/dist/features/admin/schemas/firestore.js +11 -1
  33. package/dist/features/categories/components/BundleBuyNowCta.d.ts +3 -1
  34. package/dist/features/categories/components/BundleBuyNowCta.js +3 -3
  35. package/dist/features/categories/components/BundleDynamicRuleEditor.d.ts +1 -1
  36. package/dist/features/categories/components/BundleDynamicRuleEditor.js +2 -1
  37. package/dist/features/categories/components/BundlesListView.d.ts +4 -1
  38. package/dist/features/categories/components/BundlesListView.js +2 -2
  39. package/dist/features/categories/components/CategoryBundlesListing.d.ts +4 -1
  40. package/dist/features/categories/components/CategoryBundlesListing.js +9 -8
  41. package/dist/features/categories/schemas/firestore.d.ts +29 -1
  42. package/dist/features/checkout/actions/checkout-actions.d.ts +11 -0
  43. package/dist/features/checkout/actions/checkout-actions.js +26 -0
  44. package/dist/features/filters/FilterFacetSection.d.ts +6 -1
  45. package/dist/features/filters/FilterFacetSection.js +25 -2
  46. package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +4 -1
  47. package/dist/features/homepage/components/FeaturedBundlesSection.js +9 -8
  48. package/dist/features/orders/actions/refund-actions.js +10 -7
  49. package/dist/features/orders/schemas/firestore.d.ts +16 -13
  50. package/dist/features/orders/schemas/firestore.js +6 -5
  51. package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
  52. package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -1
  53. package/dist/features/products/components/AuctionsIndexListing.js +2 -1
  54. package/dist/features/products/components/ProductDetailPageView.js +3 -1
  55. package/dist/features/products/components/ProductDetailView.d.ts +3 -1
  56. package/dist/features/products/components/ProductDetailView.js +2 -2
  57. package/dist/features/products/components/ProductFilters.d.ts +10 -4
  58. package/dist/features/products/components/ProductFilters.js +20 -16
  59. package/dist/features/products/components/ProductGrid.js +2 -2
  60. package/dist/features/products/components/ProductsIndexListing.js +2 -1
  61. package/dist/features/products/constants/product-features.constants.js +5 -1
  62. package/dist/features/products/schemas/index.d.ts +2 -2
  63. package/dist/features/products/schemas/product-features.d.ts +1 -1
  64. package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
  65. package/dist/features/products/schemas/product-features.validators.js +4 -0
  66. package/dist/features/seller/components/SellerCouponEditorView.js +2 -2
  67. package/dist/features/seller/components/analytics/SellerAnalyticsStats.js +10 -4
  68. package/dist/features/stores/components/StoreBundlesPageView.d.ts +4 -1
  69. package/dist/features/stores/components/StoreBundlesPageView.js +2 -2
  70. package/dist/features/stores/components/StoresIndexListing.js +2 -1
  71. package/dist/providers/db-firebase/admin-app-lite.js +8 -2
  72. package/dist/providers/db-firebase/admin.js +8 -2
  73. package/dist/seed/addresses-seed-data.js +24 -7
  74. package/dist/seed/cart-seed-data.d.ts +10 -10
  75. package/dist/seed/cart-seed-data.js +39 -2
  76. package/dist/seed/grouped-listings-seed-data.js +47 -0
  77. package/dist/seed/orders-seed-data.js +150 -0
  78. package/dist/seed/products-standard-seed-data.js +42 -0
  79. package/dist/seed/site-settings-seed-data.js +11 -4
  80. package/dist/seed/users-seed-data.js +1 -1
  81. package/dist/seed/wishlists-seed-data.js +7 -0
  82. package/dist/server-entry.d.ts +2 -1
  83. package/dist/server-entry.js +2 -1
  84. package/dist/server.d.ts +2 -1
  85. package/dist/server.js +3 -1
  86. package/dist/tailwind-utilities.css +1 -1
  87. package/dist/ui/components/SideDrawer.style.css +49 -11
  88. package/package.json +1 -1
@@ -232,15 +232,27 @@ export interface SiteSettingsDocument {
232
232
  codEnabled: boolean;
233
233
  };
234
234
  commissions: {
235
- razorpayFeePercent: number;
235
+ /** Our platform cut charged to the buyer as a % of order value (e.g. 5 = 5%). */
236
+ platformFeePercent: number;
237
+ /** GST applied on top of our platform fee (18%). buyer pays platformFee × (1 + gstPercent/100). */
238
+ gstPercent: number;
239
+ /** Per-transaction gateway minimum fee in rupees. Ensures total charge is never below this floor. 0 = no minimum. */
240
+ minimumTransactionFee: number;
241
+ /** Razorpay gateway cost % (absorbed by platform, not passed through separately). */
242
+ gatewayFeePercent: number;
236
243
  codDepositPercent: number;
237
244
  sellerShippingFixed: number;
238
245
  platformShippingPercent: number;
239
246
  platformShippingFixedMin: number;
240
- processingFeePercent?: number;
241
- gstPercent?: number;
242
- gatewayFeePercent?: number;
243
247
  autoPayoutWindowDays?: number;
248
+ /** Days before a settled order's funds are released for payout. */
249
+ payoutHoldDays?: number;
250
+ /** Minimum payout amount in rupees. */
251
+ minPayoutAmount?: number;
252
+ auctionListingFee?: number;
253
+ preOrderListingFee?: number;
254
+ featuredSlotFee?: number;
255
+ promotedSlotFee?: number;
244
256
  };
245
257
  socialLinks: {
246
258
  facebook?: string;
@@ -280,6 +292,8 @@ export interface SiteSettingsDocument {
280
292
  sellerRegistration: boolean;
281
293
  preOrders: boolean;
282
294
  seedPanel: boolean;
295
+ /** When true, admin users see a bypass button in checkout that skips OTP and payment. Server-enforced. */
296
+ adminCheckoutBypass?: boolean;
283
297
  listingTypes?: {
284
298
  standard?: boolean;
285
299
  auction?: boolean;
@@ -136,11 +136,21 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
136
136
  codEnabled: true,
137
137
  },
138
138
  commissions: {
139
- razorpayFeePercent: 5,
139
+ platformFeePercent: 5,
140
+ gstPercent: 18,
141
+ minimumTransactionFee: 0,
142
+ gatewayFeePercent: 2,
140
143
  codDepositPercent: 10,
141
144
  sellerShippingFixed: 0,
142
145
  platformShippingPercent: 10,
143
146
  platformShippingFixedMin: 0,
147
+ autoPayoutWindowDays: 7,
148
+ payoutHoldDays: 2,
149
+ minPayoutAmount: 100,
150
+ auctionListingFee: 0,
151
+ preOrderListingFee: 0,
152
+ featuredSlotFee: 999,
153
+ promotedSlotFee: 499,
144
154
  },
145
155
  featureFlags: {
146
156
  chats: true,
@@ -4,5 +4,7 @@ export interface BundleBuyNowCtaProps {
4
4
  onBuyNow: (input: {
5
5
  bundleSlug: string;
6
6
  }) => Promise<unknown>;
7
+ /** Renders a smaller single-line button suited for list cards. */
8
+ compact?: boolean;
7
9
  }
8
- export declare function BundleBuyNowCta({ bundleSlug, outOfStock, onBuyNow, }: BundleBuyNowCtaProps): import("react/jsx-runtime").JSX.Element;
10
+ export declare function BundleBuyNowCta({ bundleSlug, outOfStock, onBuyNow, compact, }: BundleBuyNowCtaProps): import("react/jsx-runtime").JSX.Element;
@@ -12,7 +12,7 @@ import { Button, LoginRequiredModal, Stack, Text } from "../../../ui";
12
12
  import { useToast } from "../../../ui";
13
13
  import { isAuthError } from "../../../utils/auth-error";
14
14
  import { BUNDLE_COPY } from "../../../_internal/shared/features/categories/bundle-copy";
15
- export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, }) {
15
+ export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, compact = false, }) {
16
16
  const { showToast } = useToast();
17
17
  const [submitting, setSubmitting] = useState(false);
18
18
  const [error, setError] = useState(null);
@@ -38,7 +38,7 @@ export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, }) {
38
38
  }
39
39
  }, [bundleSlug, onBuyNow, showToast]);
40
40
  if (outOfStock) {
41
- return (_jsxs(Stack, { gap: "xs", "aria-live": "polite", children: [_jsx(Button, { variant: "primary", disabled: true, "aria-disabled": true, children: BUNDLE_COPY.detail.ctaOutOfStock }), _jsx(Text, { size: "xs", color: "muted", children: BUNDLE_COPY.detail.ctaHint })] }));
41
+ return (_jsxs(Stack, { gap: "xs", "aria-live": "polite", children: [_jsx(Button, { variant: "primary", size: compact ? "sm" : "md", disabled: true, "aria-disabled": true, className: compact ? "w-full" : undefined, children: BUNDLE_COPY.detail.ctaOutOfStock }), !compact && (_jsx(Text, { size: "xs", color: "muted", children: BUNDLE_COPY.detail.ctaHint }))] }));
42
42
  }
43
- return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to purchase this bundle. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", onClick: handleClick, disabled: submitting, "aria-busy": submitting, children: submitting ? BUNDLE_COPY.detail.ctaAdding : BUNDLE_COPY.detail.ctaBuyNow }), error && (_jsx(Text, { size: "sm", color: "danger", role: "alert", children: error }))] })] }));
43
+ return (_jsxs(_Fragment, { children: [_jsx(LoginRequiredModal, { isOpen: showLoginModal, onClose: () => setShowLoginModal(false), message: "You need to be signed in to purchase this bundle. Please log in or create an account to continue." }), _jsxs(Stack, { gap: "sm", children: [_jsx(Button, { variant: "primary", size: compact ? "sm" : "md", onClick: handleClick, disabled: submitting, "aria-busy": submitting, className: compact ? "w-full" : undefined, children: submitting ? BUNDLE_COPY.detail.ctaAdding : BUNDLE_COPY.detail.ctaBuyNow }), error && (_jsx(Text, { size: "sm", color: "danger", role: "alert", children: error }))] })] }));
44
44
  }
@@ -4,7 +4,7 @@ type DynamicRule = {
4
4
  categorySlug?: string;
5
5
  brandSlug?: string;
6
6
  tags?: string[];
7
- listingType?: "standard" | "pre-order";
7
+ listingType?: "standard" | "pre-order" | "prize-draw";
8
8
  };
9
9
  orderBy?: "price-asc" | "price-desc" | "createdAt-desc";
10
10
  limit: number;
@@ -29,9 +29,10 @@ const ORDER_BY_OPTIONS = [
29
29
  { label: "Price (high → low)", value: "price-desc" },
30
30
  ];
31
31
  const LISTING_TYPE_OPTIONS = [
32
- { label: "Any (standard + pre-order)", value: "" },
32
+ { label: "Any", value: "" },
33
33
  { label: "Standard only", value: "standard" },
34
34
  { label: "Pre-order only", value: "pre-order" },
35
+ { label: "Prize draw only", value: "prize-draw" },
35
36
  ];
36
37
  export function BundleDynamicRuleEditor({ value, onChange, disabled = false, }) {
37
38
  const rule = value ?? DEFAULT_DYNAMIC_RULE;
@@ -1,6 +1,9 @@
1
1
  type SearchParams = Record<string, string | string[]>;
2
2
  export interface BundlesListViewProps {
3
3
  searchParams?: SearchParams;
4
+ onBuyNow?: (input: {
5
+ bundleSlug: string;
6
+ }) => Promise<unknown>;
4
7
  }
5
8
  /**
6
9
  * Public `/bundles` index. Bundles live as `categoryType:"bundle"` rows on
@@ -9,5 +12,5 @@ export interface BundlesListViewProps {
9
12
  * Optional `?storeId=…` post-filters to a single seller's bundles.
10
13
  * Sort UI is owned by `CategoryBundlesListing` (client).
11
14
  */
12
- export declare function BundlesListView({ searchParams, }: BundlesListViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
15
+ export declare function BundlesListView({ searchParams, onBuyNow, }: BundlesListViewProps): Promise<import("react/jsx-runtime").JSX.Element>;
13
16
  export {};
@@ -15,7 +15,7 @@ function sp(params, key) {
15
15
  * Optional `?storeId=…` post-filters to a single seller's bundles.
16
16
  * Sort UI is owned by `CategoryBundlesListing` (client).
17
17
  */
18
- export async function BundlesListView({ searchParams = {}, }) {
18
+ export async function BundlesListView({ searchParams = {}, onBuyNow, }) {
19
19
  const storeId = sp(searchParams, "storeId");
20
20
  const all = await categoriesRepository
21
21
  .listByType("bundle", { activeOnly: true, limit: DEFAULT_LIMIT })
@@ -23,5 +23,5 @@ export async function BundlesListView({ searchParams = {}, }) {
23
23
  const bundles = storeId
24
24
  ? all.filter((c) => c.createdByStoreId === storeId)
25
25
  : all;
26
- return (_jsx(Main, { children: _jsx(Section, { className: "py-10", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-2 text-3xl font-semibold text-zinc-900 dark:text-zinc-100", children: "Collectible Bundles" }), _jsx(Text, { className: "mb-6 text-sm text-[var(--appkit-color-text-muted)]", children: "Curated multi-product bundles \u2014 Pok\u00E9mon starter sets, Beyblade tournament packs, Hot Wheels collector boxes and more. One price, one checkout, one shipment." }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(CategoryBundlesListing, { initialBundles: bundles }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
26
+ return (_jsx(Main, { children: _jsx(Section, { className: "py-10", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 1, className: "mb-2 text-3xl font-semibold text-zinc-900 dark:text-zinc-100", children: "Collectible Bundles" }), _jsx(Text, { className: "mb-6 text-sm text-[var(--appkit-color-text-muted)]", children: "Curated multi-product bundles \u2014 Pok\u00E9mon starter sets, Beyblade tournament packs, Hot Wheels collector boxes and more. One price, one checkout, one shipment." }), _jsx(AdSlot, { id: "listing-sidebar-top", className: "mb-6" }), _jsx(CategoryBundlesListing, { initialBundles: bundles, onBuyNow: onBuyNow }), _jsx(AdSlot, { id: "listing-sidebar-bottom", className: "mt-8" })] }) }) }));
27
27
  }
@@ -2,5 +2,8 @@ import type { CategoryDocument } from "../schemas";
2
2
  export interface CategoryBundlesListingProps {
3
3
  initialBundles: CategoryDocument[];
4
4
  brandName?: string;
5
+ onBuyNow?: (input: {
6
+ bundleSlug: string;
7
+ }) => Promise<unknown>;
5
8
  }
6
- export declare function CategoryBundlesListing({ initialBundles, brandName, }: CategoryBundlesListingProps): import("react/jsx-runtime").JSX.Element;
9
+ export declare function CategoryBundlesListing({ initialBundles, brandName, onBuyNow, }: CategoryBundlesListingProps): import("react/jsx-runtime").JSX.Element;
@@ -12,6 +12,7 @@ import Link from "next/link";
12
12
  import { Badge, Div, Row, Select, Stack, Text } from "../../../ui";
13
13
  import { ROUTES } from "../../../next/routing/route-map";
14
14
  import { formatCurrency } from "../../../utils/number.formatter";
15
+ import { BundleBuyNowCta } from "./BundleBuyNowCta";
15
16
  const SORT_OPTIONS = [
16
17
  { label: "Newest", value: "newest" },
17
18
  { label: "Price ↑", value: "price-asc" },
@@ -47,7 +48,7 @@ function comparator(sort) {
47
48
  return bT - aT;
48
49
  };
49
50
  }
50
- export function CategoryBundlesListing({ initialBundles, brandName, }) {
51
+ export function CategoryBundlesListing({ initialBundles, brandName, onBuyNow, }) {
51
52
  const [sort, setSort] = useState("newest");
52
53
  const filtered = useMemo(() => initialBundles
53
54
  .filter((c) => c.categoryType === "bundle")
@@ -56,17 +57,17 @@ export function CategoryBundlesListing({ initialBundles, brandName, }) {
56
57
  if (filtered.length === 0) {
57
58
  return (_jsx(Div, { className: "rounded-2xl border border-dashed border-zinc-200 py-16 text-center dark:border-zinc-700", children: _jsxs(Text, { color: "muted", children: [COPY.empty, brandName ? COPY.forBrand(brandName) : "", COPY.yetSuffix] }) }));
58
59
  }
59
- return (_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "end", children: [_jsx(Text, { size: "sm", color: "muted", children: COPY.sortLabel }), _jsx(Select, { options: SORT_OPTIONS, value: sort, onValueChange: setSort, "aria-label": COPY.sortLabel })] }), _jsx(Div, { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((bundle) => (_jsx(BundleCard, { bundle: bundle }, bundle.id))) })] }));
60
+ return (_jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "end", children: [_jsx(Text, { size: "sm", color: "muted", children: COPY.sortLabel }), _jsx(Select, { options: SORT_OPTIONS, value: sort, onValueChange: setSort, "aria-label": COPY.sortLabel })] }), _jsx(Div, { className: "grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3", children: filtered.map((bundle) => (_jsx(BundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) })] }));
60
61
  }
61
- function BundleCard({ bundle }) {
62
+ function BundleCard({ bundle, onBuyNow }) {
62
63
  const memberCount = bundle.bundleProductIds?.length ?? 0;
63
64
  const stock = bundle.bundleStockStatus ?? "in_stock";
64
65
  const badge = STOCK_BADGE_TEXT[stock];
65
66
  const cover = bundle.display?.coverImage;
66
67
  const href = String(ROUTES.PUBLIC.BUNDLE_DETAIL?.(bundle.slug) ?? "#");
67
- return (_jsxs(Link, { href: href, className: "group rounded-xl border border-zinc-200 bg-white p-3 transition-colors hover:border-zinc-300 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700", children: [_jsx(Div, { className: "mb-2 aspect-video overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800", children: cover ? (
68
- // eslint-disable-next-line @next/next/no-img-element
69
- _jsx("img", { src: cover, alt: bundle.name, className: "h-full w-full object-cover transition-transform group-hover:scale-105", loading: "lazy" })) : (_jsx(Div, { className: "flex h-full w-full items-center justify-center text-3xl", children: PLACEHOLDER_EMOJI })) }), _jsx(Text, { className: "line-clamp-2 text-sm font-semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePriceInPaise
70
- ? formatCurrency(bundle.bundlePriceInPaise / 100, "INR")
71
- : COPY.priceFallback }), _jsx(Text, { size: "xs", color: "muted", children: COPY.items(memberCount) })] }), badge && (_jsx(Badge, { variant: STOCK_BADGE_VARIANT[stock], className: "mt-1", children: badge }))] }));
68
+ return (_jsxs(Stack, { gap: "none", className: "rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900", children: [_jsxs(Link, { href: href, className: "group block p-3 hover:no-underline", children: [_jsx(Div, { className: "mb-2 aspect-video overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800", children: cover ? (
69
+ // eslint-disable-next-line @next/next/no-img-element
70
+ _jsx("img", { src: cover, alt: bundle.name, className: "h-full w-full object-cover transition-transform group-hover:scale-105", loading: "lazy" })) : (_jsx(Div, { className: "flex h-full w-full items-center justify-center text-3xl", children: PLACEHOLDER_EMOJI })) }), _jsx(Text, { className: "line-clamp-2 text-sm font-semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1", children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePriceInPaise
71
+ ? formatCurrency(bundle.bundlePriceInPaise / 100, "INR")
72
+ : COPY.priceFallback }), _jsx(Text, { size: "xs", color: "muted", children: COPY.items(memberCount) })] }), badge && (_jsx(Badge, { variant: STOCK_BADGE_VARIANT[stock], className: "mt-1", children: badge }))] }), onBuyNow && (_jsx(Div, { className: "border-t border-zinc-100 p-3 pt-2 dark:border-zinc-800", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }));
72
73
  }
@@ -27,11 +27,24 @@ export type BundleQueryRule = {
27
27
  categorySlug?: string;
28
28
  brandSlug?: string;
29
29
  tags?: string[];
30
- listingType?: "standard" | "pre-order";
30
+ /** Eligible listing types that may appear in a dynamic bundle. */
31
+ listingType?: "standard" | "pre-order" | "prize-draw";
31
32
  };
32
33
  orderBy?: "price-asc" | "price-desc" | "createdAt-desc";
33
34
  limit: number;
34
35
  };
36
+ /**
37
+ * Per-member metadata stored alongside `bundleProductIds`.
38
+ * `drawCount` — only meaningful when the member product has
39
+ * `listingType === "prize-draw"`. Represents how many draw entries
40
+ * the buyer receives for that product when they purchase the bundle
41
+ * (e.g. drawCount=5 means 5 raffle entries, not 5 copies of the draw).
42
+ */
43
+ export interface BundleItemDetail {
44
+ productId: string;
45
+ /** Number of draw entries included when product is a prize-draw. */
46
+ drawCount?: number;
47
+ }
35
48
  /** Full Firestore category document (includes server-only fields) */
36
49
  export interface CategoryDocumentSEO {
37
50
  title: string;
@@ -100,6 +113,14 @@ export interface CategoryDocument {
100
113
  brandFounded?: number;
101
114
  /** Banner image for the brand storefront page — categoryType==="brand". */
102
115
  brandBannerImage?: string;
116
+ /**
117
+ * "special" — admin-curated bundle; writes reverse `partOfBundleIds`
118
+ * pointers on member products and shows the "Bundled" badge on cards.
119
+ *
120
+ * "brand" — auto-generated brand collection (dynamic brandSlug query).
121
+ * Does NOT update `partOfBundleIds`; used as a discovery surface only.
122
+ */
123
+ bundleKind?: "special" | "brand";
103
124
  /** Discounted bundle price in paise. */
104
125
  bundlePriceInPaise?: number;
105
126
  /** Rule resolving the bundle's member products — static list or live query. */
@@ -110,6 +131,13 @@ export interface CategoryDocument {
110
131
  bundleQueryResolvedAt?: Date;
111
132
  /** Hand-picked products list (mirror of bundleQueryRule for static rules); kept for index-friendly queries. */
112
133
  bundleProductIds?: string[];
134
+ /**
135
+ * Per-member metadata parallel to `bundleProductIds`.
136
+ * Carries `drawCount` for prize-draw members (how many raffle entries
137
+ * the buyer receives). Flat `bundleProductIds` is kept for Firestore
138
+ * array-contains queries; this array holds the richer shape.
139
+ */
140
+ bundleItemDetails?: BundleItemDetail[];
113
141
  seo: CategoryDocumentSEO;
114
142
  display: CategoryDocumentDisplay;
115
143
  isActive: boolean;
@@ -17,4 +17,15 @@ export declare function verifyCheckoutConsentOtp(userId: string, addressId: stri
17
17
  * registered phone number.
18
18
  */
19
19
  export declare function grantCheckoutConsentViaSms(userId: string, userPhone: string | undefined, addressId: string): Promise<void>;
20
+ /**
21
+ * Grants checkout consent for an admin test order, bypassing the normal OTP flow.
22
+ *
23
+ * The resulting Firestore doc is identical to a verified consent OTP so that
24
+ * `createCheckoutOrderAction` can proceed without modification. The
25
+ * `verifiedVia: "admin_bypass"` marker prevents bypass-credit grants.
26
+ *
27
+ * Security: callers MUST verify the requesting user is an admin AND that
28
+ * `siteSettings.featureFlags.adminCheckoutBypass === true` before calling this.
29
+ */
30
+ export declare function grantAdminCheckoutBypass(userId: string, addressId: string, bypassingAdminUid: string): Promise<void>;
20
31
  export { userRepository };
@@ -94,4 +94,30 @@ export async function grantCheckoutConsentViaSms(userId, userPhone, addressId) {
94
94
  });
95
95
  serverLogger.info(`Checkout consent granted via SMS: uid=${userId} addressId=${addressId}`);
96
96
  }
97
+ /**
98
+ * Grants checkout consent for an admin test order, bypassing the normal OTP flow.
99
+ *
100
+ * The resulting Firestore doc is identical to a verified consent OTP so that
101
+ * `createCheckoutOrderAction` can proceed without modification. The
102
+ * `verifiedVia: "admin_bypass"` marker prevents bypass-credit grants.
103
+ *
104
+ * Security: callers MUST verify the requesting user is an admin AND that
105
+ * `siteSettings.featureFlags.adminCheckoutBypass === true` before calling this.
106
+ */
107
+ export async function grantAdminCheckoutBypass(userId, addressId, bypassingAdminUid) {
108
+ const address = await findUserAddress(userId, addressId);
109
+ if (!address)
110
+ throw new ValidationError("Address not found.");
111
+ await saveConsentOtp(userId, addressId, {
112
+ codeHash: "",
113
+ expiresAt: new Date(Date.now() + CONSENT_OTP_EXPIRY_MS),
114
+ attempts: 0,
115
+ verified: true,
116
+ verifiedVia: "admin_bypass",
117
+ adminBypassBy: bypassingAdminUid,
118
+ addressId,
119
+ createdAt: new Date(),
120
+ });
121
+ serverLogger.info(`Admin checkout bypass granted: uid=${userId} addressId=${addressId} bypassedBy=${bypassingAdminUid}`);
122
+ }
97
123
  export { userRepository };
@@ -15,10 +15,15 @@ export interface FilterFacetSectionProps {
15
15
  onToggle?: () => void;
16
16
  onClear?: () => void;
17
17
  className?: string;
18
+ /**
19
+ * "accordion" (default) — collapsible inline section inside a filter drawer.
20
+ * "dropdown" — compact pill button with a floating panel; use in horizontal toolbars.
21
+ */
22
+ displayAs?: "accordion" | "dropdown";
18
23
  }
19
24
  /**
20
25
  * FilterFacetSection — collapsible checkbox/radio filter section.
21
26
  * Supports single-select and multi-select modes.
22
27
  * When searchable, the search input is always visible and typing auto-expands.
23
28
  */
24
- export declare function FilterFacetSection({ title, options, selected, onChange, selectionMode, defaultCollapsed, searchable, isOpen: controlledOpen, onToggle, onClear, className, }: FilterFacetSectionProps): import("react/jsx-runtime").JSX.Element;
29
+ export declare function FilterFacetSection({ title, options, selected, onChange, selectionMode, defaultCollapsed, searchable, isOpen: controlledOpen, onToggle, onClear, className, displayAs, }: FilterFacetSectionProps): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useState } from "react";
3
+ import { useEffect, useRef, useState } from "react";
4
4
  import { Button, Div, Input, Row, Span, Text } from "../../ui";
5
5
  import { cn } from "./filterUtils";
6
6
  /**
@@ -8,10 +8,23 @@ import { cn } from "./filterUtils";
8
8
  * Supports single-select and multi-select modes.
9
9
  * When searchable, the search input is always visible and typing auto-expands.
10
10
  */
11
- export function FilterFacetSection({ title, options, selected, onChange, selectionMode = "multi", defaultCollapsed = true, searchable = false, isOpen: controlledOpen, onToggle, onClear, className = "", }) {
11
+ export function FilterFacetSection({ title, options, selected, onChange, selectionMode = "multi", defaultCollapsed = true, searchable = false, isOpen: controlledOpen, onToggle, onClear, className = "", displayAs = "accordion", }) {
12
12
  const isControlled = controlledOpen !== undefined;
13
13
  const [internalCollapsed, setInternalCollapsed] = useState(defaultCollapsed);
14
14
  const [search, setSearch] = useState("");
15
+ const [dropdownOpen, setDropdownOpen] = useState(false);
16
+ const dropdownRef = useRef(null);
17
+ useEffect(() => {
18
+ if (displayAs !== "dropdown")
19
+ return;
20
+ function handleOutsideClick(e) {
21
+ if (dropdownRef.current && !dropdownRef.current.contains(e.target)) {
22
+ setDropdownOpen(false);
23
+ }
24
+ }
25
+ document.addEventListener("mousedown", handleOutsideClick);
26
+ return () => document.removeEventListener("mousedown", handleOutsideClick);
27
+ }, [displayAs]);
15
28
  const isCollapsed = isControlled ? !controlledOpen : internalCollapsed;
16
29
  const handleToggle = () => {
17
30
  if (onToggle)
@@ -44,6 +57,16 @@ export function FilterFacetSection({ title, options, selected, onChange, selecti
44
57
  const selectedLabels = selected
45
58
  .map((v) => options.find((o) => o.value === v)?.label ?? v)
46
59
  .slice(0, 3);
60
+ // ── Dropdown variant ────────────────────────────────────────────────────
61
+ if (displayAs === "dropdown") {
62
+ return (_jsxs(Div, { ref: dropdownRef, className: cn("relative inline-block", className), children: [_jsxs(Button, { type: "button", onClick: () => setDropdownOpen((o) => !o), variant: "ghost", className: cn("inline-flex items-center gap-1.5 rounded-full border px-3 py-1.5 text-sm font-medium transition-colors", hasValue
63
+ ? "border-emerald-500 bg-emerald-50 text-emerald-700 dark:border-emerald-500/60 dark:bg-emerald-900/20 dark:text-emerald-300"
64
+ : "border-zinc-200 bg-white text-zinc-700 hover:border-zinc-400 dark:border-slate-700 dark:bg-slate-900 dark:text-zinc-200 dark:hover:border-slate-500"), children: [title, hasValue && (_jsx(Span, { className: "inline-flex h-4 w-4 items-center justify-center rounded-full bg-emerald-600 text-[10px] font-bold text-white", children: selected.length })), _jsx("svg", { className: cn("h-3.5 w-3.5 text-zinc-400 transition-transform", dropdownOpen ? "rotate-180" : ""), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), dropdownOpen && (_jsxs(Div, { className: "absolute left-0 top-full z-[var(--appkit-z-dropdown,50)] mt-1 min-w-[180px] max-w-[260px] rounded-xl border border-zinc-200 bg-white shadow-lg dark:border-slate-700 dark:bg-slate-900", children: [searchable && (_jsx(Div, { className: "border-b border-zinc-100 p-2 dark:border-slate-800", children: _jsx(Input, { type: "search", placeholder: "Search\u2026", value: search, onChange: (e) => handleSearch(e.target.value), className: "w-full rounded-md border border-zinc-200 bg-white px-2 py-1 text-xs text-zinc-900 focus:outline-none dark:border-slate-700 dark:bg-slate-800/60 dark:text-zinc-100" }) })), _jsxs(Div, { className: "max-h-56 overflow-y-auto py-1", children: [filtered.map((option) => {
65
+ const isSelected = selected.includes(option.value);
66
+ return (_jsxs(Row, { gap: "xs", className: "cursor-pointer rounded-lg px-3 py-1.5 hover:bg-zinc-50 dark:hover:bg-slate-800 mx-1", onClick: () => toggle(option.value), children: [_jsx("input", { type: selectionMode === "single" ? "radio" : "checkbox", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className: "h-3.5 w-3.5 flex-shrink-0 cursor-pointer rounded border-zinc-300 text-primary-600 dark:border-slate-600 dark:text-secondary-500" }), _jsx(Span, { className: "flex-1 truncate text-xs text-zinc-700 dark:text-zinc-300", children: option.label }), option.count !== undefined && (_jsx(Span, { className: "flex-shrink-0 text-[10px] tabular-nums text-zinc-400 dark:text-zinc-500", children: option.count }))] }, option.value));
67
+ }), filtered.length === 0 && (_jsx(Text, { className: "px-3 py-2 text-xs text-zinc-400 dark:text-zinc-500", children: "No results" }))] }), hasValue && (_jsx(Div, { className: "border-t border-zinc-100 p-1 dark:border-slate-800", children: _jsx(Button, { type: "button", variant: "ghost", size: "sm", className: "w-full py-1 text-xs text-zinc-400 transition-colors hover:text-red-600", onClick: () => { onChange([]); onClear?.(); setDropdownOpen(false); }, children: "Clear" }) }))] }))] }));
68
+ }
69
+ // ── Accordion variant (default) ─────────────────────────────────────────
47
70
  return (_jsxs(Div, { className: cn("p-4 border-b border-zinc-200 dark:border-slate-700 last:border-b-0", className), children: [_jsxs(Row, { gap: "xs", children: [_jsxs(Button, { type: "button", onClick: handleToggle, variant: "ghost", size: "sm", className: "flex flex-1 items-center justify-between text-sm font-semibold text-zinc-900 dark:text-zinc-50 py-1 hover:opacity-80 transition-opacity", "aria-expanded": !isCollapsed, children: [_jsxs(Row, { as: Span, gap: "xs", children: [title, hasValue && (_jsx(Span, { className: "inline-flex items-center justify-center w-5 h-5 text-xs rounded-full bg-emerald-50 dark:bg-emerald-900/30 text-emerald-700 dark:text-emerald-300 ring-1 ring-emerald-600/20 dark:ring-emerald-400/20", children: selected.length }))] }), _jsx("svg", { className: cn("w-4 h-4 text-zinc-500 dark:text-zinc-400 transition-transform duration-200", isCollapsed ? "" : "rotate-180"), fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M19 9l-7 7-7-7" }) })] }), onClear && hasValue && (_jsx(Button, { type: "button", onClick: onClear, variant: "ghost", size: "sm", className: "inline-flex items-center justify-center w-5 h-5 p-0 text-zinc-500 dark:text-zinc-400 hover:text-red-600 dark:hover:text-red-400 hover:bg-zinc-100 dark:hover:bg-slate-700 transition-colors rounded-full", "aria-label": "Clear filter", children: _jsx("svg", { className: "w-3 h-3", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", strokeWidth: 2.5, "aria-hidden": "true", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", d: "M6 18L18 6M6 6l12 12" }) }) }))] }), isCollapsed && hasValue && (_jsxs(Div, { className: "mt-2 flex flex-wrap gap-1", children: [selectedLabels.map((label) => (_jsx(Span, { className: "inline-flex items-center px-2 py-0.5 rounded-full text-xs bg-zinc-100 dark:bg-slate-700 text-zinc-700 dark:text-zinc-300 border border-zinc-200 dark:border-slate-600", children: label }, label))), selected.length > 3 && (_jsxs(Span, { className: "inline-flex items-center px-2 py-0.5 rounded-full text-xs text-zinc-400 dark:text-zinc-500", children: ["+", selected.length - 3, " more"] }))] })), searchable && (_jsx(Div, { className: "mt-2", children: _jsx(Input, { type: "search", placeholder: "Search\u2026", value: search, onChange: (e) => handleSearch(e.target.value), className: "w-full rounded-md border border-zinc-200 dark:border-slate-700 bg-white dark:bg-slate-800/60 px-2.5 py-1.5 text-xs text-zinc-900 dark:text-zinc-100 focus:outline-none focus:ring-2 focus:ring-primary-500/20 dark:focus:ring-secondary-400/20" }) })), !isCollapsed && (_jsxs(Div, { className: "mt-3 space-y-1", children: [filtered.map((option) => {
48
71
  const isSelected = selected.includes(option.value);
49
72
  return (_jsxs(Row, { gap: "2.5", className: "py-1 cursor-pointer group", children: [selectionMode === "single" ? (_jsx("input", { type: "radio", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className: cn("flex-shrink-0 border-zinc-300 dark:border-slate-600 cursor-pointer", "w-4 h-4 rounded-full text-primary-600 dark:text-secondary-500", "focus:ring-primary-500/30 dark:focus:ring-secondary-400/30") })) : (_jsx("input", { type: "checkbox", checked: isSelected, onChange: () => toggle(option.value), "aria-label": option.label, className: cn("flex-shrink-0 border-zinc-300 dark:border-slate-600 cursor-pointer", "w-4 h-4 rounded text-primary-600 dark:text-secondary-500 checked:bg-primary-600 dark:checked:bg-secondary-500", "focus:ring-primary-500/30 dark:focus:ring-secondary-400/30") })), _jsx(Span, { className: "flex-1 text-sm text-zinc-700 dark:text-zinc-300 group-hover:text-zinc-900 dark:group-hover:text-zinc-100 transition-colors truncate", children: option.label }), option.count !== undefined && (_jsx(Span, { className: "text-xs text-zinc-400 dark:text-zinc-500 tabular-nums flex-shrink-0", children: option.count }))] }, option.value));
@@ -12,5 +12,8 @@ export interface FeaturedBundlesSectionProps {
12
12
  description?: string;
13
13
  initialItems?: CategoryDocument[];
14
14
  className?: string;
15
+ onBuyNow?: (input: {
16
+ bundleSlug: string;
17
+ }) => Promise<unknown>;
15
18
  }
16
- export declare function FeaturedBundlesSection({ title, description, initialItems, className, }: FeaturedBundlesSectionProps): import("react/jsx-runtime").JSX.Element;
19
+ export declare function FeaturedBundlesSection({ title, description, initialItems, className, onBuyNow, }: FeaturedBundlesSectionProps): import("react/jsx-runtime").JSX.Element;
@@ -4,19 +4,20 @@ import { Badge, Div, Heading, Row, Section, Stack, Text } from "../../../ui";
4
4
  import { ROUTES } from "../../../next/routing/route-map";
5
5
  import { formatCurrency } from "../../../utils/number.formatter";
6
6
  import { BUNDLE_COPY, BUNDLE_STOCK_VARIANT, } from "../../../_internal/shared/features/categories/bundle-copy";
7
+ import { BundleBuyNowCta } from "../../categories/components/BundleBuyNowCta";
7
8
  const PLACEHOLDER_EMOJI = "📦";
8
- export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitle, description, initialItems = [], className = "", }) {
9
+ export function FeaturedBundlesSection({ title = BUNDLE_COPY.featuredDefaultTitle, description, initialItems = [], className = "", onBuyNow, }) {
9
10
  const items = initialItems.filter((c) => c.categoryType === "bundle");
10
- return (_jsx(Section, { className: `py-10 ${className}`, children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", className: "flex-wrap", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-100", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES ?? "/bundles"), className: "text-sm font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), items.length === 0 ? (_jsx(Div, { className: "rounded-xl border border-dashed border-zinc-200 py-12 text-center dark:border-zinc-700", children: _jsx(Text, { color: "muted", children: BUNDLE_COPY.featured.empty }) })) : (_jsx(Div, { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle }, bundle.id))) }))] }) }));
11
+ return (_jsx(Section, { className: `py-10 ${className}`, children: _jsxs(Stack, { gap: "md", children: [_jsxs(Row, { gap: "sm", align: "center", justify: "between", className: "flex-wrap", children: [_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-2xl font-semibold text-zinc-900 dark:text-zinc-100", children: title }), description && (_jsx(Text, { size: "sm", color: "muted", children: description }))] }), _jsx(Link, { href: String(ROUTES.PUBLIC.BUNDLES ?? "/bundles"), className: "text-sm font-medium text-[var(--appkit-color-primary)] hover:underline", children: BUNDLE_COPY.featured.viewAll })] }), items.length === 0 ? (_jsx(Div, { className: "rounded-xl border border-dashed border-zinc-200 py-12 text-center dark:border-zinc-700", children: _jsx(Text, { color: "muted", children: BUNDLE_COPY.featured.empty }) })) : (_jsx(Div, { className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-4", children: items.map((bundle) => (_jsx(FeaturedBundleCard, { bundle: bundle, onBuyNow: onBuyNow }, bundle.id))) }))] }) }));
11
12
  }
12
- function FeaturedBundleCard({ bundle }) {
13
+ function FeaturedBundleCard({ bundle, onBuyNow }) {
13
14
  const stock = bundle.bundleStockStatus ?? "in_stock";
14
15
  const memberCount = bundle.bundleProductIds?.length ?? 0;
15
16
  const cover = bundle.display?.coverImage;
16
17
  const href = String(ROUTES.PUBLIC.BUNDLE_DETAIL?.(bundle.slug) ?? "#");
17
- return (_jsxs(Link, { href: href, className: "group rounded-xl border border-zinc-200 bg-white p-3 transition-colors hover:border-zinc-300 dark:border-zinc-800 dark:bg-zinc-900 dark:hover:border-zinc-700", children: [_jsx(Div, { className: "mb-2 aspect-square overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800", children: cover ? (
18
- // eslint-disable-next-line @next/next/no-img-element, lir/no-raw-media-elements
19
- _jsx("img", { src: cover, alt: bundle.name, className: "h-full w-full object-cover transition-transform group-hover:scale-105", loading: "lazy" })) : (_jsx(Div, { className: "flex h-full w-full items-center justify-center text-3xl", children: PLACEHOLDER_EMOJI })) }), _jsx(Text, { className: "line-clamp-2 text-sm font-semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1 flex-wrap", children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePriceInPaise
20
- ? formatCurrency(bundle.bundlePriceInPaise / 100, "INR")
21
- : BUNDLE_COPY.featured.priceFallback }), _jsxs(Text, { size: "xs", color: "muted", children: ["\u00B7 ", BUNDLE_COPY.featured.itemCount(memberCount)] }), stock !== "in_stock" && (_jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stock], children: BUNDLE_COPY.stockBadge.listVariantOutOfStock }))] })] }));
18
+ return (_jsxs(Stack, { gap: "none", className: "rounded-xl border border-zinc-200 bg-white dark:border-zinc-800 dark:bg-zinc-900", children: [_jsxs(Link, { href: href, className: "group block p-3 hover:no-underline", children: [_jsx(Div, { className: "mb-2 aspect-square overflow-hidden rounded-lg bg-zinc-100 dark:bg-zinc-800", children: cover ? (
19
+ // eslint-disable-next-line @next/next/no-img-element, lir/no-raw-media-elements
20
+ _jsx("img", { src: cover, alt: bundle.name, className: "h-full w-full object-cover transition-transform group-hover:scale-105", loading: "lazy" })) : (_jsx(Div, { className: "flex h-full w-full items-center justify-center text-3xl", children: PLACEHOLDER_EMOJI })) }), _jsx(Text, { className: "line-clamp-2 text-sm font-semibold", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", className: "mt-1 flex-wrap", children: [_jsx(Text, { size: "sm", weight: "bold", children: bundle.bundlePriceInPaise
21
+ ? formatCurrency(bundle.bundlePriceInPaise / 100, "INR")
22
+ : BUNDLE_COPY.featured.priceFallback }), _jsxs(Text, { size: "xs", color: "muted", children: ["\u00B7 ", BUNDLE_COPY.featured.itemCount(memberCount)] }), stock !== "in_stock" && (_jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stock], children: BUNDLE_COPY.stockBadge.listVariantOutOfStock }))] })] }), onBuyNow && (_jsx(Div, { className: "border-t border-zinc-100 p-3 pt-2 dark:border-zinc-800", children: _jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, compact: true }) }))] }));
22
23
  }
@@ -11,7 +11,8 @@ import { orderRepository } from "../../orders/repository/orders.repository";
11
11
  import { RefundStatusValues } from "../../orders/schemas";
12
12
  import { siteSettingsRepository } from "../../admin/repository/site-settings.repository";
13
13
  import { notificationRepository } from "../../admin/repository/notification.repository";
14
- const DEFAULT_PROCESSING_FEE_PERCENT = 2.36;
14
+ const DEFAULT_PLATFORM_FEE_PERCENT = 5;
15
+ const DEFAULT_GST_PERCENT = 18;
15
16
  export async function issuePartialRefund(adminUid, orderId, deductFees, refundNote) {
16
17
  const order = await orderRepository.findById(orderId);
17
18
  if (!order)
@@ -21,11 +22,12 @@ export async function issuePartialRefund(adminUid, orderId, deductFees, refundNo
21
22
  if (order.refundStatus === RefundStatusValues.COMPLETED)
22
23
  throw new ValidationError("Order has already been fully refunded.");
23
24
  const settings = await siteSettingsRepository.getSingleton();
24
- const feePercent = settings.commissions?.processingFeePercent ??
25
- DEFAULT_PROCESSING_FEE_PERCENT;
25
+ const platformFeePercent = settings.commissions?.platformFeePercent ?? DEFAULT_PLATFORM_FEE_PERCENT;
26
+ const gstPercent = settings.commissions?.gstPercent ?? DEFAULT_GST_PERCENT;
27
+ const effectiveRate = platformFeePercent * (1 + gstPercent / 100);
26
28
  const grossRefund = order.totalPrice;
27
29
  const feeDeducted = deductFees
28
- ? parseFloat(((grossRefund * feePercent) / 100).toFixed(2))
30
+ ? parseFloat(((grossRefund * effectiveRate) / 100).toFixed(2))
29
31
  : 0;
30
32
  const netRefund = parseFloat((grossRefund - feeDeducted).toFixed(2));
31
33
  await orderRepository.update(orderId, {
@@ -73,10 +75,11 @@ export async function previewCancellationRefund(userId, orderId) {
73
75
  if (order.paymentStatus !== "paid")
74
76
  return null;
75
77
  const settings = await siteSettingsRepository.getSingleton();
76
- const feePercent = settings.commissions?.processingFeePercent ??
77
- DEFAULT_PROCESSING_FEE_PERCENT;
78
+ const platformFeePercent = settings.commissions?.platformFeePercent ?? DEFAULT_PLATFORM_FEE_PERCENT;
79
+ const gstPercent = settings.commissions?.gstPercent ?? DEFAULT_GST_PERCENT;
80
+ const effectiveRate = platformFeePercent * (1 + gstPercent / 100);
78
81
  const grossRefund = order.totalPrice;
79
- const feeDeducted = parseFloat(((grossRefund * feePercent) / 100).toFixed(2));
82
+ const feeDeducted = parseFloat(((grossRefund * effectiveRate) / 100).toFixed(2));
80
83
  const netRefund = parseFloat((grossRefund - feeDeducted).toFixed(2));
81
84
  return {
82
85
  orderId,
@@ -7,12 +7,12 @@ export type ShippingMethod = "custom" | "shiprocket";
7
7
  export type RefundType = "full" | "partial";
8
8
  export type OrderPayoutStatus = "eligible" | "requested" | "paid";
9
9
  export type RefundStatus = "pending" | "processing" | "completed" | "rejected";
10
- /** Runtime-accessible shipping method values — use instead of bare string literals. */
10
+ /** Runtime-accessible shipping method values â€" use instead of bare string literals. */
11
11
  export declare const ShippingMethodValues: {
12
12
  readonly CUSTOM: "custom";
13
13
  readonly SHIPROCKET: "shiprocket";
14
14
  };
15
- /** Runtime-accessible order status values — use instead of bare string literals. */
15
+ /** Runtime-accessible order status values â€" use instead of bare string literals. */
16
16
  export declare const OrderStatusValues: {
17
17
  readonly PENDING: "pending";
18
18
  readonly CONFIRMED: "confirmed";
@@ -24,7 +24,7 @@ export declare const OrderStatusValues: {
24
24
  readonly RETURN_REQUESTED: "return_requested";
25
25
  readonly RETURNED: "returned";
26
26
  };
27
- /** Runtime-accessible payment status values — use instead of bare string literals. */
27
+ /** Runtime-accessible payment status values â€" use instead of bare string literals. */
28
28
  export declare const PaymentStatusValues: {
29
29
  readonly PENDING: "pending";
30
30
  readonly PROCESSING: "processing";
@@ -33,34 +33,35 @@ export declare const PaymentStatusValues: {
33
33
  readonly REFUNDED: "refunded";
34
34
  readonly PARTIAL_REFUND: "partial_refund";
35
35
  };
36
- /** Runtime-accessible payment method values — use instead of bare string literals. */
36
+ /** Runtime-accessible payment method values â€" use instead of bare string literals. */
37
37
  export declare const PaymentMethodValues: {
38
38
  readonly COD: "cod";
39
39
  readonly ONLINE: "online";
40
40
  readonly UPI_MANUAL: "upi_manual";
41
41
  readonly RAZORPAY: "razorpay";
42
+ readonly ADMIN_BYPASS: "admin_bypass";
42
43
  };
43
- /** Runtime-accessible refund status values — use instead of bare string literals. */
44
+ /** Runtime-accessible refund status values â€" use instead of bare string literals. */
44
45
  export declare const RefundStatusValues: {
45
46
  readonly PENDING: "pending";
46
47
  readonly PROCESSING: "processing";
47
48
  readonly COMPLETED: "completed";
48
49
  readonly REJECTED: "rejected";
49
50
  };
50
- /** Firestore storage shape for an order line item — distinct from the API display model OrderItem */
51
+ /** Firestore storage shape for an order line item â€" distinct from the API display model OrderItem */
51
52
  export interface OrderDocumentItem {
52
53
  productId: string;
53
54
  productTitle: string;
54
55
  quantity: number;
55
56
  unitPrice: number;
56
57
  totalPrice: number;
57
- /** SB8-F — set when the item is a prize-draw entry; drives the reveals badge. */
58
+ /** SB8-F â€" set when the item is a prize-draw entry; drives the reveals badge. */
58
59
  listingType?: "standard" | "auction" | "pre-order" | "prize-draw" | "classified" | "digital-code" | "live";
59
- /** SB8-F — per-item reveal status; flips through pending → open → revealed/closed. */
60
+ /** SB8-F â€" per-item reveal status; flips through pending â†' open â†' revealed/closed. */
60
61
  prizeRevealStatus?: "pending" | "open" | "closed" | "revealed";
61
- /** SB8-F — ISO timestamp; deadline by which the buyer must claim the prize. */
62
+ /** SB8-F â€" ISO timestamp; deadline by which the buyer must claim the prize. */
62
63
  prizeRevealDeadline?: string;
63
- /** SB8-F — set after the reveal API picks a winner. */
64
+ /** SB8-F â€" set after the reveal API picks a winner. */
64
65
  revealedItemNumber?: number;
65
66
  }
66
67
  /**
@@ -163,13 +164,13 @@ export interface OrderDocument {
163
164
  };
164
165
  /** Deadline for the buyer to claim the won prize (typically 7 days). */
165
166
  prizeRevealDeadline?: Date;
166
- /** True once `prizeRevealDeadline` passes without a claim — auto-forfeit. */
167
+ /** True once `prizeRevealDeadline` passes without a claim â€" auto-forfeit. */
167
168
  prizeRevealExpired?: boolean;
168
169
  /** Source product id when the order came from a prize-draw entry. */
169
170
  prizeDrawProductId?: string;
170
171
  /** True for prize-draw entries and bundle purchases that bypass refund. */
171
172
  isNonRefundable?: boolean;
172
- /** Set when the order came from a bundle — points back to `bundles/{id}`. */
173
+ /** Set when the order came from a bundle â€" points back to `bundles/{id}`. */
173
174
  bundleId?: string;
174
175
  /**
175
176
  * Internal reference linking sibling orders created from the same checkout
@@ -184,11 +185,13 @@ export interface OrderDocument {
184
185
  refunds?: OrderRefundEvent[];
185
186
  /**
186
187
  * False once ANY refund (full or partial) has been posted. Once false it
187
- * stays false — no dispute, RMA request, or Item Not Received claim may
188
+ * stays false — no dispute, RMA request, or "Item Not Received" claim may
188
189
  * be filed. Both buyer and seller must acknowledge this in the refund UI
189
190
  * via `confirmIrrevocable: true` before the refund is posted.
190
191
  */
191
192
  contestable?: boolean;
193
+ /** UID of the admin who triggered an admin-bypass checkout. Set only when paymentMethod === "admin_bypass". */
194
+ adminBypassBy?: string;
192
195
  /** Signed-URL media slug for a shipping-proof document (label, AWB scan). */
193
196
  shippingProofUrl?: string;
194
197
  shippingProofMimeType?: string;