@mohasinac/appkit 4.25.0 → 4.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_internal/server/features/bundles/actions.d.ts +4 -1
- package/dist/_internal/server/features/bundles/actions.js +54 -5
- package/dist/_internal/server/features/bundles/data.d.ts +19 -0
- package/dist/_internal/server/features/bundles/data.js +32 -0
- package/dist/_internal/server/features/bundles/index.d.ts +1 -1
- package/dist/_internal/server/features/bundles/index.js +1 -1
- package/dist/_internal/server/features/checkout/actions.js +38 -68
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +35 -0
- package/dist/_internal/server/features/checkout/expand-order-items.js +85 -0
- package/dist/_internal/server/features/grouped/index.d.ts +2 -0
- package/dist/_internal/server/features/grouped/index.js +2 -0
- package/dist/_internal/server/features/grouped/metadata.d.ts +17 -0
- package/dist/_internal/server/features/grouped/metadata.js +48 -0
- package/dist/_internal/server/features/grouped/og.d.ts +16 -0
- package/dist/_internal/server/features/grouped/og.js +24 -0
- package/dist/_internal/server/features/seo/sitemap.js +24 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +63 -4
- package/dist/_internal/shared/checkout/order-math.js +119 -7
- package/dist/client.d.ts +7 -3
- package/dist/client.js +3 -2
- package/dist/constants/api-endpoints.d.ts +0 -6
- package/dist/constants/api-endpoints.js +0 -2
- package/dist/features/analytics/types.d.ts +1 -1
- package/dist/features/analytics/types.js +3 -0
- package/dist/features/cart/components/CartGroupLineRow.d.ts +42 -0
- package/dist/features/cart/components/CartGroupLineRow.js +33 -0
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.js +6 -0
- package/dist/features/cart/schemas/firestore.d.ts +7 -2
- package/dist/features/categories/components/BundleBuyNowCta.d.ts +18 -5
- package/dist/features/categories/components/BundleBuyNowCta.js +24 -10
- package/dist/features/categories/components/BundleDetailView.d.ts +8 -1
- package/dist/features/categories/components/BundleDetailView.js +2 -2
- package/dist/features/categories/components/BundlesListView.d.ts +1 -0
- package/dist/features/categories/components/CategoryBundlesListing.d.ts +3 -0
- package/dist/features/grouped/components/GroupedListingDetailView.d.ts +21 -0
- package/dist/features/grouped/components/GroupedListingDetailView.js +17 -0
- package/dist/features/grouped/components/GroupedListingsCarousel.js +6 -1
- package/dist/features/orders/index.d.ts +1 -1
- package/dist/features/orders/index.js +1 -1
- package/dist/features/orders/schemas/firestore.d.ts +11 -0
- package/dist/features/orders/utils/bundle-grouping.d.ts +29 -19
- package/dist/features/orders/utils/bundle-grouping.js +32 -22
- package/dist/features/tester/seed-data/categories-tester-seed-data.js +6 -0
- package/dist/features/tester/seed-data/products-tester-seed-data.js +150 -1
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +118 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/next/routing/route-map.d.ts +12 -0
- package/dist/next/routing/route-map.js +6 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +1 -1
- package/dist/server.d.ts +3 -1
- package/dist/server.js +5 -1
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/package.json +1 -1
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
/**
|
|
4
|
-
* BundleBuyNowCta —
|
|
4
|
+
* BundleBuyNowCta — purchase CTAs for bundle detail pages.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* A bundle is ALL-OR-NOTHING: its members are not pickable, and the only
|
|
7
|
+
* quantity the buyer controls is how many COPIES of the whole bundle they want.
|
|
8
|
+
* That is why there is one stepper here and no per-member controls anywhere on
|
|
9
|
+
* the bundle page — the mirror image of `GroupMemberPicker`, which has
|
|
10
|
+
* per-member steppers and no line-level one.
|
|
11
|
+
*
|
|
12
|
+
* Two CTAs, not one. "Buy now" adds and jumps straight to checkout (the
|
|
13
|
+
* pre-existing express path). "Add to cart" adds and stays — without it a
|
|
14
|
+
* bundle could never sit in the cart to be edited, because the only route in
|
|
15
|
+
* pushed the buyer past the cart immediately.
|
|
9
16
|
*/
|
|
10
17
|
import { useState, useCallback } from "react";
|
|
11
|
-
import { Button, LoginRequiredModal, Stack, Text } from "../../../ui";
|
|
18
|
+
import { Button, LoginRequiredModal, Row, Stack, Text } from "../../../ui";
|
|
19
|
+
import { QuantityStepper } from "../../../ui/components/QuantityStepper";
|
|
12
20
|
import { useToast } from "../../../ui";
|
|
13
21
|
import { isAuthError } from "../../../utils/auth-error";
|
|
22
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry";
|
|
14
23
|
import { BUNDLE_COPY } from "../../../_internal/shared/features/categories/bundle-copy";
|
|
15
24
|
import { normalizeError } from "../../../errors/normalize";
|
|
16
|
-
export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, compact = false, }) {
|
|
25
|
+
export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, onAddToCart, compact = false, }) {
|
|
17
26
|
const { showToast } = useToast();
|
|
18
27
|
const [submitting, setSubmitting] = useState(false);
|
|
28
|
+
const [quantity, setQuantity] = useState(1);
|
|
19
29
|
const [error, setError] = useState(null);
|
|
20
30
|
const [showLoginModal, setShowLoginModal] = useState(false);
|
|
21
|
-
const
|
|
31
|
+
const run = useCallback(async (fn, successMsg) => {
|
|
22
32
|
setError(null);
|
|
23
33
|
setSubmitting(true);
|
|
24
34
|
try {
|
|
25
|
-
await
|
|
35
|
+
await fn({ bundleSlug, quantity });
|
|
36
|
+
if (successMsg)
|
|
37
|
+
showToast(successMsg, "success");
|
|
26
38
|
}
|
|
27
39
|
catch (err) {
|
|
28
40
|
void normalizeError(err);
|
|
@@ -38,9 +50,11 @@ export function BundleBuyNowCta({ bundleSlug, outOfStock = false, onBuyNow, comp
|
|
|
38
50
|
finally {
|
|
39
51
|
setSubmitting(false);
|
|
40
52
|
}
|
|
41
|
-
}, [bundleSlug,
|
|
53
|
+
}, [bundleSlug, quantity, showToast]);
|
|
54
|
+
const handleClick = useCallback(() => run(onBuyNow), [run, onBuyNow]);
|
|
55
|
+
const handleAddToCart = useCallback(() => (onAddToCart ? run(onAddToCart, "Bundle added to your cart.") : Promise.resolve()), [run, onAddToCart]);
|
|
42
56
|
if (outOfStock) {
|
|
43
57
|
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 }))] }));
|
|
44
58
|
}
|
|
45
|
-
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 }))] })] }));
|
|
59
|
+
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: [!compact && (_jsxs(Row, { gap: "sm", align: "center", children: [_jsx(Text, { size: "sm", color: "muted", children: "Copies" }), _jsx(QuantityStepper, { value: quantity, onChange: setQuantity, min: 1, max: 99, disabled: submitting, size: "md", ariaLabel: "Number of copies of this bundle", decrementLabel: ACTIONS.CART["decrease-quantity"].ariaLabel, incrementLabel: ACTIONS.CART["increase-quantity"].ariaLabel })] })), _jsxs(Row, { gap: "sm", wrap: true, className: compact ? "w-full" : undefined, 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 }), onAddToCart && !compact && (_jsx(Button, { action: ACTIONS.CART["add-bundle-to-cart"], variant: "outline", size: "md", onClick: handleAddToCart, disabled: submitting, "aria-busy": submitting }))] }), error && (_jsx(Text, { size: "sm", color: "danger", role: "alert", children: error }))] })] }));
|
|
46
60
|
}
|
|
@@ -17,8 +17,15 @@ export interface BundleDetailViewProps {
|
|
|
17
17
|
/** Direct-checkout callback (S-SBUNI-RULES). When supplied, renders the "Buy now" CTA. */
|
|
18
18
|
onBuyNow?: (input: {
|
|
19
19
|
bundleSlug: string;
|
|
20
|
+
quantity: number;
|
|
21
|
+
}) => Promise<unknown>;
|
|
22
|
+
/** Adds the bundle without leaving the page — the only way it can reach the
|
|
23
|
+
* cart and stay there to be edited. */
|
|
24
|
+
onAddToCart?: (input: {
|
|
25
|
+
bundleSlug: string;
|
|
26
|
+
quantity: number;
|
|
20
27
|
}) => Promise<unknown>;
|
|
21
28
|
/** Other active bundles, excluding this one — renders a "Related Bundles" section when non-empty. */
|
|
22
29
|
relatedBundles?: CategoryDocument[];
|
|
23
30
|
}
|
|
24
|
-
export declare function BundleDetailView({ bundle, members, onBuyNow, relatedBundles, }: BundleDetailViewProps): React.JSX.Element;
|
|
31
|
+
export declare function BundleDetailView({ bundle, members, onBuyNow, onAddToCart, relatedBundles, }: BundleDetailViewProps): React.JSX.Element;
|
|
@@ -17,7 +17,7 @@ const STOCK_BADGE_TEXT = {
|
|
|
17
17
|
out_of_stock: BUNDLE_COPY.stockBadge.out_of_stock,
|
|
18
18
|
};
|
|
19
19
|
const PLACEHOLDER_EMOJI = "📦";
|
|
20
|
-
export function BundleDetailView({ bundle, members = [], onBuyNow, relatedBundles = [], }) {
|
|
20
|
+
export function BundleDetailView({ bundle, members = [], onBuyNow, onAddToCart, relatedBundles = [], }) {
|
|
21
21
|
const memberCount = members.length || bundle.bundleProductIds?.length || 0;
|
|
22
22
|
const stock = bundle.bundleStockStatus ?? "in_stock";
|
|
23
23
|
const cover = bundle.display?.coverImage;
|
|
@@ -25,5 +25,5 @@ export function BundleDetailView({ bundle, members = [], onBuyNow, relatedBundle
|
|
|
25
25
|
? formatCurrency(bundle.bundlePrice, "INR")
|
|
26
26
|
: BUNDLE_COPY.detail.priceFallback;
|
|
27
27
|
const discount = computeBundleDiscount(bundle.bundlePrice, bundle.bundleOriginalTotal);
|
|
28
|
-
return (_jsxs(Main, { children: [_jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Stack, { gap: "lg", direction: "md-row", align: "start", children: [_jsx(Div, { className: "w-full md:w-2/5", children: members.length > 0 ? (_jsx(BundleCollage, { members: members })) : (_jsx(Div, { className: `aspect-video w-full ${__O.hidden}`, rounded: "2xl", surface: "subtle", children: _jsx(Div, { className: "h-full w-full", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "hero", fallback: PLACEHOLDER_EMOJI }) }) })) }), _jsxs(Stack, { gap: "md", className: "flex-1", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "semibold", color: "primary", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsx(Text, { size: "lg", weight: "bold", children: priceLabel }), discount && (_jsxs(_Fragment, { children: [_jsx(Text, { size: "sm", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, "INR") }), _jsx(Badge, { variant: "success", children: BUNDLE_COPY.detail.discountBadge(discount.percent) })] })), _jsxs(Text, { size: "sm", color: "muted", children: ["\u00B7 ", BUNDLE_COPY.detail.itemCount(memberCount)] }), _jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stock], children: STOCK_BADGE_TEXT[stock] })] }), discount && (_jsx(Text, { size: "sm", color: "success", weight: "medium", children: BUNDLE_COPY.detail.savingsLabel(formatCurrency(discount.savings, "INR")) })), bundle.description && (_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text-muted)]", size: "sm", weight: "semibold", children: BUNDLE_COPY.detailDescriptionHeading }), _jsx(Text, { className: "whitespace-pre-line", children: bundle.description })] })), onBuyNow ? (_jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow })) : (_jsxs(Div, { textSize: "sm", className: "border-dashed", border: "strong", surface: "muted", rounded: "xl", padding: "md", "aria-live": "polite", children: [_jsx(Text, { weight: "semibold", className: "mb-1 block", children: BUNDLE_COPY.detail.ctaDisabled }), _jsx(Text, { size: "sm", color: "muted", children: BUNDLE_COPY.detail.ctaHint })] }))] })] }), _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", color: "primary", children: BUNDLE_COPY.detailItemsHeading }), members.length === 0 ? (_jsx(Text, { color: "muted", children: BUNDLE_COPY.detail.emptyMembers })) : (_jsx(Ul, { gap: "2", className: "grid grid-cols-1 sm:grid-cols-2", children: members.map((p, i) => (_jsxs(Li, { layout: "flex-center", gap: "2", rounded: "lg", border: "subtle", padding: "inlineSm", textSize: "sm", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: "w-5", color: "faint", children: ["#", i + 1] }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRODUCT_DETAIL?.(p.slug ?? p.id) ?? "#"), className: "flex-1 truncate font-medium hover:underline hover:text-primary", children: p.title }), _jsx(Span, { size: "xs", color: "muted", children: formatCurrency(p.price ?? 0, p.currency ?? "INR") })] }, p.id))) }))] })] }) }) }), relatedBundles.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Bundles" }), _jsx(CategoryGrid, { categories: relatedBundles, getHref: (c) => String(ROUTES.PUBLIC.BUNDLE_DETAIL(c.slug)) })] }) }))] }));
|
|
28
|
+
return (_jsxs(Main, { children: [_jsx(Section, { padding: "y-2xl", children: _jsx(Container, { size: "xl", children: _jsxs(Stack, { gap: "lg", children: [_jsxs(Stack, { gap: "lg", direction: "md-row", align: "start", children: [_jsx(Div, { className: "w-full md:w-2/5", children: members.length > 0 ? (_jsx(BundleCollage, { members: members })) : (_jsx(Div, { className: `aspect-video w-full ${__O.hidden}`, rounded: "2xl", surface: "subtle", children: _jsx(Div, { className: "h-full w-full", children: _jsx(MediaImage, { src: cover, alt: bundle.name, size: "hero", fallback: PLACEHOLDER_EMOJI }) }) })) }), _jsxs(Stack, { gap: "md", className: "flex-1", children: [_jsx(Heading, { level: 1, size: "3xl", weight: "semibold", color: "primary", children: bundle.name }), _jsxs(Row, { gap: "sm", align: "center", wrap: true, children: [_jsx(Text, { size: "lg", weight: "bold", children: priceLabel }), discount && (_jsxs(_Fragment, { children: [_jsx(Text, { size: "sm", color: "muted", className: "line-through", children: formatCurrency(discount.originalTotal, "INR") }), _jsx(Badge, { variant: "success", children: BUNDLE_COPY.detail.discountBadge(discount.percent) })] })), _jsxs(Text, { size: "sm", color: "muted", children: ["\u00B7 ", BUNDLE_COPY.detail.itemCount(memberCount)] }), _jsx(Badge, { variant: BUNDLE_STOCK_VARIANT[stock], children: STOCK_BADGE_TEXT[stock] })] }), discount && (_jsx(Text, { size: "sm", color: "success", weight: "medium", children: BUNDLE_COPY.detail.savingsLabel(formatCurrency(discount.savings, "INR")) })), bundle.description && (_jsxs(Stack, { gap: "xs", children: [_jsx(Heading, { level: 2, className: "text-[var(--appkit-color-text-muted)]", size: "sm", weight: "semibold", children: BUNDLE_COPY.detailDescriptionHeading }), _jsx(Text, { className: "whitespace-pre-line", children: bundle.description })] })), onBuyNow ? (_jsx(BundleBuyNowCta, { bundleSlug: bundle.slug, outOfStock: stock === "out_of_stock", onBuyNow: onBuyNow, onAddToCart: onAddToCart })) : (_jsxs(Div, { textSize: "sm", className: "border-dashed", border: "strong", surface: "muted", rounded: "xl", padding: "md", "aria-live": "polite", children: [_jsx(Text, { weight: "semibold", className: "mb-1 block", children: BUNDLE_COPY.detail.ctaDisabled }), _jsx(Text, { size: "sm", color: "muted", children: BUNDLE_COPY.detail.ctaHint })] }))] })] }), _jsxs(Stack, { gap: "md", children: [_jsx(Heading, { level: 2, size: "xl", weight: "semibold", color: "primary", children: BUNDLE_COPY.detailItemsHeading }), members.length === 0 ? (_jsx(Text, { color: "muted", children: BUNDLE_COPY.detail.emptyMembers })) : (_jsx(Ul, { gap: "2", className: "grid grid-cols-1 sm:grid-cols-2", children: members.map((p, i) => (_jsxs(Li, { layout: "flex-center", gap: "2", rounded: "lg", border: "subtle", padding: "inlineSm", textSize: "sm", children: [_jsxs(Span, { size: "xs", weight: "semibold", className: "w-5", color: "faint", children: ["#", i + 1] }), _jsx(Link, { href: String(ROUTES.PUBLIC.PRODUCT_DETAIL?.(p.slug ?? p.id) ?? "#"), className: "flex-1 truncate font-medium hover:underline hover:text-primary", children: p.title }), _jsx(Span, { size: "xs", color: "muted", children: formatCurrency(p.price ?? 0, p.currency ?? "INR") })] }, p.id))) }))] })] }) }) }), relatedBundles.length > 0 && (_jsx(Section, { border: "subtle", surface: "default", className: "border-t", padding: "y-lg", children: _jsxs(Container, { size: "xl", children: [_jsx(Heading, { level: 2, className: "mb-4", size: "xl", weight: "semibold", children: "Related Bundles" }), _jsx(CategoryGrid, { categories: relatedBundles, getHref: (c) => String(ROUTES.PUBLIC.BUNDLE_DETAIL(c.slug)) })] }) }))] }));
|
|
29
29
|
}
|
|
@@ -3,8 +3,11 @@ import type { CategoryDocument } from "../schemas";
|
|
|
3
3
|
export interface CategoryBundlesListingProps {
|
|
4
4
|
initialBundles: CategoryDocument[];
|
|
5
5
|
brandName?: string;
|
|
6
|
+
/** Cards render the CTA in `compact` mode, which has no stepper — quantity
|
|
7
|
+
* is always 1 from here. The full stepper lives on the bundle detail page. */
|
|
6
8
|
onBuyNow?: (input: {
|
|
7
9
|
bundleSlug: string;
|
|
10
|
+
quantity: number;
|
|
8
11
|
}) => Promise<unknown>;
|
|
9
12
|
}
|
|
10
13
|
export declare function CategoryBundlesListing({ initialBundles, brandName, onBuyNow, }: CategoryBundlesListingProps): React.JSX.Element;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GroupedListingDetailView — the public page for a `groupedListings` document.
|
|
3
|
+
*
|
|
4
|
+
* Before this, a grouped listing had no page at all: it existed only as a
|
|
5
|
+
* horizontal carousel on other pages, so there was nowhere to see the whole
|
|
6
|
+
* group and nothing to link to from a cart line built out of it.
|
|
7
|
+
*
|
|
8
|
+
* A grouped listing has NO price of its own — SB-UNI-V removed its pricing
|
|
9
|
+
* fields on purpose. That is not a gap to fill: under "pick as you wish" the
|
|
10
|
+
* price simply IS the sum of what the buyer selected, which is exactly what
|
|
11
|
+
* `GroupMemberPicker` computes and what `unitPriceFor` recomputes server-side.
|
|
12
|
+
*/
|
|
13
|
+
import React from "react";
|
|
14
|
+
import { type GroupPickerMember } from "../../products/components/GroupMemberPicker";
|
|
15
|
+
import type { GroupedListingDocument } from "../schemas/firestore";
|
|
16
|
+
export interface GroupedListingDetailViewProps {
|
|
17
|
+
group: GroupedListingDocument;
|
|
18
|
+
/** Members, already projected for public consumption. */
|
|
19
|
+
members: GroupPickerMember[];
|
|
20
|
+
}
|
|
21
|
+
export declare function GroupedListingDetailView({ group, members }: GroupedListingDetailViewProps): React.JSX.Element;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { Container, Div, Heading, Section, Span, Stack, Text, } from "../../../ui";
|
|
4
|
+
import { MediaImage } from "../../media/MediaImage";
|
|
5
|
+
import { GroupMemberPicker } from "../../products/components/GroupMemberPicker";
|
|
6
|
+
const __O = { hidden: "overflow-hidden" };
|
|
7
|
+
/** Human label per theme — mirrors GROUP_THEME_TITLE on the carousel. */
|
|
8
|
+
const THEME_LABEL = {
|
|
9
|
+
related: "Related items",
|
|
10
|
+
character: "Same character",
|
|
11
|
+
lineage: "Same lineage",
|
|
12
|
+
set: "Complete the set",
|
|
13
|
+
generic: "Grouped listing",
|
|
14
|
+
};
|
|
15
|
+
export function GroupedListingDetailView({ group, members }) {
|
|
16
|
+
return (_jsx(Section, { padding: "section", children: _jsx(Container, { children: _jsxs(Stack, { gap: "comfortable", children: [group.coverImage && (_jsx(Div, { className: `relative h-48 w-full md:h-64 ${__O.hidden}`, rounded: "xl", surface: "muted", children: _jsx(MediaImage, { src: group.coverImage, alt: group.title, size: "banner" }) })), _jsxs(Stack, { gap: "xs", children: [_jsx(Span, { size: "xs", color: "muted", transform: "uppercase", weight: "semibold", children: THEME_LABEL[group.groupTheme] }), _jsx(Heading, { level: 1, children: group.title }), group.description && (_jsx(Text, { color: "muted", children: group.description })), _jsxs(Text, { size: "sm", color: "muted", children: [members.length, " item", members.length === 1 ? "" : "s", " in this group"] })] }), members.length > 0 ? (_jsxs(Stack, { gap: "sm", children: [_jsx(Heading, { level: 2, size: "lg", children: "Pick what you want" }), _jsx(Text, { size: "sm", color: "muted", children: "Choose a quantity for each item \u2014 they'll go into your cart as a single line you can edit later." }), _jsx(GroupMemberPicker, { groupId: group.id, groupSource: "grouped-listing", members: members })] })) : (_jsx(Text, { color: "muted", children: "Nothing in this group is available right now." }))] }) }) }));
|
|
17
|
+
}
|
|
@@ -9,6 +9,7 @@ import { ProductCard } from "../../products/components/ProductGrid";
|
|
|
9
9
|
import { toProductItem } from "../../../_internal/shared/features/products/to-product-item";
|
|
10
10
|
import { CAROUSEL_PER_VIEW } from "../../homepage/constants/carousel-per-view";
|
|
11
11
|
import { Stack } from "../../../ui";
|
|
12
|
+
import { ROUTES } from "../../../next/routing/route-map";
|
|
12
13
|
const GROUP_THEME_TITLE = {
|
|
13
14
|
related: "Other listings you might like",
|
|
14
15
|
character: "Same character",
|
|
@@ -29,6 +30,10 @@ export function GroupedListingsCarousel({ groups }) {
|
|
|
29
30
|
return null;
|
|
30
31
|
return (_jsx(Stack, { gap: "xl", children: visibleGroups.map((group) => {
|
|
31
32
|
const items = group.items.map((p) => toProductItem(p));
|
|
32
|
-
return (_jsx(SectionCarousel, { title: group.title || GROUP_THEME_TITLE[group.groupTheme], pillLabel: "Grouped Listings", headingVariant: "editorial",
|
|
33
|
+
return (_jsx(SectionCarousel, { title: group.title || GROUP_THEME_TITLE[group.groupTheme], pillLabel: "Grouped Listings", headingVariant: "editorial",
|
|
34
|
+
// The group now has a page of its own, where the buyer can pick
|
|
35
|
+
// several members and add them as one cart line. Until it existed,
|
|
36
|
+
// this carousel was the only way a grouped listing was ever seen.
|
|
37
|
+
viewMoreHref: group.slug ? String(ROUTES.PUBLIC.GROUP_DETAIL(group.slug)) : undefined, viewMoreLabel: "Pick items from this group \u2192", items: items, isLoading: false, perView: CAROUSEL_PER_VIEW.standard, keyExtractor: (item) => item.id, renderItem: (item) => (_jsx(ProductCard, { product: item, href: item.slug || item.id ? pluginFor(item.listingType ?? "standard").detailRoute(item.slug || item.id) : undefined })) }, group.id));
|
|
33
38
|
}) }));
|
|
34
39
|
}
|
|
@@ -4,5 +4,5 @@ export * from "./components";
|
|
|
4
4
|
export * from "./schemas";
|
|
5
5
|
export * from "./columns";
|
|
6
6
|
export * from "./utils/order-splitter";
|
|
7
|
-
export {
|
|
7
|
+
export { groupOrderItemsByLine, type LineOrderGroup, type OrderItemForLineGrouping, } from "./utils/bundle-grouping";
|
|
8
8
|
export { manifest } from "./manifest";
|
|
@@ -5,5 +5,5 @@ export * from "./schemas";
|
|
|
5
5
|
export * from "./columns";
|
|
6
6
|
export * from "./utils/order-splitter";
|
|
7
7
|
// S-SBUNI-5 2026-05-13 — bundle-aware order item grouping.
|
|
8
|
-
export {
|
|
8
|
+
export { groupOrderItemsByLine, } from "./utils/bundle-grouping";
|
|
9
9
|
export { manifest } from "./manifest";
|
|
@@ -188,7 +188,18 @@ export interface OrderDocumentItem {
|
|
|
188
188
|
cancelledReason?: string;
|
|
189
189
|
/** SB-UNI-5 — forwarded from the cart line when this order item is an expanded bundle, so the order-detail UI can collapse it back under a "Bundle: <name>" header and stock-restore logic can fan back out to member products. */
|
|
190
190
|
bundleCategorySlug?: string;
|
|
191
|
+
/** @deprecated Legacy collapsed rows only — expanded rows carry one product each. */
|
|
191
192
|
bundleProductIds?: string[];
|
|
193
|
+
/**
|
|
194
|
+
* Collapse key for a multi-member cart line, shared by every row the line
|
|
195
|
+
* expanded into. `groupOrderItemsByLine` groups on it so the receipt still
|
|
196
|
+
* reads as one "Bundle: X" / "Group: X" block.
|
|
197
|
+
*/
|
|
198
|
+
groupSlug?: string;
|
|
199
|
+
/** Display name of the bundle / group these rows came from. */
|
|
200
|
+
groupTitle?: string;
|
|
201
|
+
/** Which kind of multi-member line produced these rows. */
|
|
202
|
+
groupSource?: "bundle" | "product-group" | "grouped-listing";
|
|
192
203
|
/** P-8 GST — snapshotted from the product at order time so the invoice stays accurate even if the product's HSN/rate later changes. */
|
|
193
204
|
hsnCode?: string;
|
|
194
205
|
gstRate?: 0 | 5 | 12 | 18 | 28;
|
|
@@ -7,42 +7,52 @@
|
|
|
7
7
|
* checkout receipt mirrors the buyer's intent. This helper is the pure
|
|
8
8
|
* grouping logic — the UI primitive lives next to the consumer view.
|
|
9
9
|
*
|
|
10
|
-
* Two flavours of
|
|
11
|
-
* 1. ONE
|
|
12
|
-
* describe the bundle
|
|
13
|
-
* bundle
|
|
14
|
-
*
|
|
15
|
-
* 2.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* Two flavours of multi-member row can appear on an order:
|
|
11
|
+
* 1. LEGACY — ONE row whose `bundleCategorySlug` + `bundleProductIds[]`
|
|
12
|
+
* describe the whole bundle, with the locked bundlePrice as its line
|
|
13
|
+
* total. Every bundle order placed before row expansion looks like this,
|
|
14
|
+
* and they keep rendering unchanged.
|
|
15
|
+
* 2. CURRENT — expanded N rows, one per member, all sharing a `groupSlug`
|
|
16
|
+
* (and, for bundles, the same `bundleCategorySlug`). This is what
|
|
17
|
+
* checkout writes now, so each row can carry its own HSN/GST and be
|
|
18
|
+
* cancelled independently.
|
|
18
19
|
*
|
|
19
|
-
* Either way
|
|
20
|
-
*
|
|
20
|
+
* Either way this returns a stable, ordered list of groups so the renderer
|
|
21
|
+
* iterates once and the buyer's receipt reads the same.
|
|
21
22
|
*/
|
|
22
23
|
/** Minimal shape consumed from an order item (Firestore wire format). */
|
|
23
|
-
export interface
|
|
24
|
-
/**
|
|
24
|
+
export interface OrderItemForLineGrouping {
|
|
25
|
+
/** Collapse key for expanded rows (bundle OR buyer-assembled group). */
|
|
26
|
+
groupSlug?: string;
|
|
27
|
+
/** Display name of the bundle / group. */
|
|
28
|
+
groupTitle?: string;
|
|
29
|
+
/** Bundle discriminator — also set on expanded bundle rows. */
|
|
25
30
|
bundleCategorySlug?: string;
|
|
26
|
-
/**
|
|
31
|
+
/** @deprecated Legacy collapsed rows only. */
|
|
27
32
|
bundleProductIds?: string[];
|
|
28
33
|
}
|
|
29
|
-
export type
|
|
34
|
+
export type LineOrderGroup<T extends OrderItemForLineGrouping> = {
|
|
30
35
|
kind: "single";
|
|
31
36
|
item: T;
|
|
32
37
|
index: number;
|
|
33
38
|
} | {
|
|
34
39
|
kind: "bundle";
|
|
40
|
+
/** The key these rows were collapsed on. */
|
|
35
41
|
bundleCategorySlug: string;
|
|
42
|
+
groupTitle?: string;
|
|
36
43
|
items: Array<{
|
|
37
44
|
item: T;
|
|
38
45
|
index: number;
|
|
39
46
|
}>;
|
|
40
|
-
/**
|
|
47
|
+
/** Members represented: the row count when expanded, the snapshot length when legacy. */
|
|
41
48
|
memberCount: number;
|
|
42
49
|
};
|
|
43
50
|
/**
|
|
44
|
-
* Walk the items array preserving original order; collapse
|
|
45
|
-
*
|
|
46
|
-
*
|
|
51
|
+
* Walk the items array preserving original order; collapse rows sharing a
|
|
52
|
+
* collapse key into one group. Single rows pass through unchanged.
|
|
53
|
+
*
|
|
54
|
+
* The key is `groupSlug ?? bundleCategorySlug` — the fallback is what keeps
|
|
55
|
+
* legacy collapsed bundle rows (written before `groupSlug` existed) grouping
|
|
56
|
+
* exactly as they always did, with no order migration.
|
|
47
57
|
*/
|
|
48
|
-
export declare function
|
|
58
|
+
export declare function groupOrderItemsByLine<T extends OrderItemForLineGrouping>(items: T[]): Array<LineOrderGroup<T>>;
|
|
@@ -7,47 +7,57 @@
|
|
|
7
7
|
* checkout receipt mirrors the buyer's intent. This helper is the pure
|
|
8
8
|
* grouping logic — the UI primitive lives next to the consumer view.
|
|
9
9
|
*
|
|
10
|
-
* Two flavours of
|
|
11
|
-
* 1. ONE
|
|
12
|
-
* describe the bundle
|
|
13
|
-
* bundle
|
|
14
|
-
*
|
|
15
|
-
* 2.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
10
|
+
* Two flavours of multi-member row can appear on an order:
|
|
11
|
+
* 1. LEGACY — ONE row whose `bundleCategorySlug` + `bundleProductIds[]`
|
|
12
|
+
* describe the whole bundle, with the locked bundlePrice as its line
|
|
13
|
+
* total. Every bundle order placed before row expansion looks like this,
|
|
14
|
+
* and they keep rendering unchanged.
|
|
15
|
+
* 2. CURRENT — expanded N rows, one per member, all sharing a `groupSlug`
|
|
16
|
+
* (and, for bundles, the same `bundleCategorySlug`). This is what
|
|
17
|
+
* checkout writes now, so each row can carry its own HSN/GST and be
|
|
18
|
+
* cancelled independently.
|
|
18
19
|
*
|
|
19
|
-
* Either way
|
|
20
|
-
*
|
|
20
|
+
* Either way this returns a stable, ordered list of groups so the renderer
|
|
21
|
+
* iterates once and the buyer's receipt reads the same.
|
|
21
22
|
*/
|
|
22
23
|
/**
|
|
23
|
-
* Walk the items array preserving original order; collapse
|
|
24
|
-
*
|
|
25
|
-
*
|
|
24
|
+
* Walk the items array preserving original order; collapse rows sharing a
|
|
25
|
+
* collapse key into one group. Single rows pass through unchanged.
|
|
26
|
+
*
|
|
27
|
+
* The key is `groupSlug ?? bundleCategorySlug` — the fallback is what keeps
|
|
28
|
+
* legacy collapsed bundle rows (written before `groupSlug` existed) grouping
|
|
29
|
+
* exactly as they always did, with no order migration.
|
|
26
30
|
*/
|
|
27
|
-
export function
|
|
31
|
+
export function groupOrderItemsByLine(items) {
|
|
28
32
|
const groups = [];
|
|
29
|
-
const
|
|
33
|
+
const indexByKey = new Map();
|
|
30
34
|
items.forEach((item, index) => {
|
|
31
|
-
const
|
|
32
|
-
if (!
|
|
35
|
+
const key = item.groupSlug ?? item.bundleCategorySlug;
|
|
36
|
+
if (!key) {
|
|
33
37
|
groups.push({ kind: "single", item, index });
|
|
34
38
|
return;
|
|
35
39
|
}
|
|
36
|
-
const existingIdx =
|
|
40
|
+
const existingIdx = indexByKey.get(key);
|
|
37
41
|
if (existingIdx === undefined) {
|
|
38
|
-
const memberCount = item.bundleProductIds?.length ?? 1;
|
|
39
42
|
groups.push({
|
|
40
43
|
kind: "bundle",
|
|
41
|
-
bundleCategorySlug:
|
|
44
|
+
bundleCategorySlug: key,
|
|
45
|
+
groupTitle: item.groupTitle,
|
|
46
|
+
// Legacy rows carry the member list; expanded rows ARE the members, so
|
|
47
|
+
// the count is grown as siblings arrive below.
|
|
48
|
+
memberCount: item.bundleProductIds?.length ?? 1,
|
|
42
49
|
items: [{ item, index }],
|
|
43
|
-
memberCount,
|
|
44
50
|
});
|
|
45
|
-
|
|
51
|
+
indexByKey.set(key, groups.length - 1);
|
|
46
52
|
return;
|
|
47
53
|
}
|
|
48
54
|
const existing = groups[existingIdx];
|
|
49
55
|
if (existing.kind === "bundle") {
|
|
50
56
|
existing.items.push({ item, index });
|
|
57
|
+
// Expanded rows: one row per member, so the row count IS the member count.
|
|
58
|
+
if (!item.bundleProductIds?.length) {
|
|
59
|
+
existing.memberCount = existing.items.length;
|
|
60
|
+
}
|
|
51
61
|
}
|
|
52
62
|
});
|
|
53
63
|
return groups;
|
|
@@ -188,6 +188,12 @@ export const categoriesTesterSeedData = [
|
|
|
188
188
|
bundleProductIds: ["product-tester-standard-1", "product-tester-standard-2"],
|
|
189
189
|
bundleOriginalTotal: 348, // 199 + 149
|
|
190
190
|
bundleStockStatus: "in_stock",
|
|
191
|
+
// Without these, `addBundleToCartAction` wrote `storeId: ""` on the cart
|
|
192
|
+
// line, which produced an order group keyed `standard:` with an undefined
|
|
193
|
+
// storeId — no seller notification, no shipping resolution, no payout. The
|
|
194
|
+
// bug was seeded in, so it reproduced on every tester run.
|
|
195
|
+
createdByStoreId: "store-tester-qa-seller",
|
|
196
|
+
createdByStoreName: "Tester Sandbox Store",
|
|
191
197
|
rootId: "bundle-tester-sandbox",
|
|
192
198
|
parentIds: [],
|
|
193
199
|
childrenIds: [],
|
|
@@ -232,6 +232,9 @@ export const productsTesterSeedData = [
|
|
|
232
232
|
customSections: [],
|
|
233
233
|
isPromoted: false,
|
|
234
234
|
isOnSale: false,
|
|
235
|
+
// 18% — contrasts with member #3's 12% so one grouped line spans two rates.
|
|
236
|
+
gstRate: 18,
|
|
237
|
+
hsnCode: "95030090",
|
|
235
238
|
// Child of the "group-tester-sandbox-bundle" product-group ("Set") parent
|
|
236
239
|
// below — findByGroupId() queries products by this field directly, so
|
|
237
240
|
// without it the parent's groupChildSlugs[] mirror is never actually
|
|
@@ -265,12 +268,151 @@ export const productsTesterSeedData = [
|
|
|
265
268
|
customSections: [],
|
|
266
269
|
isPromoted: false,
|
|
267
270
|
isOnSale: false,
|
|
271
|
+
gstRate: 18,
|
|
272
|
+
hsnCode: "95030090",
|
|
268
273
|
groupId: "group-tester-sandbox-bundle",
|
|
269
274
|
groupParentSlug: "group-tester-sandbox-bundle",
|
|
270
275
|
groupTitle: TESTER_GROUP_TITLE,
|
|
271
276
|
createdAt: new Date(),
|
|
272
277
|
updatedAt: new Date(),
|
|
273
278
|
}),
|
|
279
|
+
withTokens({
|
|
280
|
+
id: "product-tester-standard-3",
|
|
281
|
+
slug: "product-tester-standard-3",
|
|
282
|
+
title: "Test Sticker Pack — Standard Listing #3",
|
|
283
|
+
description: "Third member of the test product group. Carries a DIFFERENT gstRate from its siblings on purpose: it is the only fixture that exercises per-member GST proration on a grouped/bundled cart line, where taxing the whole line at one product's rate would be wrong in both directions. Auto-expires in 7 days.",
|
|
284
|
+
categorySlugs: COLLECTIBLES_CATEGORY_SLUGS,
|
|
285
|
+
categoryNames: COLLECTIBLES_CATEGORY_NAMES,
|
|
286
|
+
brandSlug: "brand-tester-sandbox",
|
|
287
|
+
brand: "TestBrand",
|
|
288
|
+
price: 99,
|
|
289
|
+
currency: "INR",
|
|
290
|
+
stockQuantity: 10,
|
|
291
|
+
availableQuantity: 10,
|
|
292
|
+
isSold: false,
|
|
293
|
+
mainImage: seedExtMedia("https://picsum.photos/seed/product-image-tester-standard-3-20260101/900/900"),
|
|
294
|
+
images: [seedExtMedia("https://picsum.photos/seed/product-image-tester-standard-3-20260101/900/900")],
|
|
295
|
+
status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
|
|
296
|
+
condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
|
|
297
|
+
listingType: "standard",
|
|
298
|
+
customFields: [],
|
|
299
|
+
customSections: [],
|
|
300
|
+
isPromoted: false,
|
|
301
|
+
isOnSale: false,
|
|
302
|
+
// 12% against the siblings' 18% — the whole point of this fixture.
|
|
303
|
+
gstRate: 12,
|
|
304
|
+
hsnCode: "49081000",
|
|
305
|
+
groupId: "group-tester-sandbox-bundle",
|
|
306
|
+
groupParentSlug: "group-tester-sandbox-bundle",
|
|
307
|
+
groupTitle: TESTER_GROUP_TITLE,
|
|
308
|
+
createdAt: new Date(),
|
|
309
|
+
updatedAt: new Date(),
|
|
310
|
+
}),
|
|
311
|
+
// ── Grouped-cart-line fixtures ───────────────────────────────────────────
|
|
312
|
+
// Each of the three below exists to make ONE picker branch testable by hand.
|
|
313
|
+
// Without them the branch can only be reached by editing data live, which a
|
|
314
|
+
// reseed silently wipes out.
|
|
315
|
+
withTokens({
|
|
316
|
+
id: "product-tester-group-soldout",
|
|
317
|
+
slug: "product-tester-group-soldout",
|
|
318
|
+
title: "Test Part — Sold Out (Group Member)",
|
|
319
|
+
description: "A member of the tester product group that is deliberately out of stock. Exists so the group picker's blocked-member branch has real data: it must render a reason chip instead of a +/- stepper, and must not be selectable. Auto-expires in 7 days.",
|
|
320
|
+
categorySlugs: COLLECTIBLES_CATEGORY_SLUGS,
|
|
321
|
+
categoryNames: COLLECTIBLES_CATEGORY_NAMES,
|
|
322
|
+
brandSlug: "brand-tester-sandbox",
|
|
323
|
+
brand: "TestBrand",
|
|
324
|
+
price: 129,
|
|
325
|
+
currency: "INR",
|
|
326
|
+
stockQuantity: 0,
|
|
327
|
+
availableQuantity: 0,
|
|
328
|
+
isSold: true,
|
|
329
|
+
mainImage: seedExtMedia("https://picsum.photos/seed/product-image-tester-group-soldout-20260101/900/900"),
|
|
330
|
+
images: [seedExtMedia("https://picsum.photos/seed/product-image-tester-group-soldout-20260101/900/900")],
|
|
331
|
+
status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
|
|
332
|
+
condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
|
|
333
|
+
listingType: "standard",
|
|
334
|
+
customFields: [],
|
|
335
|
+
customSections: [],
|
|
336
|
+
isPromoted: false,
|
|
337
|
+
isOnSale: false,
|
|
338
|
+
gstRate: 18,
|
|
339
|
+
hsnCode: "95030090",
|
|
340
|
+
groupId: "group-tester-sandbox-bundle",
|
|
341
|
+
groupParentSlug: "group-tester-sandbox-bundle",
|
|
342
|
+
groupTitle: TESTER_GROUP_TITLE,
|
|
343
|
+
createdAt: new Date(),
|
|
344
|
+
updatedAt: new Date(),
|
|
345
|
+
}),
|
|
346
|
+
// A group whose members span TWO stores. `storeId` is the order-splitting key
|
|
347
|
+
// and the key per-store add-ons/coupons/shipping/payout hang off, so such a
|
|
348
|
+
// group can never become one cart line — the picker must stand down to a
|
|
349
|
+
// read-only table rather than offer a button that always fails.
|
|
350
|
+
// `withTokens` spreads `...p` last, so the storeId below overrides the
|
|
351
|
+
// sandbox default on purpose.
|
|
352
|
+
withTokens({
|
|
353
|
+
id: "product-tester-crossstore-a",
|
|
354
|
+
slug: "product-tester-crossstore-a",
|
|
355
|
+
title: "Test Cross-Store Set — Seller A",
|
|
356
|
+
description: "Parent of a deliberately CROSS-STORE tester group. Its sibling belongs to a different seller, so the group picker must render read-only with no quantity column and no \"Add selected\" button. Auto-expires in 7 days.",
|
|
357
|
+
categorySlugs: COLLECTIBLES_CATEGORY_SLUGS,
|
|
358
|
+
categoryNames: COLLECTIBLES_CATEGORY_NAMES,
|
|
359
|
+
brandSlug: "brand-tester-sandbox",
|
|
360
|
+
brand: "TestBrand",
|
|
361
|
+
price: 259,
|
|
362
|
+
currency: "INR",
|
|
363
|
+
stockQuantity: 6,
|
|
364
|
+
availableQuantity: 6,
|
|
365
|
+
isSold: false,
|
|
366
|
+
mainImage: seedExtMedia("https://picsum.photos/seed/product-image-tester-crossstore-a-20260101/900/900"),
|
|
367
|
+
images: [seedExtMedia("https://picsum.photos/seed/product-image-tester-crossstore-a-20260101/900/900")],
|
|
368
|
+
status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
|
|
369
|
+
condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
|
|
370
|
+
listingType: "standard",
|
|
371
|
+
customFields: [],
|
|
372
|
+
customSections: [],
|
|
373
|
+
isPromoted: false,
|
|
374
|
+
isOnSale: false,
|
|
375
|
+
gstRate: 18,
|
|
376
|
+
isGroupParent: true,
|
|
377
|
+
groupId: "group-tester-cross-store",
|
|
378
|
+
groupTitle: "Tester Cross-Store Set",
|
|
379
|
+
groupChildSlugs: ["product-tester-crossstore-b"],
|
|
380
|
+
createdAt: new Date(),
|
|
381
|
+
updatedAt: new Date(),
|
|
382
|
+
}),
|
|
383
|
+
withTokens({
|
|
384
|
+
id: "product-tester-crossstore-b",
|
|
385
|
+
slug: "product-tester-crossstore-b",
|
|
386
|
+
title: "Test Cross-Store Set — Seller B",
|
|
387
|
+
description: "Sibling of product-tester-crossstore-a, deliberately owned by a DIFFERENT store so the group spans two sellers. Still isTestData, so filterTestDataForViewer keeps it off that store's public listings for non-testers. Auto-expires in 7 days.",
|
|
388
|
+
categorySlugs: COLLECTIBLES_CATEGORY_SLUGS,
|
|
389
|
+
categoryNames: COLLECTIBLES_CATEGORY_NAMES,
|
|
390
|
+
brandSlug: "brand-tester-sandbox",
|
|
391
|
+
brand: "TestBrand",
|
|
392
|
+
price: 189,
|
|
393
|
+
currency: "INR",
|
|
394
|
+
stockQuantity: 6,
|
|
395
|
+
availableQuantity: 6,
|
|
396
|
+
isSold: false,
|
|
397
|
+
mainImage: seedExtMedia("https://picsum.photos/seed/product-image-tester-crossstore-b-20260101/900/900"),
|
|
398
|
+
images: [seedExtMedia("https://picsum.photos/seed/product-image-tester-crossstore-b-20260101/900/900")],
|
|
399
|
+
status: PRODUCT_FIELDS.STATUS_VALUES.PUBLISHED,
|
|
400
|
+
condition: PRODUCT_FIELDS.CONDITION_VALUES.NEW,
|
|
401
|
+
listingType: "standard",
|
|
402
|
+
customFields: [],
|
|
403
|
+
customSections: [],
|
|
404
|
+
isPromoted: false,
|
|
405
|
+
isOnSale: false,
|
|
406
|
+
gstRate: 12,
|
|
407
|
+
// The whole point of this fixture — a different seller from its sibling.
|
|
408
|
+
storeId: "store-beyblade-arena",
|
|
409
|
+
storeName: "Beyblade Arena",
|
|
410
|
+
groupId: "group-tester-cross-store",
|
|
411
|
+
groupParentSlug: "product-tester-crossstore-a",
|
|
412
|
+
groupTitle: "Tester Cross-Store Set",
|
|
413
|
+
createdAt: new Date(),
|
|
414
|
+
updatedAt: new Date(),
|
|
415
|
+
}),
|
|
274
416
|
withTokens({
|
|
275
417
|
id: "product-tester-standard-sold",
|
|
276
418
|
slug: "product-tester-standard-sold",
|
|
@@ -644,7 +786,14 @@ export const productsTesterSeedData = [
|
|
|
644
786
|
isGroupParent: true,
|
|
645
787
|
groupId: "group-tester-sandbox-bundle",
|
|
646
788
|
groupTitle: TESTER_GROUP_TITLE,
|
|
647
|
-
|
|
789
|
+
// Parent↔child mirror: every id here must also carry `groupId` on its own
|
|
790
|
+
// document. The pair drifting is Root Cause #42's second instance.
|
|
791
|
+
groupChildSlugs: [
|
|
792
|
+
"product-tester-standard-1",
|
|
793
|
+
"product-tester-standard-2",
|
|
794
|
+
"product-tester-standard-3",
|
|
795
|
+
"product-tester-group-soldout",
|
|
796
|
+
],
|
|
648
797
|
createdAt: new Date(),
|
|
649
798
|
updatedAt: new Date(),
|
|
650
799
|
}),
|