@mohasinac/appkit 4.24.0 → 4.25.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 (34) hide show
  1. package/dist/_internal/server/features/checkout/actions.js +6 -5
  2. package/dist/_internal/server/features/checkout/bundle-expansion.d.ts +4 -6
  3. package/dist/_internal/server/features/checkout/bundle-expansion.js +19 -17
  4. package/dist/_internal/server/features/checkout/stock-restore.js +6 -5
  5. package/dist/_internal/server/features/products/group-members.d.ts +55 -0
  6. package/dist/_internal/server/features/products/group-members.js +41 -0
  7. package/dist/_internal/shared/checkout/line-members.d.ts +60 -0
  8. package/dist/_internal/shared/checkout/line-members.js +68 -0
  9. package/dist/_internal/shared/checkout/rules/_registry.js +8 -2
  10. package/dist/client.d.ts +2 -0
  11. package/dist/client.js +1 -0
  12. package/dist/constants/api-endpoints.d.ts +27 -0
  13. package/dist/constants/api-endpoints.js +9 -0
  14. package/dist/features/admin/schemas/firestore.d.ts +10 -26
  15. package/dist/features/admin/schemas/firestore.js +10 -26
  16. package/dist/features/cart/actions/cart-actions.d.ts +31 -1
  17. package/dist/features/cart/actions/cart-actions.js +163 -0
  18. package/dist/features/cart/repository/cart.repository.d.ts +33 -1
  19. package/dist/features/cart/repository/cart.repository.js +146 -2
  20. package/dist/features/products/components/GroupMemberPicker.d.ts +46 -0
  21. package/dist/features/products/components/GroupMemberPicker.js +108 -0
  22. package/dist/features/products/components/ShowGroupSection.js +7 -9
  23. package/dist/index.d.ts +2 -1
  24. package/dist/index.js +5 -1
  25. package/dist/seed/notifications-seed-data.js +225 -0
  26. package/dist/seed/payouts-seed-data.js +47 -0
  27. package/dist/seed/scammers-seed-data.js +34 -0
  28. package/dist/seed/stores-seed-data.js +34 -0
  29. package/dist/seed/support-tickets-seed-data.js +34 -0
  30. package/dist/server.d.ts +1 -1
  31. package/dist/server.js +1 -1
  32. package/dist/styles.css +1 -1
  33. package/dist/tailwind-utilities.css +1 -1
  34. package/package.json +1 -1
@@ -26,7 +26,7 @@ import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../provide
26
26
  import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore";
27
27
  import { CART_COLLECTION } from "../../../../features/cart/schemas/index";
28
28
  // S-SBUNI-RULES 2026-05-13 — bundle cart-line stock fan-out helpers.
29
- import { getCartItemMemberIds, getExpandedDecrements, bucketCartItemsByStock, } from "./bundle-expansion";
29
+ import { getCartItemMemberIds, getExpandedDecrements, bucketCartItemsByStock, isMultiMemberLine, } from "./bundle-expansion";
30
30
  import { OrderStatusValues, PaymentStatusValues, PaymentMethodValues, OutOfStockPolicyValues, } from "../../../../features/orders/schemas/index";
31
31
  import { getDefaultCurrency } from "../../../../core/index";
32
32
  import { verifyPaymentSignatureWithKeys, fetchRazorpayOrder, paiseToRupees, } from "../../../../providers/payment-razorpay/index";
@@ -756,8 +756,9 @@ export async function createCheckoutOrderAction(input) {
756
756
  const availableItems = [];
757
757
  for (const { item, product } of bucketed.available) {
758
758
  // S-SBUNI-RULES 2026-05-13 — sync preflight (prize-pool cap, pre-order
759
- // quota) via rule registry. Bundles skip — they bypass the cart path.
760
- if (!item.bundleProductIds?.length) {
759
+ // quota) via rule registry. Multi-member lines skip: their listingType
760
+ // describes the wrapper, not the members.
761
+ if (!isMultiMemberLine(item)) {
761
762
  runSyncPreflight([{ item, product }], paymentMethod);
762
763
  }
763
764
  availableItems.push({ item, product });
@@ -1406,8 +1407,8 @@ export async function verifyAndPlaceRazorpayOrderAction(input) {
1406
1407
  .map(({ item, product }) => ({ item, product })),
1407
1408
  });
1408
1409
  // S-SBUNI-RULES 2026-05-13 — sync preflight via rule registry.
1409
- // Bundle items bypass the cart flow so skip the prize-pool cap.
1410
- const preflightPairs = productChecks.filter((p) => p.product !== null && p.product !== undefined && !p.item.bundleProductIds?.length);
1410
+ // Multi-member lines skip the prize-pool cap, same reasoning as the COD path.
1411
+ const preflightPairs = productChecks.filter((p) => p.product !== null && p.product !== undefined && !isMultiMemberLine(p.item));
1411
1412
  // This whole action is the Razorpay path — paymentMethod is always "online".
1412
1413
  runSyncPreflight(preflightPairs, "online");
1413
1414
  // SB-UNI-O 2026-05-15 — Live-item jurisdiction guard.
@@ -22,19 +22,17 @@
22
22
  */
23
23
  import type { CartItemDocument } from "../../../../features/cart/schemas";
24
24
  import type { ProductDocument } from "../../../../features/products/schemas/firestore";
25
+ export { getCartLineMembers, getCartItemMemberIds, isMultiMemberLine, type CartLineMemberRef, } from "../../../shared/checkout/line-members";
25
26
  export interface BundleExpansion {
26
27
  /** Unique product ids referenced by any cart item (regular or bundle member). */
27
28
  productIds: string[];
28
29
  /**
29
- * Sum of requested quantities per product id. Bundle members get
30
- * `item.quantity` per occurrence (so 2x of a bundle with member X
31
- * decrements X by 2, not 1).
30
+ * Sum of requested quantities per product id, weighted by BOTH the number of
31
+ * copies of the line and each member's per-copy quantity — so 2 copies of a
32
+ * selection containing 3× member X decrement X by 6.
32
33
  */
33
34
  decrements: Map<string, number>;
34
35
  }
35
- /** Resolve the member-product ids for a cart item. Bundle items expand to
36
- * `bundleProductIds[]`; regular items collapse to `[productId]`. */
37
- export declare function getCartItemMemberIds(item: CartItemDocument): string[];
38
36
  /** Build the unique-product-id + per-product decrement map for a cart. */
39
37
  export declare function getExpandedDecrements(cartItems: CartItemDocument[]): BundleExpansion;
40
38
  export interface StockShortfall {
@@ -21,21 +21,21 @@
21
21
  * the first's decrement before re-running its own validation.
22
22
  */
23
23
  import { ProductStatusValues } from "../../../../features/products/schemas/firestore";
24
- /** Resolve the member-product ids for a cart item. Bundle items expand to
25
- * `bundleProductIds[]`; regular items collapse to `[productId]`. */
26
- export function getCartItemMemberIds(item) {
27
- if (item.bundleProductIds && item.bundleProductIds.length > 0) {
28
- return item.bundleProductIds;
29
- }
30
- return [item.productId];
31
- }
24
+ import { getCartLineMembers } from "../../../shared/checkout/line-members";
25
+ // Re-exported here so the many existing importers of this module keep working;
26
+ // the definitions live in shared/ because `rules/_registry.ts` and
27
+ // `order-math.ts` need them too and cannot import from server/.
28
+ export { getCartLineMembers, getCartItemMemberIds, isMultiMemberLine, } from "../../../shared/checkout/line-members";
32
29
  /** Build the unique-product-id + per-product decrement map for a cart. */
33
30
  export function getExpandedDecrements(cartItems) {
34
31
  const decrements = new Map();
35
32
  for (const item of cartItems) {
36
- const memberIds = getCartItemMemberIds(item);
37
- for (const pid of memberIds) {
38
- decrements.set(pid, (decrements.get(pid) ?? 0) + item.quantity);
33
+ for (const member of getCartLineMembers(item)) {
34
+ // item.quantity = copies of the line, member.quantity = units per copy.
35
+ // Multiplying is the whole point: this used to add `item.quantity` alone,
36
+ // which silently assumed exactly one of each member.
37
+ const needed = item.quantity * member.quantity;
38
+ decrements.set(member.productId, (decrements.get(member.productId) ?? 0) + needed);
39
39
  }
40
40
  }
41
41
  return {
@@ -55,15 +55,17 @@ export function getExpandedDecrements(cartItems) {
55
55
  * that product, not just this item's slice.
56
56
  */
57
57
  export function validateCartItemStock(item, productById, decrements) {
58
- const memberIds = getCartItemMemberIds(item);
59
- for (const pid of memberIds) {
60
- const product = productById.get(pid);
58
+ for (const member of getCartLineMembers(item)) {
59
+ const product = productById.get(member.productId);
61
60
  if (!product || product.status !== ProductStatusValues.PUBLISHED) {
62
- return { productId: pid, availableQty: product?.availableQuantity ?? 0 };
61
+ return { productId: member.productId, availableQty: product?.availableQuantity ?? 0 };
63
62
  }
64
- const totalNeeded = decrements.get(pid) ?? item.quantity;
63
+ // The fallback must be member-weighted too. Left as a bare `item.quantity`
64
+ // it would validate a line needing 3× of a member against demand for 1
65
+ // whenever the cumulative map didn't happen to carry that product.
66
+ const totalNeeded = decrements.get(member.productId) ?? item.quantity * member.quantity;
65
67
  if (product.availableQuantity < totalNeeded) {
66
- return { productId: pid, availableQty: product.availableQuantity };
68
+ return { productId: member.productId, availableQty: product.availableQuantity };
67
69
  }
68
70
  }
69
71
  return null;
@@ -1,6 +1,7 @@
1
1
  import { ORDER_COLLECTION } from "../../../../features/orders/schemas/firestore";
2
2
  import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firestore";
3
3
  import { getListingRule } from "../../../shared/checkout/rules";
4
+ import { getOrderItemMemberRefs } from "../../../shared/checkout/line-members";
4
5
  /**
5
6
  * Per-product quantity to restore for one order — the inverse of
6
7
  * `getExpandedDecrements` (bundle-expansion.ts), but reads from
@@ -15,11 +16,11 @@ function getOrderRestoreMap(order) {
15
16
  const remainingQty = item.quantity - (item.cancelledQuantity ?? 0);
16
17
  if (remainingQty <= 0)
17
18
  continue;
18
- const memberIds = item.bundleProductIds && item.bundleProductIds.length > 0
19
- ? item.bundleProductIds
20
- : [item.productId];
21
- for (const pid of memberIds) {
22
- map.set(pid, (map.get(pid) ?? 0) + remainingQty);
19
+ // Must mirror `getExpandedDecrements` exactly, including the per-member
20
+ // weighting — restoring fewer units than were taken silently leaks stock.
21
+ for (const member of getOrderItemMemberRefs(item)) {
22
+ const pid = member.productId;
23
+ map.set(pid, (map.get(pid) ?? 0) + remainingQty * member.quantity);
23
24
  }
24
25
  }
25
26
  return map;
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Public projection for a product-group member.
3
+ *
4
+ * `GET /api/products/group/[groupId]` previously returned
5
+ * `sanitizeProductsForPublic(items)`, which is a DELETE-LIST of four
6
+ * seller-identity keys — every other field of `ProductDocument` went to the
7
+ * browser, including fields nobody had thought about. Per § "Public Data
8
+ * Projections", a public payload is built by naming what goes out, never by
9
+ * spreading a document and removing keys: a denial keyed on today's field list
10
+ * is structurally blind to the field added tomorrow.
11
+ *
12
+ * The member picker needs more than the old response carried (stock, store,
13
+ * sold state, gstRate), so widening the payload was required either way —
14
+ * which made this the moment to make it an allow-list rather than a wider
15
+ * deny-list.
16
+ */
17
+ import type { ProductDocument } from "../../../../features/products/schemas/firestore";
18
+ import type { ListingType } from "../../../../features/products/types/index";
19
+ /**
20
+ * Every field the group picker and the group table render. If a field is not
21
+ * here it does not reach the browser.
22
+ *
23
+ * Deliberately absent, with reasons:
24
+ * - `sellerId` / `ownerId` / `sellerName` / `sellerEmail` — seller identity;
25
+ * public clients see store-scoped identity only.
26
+ * - `costPrice`, `profitMargin`, `supplier*`, `adminNotes` — seller-private
27
+ * commercial data.
28
+ * - `stockQuantity` — the internal figure; `availableQuantity` is the one the
29
+ * buyer may act on.
30
+ */
31
+ export interface PublicGroupMember {
32
+ id: string;
33
+ slug?: string;
34
+ title: string;
35
+ price: number;
36
+ currency?: string;
37
+ /** First image only — the picker renders a thumbnail, never a gallery. */
38
+ image?: string;
39
+ condition?: string;
40
+ listingType: ListingType;
41
+ isGroupParent?: boolean;
42
+ groupTitle?: string;
43
+ /** Drives the stepper's `max`. */
44
+ availableQuantity: number;
45
+ storeId: string;
46
+ storeName?: string;
47
+ status: string;
48
+ isSold?: boolean;
49
+ /** Needed so the picker can show a tax-inclusive hint and the cart line can
50
+ * snapshot it for per-member GST proration at checkout. */
51
+ gstRate?: 0 | 5 | 12 | 18 | 28;
52
+ hsnCode?: string;
53
+ }
54
+ export declare function toPublicGroupMember(doc: ProductDocument): PublicGroupMember;
55
+ export declare function toPublicGroupMembers(docs: readonly ProductDocument[]): PublicGroupMember[];
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Public projection for a product-group member.
3
+ *
4
+ * `GET /api/products/group/[groupId]` previously returned
5
+ * `sanitizeProductsForPublic(items)`, which is a DELETE-LIST of four
6
+ * seller-identity keys — every other field of `ProductDocument` went to the
7
+ * browser, including fields nobody had thought about. Per § "Public Data
8
+ * Projections", a public payload is built by naming what goes out, never by
9
+ * spreading a document and removing keys: a denial keyed on today's field list
10
+ * is structurally blind to the field added tomorrow.
11
+ *
12
+ * The member picker needs more than the old response carried (stock, store,
13
+ * sold state, gstRate), so widening the payload was required either way —
14
+ * which made this the moment to make it an allow-list rather than a wider
15
+ * deny-list.
16
+ */
17
+ import { normalizeListingType } from "../../../../features/products/utils/listing-type";
18
+ export function toPublicGroupMember(doc) {
19
+ return {
20
+ id: doc.id,
21
+ slug: doc.slug,
22
+ title: doc.title,
23
+ price: doc.price,
24
+ currency: doc.currency,
25
+ image: doc.images?.[0] ?? doc.mainImage,
26
+ condition: doc.condition,
27
+ listingType: normalizeListingType(doc),
28
+ isGroupParent: doc.isGroupParent,
29
+ groupTitle: doc.groupTitle,
30
+ availableQuantity: doc.availableQuantity ?? 0,
31
+ storeId: doc.storeId,
32
+ storeName: doc.storeName,
33
+ status: doc.status,
34
+ isSold: doc.isSold,
35
+ gstRate: doc.gstRate,
36
+ hsnCode: doc.hsnCode,
37
+ };
38
+ }
39
+ export function toPublicGroupMembers(docs) {
40
+ return docs.map(toPublicGroupMember);
41
+ }
@@ -0,0 +1,60 @@
1
+ /**
2
+ * "What products does this cart line actually consume?" — the one answer.
3
+ *
4
+ * Lives in `shared/` rather than `server/features/checkout/` because three
5
+ * different layers need it and two of them are not server code:
6
+ * - `order-math.ts` (pricing + tax, shared)
7
+ * - `rules/_registry.ts` (preflight skip, shared)
8
+ * - `bundle-expansion.ts` (stock fan-out, server)
9
+ * - `stock-restore.ts` (restore fan-out, server)
10
+ *
11
+ * Pure functions, no Firestore, no framework imports.
12
+ */
13
+ import type { CartItemDocument } from "../../../features/cart/schemas/firestore";
14
+ /** One member of a cart line, with the units required per COPY of that line. */
15
+ export interface CartLineMemberRef {
16
+ productId: string;
17
+ quantity: number;
18
+ }
19
+ /**
20
+ * Resolve a line's members. Three sources, in priority order — never read
21
+ * `bundleProductIds` directly at a call site:
22
+ *
23
+ * 1. `groupMembers` — authoritative, and the only shape that can express
24
+ * per-member quantities.
25
+ * 2. `bundleProductIds` — legacy bundle lines written before `groupMembers`
26
+ * existed, resolved at quantity 1 each. This fallback is what makes carts
27
+ * that were already in flight behave byte-identically, with no migration.
28
+ * 3. the line's own `productId` — an ordinary single-product line.
29
+ *
30
+ * Keeping this a derivation rather than storing a second array is deliberate: a
31
+ * hand-maintained mirror drifts the first time one write path forgets it
32
+ * (Recurrent Root Cause #42).
33
+ */
34
+ export declare function getCartLineMembers(item: CartItemDocument): CartLineMemberRef[];
35
+ /**
36
+ * True when the line stands for several products rather than one.
37
+ *
38
+ * Replaces the scattered `item.bundleProductIds?.length` tests, which each
39
+ * meant "is this a bundle" and would all have silently answered "no" for a
40
+ * `groupMembers`-backed line.
41
+ */
42
+ export declare function isMultiMemberLine(item: CartItemDocument): boolean;
43
+ /** Just the product ids — see `getCartLineMembers` for the resolution order. */
44
+ export declare function getCartItemMemberIds(item: CartItemDocument): string[];
45
+ /** The order-side shape this module needs. Structural on purpose — it matches
46
+ * `OrderDocumentItem` without dragging the orders schema into shared checkout. */
47
+ export interface OrderItemMemberSource {
48
+ productId: string;
49
+ bundleProductIds?: string[];
50
+ }
51
+ /**
52
+ * The order-side twin of `getCartLineMembers`, used by stock restore.
53
+ *
54
+ * A LEGACY order row is collapsed: one row standing for a whole bundle, with
55
+ * its members listed in `bundleProductIds` at one unit each. Rows written after
56
+ * grouped lines were expanded carry no `bundleProductIds` at all — each row is
57
+ * already exactly one product — so they fall through to the single-member
58
+ * branch and need no special case.
59
+ */
60
+ export declare function getOrderItemMemberRefs(item: OrderItemMemberSource): CartLineMemberRef[];
@@ -0,0 +1,68 @@
1
+ /**
2
+ * "What products does this cart line actually consume?" — the one answer.
3
+ *
4
+ * Lives in `shared/` rather than `server/features/checkout/` because three
5
+ * different layers need it and two of them are not server code:
6
+ * - `order-math.ts` (pricing + tax, shared)
7
+ * - `rules/_registry.ts` (preflight skip, shared)
8
+ * - `bundle-expansion.ts` (stock fan-out, server)
9
+ * - `stock-restore.ts` (restore fan-out, server)
10
+ *
11
+ * Pure functions, no Firestore, no framework imports.
12
+ */
13
+ /**
14
+ * Resolve a line's members. Three sources, in priority order — never read
15
+ * `bundleProductIds` directly at a call site:
16
+ *
17
+ * 1. `groupMembers` — authoritative, and the only shape that can express
18
+ * per-member quantities.
19
+ * 2. `bundleProductIds` — legacy bundle lines written before `groupMembers`
20
+ * existed, resolved at quantity 1 each. This fallback is what makes carts
21
+ * that were already in flight behave byte-identically, with no migration.
22
+ * 3. the line's own `productId` — an ordinary single-product line.
23
+ *
24
+ * Keeping this a derivation rather than storing a second array is deliberate: a
25
+ * hand-maintained mirror drifts the first time one write path forgets it
26
+ * (Recurrent Root Cause #42).
27
+ */
28
+ export function getCartLineMembers(item) {
29
+ if (item.groupMembers?.length) {
30
+ return item.groupMembers.map((m) => ({
31
+ productId: m.productId,
32
+ quantity: m.quantity,
33
+ }));
34
+ }
35
+ if (item.bundleProductIds?.length) {
36
+ return item.bundleProductIds.map((productId) => ({ productId, quantity: 1 }));
37
+ }
38
+ return [{ productId: item.productId, quantity: 1 }];
39
+ }
40
+ /**
41
+ * True when the line stands for several products rather than one.
42
+ *
43
+ * Replaces the scattered `item.bundleProductIds?.length` tests, which each
44
+ * meant "is this a bundle" and would all have silently answered "no" for a
45
+ * `groupMembers`-backed line.
46
+ */
47
+ export function isMultiMemberLine(item) {
48
+ return Boolean(item.groupMembers?.length || item.bundleProductIds?.length);
49
+ }
50
+ /** Just the product ids — see `getCartLineMembers` for the resolution order. */
51
+ export function getCartItemMemberIds(item) {
52
+ return getCartLineMembers(item).map((m) => m.productId);
53
+ }
54
+ /**
55
+ * The order-side twin of `getCartLineMembers`, used by stock restore.
56
+ *
57
+ * A LEGACY order row is collapsed: one row standing for a whole bundle, with
58
+ * its members listed in `bundleProductIds` at one unit each. Rows written after
59
+ * grouped lines were expanded carry no `bundleProductIds` at all — each row is
60
+ * already exactly one product — so they fall through to the single-member
61
+ * branch and need no special case.
62
+ */
63
+ export function getOrderItemMemberRefs(item) {
64
+ if (item.bundleProductIds?.length) {
65
+ return item.bundleProductIds.map((productId) => ({ productId, quantity: 1 }));
66
+ }
67
+ return [{ productId: item.productId, quantity: 1 }];
68
+ }
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * No grep of the consumer codebase needed.
10
10
  */
11
+ import { isMultiMemberLine } from "../line-members";
11
12
  import { standardRule } from "./standard.rule";
12
13
  import { auctionRule } from "./auction.rule";
13
14
  import { preOrderRule } from "./preorder.rule";
@@ -77,8 +78,13 @@ export function getSplitKey(item) {
77
78
  export function runSyncPreflight(pairs, paymentMethod) {
78
79
  const byType = new Map();
79
80
  for (const pair of pairs) {
80
- if (pair.item.bundleProductIds?.length)
81
- continue; // bundles skip — direct checkout
81
+ // Multi-member lines (bundle or grouped selection) skip the per-listing-type
82
+ // preflight: their `listingType` describes the wrapper, not the members, so
83
+ // a per-type rule would be applied to the wrong thing. Tested via the shared
84
+ // predicate rather than `bundleProductIds?.length`, which answers "is this a
85
+ // legacy bundle" and would say no for a `groupMembers`-backed line.
86
+ if (isMultiMemberLine(pair.item))
87
+ continue;
82
88
  const lt = (pair.item.listingType ?? "standard");
83
89
  const bucket = byType.get(lt) ?? [];
84
90
  bucket.push(pair);
package/dist/client.d.ts CHANGED
@@ -262,6 +262,8 @@ export type { WishlistCapEventDetail } from "./features/wishlist/hooks/useWishli
262
262
  export { getGuestWishlistItems, addToGuestWishlist, removeFromGuestWishlist, isInGuestWishlist, clearGuestWishlist, getGuestWishlistCount, getGuestWishlistByType, } from "./features/wishlist/utils/guest-wishlist";
263
263
  export { InteractiveProductCard } from "./features/products/index";
264
264
  export type { InteractiveProductCardProps } from "./features/products/index";
265
+ export { GroupMemberPicker } from "./features/products/components/GroupMemberPicker";
266
+ export type { GroupMemberPickerProps, GroupPickerMember, } from "./features/products/components/GroupMemberPicker";
265
267
  export { MarketplaceBundleCard } from "./features/products/components/MarketplaceBundleCard";
266
268
  export type { MarketplaceBundleCardProps, MarketplaceBundleCardData, MarketplaceBundleCardLabels, } from "./features/products/components/MarketplaceBundleCard";
267
269
  export { CompareOverlay } from "./features/products/components/CompareOverlay";
package/dist/client.js CHANGED
@@ -282,6 +282,7 @@ export { SyncManagerMount } from "./core/components/SyncManagerMount";
282
282
  export { useWishlistCount, useWishlistCountWithLimit, WISHLIST_CAP_EVENT } from "./features/wishlist/hooks/useWishlistCount";
283
283
  export { getGuestWishlistItems, addToGuestWishlist, removeFromGuestWishlist, isInGuestWishlist, clearGuestWishlist, getGuestWishlistCount, getGuestWishlistByType, } from "./features/wishlist/utils/guest-wishlist";
284
284
  export { InteractiveProductCard } from "./features/products/index";
285
+ export { GroupMemberPicker } from "./features/products/components/GroupMemberPicker";
285
286
  export { MarketplaceBundleCard } from "./features/products/components/MarketplaceBundleCard";
286
287
  export { CompareOverlay } from "./features/products/components/CompareOverlay";
287
288
  export { COMPARE_MAX_ITEMS } from "./features/products/constants/action-defs";
@@ -239,6 +239,15 @@ export declare const CART_ENDPOINTS: {
239
239
  /** Per-store paid add-on selections — the source of truth for what's charged. */
240
240
  readonly ADDONS: "/api/cart/addons";
241
241
  readonly COUPON: "/api/cart/coupon";
242
+ /**
243
+ * Add a buyer-assembled group selection as ONE line. Separate from `GET`'s
244
+ * POST on purpose: that is the hot path behind every product card's
245
+ * Add-to-cart, and widening its `{productId, quantity}` schema into a
246
+ * discriminated union would put this feature's blast radius on it.
247
+ */
248
+ readonly GROUP: "/api/cart/group";
249
+ /** Replace a grouped line's member quantities (whole array, not a delta). */
250
+ readonly GROUP_MEMBERS: (itemId: string) => string;
242
251
  };
243
252
  export declare const CATEGORY_ENDPOINTS: {
244
253
  readonly LIST: "/api/categories";
@@ -695,6 +704,15 @@ export declare const API_ENDPOINTS: {
695
704
  /** Per-store paid add-on selections — the source of truth for what's charged. */
696
705
  readonly ADDONS: "/api/cart/addons";
697
706
  readonly COUPON: "/api/cart/coupon";
707
+ /**
708
+ * Add a buyer-assembled group selection as ONE line. Separate from `GET`'s
709
+ * POST on purpose: that is the hot path behind every product card's
710
+ * Add-to-cart, and widening its `{productId, quantity}` schema into a
711
+ * discriminated union would put this feature's blast radius on it.
712
+ */
713
+ readonly GROUP: "/api/cart/group";
714
+ /** Replace a grouped line's member quantities (whole array, not a delta). */
715
+ readonly GROUP_MEMBERS: (itemId: string) => string;
698
716
  };
699
717
  readonly CATEGORIES: {
700
718
  readonly LIST: "/api/categories";
@@ -1153,6 +1171,15 @@ export declare const API_ROUTES: {
1153
1171
  /** Per-store paid add-on selections — the source of truth for what's charged. */
1154
1172
  readonly ADDONS: "/api/cart/addons";
1155
1173
  readonly COUPON: "/api/cart/coupon";
1174
+ /**
1175
+ * Add a buyer-assembled group selection as ONE line. Separate from `GET`'s
1176
+ * POST on purpose: that is the hot path behind every product card's
1177
+ * Add-to-cart, and widening its `{productId, quantity}` schema into a
1178
+ * discriminated union would put this feature's blast radius on it.
1179
+ */
1180
+ readonly GROUP: "/api/cart/group";
1181
+ /** Replace a grouped line's member quantities (whole array, not a delta). */
1182
+ readonly GROUP_MEMBERS: (itemId: string) => string;
1156
1183
  };
1157
1184
  readonly CATEGORIES: {
1158
1185
  readonly LIST: "/api/categories";
@@ -279,6 +279,15 @@ export const CART_ENDPOINTS = {
279
279
  /** Per-store paid add-on selections — the source of truth for what's charged. */
280
280
  ADDONS: "/api/cart/addons",
281
281
  COUPON: "/api/cart/coupon",
282
+ /**
283
+ * Add a buyer-assembled group selection as ONE line. Separate from `GET`'s
284
+ * POST on purpose: that is the hot path behind every product card's
285
+ * Add-to-cart, and widening its `{productId, quantity}` schema into a
286
+ * discriminated union would put this feature's blast radius on it.
287
+ */
288
+ GROUP: "/api/cart/group",
289
+ /** Replace a grouped line's member quantities (whole array, not a delta). */
290
+ GROUP_MEMBERS: (itemId) => `/api/cart/${encodeURIComponent(itemId)}/members`,
282
291
  };
283
292
  // ---------------------------------------------------------------------------
284
293
  // Categories
@@ -54,32 +54,16 @@ export declare const NOTIFICATION_FIELDS: {
54
54
  readonly RELATED_TYPE: "relatedType";
55
55
  readonly CREATED_AT: "createdAt";
56
56
  readonly UPDATED_AT: "updatedAt";
57
- readonly TYPE_VALUES: {
58
- readonly ORDER_PLACED: NotificationType;
59
- readonly ORDER_CONFIRMED: NotificationType;
60
- readonly ORDER_SHIPPED: NotificationType;
61
- readonly ORDER_DELIVERED: NotificationType;
62
- readonly ORDER_CANCELLED: NotificationType;
63
- readonly BID_PLACED: NotificationType;
64
- readonly BID_OUTBID: NotificationType;
65
- readonly BID_WON: NotificationType;
66
- readonly BID_LOST: NotificationType;
67
- readonly AUCTION_ENDED: NotificationType;
68
- readonly REVIEW_APPROVED: NotificationType;
69
- readonly REVIEW_REPLIED: NotificationType;
70
- readonly PRODUCT_AVAILABLE: NotificationType;
71
- readonly PROMOTION: NotificationType;
72
- readonly SYSTEM: NotificationType;
73
- readonly WELCOME: NotificationType;
74
- readonly OFFER_RECEIVED: NotificationType;
75
- readonly OFFER_RESPONDED: NotificationType;
76
- readonly OFFER_EXPIRED: NotificationType;
77
- readonly OFFER_COUNTER_ACCEPTED: NotificationType;
78
- readonly REFUND_INITIATED: NotificationType;
79
- readonly ACCOUNT_ACTION: NotificationType;
80
- readonly PRIZE_WON: NotificationType;
81
- readonly PRIZE_REVEAL_EXPIRED: NotificationType;
82
- };
57
+ /**
58
+ * SCREAMING_SNAKE accessors, DERIVED from `NOTIFICATION_TYPE_VALUES`.
59
+ *
60
+ * This was a fifth hand-written copy of the union — 24 of the 28 values,
61
+ * missing `emi_installment_due_soon`, `emi_installment_overdue`,
62
+ * `payment_review` and `catalogue_images_stale` — and every entry carried an
63
+ * `as NotificationType` cast, which is precisely the assertion that lets a
64
+ * wrong string through. Built from the array now, so it cannot be short.
65
+ */
66
+ readonly TYPE_VALUES: { [K in NotificationType as Uppercase<K>]: K; };
83
67
  readonly PRIORITY_VALUES: {
84
68
  readonly LOW: NotificationPriority;
85
69
  readonly NORMAL: NotificationPriority;
@@ -110,32 +110,16 @@ export const NOTIFICATION_FIELDS = {
110
110
  RELATED_TYPE: "relatedType",
111
111
  CREATED_AT: "createdAt",
112
112
  UPDATED_AT: "updatedAt",
113
- TYPE_VALUES: {
114
- ORDER_PLACED: "order_placed",
115
- ORDER_CONFIRMED: "order_confirmed",
116
- ORDER_SHIPPED: "order_shipped",
117
- ORDER_DELIVERED: "order_delivered",
118
- ORDER_CANCELLED: "order_cancelled",
119
- BID_PLACED: "bid_placed",
120
- BID_OUTBID: "bid_outbid",
121
- BID_WON: "bid_won",
122
- BID_LOST: "bid_lost",
123
- AUCTION_ENDED: "auction_ended",
124
- REVIEW_APPROVED: "review_approved",
125
- REVIEW_REPLIED: "review_replied",
126
- PRODUCT_AVAILABLE: "product_available",
127
- PROMOTION: "promotion",
128
- SYSTEM: "system",
129
- WELCOME: "welcome",
130
- OFFER_RECEIVED: "offer_received",
131
- OFFER_RESPONDED: "offer_responded",
132
- OFFER_EXPIRED: "offer_expired",
133
- OFFER_COUNTER_ACCEPTED: "offer_counter_accepted",
134
- REFUND_INITIATED: "refund_initiated",
135
- ACCOUNT_ACTION: "account_action",
136
- PRIZE_WON: "prize_won",
137
- PRIZE_REVEAL_EXPIRED: "prize_reveal_expired",
138
- },
113
+ /**
114
+ * SCREAMING_SNAKE accessors, DERIVED from `NOTIFICATION_TYPE_VALUES`.
115
+ *
116
+ * This was a fifth hand-written copy of the union — 24 of the 28 values,
117
+ * missing `emi_installment_due_soon`, `emi_installment_overdue`,
118
+ * `payment_review` and `catalogue_images_stale` — and every entry carried an
119
+ * `as NotificationType` cast, which is precisely the assertion that lets a
120
+ * wrong string through. Built from the array now, so it cannot be short.
121
+ */
122
+ TYPE_VALUES: Object.fromEntries(NOTIFICATION_TYPE_VALUES.map((v) => [v.toUpperCase(), v])),
139
123
  PRIORITY_VALUES: {
140
124
  LOW: "low",
141
125
  NORMAL: "normal",
@@ -4,7 +4,7 @@
4
4
  * Pure business functions — auth, rate-limiting, and Next.js specifics
5
5
  * are handled by the calling server action in the consumer.
6
6
  */
7
- import type { AddToCartInput, CartDocument, UpdateCartItemInput } from "../schemas";
7
+ import type { AddToCartInput, CartDocument, CartGroupSource, UpdateCartItemInput, UpdateCartGroupMembersInput } from "../schemas";
8
8
  /**
9
9
  * Lane gate. While a higher-obligation lane (a won auction, then an accepted
10
10
  * offer) is outstanding, no NEW shopping may be added — otherwise the buyer can
@@ -24,6 +24,30 @@ export declare function assertCanAddNewItems(userId: string): Promise<CartDocume
24
24
  export declare function addItemToCart(userId: string, input: AddToCartInput): Promise<CartDocument>;
25
25
  /** Error code the client maps to a lane-aware message. */
26
26
  export declare const CART_LANE_BLOCKED = "CART_LANE_BLOCKED";
27
+ /** Max members one grouped line may hold. Keeps the batch read bounded (Rule #6). */
28
+ export declare const GROUP_LINE_MAX_MEMBERS = 20;
29
+ export interface AddGroupLineToCartInput {
30
+ /** `product.groupId` for a product group, or the `groupedListings` doc id. */
31
+ groupId: string;
32
+ groupSource: CartGroupSource;
33
+ /** Ids and quantities ONLY — every other field is re-resolved server-side. */
34
+ members: Array<{
35
+ productId: string;
36
+ quantity: number;
37
+ }>;
38
+ }
39
+ /**
40
+ * Add a buyer-assembled selection of group members as ONE cart line.
41
+ *
42
+ * The client sends ids and quantities and nothing else. Accepting a price, a
43
+ * title or an image from the browser would be a ₹1-group exploit; every
44
+ * snapshot below is read from Firestore here.
45
+ *
46
+ * A rejected member fails the WHOLE request, naming the offender. Silently
47
+ * dropping it would hand the buyer a line that quietly isn't what they picked —
48
+ * the "wrong data, no error" failure nobody can report.
49
+ */
50
+ export declare function addGroupLineToCart(userId: string, input: AddGroupLineToCartInput): Promise<CartDocument>;
27
51
  /**
28
52
  * Won-auction lines are non-removable and fixed-quantity — the buyer committed
29
53
  * to the purchase by bidding, and letting them delete it would turn every
@@ -34,6 +58,12 @@ export declare const CART_LANE_BLOCKED = "CART_LANE_BLOCKED";
34
58
  * and the offer simply lapses at its `checkoutDeadline`.
35
59
  */
36
60
  export declare function updateCartItem(userId: string, itemId: string, input: UpdateCartItemInput): Promise<CartDocument>;
61
+ /**
62
+ * Replace a grouped line's member quantities. A member set to 0 is dropped;
63
+ * dropping the last one removes the line (enforced in the repository so a
64
+ * forgetful caller can't leave an empty ghost line behind).
65
+ */
66
+ export declare function updateCartGroupMembers(userId: string, itemId: string, input: UpdateCartGroupMembersInput): Promise<CartDocument>;
37
67
  export declare function removeCartItem(userId: string, itemId: string): Promise<CartDocument>;
38
68
  export declare function clearCart(userId: string): Promise<CartDocument>;
39
69
  export declare function mergeGuestCart(userId: string, items: Array<{