@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.
- package/dist/_internal/server/features/bundles/actions.d.ts +15 -0
- package/dist/_internal/server/features/bundles/actions.js +48 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -0
- package/dist/_internal/server/features/bundles/index.js +1 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +4 -0
- package/dist/_internal/server/features/checkout/actions.js +23 -12
- package/dist/_internal/server/features/payments/data.d.ts +5 -3
- package/dist/_internal/server/features/payments/data.js +13 -8
- package/dist/_internal/server/features/payments/index.d.ts +1 -1
- package/dist/_internal/server/features/payments/index.js +1 -1
- package/dist/_internal/shared/features/categories/bundle-config.d.ts +24 -0
- package/dist/_internal/shared/features/categories/bundle-config.js +28 -0
- package/dist/_internal/shared/features/categories/bundle-copy.d.ts +7 -0
- package/dist/_internal/shared/features/categories/bundle-copy.js +9 -0
- package/dist/_internal/shared/features/categories/bundle-schemas.d.ts +69 -19
- package/dist/_internal/shared/features/categories/bundle-schemas.js +20 -2
- package/dist/_internal/shared/features/checkout/config.d.ts +1 -1
- package/dist/_internal/shared/features/checkout/config.js +1 -1
- package/dist/_internal/shared/features/payments/config.d.ts +2 -1
- package/dist/_internal/shared/features/payments/config.js +2 -1
- package/dist/_internal/shared/features/products/schema.d.ts +8 -8
- package/dist/constants/field-names.d.ts +8 -1
- package/dist/constants/field-names.js +9 -2
- package/dist/constants/table-keys.d.ts +3 -0
- package/dist/constants/table-keys.js +3 -0
- package/dist/features/admin/components/AdminSiteSettingsView.js +17 -15
- package/dist/features/admin/components/DashboardStats.js +20 -12
- package/dist/features/admin/components/analytics/AdminAnalyticsCharts.js +18 -2
- package/dist/features/admin/components/analytics/AdminStatCard.d.ts +7 -1
- package/dist/features/admin/components/analytics/AdminStatCard.js +13 -2
- package/dist/features/admin/schemas/firestore.d.ts +18 -4
- package/dist/features/admin/schemas/firestore.js +11 -1
- package/dist/features/categories/components/BundleBuyNowCta.d.ts +3 -1
- package/dist/features/categories/components/BundleBuyNowCta.js +3 -3
- package/dist/features/categories/components/BundleDynamicRuleEditor.d.ts +1 -1
- package/dist/features/categories/components/BundleDynamicRuleEditor.js +2 -1
- package/dist/features/categories/components/BundlesListView.d.ts +4 -1
- package/dist/features/categories/components/BundlesListView.js +2 -2
- package/dist/features/categories/components/CategoryBundlesListing.d.ts +4 -1
- package/dist/features/categories/components/CategoryBundlesListing.js +9 -8
- package/dist/features/categories/schemas/firestore.d.ts +29 -1
- package/dist/features/checkout/actions/checkout-actions.d.ts +11 -0
- package/dist/features/checkout/actions/checkout-actions.js +26 -0
- package/dist/features/filters/FilterFacetSection.d.ts +6 -1
- package/dist/features/filters/FilterFacetSection.js +25 -2
- package/dist/features/homepage/components/FeaturedBundlesSection.d.ts +4 -1
- package/dist/features/homepage/components/FeaturedBundlesSection.js +9 -8
- package/dist/features/orders/actions/refund-actions.js +10 -7
- package/dist/features/orders/schemas/firestore.d.ts +16 -13
- package/dist/features/orders/schemas/firestore.js +6 -5
- package/dist/features/pre-orders/components/MarketplacePreorderCard.js +1 -1
- package/dist/features/pre-orders/components/PreOrdersIndexListing.js +2 -1
- package/dist/features/products/components/AuctionsIndexListing.js +2 -1
- package/dist/features/products/components/ProductDetailPageView.js +3 -1
- package/dist/features/products/components/ProductDetailView.d.ts +3 -1
- package/dist/features/products/components/ProductDetailView.js +2 -2
- package/dist/features/products/components/ProductFilters.d.ts +10 -4
- package/dist/features/products/components/ProductFilters.js +20 -16
- package/dist/features/products/components/ProductGrid.js +2 -2
- package/dist/features/products/components/ProductsIndexListing.js +2 -1
- package/dist/features/products/constants/product-features.constants.js +5 -1
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/products/schemas/product-features.d.ts +1 -1
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/products/schemas/product-features.validators.js +4 -0
- package/dist/features/seller/components/SellerCouponEditorView.js +2 -2
- package/dist/features/seller/components/analytics/SellerAnalyticsStats.js +10 -4
- package/dist/features/stores/components/StoreBundlesPageView.d.ts +4 -1
- package/dist/features/stores/components/StoreBundlesPageView.js +2 -2
- package/dist/features/stores/components/StoresIndexListing.js +2 -1
- package/dist/providers/db-firebase/admin-app-lite.js +8 -2
- package/dist/providers/db-firebase/admin.js +8 -2
- package/dist/seed/addresses-seed-data.js +24 -7
- package/dist/seed/cart-seed-data.d.ts +10 -10
- package/dist/seed/cart-seed-data.js +39 -2
- package/dist/seed/grouped-listings-seed-data.js +47 -0
- package/dist/seed/orders-seed-data.js +150 -0
- package/dist/seed/products-standard-seed-data.js +42 -0
- package/dist/seed/site-settings-seed-data.js +11 -4
- package/dist/seed/users-seed-data.js +1 -1
- package/dist/seed/wishlists-seed-data.js +7 -0
- package/dist/server-entry.d.ts +2 -1
- package/dist/server-entry.js +2 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +3 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/SideDrawer.style.css +49 -11
- package/package.json +1 -1
|
@@ -232,15 +232,27 @@ export interface SiteSettingsDocument {
|
|
|
232
232
|
codEnabled: boolean;
|
|
233
233
|
};
|
|
234
234
|
commissions: {
|
|
235
|
-
|
|
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
|
-
|
|
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
|
|
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(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
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(
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
|
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
|
|
25
|
-
|
|
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 *
|
|
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
|
|
77
|
-
|
|
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 *
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
60
|
+
/** SB8-F â€" per-item reveal status; flips through pending â†' open â†' revealed/closed. */
|
|
60
61
|
prizeRevealStatus?: "pending" | "open" | "closed" | "revealed";
|
|
61
|
-
/** SB8-F
|
|
62
|
+
/** SB8-F â€" ISO timestamp; deadline by which the buyer must claim the prize. */
|
|
62
63
|
prizeRevealDeadline?: string;
|
|
63
|
-
/** SB8-F
|
|
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
|
|
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
|
|
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
|
|
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;
|