@mohasinac/appkit 4.25.0 โ†’ 4.26.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.
Files changed (54) hide show
  1. package/dist/_internal/server/features/bundles/actions.d.ts +4 -1
  2. package/dist/_internal/server/features/bundles/actions.js +54 -5
  3. package/dist/_internal/server/features/checkout/actions.js +38 -68
  4. package/dist/_internal/server/features/checkout/expand-order-items.d.ts +35 -0
  5. package/dist/_internal/server/features/checkout/expand-order-items.js +85 -0
  6. package/dist/_internal/server/features/grouped/index.d.ts +2 -0
  7. package/dist/_internal/server/features/grouped/index.js +2 -0
  8. package/dist/_internal/server/features/grouped/metadata.d.ts +17 -0
  9. package/dist/_internal/server/features/grouped/metadata.js +48 -0
  10. package/dist/_internal/server/features/grouped/og.d.ts +16 -0
  11. package/dist/_internal/server/features/grouped/og.js +24 -0
  12. package/dist/_internal/server/features/seo/sitemap.js +24 -1
  13. package/dist/_internal/shared/checkout/order-math.d.ts +63 -4
  14. package/dist/_internal/shared/checkout/order-math.js +119 -7
  15. package/dist/client.d.ts +7 -3
  16. package/dist/client.js +3 -2
  17. package/dist/constants/api-endpoints.d.ts +0 -6
  18. package/dist/constants/api-endpoints.js +0 -2
  19. package/dist/features/analytics/types.d.ts +1 -1
  20. package/dist/features/analytics/types.js +3 -0
  21. package/dist/features/cart/components/CartGroupLineRow.d.ts +42 -0
  22. package/dist/features/cart/components/CartGroupLineRow.js +33 -0
  23. package/dist/features/cart/components/index.d.ts +2 -0
  24. package/dist/features/cart/components/index.js +1 -0
  25. package/dist/features/cart/repository/cart.repository.js +6 -0
  26. package/dist/features/cart/schemas/firestore.d.ts +7 -2
  27. package/dist/features/categories/components/BundleBuyNowCta.d.ts +18 -5
  28. package/dist/features/categories/components/BundleBuyNowCta.js +24 -10
  29. package/dist/features/categories/components/BundleDetailView.d.ts +8 -1
  30. package/dist/features/categories/components/BundleDetailView.js +2 -2
  31. package/dist/features/categories/components/BundlesListView.d.ts +1 -0
  32. package/dist/features/categories/components/CategoryBundlesListing.d.ts +3 -0
  33. package/dist/features/grouped/components/GroupedListingDetailView.d.ts +21 -0
  34. package/dist/features/grouped/components/GroupedListingDetailView.js +17 -0
  35. package/dist/features/grouped/components/GroupedListingsCarousel.js +6 -1
  36. package/dist/features/orders/index.d.ts +1 -1
  37. package/dist/features/orders/index.js +1 -1
  38. package/dist/features/orders/schemas/firestore.d.ts +11 -0
  39. package/dist/features/orders/utils/bundle-grouping.d.ts +29 -19
  40. package/dist/features/orders/utils/bundle-grouping.js +32 -22
  41. package/dist/features/tester/seed-data/categories-tester-seed-data.js +6 -0
  42. package/dist/features/tester/seed-data/products-tester-seed-data.js +44 -1
  43. package/dist/features/tester/seed-data/tester-checklist-seed-data.js +33 -0
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.js +1 -1
  46. package/dist/next/routing/route-map.d.ts +12 -0
  47. package/dist/next/routing/route-map.js +6 -0
  48. package/dist/server-entry.d.ts +1 -1
  49. package/dist/server-entry.js +1 -1
  50. package/dist/server.d.ts +2 -0
  51. package/dist/server.js +4 -0
  52. package/dist/styles.css +1 -1
  53. package/dist/tailwind-utilities.css +1 -1
  54. package/package.json +1 -1
@@ -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
  }
@@ -4,6 +4,7 @@ export interface BundlesListViewProps {
4
4
  searchParams?: SearchParams;
5
5
  onBuyNow?: (input: {
6
6
  bundleSlug: string;
7
+ quantity: number;
7
8
  }) => Promise<unknown>;
8
9
  }
9
10
  /**
@@ -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", 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
+ 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 { groupOrderItemsByBundle, type BundleOrderGroup, type OrderItemForBundleGrouping, } from "./utils/bundle-grouping";
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 { groupOrderItemsByBundle, } from "./utils/bundle-grouping";
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 "bundle row" can appear on an order:
11
- * 1. ONE order row whose `bundleCategorySlug` + `bundleProductIds[]` set
12
- * describe the bundle (this is what S-SBUNI-4's checkout writes โ€” the
13
- * bundle stays a single row in the order with the locked
14
- * bundlePrice as its line total).
15
- * 2. (Future) Expanded N rows, each tagged with the same
16
- * `bundleCategorySlug`. The grouping handles this too in case a future
17
- * session pivots to row expansion.
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, `groupOrderItemsByBundle` returns a stable, ordered list of
20
- * "groups" so the renderer can iterate once.
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 OrderItemForBundleGrouping {
24
- /** Bundle discriminator โ€” set when this row is part of a bundle. */
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
- /** Snapshot of bundle members at order-creation time. */
31
+ /** @deprecated Legacy collapsed rows only. */
27
32
  bundleProductIds?: string[];
28
33
  }
29
- export type BundleOrderGroup<T extends OrderItemForBundleGrouping> = {
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
- /** Pulled from the first row in the group; reflects what checkout snapshotted. */
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 contiguous (or
45
- * non-contiguous) bundle rows sharing the same `bundleCategorySlug` into a
46
- * single group. Single rows pass through unchanged.
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 groupOrderItemsByBundle<T extends OrderItemForBundleGrouping>(items: T[]): Array<BundleOrderGroup<T>>;
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 "bundle row" can appear on an order:
11
- * 1. ONE order row whose `bundleCategorySlug` + `bundleProductIds[]` set
12
- * describe the bundle (this is what S-SBUNI-4's checkout writes โ€” the
13
- * bundle stays a single row in the order with the locked
14
- * bundlePrice as its line total).
15
- * 2. (Future) Expanded N rows, each tagged with the same
16
- * `bundleCategorySlug`. The grouping handles this too in case a future
17
- * session pivots to row expansion.
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, `groupOrderItemsByBundle` returns a stable, ordered list of
20
- * "groups" so the renderer can iterate once.
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 contiguous (or
24
- * non-contiguous) bundle rows sharing the same `bundleCategorySlug` into a
25
- * single group. Single rows pass through unchanged.
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 groupOrderItemsByBundle(items) {
31
+ export function groupOrderItemsByLine(items) {
28
32
  const groups = [];
29
- const bundleIndexBySlug = new Map();
33
+ const indexByKey = new Map();
30
34
  items.forEach((item, index) => {
31
- const slug = item.bundleCategorySlug;
32
- if (!slug) {
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 = bundleIndexBySlug.get(slug);
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: slug,
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
- bundleIndexBySlug.set(slug, groups.length - 1);
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,6 +268,40 @@ export const productsTesterSeedData = [
265
268
  customSections: [],
266
269
  isPromoted: false,
267
270
  isOnSale: false,
271
+ gstRate: 18,
272
+ hsnCode: "95030090",
273
+ groupId: "group-tester-sandbox-bundle",
274
+ groupParentSlug: "group-tester-sandbox-bundle",
275
+ groupTitle: TESTER_GROUP_TITLE,
276
+ createdAt: new Date(),
277
+ updatedAt: new Date(),
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",
268
305
  groupId: "group-tester-sandbox-bundle",
269
306
  groupParentSlug: "group-tester-sandbox-bundle",
270
307
  groupTitle: TESTER_GROUP_TITLE,
@@ -644,7 +681,13 @@ export const productsTesterSeedData = [
644
681
  isGroupParent: true,
645
682
  groupId: "group-tester-sandbox-bundle",
646
683
  groupTitle: TESTER_GROUP_TITLE,
647
- groupChildSlugs: ["product-tester-standard-1", "product-tester-standard-2"],
684
+ // Parentโ†”child mirror: every id here must also carry `groupId` on its own
685
+ // document. The pair drifting is Root Cause #42's second instance.
686
+ groupChildSlugs: [
687
+ "product-tester-standard-1",
688
+ "product-tester-standard-2",
689
+ "product-tester-standard-3",
690
+ ],
648
691
  createdAt: new Date(),
649
692
  updatedAt: new Date(),
650
693
  }),
@@ -729,6 +729,39 @@ const rawTesterChecklistItems = [
729
729
  href: "/cart",
730
730
  cases: [
731
731
  { key: "update-qty", label: "Updating item quantity in cart recalculates the total" },
732
+ {
733
+ key: "group-picker-add-selected",
734
+ label: "On a product that is part of a group, \"Pick items โ†’\" opens a picker with +/- per member, and \"Add selected to cart\" creates ONE cart line (not one per item)",
735
+ description: "Use the Test Gadget page. Set two different members to different quantities, check the running total in the picker footer, then add. The cart should show a single line, not three.",
736
+ href: "/products/product-tester-standard-1",
737
+ },
738
+ {
739
+ key: "group-line-editable-in-cart",
740
+ label: "A grouped cart line expands to show its members, each with its own +/- and remove; removing the last member removes the whole line",
741
+ description: "There should be NO line-level quantity stepper on a grouped line โ€” the per-member steppers carry the whole selection. Changing one member should update both the line total and the seller subtotal.",
742
+ href: "/cart",
743
+ },
744
+ {
745
+ key: "group-picker-cross-store",
746
+ label: "A group whose members belong to different sellers shows the read-only table with no quantity column and an explanation, rather than an \"Add selected\" button that always fails",
747
+ href: "/products/product-tester-standard-1",
748
+ },
749
+ {
750
+ key: "bundle-copies-stepper",
751
+ label: "A bundle page has a \"Copies\" +/- stepper and BOTH \"Buy now\" and \"Add to cart\" โ€” Add to cart stays on the page, Buy now goes to checkout",
752
+ description: "A bundle is all-or-nothing: there must be no per-member quantity controls on the bundle page, only the copies stepper.",
753
+ href: "/bundles/bundle-tester-sandbox",
754
+ },
755
+ {
756
+ key: "bundle-line-editable-in-cart",
757
+ label: "A bundle in the cart shows its member list read-only and a copies stepper; changing copies recalculates the line at the bundle's discounted price, not the sum of member prices",
758
+ href: "/cart",
759
+ },
760
+ {
761
+ key: "grouped-listing-page-picker",
762
+ label: "A grouped listing has its own page with the same picker, reachable from the \"Pick items from this group โ†’\" button on a group carousel",
763
+ href: "/groups/group-beyblade-original-lineage",
764
+ },
732
765
  { key: "apply-coupon", label: "Applying a coupon code at checkout works" },
733
766
  { key: "remove-coupon", label: "Removing an already-applied coupon from the cart recalculates the total back down" },
734
767
  { key: "remove-item", label: "Removing an item from the cart works" },
package/dist/index.d.ts CHANGED
@@ -3125,7 +3125,7 @@ export type { SessionUser } from "./react/index";
3125
3125
  export type { UseUrlTableOptions } from "./react/index";
3126
3126
  export { SUCCESS_MESSAGES } from "./values/index";
3127
3127
  export { splitCartIntoOrderGroups } from "./features/orders/index";
3128
- export { groupOrderItemsByBundle, type BundleOrderGroup, type OrderItemForBundleGrouping, } from "./features/orders/index";
3128
+ export { groupOrderItemsByLine, type LineOrderGroup, type OrderItemForLineGrouping, } from "./features/orders/index";
3129
3129
  export { buildSieveFilters } from "./utils/filter.helper";
3130
3130
  export { formatCurrency, formatFileSize } from "./utils/number.formatter";
3131
3131
  export { formatMonthYear } from "./utils/date.formatter";
package/dist/index.js CHANGED
@@ -5530,7 +5530,7 @@ export { SUCCESS_MESSAGES } from "./values/index";
5530
5530
  // -- Missing order utils
5531
5531
  export { splitCartIntoOrderGroups } from "./features/orders/index";
5532
5532
  // S-SBUNI-5 2026-05-13 รขโ‚ฌ" bundle-aware order item grouping for receipts.
5533
- export { groupOrderItemsByBundle, } from "./features/orders/index";
5533
+ export { groupOrderItemsByLine, } from "./features/orders/index";
5534
5534
  // -- Missing utils
5535
5535
  export { buildSieveFilters } from "./utils/filter.helper";
5536
5536
  export { formatCurrency, formatFileSize } from "./utils/number.formatter";
@@ -25,6 +25,12 @@ export declare const DEFAULT_ROUTE_MAP: {
25
25
  readonly PRE_ORDER_DETAIL: (id: string) => string;
26
26
  readonly BUNDLES: "/bundles";
27
27
  readonly BUNDLE_DETAIL: (slug: string) => string;
28
+ /**
29
+ * A `groupedListings` document's public page. Distinct from a product
30
+ * GROUP (`product.groupId`), whose canonical page is its parent product's
31
+ * detail page โ€” see ยง "Bundles vs Product Groups vs Grouped Listings".
32
+ */
33
+ readonly GROUP_DETAIL: (slug: string) => string;
28
34
  readonly PRIZE_DRAWS: "/prize-draws";
29
35
  readonly PRIZE_DRAW_DETAIL: (slug: string) => string;
30
36
  readonly SELLER_GUIDE_BUNDLES: "/seller-guide/bundles";
@@ -407,6 +413,12 @@ export declare const ROUTES: {
407
413
  readonly PRE_ORDER_DETAIL: (id: string) => string;
408
414
  readonly BUNDLES: "/bundles";
409
415
  readonly BUNDLE_DETAIL: (slug: string) => string;
416
+ /**
417
+ * A `groupedListings` document's public page. Distinct from a product
418
+ * GROUP (`product.groupId`), whose canonical page is its parent product's
419
+ * detail page โ€” see ยง "Bundles vs Product Groups vs Grouped Listings".
420
+ */
421
+ readonly GROUP_DETAIL: (slug: string) => string;
410
422
  readonly PRIZE_DRAWS: "/prize-draws";
411
423
  readonly PRIZE_DRAW_DETAIL: (slug: string) => string;
412
424
  readonly SELLER_GUIDE_BUNDLES: "/seller-guide/bundles";
@@ -14,6 +14,12 @@ export const DEFAULT_ROUTE_MAP = {
14
14
  // SB1-J โ€” bundles + prize draws (additive, pages not yet built)
15
15
  BUNDLES: "/bundles",
16
16
  BUNDLE_DETAIL: (slug) => `/bundles/${slug}`,
17
+ /**
18
+ * A `groupedListings` document's public page. Distinct from a product
19
+ * GROUP (`product.groupId`), whose canonical page is its parent product's
20
+ * detail page โ€” see ยง "Bundles vs Product Groups vs Grouped Listings".
21
+ */
22
+ GROUP_DETAIL: (slug) => `/groups/${slug}`,
17
23
  PRIZE_DRAWS: "/prize-draws",
18
24
  PRIZE_DRAW_DETAIL: (slug) => `/prize-draws/${slug}`,
19
25
  SELLER_GUIDE_BUNDLES: "/seller-guide/bundles",
@@ -65,7 +65,7 @@ export { renderFaqOg, renderFaqOgImage, type FaqOgData, } from "./_internal/serv
65
65
  export { renderScamOg, renderScamOgImage, type ScamOgData, getScammerForDetail, getScammerProfilePageData as getScammerProfilePageDataCached, } from "./_internal/server/features/scams/index";
66
66
  export { getSearchResults, searchAction, type SearchQuery, } from "./_internal/server/features/search/index";
67
67
  export { getCategoryForDetail, listRootCategories, listFeaturedCategories, listMenuCategories, getCategoryTree, listSitemapCategories, CATEGORIES_PAGE_SIZE, CATEGORIES_ROOT_TIER, CATEGORIES_MAX_DEPTH, CATEGORIES_SITEMAP_LIMIT, CATEGORIES_FEATURED_LIMIT, CATEGORIES_MENU_LIMIT, } from "./_internal/server/features/categories/index";
68
- export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, getGroupsForProduct, getGroupsWithItemsForProduct, GROUPED_LISTINGS_PAGE_SIZE, GROUPED_LISTINGS_FEATURED_LIMIT, GROUPED_LISTINGS_SITEMAP_LIMIT, type GroupedListingWithItems, type ListGroupedListingsParams, type SitemapGroupedListing, } from "./_internal/server/features/grouped/index";
68
+ export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, getGroupsForProduct, getGroupsWithItemsForProduct, GROUPED_LISTINGS_PAGE_SIZE, GROUPED_LISTINGS_FEATURED_LIMIT, GROUPED_LISTINGS_SITEMAP_LIMIT, buildGroupedListingMetadata, renderGroupedListingOg, type GroupedListingWithItems, type ListGroupedListingsParams, type SitemapGroupedListing, type GroupedListingMetadataOptions, } from "./_internal/server/features/grouped/index";
69
69
  export { createCheckoutOrderAction, attachPaymentAction, verifyAndPlaceRazorpayOrderAction, previewCheckoutPricing, resolveShippingCost, formatShippingAddress, CHECKOUT_DEFAULT_COMMISSIONS, CHECKOUT_PAYMENT_METHODS, type CreateCheckoutOrderInput, type VerifyAndPlaceRazorpayOrderInput, type CheckoutOrderResult, type CheckoutPaymentMethod, type CheckoutPricingPreviewInput, type CheckoutPricingPreview, } from "./_internal/server/features/checkout/index";
70
70
  export { unitPriceFor, lineTotalFor, computePreOrderDepositAmount, } from "./_internal/shared/checkout/order-math";
71
71
  export { processRefundAction, type ProcessRefundInput, } from "./_internal/server/features/refunds/actions";
@@ -91,7 +91,7 @@ export { getSearchResults, searchAction, } from "./_internal/server/features/sea
91
91
  // S3: categories data layer
92
92
  export { getCategoryForDetail, listRootCategories, listFeaturedCategories, listMenuCategories, getCategoryTree, listSitemapCategories, CATEGORIES_PAGE_SIZE, CATEGORIES_ROOT_TIER, CATEGORIES_MAX_DEPTH, CATEGORIES_SITEMAP_LIMIT, CATEGORIES_FEATURED_LIMIT, CATEGORIES_MENU_LIMIT, } from "./_internal/server/features/categories/index";
93
93
  // S3: grouped listings data layer
94
- export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, getGroupsForProduct, getGroupsWithItemsForProduct, GROUPED_LISTINGS_PAGE_SIZE, GROUPED_LISTINGS_FEATURED_LIMIT, GROUPED_LISTINGS_SITEMAP_LIMIT, } from "./_internal/server/features/grouped/index";
94
+ export { getGroupedListingForDetail, getGroupedListingWithItems, listGroupedListings, listFeaturedGroupedListings, listSitemapGroupedListings, getGroupsForProduct, getGroupsWithItemsForProduct, GROUPED_LISTINGS_PAGE_SIZE, GROUPED_LISTINGS_FEATURED_LIMIT, GROUPED_LISTINGS_SITEMAP_LIMIT, buildGroupedListingMetadata, renderGroupedListingOg, } from "./_internal/server/features/grouped/index";
95
95
  // SB-UNI-V โ€” bundles data layer deleted; consumers should query
96
96
  // `categoriesRepository.findBySlugAndType(slug, "bundle")` /
97
97
  // `.listByType("bundle", opts)` / `.findById(id)` directly.
package/dist/server.d.ts CHANGED
@@ -503,6 +503,8 @@ export { getProductForDetail, listSitemapProducts, computeRelatedItems, toProduc
503
503
  export { listPublicProducts, parsePublicProductParams, defaultAvailabilityForListingTypes, PUBLIC_PRODUCT_MAX_PAGE_SIZE, ANY_STATUS, type PublicProductListInput, type PublicProductListResult, type PublicProductListOptions, type PublicProductExecutor, type PublicProductQuery, } from "./_internal/server/features/products/index";
504
504
  export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, resolveBundleCategorySlugs, buildBundleMetadata, renderBundleOg, renderBundleOgImage, addBundleToCartAction, type BundleDataOptions, type BundleMetadataOptions, type BundleOgData, } from "./_internal/server/features/bundles/index";
505
505
  export { getGroupsForProduct, getGroupsWithItemsForProduct, getGroupsForCategory, getGroupsForBrand } from "./_internal/server/features/grouped/index";
506
+ export { getGroupedListingForDetail, getGroupedListingWithItems, buildGroupedListingMetadata, renderGroupedListingOg, type GroupedListingWithItems, type GroupedListingMetadataOptions, } from "./_internal/server/features/grouped/index";
507
+ export { toPublicGroupMember, toPublicGroupMembers, type PublicGroupMember, } from "./_internal/server/features/products/group-members";
506
508
  export { getAuctionForDetail, getProductFeaturesForAuction } from "./_internal/server/features/auctions/index";
507
509
  export { getPreOrderForDetail, getProductFeaturesForPreOrder } from "./_internal/server/features/pre-orders/index";
508
510
  export { getStoreForDetail, listStoreProductsInitial, listStoreAuctionsInitial, listStorePreOrdersInitial, listSitemapStores } from "./_internal/server/features/stores/index";
package/dist/server.js CHANGED
@@ -1358,6 +1358,10 @@ export { listPublicProducts, parsePublicProductParams, defaultAvailabilityForLis
1358
1358
  // S-SBUNI-3/4 2026-05-13 โ€” bundle data/metadata/og layer.
1359
1359
  export { getBundleForDetail, listBundleMembers, listFeaturedBundles, getRelatedBundles, resolveBundleMemberIds, resolveBundleOriginalTotal, resolveBundleCategorySlugs, buildBundleMetadata, renderBundleOg, renderBundleOgImage, addBundleToCartAction, } from "./_internal/server/features/bundles/index";
1360
1360
  export { getGroupsForProduct, getGroupsWithItemsForProduct, getGroupsForCategory, getGroupsForBrand } from "./_internal/server/features/grouped/index";
1361
+ // Public /groups/{slug} page โ€” data, metadata and OG.
1362
+ export { getGroupedListingForDetail, getGroupedListingWithItems, buildGroupedListingMetadata, renderGroupedListingOg, } from "./_internal/server/features/grouped/index";
1363
+ // Allow-list projection for group members โ€” see ยง "Public Data Projections".
1364
+ export { toPublicGroupMember, toPublicGroupMembers, } from "./_internal/server/features/products/group-members";
1361
1365
  export { getAuctionForDetail, getProductFeaturesForAuction } from "./_internal/server/features/auctions/index";
1362
1366
  export { getPreOrderForDetail, getProductFeaturesForPreOrder } from "./_internal/server/features/pre-orders/index";
1363
1367
  export { getStoreForDetail, listStoreProductsInitial, listStoreAuctionsInitial, listStorePreOrdersInitial, listSitemapStores } from "./_internal/server/features/stores/index";