@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.
- package/dist/_internal/server/features/checkout/actions.js +6 -5
- package/dist/_internal/server/features/checkout/bundle-expansion.d.ts +4 -6
- package/dist/_internal/server/features/checkout/bundle-expansion.js +19 -17
- package/dist/_internal/server/features/checkout/stock-restore.js +6 -5
- package/dist/_internal/server/features/products/group-members.d.ts +55 -0
- package/dist/_internal/server/features/products/group-members.js +41 -0
- package/dist/_internal/shared/checkout/line-members.d.ts +60 -0
- package/dist/_internal/shared/checkout/line-members.js +68 -0
- package/dist/_internal/shared/checkout/rules/_registry.js +8 -2
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +27 -0
- package/dist/constants/api-endpoints.js +9 -0
- package/dist/features/admin/schemas/firestore.d.ts +10 -26
- package/dist/features/admin/schemas/firestore.js +10 -26
- package/dist/features/cart/actions/cart-actions.d.ts +31 -1
- package/dist/features/cart/actions/cart-actions.js +163 -0
- package/dist/features/cart/repository/cart.repository.d.ts +33 -1
- package/dist/features/cart/repository/cart.repository.js +146 -2
- package/dist/features/products/components/GroupMemberPicker.d.ts +46 -0
- package/dist/features/products/components/GroupMemberPicker.js +108 -0
- package/dist/features/products/components/ShowGroupSection.js +7 -9
- package/dist/index.d.ts +2 -1
- package/dist/index.js +5 -1
- package/dist/seed/notifications-seed-data.js +225 -0
- package/dist/seed/payouts-seed-data.js +47 -0
- package/dist/seed/scammers-seed-data.js +34 -0
- package/dist/seed/stores-seed-data.js +34 -0
- package/dist/seed/support-tickets-seed-data.js +34 -0
- package/dist/server.d.ts +1 -1
- package/dist/server.js +1 -1
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- 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.
|
|
760
|
-
|
|
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
|
-
//
|
|
1410
|
-
const preflightPairs = productChecks.filter((p) => p.product !== null && p.product !== undefined && !p.item
|
|
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
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
59
|
-
|
|
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:
|
|
61
|
+
return { productId: member.productId, availableQty: product?.availableQuantity ?? 0 };
|
|
63
62
|
}
|
|
64
|
-
|
|
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:
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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<{
|