@mohasinac/appkit 4.23.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/bundles/actions.js +7 -0
- package/dist/_internal/server/features/checkout/actions.js +30 -21
- 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/actions/action-registry.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/order-math.d.ts +23 -5
- package/dist/_internal/shared/checkout/order-math.js +29 -6
- package/dist/_internal/shared/checkout/rules/_limits.d.ts +12 -2
- package/dist/_internal/shared/checkout/rules/_limits.js +12 -2
- package/dist/_internal/shared/checkout/rules/_registry.js +8 -2
- package/dist/_internal/shared/features/cart/config.d.ts +2 -1
- package/dist/_internal/shared/features/cart/config.js +2 -1
- package/dist/_internal/shared/features/cart/schema.d.ts +6 -6
- package/dist/_internal/shared/features/notifications/action-url.d.ts +47 -0
- package/dist/_internal/shared/features/notifications/action-url.js +111 -0
- 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/constants/notification-types.d.ts +38 -10
- package/dist/constants/notification-types.js +39 -28
- package/dist/features/account/types/index.d.ts +1 -10
- package/dist/features/admin/actions/notification-actions.d.ts +12 -0
- package/dist/features/admin/actions/notification-actions.js +149 -4
- package/dist/features/admin/schemas/firestore.d.ts +24 -29
- package/dist/features/admin/schemas/firestore.js +89 -26
- package/dist/features/cart/actions/cart-actions.d.ts +47 -1
- package/dist/features/cart/actions/cart-actions.js +189 -12
- package/dist/features/cart/components/CartDrawer.js +2 -1
- package/dist/features/cart/repository/cart.repository.d.ts +33 -1
- package/dist/features/cart/repository/cart.repository.js +146 -2
- package/dist/features/cart/schemas/firestore.d.ts +132 -8
- package/dist/features/cart/schemas/firestore.js +22 -0
- package/dist/features/email/index.d.ts +1 -1
- package/dist/features/email/index.js +5 -1
- package/dist/features/email/notification-templates.d.ts +35 -0
- package/dist/features/email/notification-templates.js +96 -0
- package/dist/features/orders/schemas/index.d.ts +2 -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/features/tester/seed-data/tester-checklist-seed-data.js +30 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +7 -1
- package/dist/schemas/registry.d.ts +2 -2
- package/dist/seed/factories/notification.factory.d.ts +2 -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-entry.d.ts +1 -0
- package/dist/server-entry.js +5 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.js +10 -1
- package/dist/styles.css +58 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/QuantityStepper.d.ts +38 -0
- package/dist/ui/components/QuantityStepper.js +20 -0
- package/dist/ui/components/QuantityStepper.style.css +56 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/package.json +1 -1
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { roundRupees } from "../../../utils/number.formatter";
|
|
2
2
|
/**
|
|
3
|
-
* The single source of truth for "what do we charge for this line".
|
|
3
|
+
* The single source of truth for "what do we charge for ONE COPY of this line".
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Four cases, in priority order:
|
|
6
6
|
*
|
|
7
7
|
* 1. **A locked price** — an accepted offer or a won auction. `lockedPrice` is
|
|
8
8
|
* the negotiated/won amount and it OVERRIDES the current listing price, which
|
|
@@ -10,18 +10,33 @@ import { roundRupees } from "../../../utils/number.formatter";
|
|
|
10
10
|
* copies of this function looked at `lockedPrice`, so an accepted offer was
|
|
11
11
|
* billed at the seller's listed price while the cart UI displayed the agreed
|
|
12
12
|
* one (`item.lockedPrice ?? item.price`) — the buyer saw ₹X and paid ₹Y.
|
|
13
|
-
* 2. **A
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* 2. **A multi-member line** — the sum of its members' own snapshotted prices,
|
|
14
|
+
* weighted by each member's per-copy quantity. A `"group"` line pins
|
|
15
|
+
* `item.quantity` to 1, so this is both the unit price and the line total;
|
|
16
|
+
* see the invariant on `CartLineMember`.
|
|
17
|
+
* 3. **A legacy bundle line** (SB-UNI-5 2026-05-13) — written before
|
|
18
|
+
* `groupMembers` existed. `item.price` is the bundle price locked at
|
|
19
|
+
* add-time.
|
|
20
|
+
* 4. **Everything else** — the live Firestore price, so a stale cart-cached
|
|
16
21
|
* price is never charged on a COD/UPI order.
|
|
17
22
|
*
|
|
18
23
|
* `product` may be null for a locked line whose listing has since been archived;
|
|
19
24
|
* the locked price is still owed, so that case must not dereference `product`.
|
|
25
|
+
*
|
|
26
|
+
* 🛑 Do not hand-roll this rule at a call site. It has been re-derived by hand
|
|
27
|
+
* six times, and the two copies that lived in the consumer repo
|
|
28
|
+
* (`src/app/api/payment/create-order/route.ts`) each reproduced the bundle
|
|
29
|
+
* branch while silently omitting case 1 — so Razorpay captured the LIST price
|
|
30
|
+
* for an accepted offer while the cart displayed the negotiated one. Call
|
|
31
|
+
* `lineTotalFor` and let this function stay the only definition.
|
|
20
32
|
*/
|
|
21
33
|
export function unitPriceFor(item, product) {
|
|
22
34
|
if (typeof item.lockedPrice === "number" && item.lockedPrice > 0) {
|
|
23
35
|
return item.lockedPrice;
|
|
24
36
|
}
|
|
37
|
+
if (item.groupMembers?.length) {
|
|
38
|
+
return roundRupees(item.groupMembers.reduce((sum, m) => sum + m.unitPrice * m.quantity, 0));
|
|
39
|
+
}
|
|
25
40
|
if (item.bundleCategorySlug && item.bundleProductIds?.length) {
|
|
26
41
|
return item.price;
|
|
27
42
|
}
|
|
@@ -31,6 +46,14 @@ export function unitPriceFor(item, product) {
|
|
|
31
46
|
// crash whenever a cart line outlives its listing.
|
|
32
47
|
return product?.price ?? item.price;
|
|
33
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* What this line contributes to a subtotal: one copy's price × the number of
|
|
51
|
+
* copies. THE call site should use this rather than multiplying `unitPriceFor`
|
|
52
|
+
* itself — that multiplication is where the six hand-rolled copies diverged.
|
|
53
|
+
*/
|
|
54
|
+
export function lineTotalFor(item, product) {
|
|
55
|
+
return unitPriceFor(item, product) * item.quantity;
|
|
56
|
+
}
|
|
34
57
|
/**
|
|
35
58
|
* P-6 — pre-order groups charge each product's own `preOrderDepositPercent`
|
|
36
59
|
* (falling back to the generic COD deposit % when a product doesn't have one
|
|
@@ -41,7 +64,7 @@ export function unitPriceFor(item, product) {
|
|
|
41
64
|
*/
|
|
42
65
|
export function computePreOrderDepositAmount(group, defaultDepositPercent) {
|
|
43
66
|
const raw = group.reduce((sum, { item, product }) => {
|
|
44
|
-
const lineTotal =
|
|
67
|
+
const lineTotal = lineTotalFor(item, product);
|
|
45
68
|
const pct = product.preOrderDepositPercent ?? defaultDepositPercent;
|
|
46
69
|
return sum + lineTotal * (pct / 100); // audit-money-units-ok: percentage divisor, not paise
|
|
47
70
|
}, 0);
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
/** Checkout window / capacity constants — single source of truth. */
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Maximum distinct line items in one cart (hard cap, enforced at every
|
|
4
|
+
* add-to-cart entry point).
|
|
5
|
+
*
|
|
6
|
+
* Re-exported, not re-declared. This value existed as three independent `= 50`
|
|
7
|
+
* literals — here, in `constants/limits.ts`, and in
|
|
8
|
+
* `_internal/shared/features/cart/config.ts` — with nothing tying them
|
|
9
|
+
* together, so raising the cap in one place would have left two silently
|
|
10
|
+
* disagreeing. `constants/limits.ts` is a dependency-free leaf module, so this
|
|
11
|
+
* costs no import chain.
|
|
12
|
+
*/
|
|
13
|
+
export { CART_MAX_ITEMS } from "../../../../constants/limits";
|
|
4
14
|
/**
|
|
5
15
|
* Maximum number of order documents that can be created in one checkout
|
|
6
16
|
* transaction. If the cart produces more groups than this the checkout is
|
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
/** Checkout window / capacity constants — single source of truth. */
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Maximum distinct line items in one cart (hard cap, enforced at every
|
|
4
|
+
* add-to-cart entry point).
|
|
5
|
+
*
|
|
6
|
+
* Re-exported, not re-declared. This value existed as three independent `= 50`
|
|
7
|
+
* literals — here, in `constants/limits.ts`, and in
|
|
8
|
+
* `_internal/shared/features/cart/config.ts` — with nothing tying them
|
|
9
|
+
* together, so raising the cap in one place would have left two silently
|
|
10
|
+
* disagreeing. `constants/limits.ts` is a dependency-free leaf module, so this
|
|
11
|
+
* costs no import chain.
|
|
12
|
+
*/
|
|
13
|
+
export { CART_MAX_ITEMS } from "../../../../constants/limits";
|
|
4
14
|
/**
|
|
5
15
|
* Maximum number of order documents that can be created in one checkout
|
|
6
16
|
* transaction. If the cart produces more groups than this the checkout is
|
|
@@ -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);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/** Re-exported from the single declaration — see `constants/limits.ts`. */
|
|
2
|
+
export { CART_MAX_ITEMS } from "../../../../constants/limits";
|
|
2
3
|
export declare const CART_SESSION_TTL_DAYS = 30;
|
|
3
4
|
export declare const CART_GUEST_STORAGE_KEY = "appkit:cart";
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
/** Re-exported from the single declaration — see `constants/limits.ts`. */
|
|
2
|
+
export { CART_MAX_ITEMS } from "../../../../constants/limits";
|
|
2
3
|
export const CART_SESSION_TTL_DAYS = 30;
|
|
3
4
|
export const CART_GUEST_STORAGE_KEY = "appkit:cart";
|
|
@@ -22,9 +22,9 @@ export declare const addToCartSchema: z.ZodObject<{
|
|
|
22
22
|
productId: string;
|
|
23
23
|
productTitle: string;
|
|
24
24
|
productImage: string;
|
|
25
|
+
isOffer?: boolean | undefined;
|
|
25
26
|
offerId?: string | undefined;
|
|
26
27
|
lockedPrice?: number | undefined;
|
|
27
|
-
isOffer?: boolean | undefined;
|
|
28
28
|
}, {
|
|
29
29
|
storeId: string;
|
|
30
30
|
price: number;
|
|
@@ -35,9 +35,9 @@ export declare const addToCartSchema: z.ZodObject<{
|
|
|
35
35
|
storeName?: string | undefined;
|
|
36
36
|
quantity?: number | undefined;
|
|
37
37
|
productImage?: string | undefined;
|
|
38
|
+
isOffer?: boolean | undefined;
|
|
38
39
|
offerId?: string | undefined;
|
|
39
40
|
lockedPrice?: number | undefined;
|
|
40
|
-
isOffer?: boolean | undefined;
|
|
41
41
|
}>;
|
|
42
42
|
export declare const updateCartItemSchema: z.ZodObject<{
|
|
43
43
|
productId: z.ZodString;
|
|
@@ -80,9 +80,9 @@ export declare const mergeGuestCartSchema: z.ZodObject<{
|
|
|
80
80
|
productId: string;
|
|
81
81
|
productTitle: string;
|
|
82
82
|
productImage: string;
|
|
83
|
+
isOffer?: boolean | undefined;
|
|
83
84
|
offerId?: string | undefined;
|
|
84
85
|
lockedPrice?: number | undefined;
|
|
85
|
-
isOffer?: boolean | undefined;
|
|
86
86
|
}, {
|
|
87
87
|
storeId: string;
|
|
88
88
|
price: number;
|
|
@@ -93,9 +93,9 @@ export declare const mergeGuestCartSchema: z.ZodObject<{
|
|
|
93
93
|
storeName?: string | undefined;
|
|
94
94
|
quantity?: number | undefined;
|
|
95
95
|
productImage?: string | undefined;
|
|
96
|
+
isOffer?: boolean | undefined;
|
|
96
97
|
offerId?: string | undefined;
|
|
97
98
|
lockedPrice?: number | undefined;
|
|
98
|
-
isOffer?: boolean | undefined;
|
|
99
99
|
}>, "many">;
|
|
100
100
|
}, "strip", z.ZodTypeAny, {
|
|
101
101
|
guestItems: {
|
|
@@ -108,9 +108,9 @@ export declare const mergeGuestCartSchema: z.ZodObject<{
|
|
|
108
108
|
productId: string;
|
|
109
109
|
productTitle: string;
|
|
110
110
|
productImage: string;
|
|
111
|
+
isOffer?: boolean | undefined;
|
|
111
112
|
offerId?: string | undefined;
|
|
112
113
|
lockedPrice?: number | undefined;
|
|
113
|
-
isOffer?: boolean | undefined;
|
|
114
114
|
}[];
|
|
115
115
|
}, {
|
|
116
116
|
guestItems: {
|
|
@@ -123,9 +123,9 @@ export declare const mergeGuestCartSchema: z.ZodObject<{
|
|
|
123
123
|
storeName?: string | undefined;
|
|
124
124
|
quantity?: number | undefined;
|
|
125
125
|
productImage?: string | undefined;
|
|
126
|
+
isOffer?: boolean | undefined;
|
|
126
127
|
offerId?: string | undefined;
|
|
127
128
|
lockedPrice?: number | undefined;
|
|
128
|
-
isOffer?: boolean | undefined;
|
|
129
129
|
}[];
|
|
130
130
|
}>;
|
|
131
131
|
/**
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where does a notification LAND?
|
|
3
|
+
*
|
|
4
|
+
* ## The problem this solves
|
|
5
|
+
*
|
|
6
|
+
* `actionUrl` was set by **5 of 40** `sendNotification()` call sites, so the
|
|
7
|
+
* other 35 arrived in the bell as text with nothing to click. Every writer
|
|
8
|
+
* that did set one hand-wrote the path, which is the same drift the ROUTES
|
|
9
|
+
* map exists to prevent.
|
|
10
|
+
*
|
|
11
|
+
* Worse, the destination is **role-dependent**: an offer notification sent to
|
|
12
|
+
* the buyer belongs at `/user/offers`, the same event sent to the seller at
|
|
13
|
+
* `/store/offers`, and an admin escalation at `/admin/offers`. A per-writer
|
|
14
|
+
* literal cannot express that, so writers that guessed a single path sent
|
|
15
|
+
* half their recipients somewhere they have no permission to be.
|
|
16
|
+
*
|
|
17
|
+
* ## Honest fallbacks, not invented pages
|
|
18
|
+
*
|
|
19
|
+
* `bid`, `offer` and `review` still have **no per-record page in any role** —
|
|
20
|
+
* W8 fixed those dead-end listings with a detail MODAL, not a route. So this
|
|
21
|
+
* resolver points them at the list the record lives in. That is a real,
|
|
22
|
+
* permitted, useful destination; fabricating `/user/bids/{id}` would produce
|
|
23
|
+
* a 404 and `audit-nav-page-wiring` would (correctly) fail on the ROUTES key.
|
|
24
|
+
*
|
|
25
|
+
* When those pages are built, change the entry here and all 40 call sites
|
|
26
|
+
* follow. That is the whole point of the indirection.
|
|
27
|
+
*/
|
|
28
|
+
import type { NotificationRelatedType } from "../../../../features/admin/schemas/firestore";
|
|
29
|
+
/**
|
|
30
|
+
* Which portal the recipient reads this in.
|
|
31
|
+
*
|
|
32
|
+
* Deliberately NOT the `role` string off the session: a seller is also a
|
|
33
|
+
* buyer, and which portal a notification belongs to is a property of the
|
|
34
|
+
* EVENT, not of the person. The order-shipped notification goes to the buyer
|
|
35
|
+
* in `/user`, and the same shipment's payout notification goes to the same
|
|
36
|
+
* human in `/store`.
|
|
37
|
+
*/
|
|
38
|
+
export type NotificationAudience = "buyer" | "seller" | "admin";
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the page a notification should open.
|
|
41
|
+
*
|
|
42
|
+
* Returns `undefined` when there is genuinely nowhere to go — a `promotion`
|
|
43
|
+
* or a `welcome` has no record behind it — so the caller renders a plain,
|
|
44
|
+
* non-clickable row rather than a link to the dashboard the user is already
|
|
45
|
+
* looking at.
|
|
46
|
+
*/
|
|
47
|
+
export declare function resolveNotificationActionUrl(relatedType: NotificationRelatedType | undefined, relatedId: string | undefined, audience: NotificationAudience): string | undefined;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where does a notification LAND?
|
|
3
|
+
*
|
|
4
|
+
* ## The problem this solves
|
|
5
|
+
*
|
|
6
|
+
* `actionUrl` was set by **5 of 40** `sendNotification()` call sites, so the
|
|
7
|
+
* other 35 arrived in the bell as text with nothing to click. Every writer
|
|
8
|
+
* that did set one hand-wrote the path, which is the same drift the ROUTES
|
|
9
|
+
* map exists to prevent.
|
|
10
|
+
*
|
|
11
|
+
* Worse, the destination is **role-dependent**: an offer notification sent to
|
|
12
|
+
* the buyer belongs at `/user/offers`, the same event sent to the seller at
|
|
13
|
+
* `/store/offers`, and an admin escalation at `/admin/offers`. A per-writer
|
|
14
|
+
* literal cannot express that, so writers that guessed a single path sent
|
|
15
|
+
* half their recipients somewhere they have no permission to be.
|
|
16
|
+
*
|
|
17
|
+
* ## Honest fallbacks, not invented pages
|
|
18
|
+
*
|
|
19
|
+
* `bid`, `offer` and `review` still have **no per-record page in any role** —
|
|
20
|
+
* W8 fixed those dead-end listings with a detail MODAL, not a route. So this
|
|
21
|
+
* resolver points them at the list the record lives in. That is a real,
|
|
22
|
+
* permitted, useful destination; fabricating `/user/bids/{id}` would produce
|
|
23
|
+
* a 404 and `audit-nav-page-wiring` would (correctly) fail on the ROUTES key.
|
|
24
|
+
*
|
|
25
|
+
* When those pages are built, change the entry here and all 40 call sites
|
|
26
|
+
* follow. That is the whole point of the indirection.
|
|
27
|
+
*/
|
|
28
|
+
import { ROUTES } from "../../../../next/routing/route-map";
|
|
29
|
+
/**
|
|
30
|
+
* Resolve the page a notification should open.
|
|
31
|
+
*
|
|
32
|
+
* Returns `undefined` when there is genuinely nowhere to go — a `promotion`
|
|
33
|
+
* or a `welcome` has no record behind it — so the caller renders a plain,
|
|
34
|
+
* non-clickable row rather than a link to the dashboard the user is already
|
|
35
|
+
* looking at.
|
|
36
|
+
*/
|
|
37
|
+
export function resolveNotificationActionUrl(relatedType, relatedId, audience) {
|
|
38
|
+
if (!relatedType || !relatedId)
|
|
39
|
+
return undefined;
|
|
40
|
+
switch (relatedType) {
|
|
41
|
+
case "order":
|
|
42
|
+
return audience === "admin"
|
|
43
|
+
? String(ROUTES.ADMIN.ORDER_DETAIL(relatedId))
|
|
44
|
+
: audience === "seller"
|
|
45
|
+
? String(ROUTES.STORE.ORDER_DETAIL(relatedId))
|
|
46
|
+
: String(ROUTES.USER.ORDER_DETAIL(relatedId));
|
|
47
|
+
case "product":
|
|
48
|
+
// The public listing page — the one destination that is the same for
|
|
49
|
+
// everyone, because a product page is public.
|
|
50
|
+
return String(ROUTES.PUBLIC.PRODUCT_DETAIL(relatedId));
|
|
51
|
+
// ── List-level, because no per-record page exists in ANY role ─────────
|
|
52
|
+
// W8 gave these listings a detail modal rather than a route. Linking the
|
|
53
|
+
// list is honest; a fabricated `/…/{id}` would 404.
|
|
54
|
+
case "bid":
|
|
55
|
+
return audience === "admin"
|
|
56
|
+
? String(ROUTES.ADMIN.BIDS)
|
|
57
|
+
: audience === "seller"
|
|
58
|
+
? String(ROUTES.STORE.BIDS)
|
|
59
|
+
: String(ROUTES.USER.BIDS);
|
|
60
|
+
case "offer":
|
|
61
|
+
return audience === "admin"
|
|
62
|
+
? String(ROUTES.ADMIN.OFFERS)
|
|
63
|
+
: audience === "seller"
|
|
64
|
+
? String(ROUTES.STORE.OFFERS)
|
|
65
|
+
: String(ROUTES.USER.OFFERS);
|
|
66
|
+
case "review":
|
|
67
|
+
// The public review page exists; the admin/seller sides are modals.
|
|
68
|
+
return String(ROUTES.PUBLIC.REVIEW_DETAIL(relatedId));
|
|
69
|
+
case "blog":
|
|
70
|
+
return String(ROUTES.BLOG.ARTICLE(relatedId));
|
|
71
|
+
case "user":
|
|
72
|
+
// Only an admin has a per-user page. A user notified ABOUT themselves
|
|
73
|
+
// (a ban, a role change) belongs on their own account hub, not on a
|
|
74
|
+
// page they cannot open.
|
|
75
|
+
return audience === "admin"
|
|
76
|
+
? String(ROUTES.ADMIN.USER_DETAIL(relatedId))
|
|
77
|
+
: String(ROUTES.USER.PROFILE);
|
|
78
|
+
case "support_ticket":
|
|
79
|
+
return audience === "admin"
|
|
80
|
+
? String(ROUTES.ADMIN.SUPPORT_TICKET_BY_ID(relatedId))
|
|
81
|
+
: String(ROUTES.USER.SUPPORT_TICKET(relatedId));
|
|
82
|
+
case "scammer":
|
|
83
|
+
// Admin-only surface — the public scam registry is browsed, not
|
|
84
|
+
// notified about.
|
|
85
|
+
return audience === "admin" ? String(ROUTES.ADMIN.SCAMMER_BY_ID(relatedId)) : undefined;
|
|
86
|
+
case "catalogueItem":
|
|
87
|
+
// The owner edits it; an admin reviews it from the approvals queue,
|
|
88
|
+
// which is a list because approval is a queue-shaped job.
|
|
89
|
+
return audience === "admin"
|
|
90
|
+
? String(ROUTES.ADMIN.CATALOGUE_APPROVALS)
|
|
91
|
+
: String(ROUTES.USER.CATALOGUE_EDIT(relatedId));
|
|
92
|
+
case "payout":
|
|
93
|
+
return audience === "admin"
|
|
94
|
+
? String(ROUTES.ADMIN.PAYOUT_DETAIL(relatedId))
|
|
95
|
+
: String(ROUTES.STORE.PAYOUT_DETAIL(relatedId));
|
|
96
|
+
case "store":
|
|
97
|
+
return audience === "admin"
|
|
98
|
+
? String(ROUTES.ADMIN.STORE_DETAIL(relatedId))
|
|
99
|
+
: String(ROUTES.STORE.DASHBOARD);
|
|
100
|
+
default: {
|
|
101
|
+
/*
|
|
102
|
+
* Exhaustiveness. A new `relatedType` cannot compile without an answer
|
|
103
|
+
* to "where does this land", which is precisely the check that was
|
|
104
|
+
* missing when `catalogueItem` was smuggled in behind an `as never`.
|
|
105
|
+
*/
|
|
106
|
+
const _exhaustive = relatedType;
|
|
107
|
+
void _exhaustive;
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
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
|
|
@@ -1,18 +1,46 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Notification Type Registry —
|
|
2
|
+
* Notification Type Registry — DERIVED, not restated.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - AdminNotificationsView filter chips
|
|
6
|
-
* - NotificationPreferencesPanel (per-type toggles)
|
|
7
|
-
* - sendNotification() dispatch (type-based channel routing)
|
|
4
|
+
* ## What this file used to be, and why it mattered
|
|
8
5
|
*
|
|
9
|
-
*
|
|
6
|
+
* A hand-written 9-value list that called itself "canonical" while
|
|
7
|
+
* `NotificationDocument.type` held 27. Two of its nine —`review_posted` and
|
|
8
|
+
* `payout_processed` — were not real values at all, so those chips matched
|
|
9
|
+
* zero rows forever. Its two consumers were not cosmetic:
|
|
10
|
+
*
|
|
11
|
+
* · `AdminNotificationsView`'s filter chips, so an admin could not filter
|
|
12
|
+
* **18 of the 27 real types** and was offered two that never appear.
|
|
13
|
+
* · `AdminSiteSettingsView`'s per-channel allow-list, so an admin literally
|
|
14
|
+
* **could not allow-list `offer_received` or `payment_review`** for email
|
|
15
|
+
* or WhatsApp — the toggle did not exist.
|
|
16
|
+
*
|
|
17
|
+
* Everything here now derives from `NOTIFICATION_TYPE_VALUES`, so a new
|
|
18
|
+
* notification type is one edit and cannot be missing from either surface.
|
|
19
|
+
*
|
|
20
|
+
* ## Why the import points where it does
|
|
21
|
+
*
|
|
22
|
+
* `features/admin/schemas/firestore.ts` imports nothing from `constants/`,
|
|
23
|
+
* so this direction is acyclic. That is NOT true of `field-names.ts`, which
|
|
24
|
+
* is a deliberate leaf that `features/products/types` imports — which is why
|
|
25
|
+
* that file keeps its duplicated values and relies on an audit instead.
|
|
26
|
+
*/
|
|
27
|
+
import { NOTIFICATION_TYPE_VALUES } from "../features/admin/schemas/firestore";
|
|
28
|
+
import type { NotificationType } from "../features/admin/schemas/firestore";
|
|
29
|
+
export { NOTIFICATION_TYPE_VALUES };
|
|
30
|
+
export type { NotificationType };
|
|
31
|
+
/** Alias kept for the two existing importers. */
|
|
32
|
+
export declare const NOTIFICATION_TYPES: readonly ["order_placed", "order_confirmed", "order_shipped", "order_delivered", "order_cancelled", "bid_placed", "bid_outbid", "bid_won", "bid_lost", "auction_ended", "review_approved", "review_replied", "product_available", "promotion", "system", "welcome", "account_action", "offer_received", "offer_responded", "offer_expired", "offer_counter_accepted", "refund_initiated", "prize_won", "prize_reveal_expired", "emi_installment_due_soon", "emi_installment_overdue", "payment_review", "catalogue_images_stale"];
|
|
33
|
+
/**
|
|
34
|
+
* `order_shipped` → `Order shipped`.
|
|
35
|
+
*
|
|
36
|
+
* Derived rather than hand-labelled: a hand-written label map is the same
|
|
37
|
+
* drift risk one layer down, and every one of these values is already a
|
|
38
|
+
* readable snake_case phrase.
|
|
10
39
|
*/
|
|
11
|
-
export declare
|
|
12
|
-
export type NotificationType = (typeof NOTIFICATION_TYPES)[number];
|
|
40
|
+
export declare function notificationTypeLabel(type: NotificationType | string): string;
|
|
13
41
|
/**
|
|
14
|
-
* Shape used by filter-chip rows (id + display label)
|
|
15
|
-
*
|
|
42
|
+
* Shape used by filter-chip rows (id + display label), with the usual "All"
|
|
43
|
+
* option prepended.
|
|
16
44
|
*/
|
|
17
45
|
export declare const NOTIFICATION_TYPE_TABS: ReadonlyArray<{
|
|
18
46
|
id: string;
|
|
@@ -1,37 +1,48 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Notification Type Registry —
|
|
2
|
+
* Notification Type Registry — DERIVED, not restated.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - AdminNotificationsView filter chips
|
|
6
|
-
* - NotificationPreferencesPanel (per-type toggles)
|
|
7
|
-
* - sendNotification() dispatch (type-based channel routing)
|
|
4
|
+
* ## What this file used to be, and why it mattered
|
|
8
5
|
*
|
|
9
|
-
*
|
|
6
|
+
* A hand-written 9-value list that called itself "canonical" while
|
|
7
|
+
* `NotificationDocument.type` held 27. Two of its nine —`review_posted` and
|
|
8
|
+
* `payout_processed` — were not real values at all, so those chips matched
|
|
9
|
+
* zero rows forever. Its two consumers were not cosmetic:
|
|
10
|
+
*
|
|
11
|
+
* · `AdminNotificationsView`'s filter chips, so an admin could not filter
|
|
12
|
+
* **18 of the 27 real types** and was offered two that never appear.
|
|
13
|
+
* · `AdminSiteSettingsView`'s per-channel allow-list, so an admin literally
|
|
14
|
+
* **could not allow-list `offer_received` or `payment_review`** for email
|
|
15
|
+
* or WhatsApp — the toggle did not exist.
|
|
16
|
+
*
|
|
17
|
+
* Everything here now derives from `NOTIFICATION_TYPE_VALUES`, so a new
|
|
18
|
+
* notification type is one edit and cannot be missing from either surface.
|
|
19
|
+
*
|
|
20
|
+
* ## Why the import points where it does
|
|
21
|
+
*
|
|
22
|
+
* `features/admin/schemas/firestore.ts` imports nothing from `constants/`,
|
|
23
|
+
* so this direction is acyclic. That is NOT true of `field-names.ts`, which
|
|
24
|
+
* is a deliberate leaf that `features/products/types` imports — which is why
|
|
25
|
+
* that file keeps its duplicated values and relies on an audit instead.
|
|
10
26
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
27
|
+
import { NOTIFICATION_TYPE_VALUES } from "../features/admin/schemas/firestore";
|
|
28
|
+
export { NOTIFICATION_TYPE_VALUES };
|
|
29
|
+
/** Alias kept for the two existing importers. */
|
|
30
|
+
export const NOTIFICATION_TYPES = NOTIFICATION_TYPE_VALUES;
|
|
31
|
+
/**
|
|
32
|
+
* `order_shipped` → `Order shipped`.
|
|
33
|
+
*
|
|
34
|
+
* Derived rather than hand-labelled: a hand-written label map is the same
|
|
35
|
+
* drift risk one layer down, and every one of these values is already a
|
|
36
|
+
* readable snake_case phrase.
|
|
37
|
+
*/
|
|
38
|
+
export function notificationTypeLabel(type) {
|
|
39
|
+
return String(type).replace(/_/g, " ").replace(/^./, (c) => c.toUpperCase());
|
|
40
|
+
}
|
|
22
41
|
/**
|
|
23
|
-
* Shape used by filter-chip rows (id + display label)
|
|
24
|
-
*
|
|
42
|
+
* Shape used by filter-chip rows (id + display label), with the usual "All"
|
|
43
|
+
* option prepended.
|
|
25
44
|
*/
|
|
26
45
|
export const NOTIFICATION_TYPE_TABS = [
|
|
27
46
|
{ id: "All", label: "All" },
|
|
28
|
-
{ id
|
|
29
|
-
{ id: "order_shipped", label: "Order shipped" },
|
|
30
|
-
{ id: "order_delivered", label: "Order delivered" },
|
|
31
|
-
{ id: "order_cancelled", label: "Order cancelled" },
|
|
32
|
-
{ id: "bid_placed", label: "Bid placed" },
|
|
33
|
-
{ id: "bid_outbid", label: "Bid outbid" },
|
|
34
|
-
{ id: "bid_won", label: "Bid won" },
|
|
35
|
-
{ id: "review_posted", label: "Review posted" },
|
|
36
|
-
{ id: "payout_processed", label: "Payout processed" },
|
|
47
|
+
...NOTIFICATION_TYPE_VALUES.map((id) => ({ id, label: notificationTypeLabel(id) })),
|
|
37
48
|
];
|
|
@@ -37,16 +37,7 @@ export interface UserOrder {
|
|
|
37
37
|
createdAt?: string;
|
|
38
38
|
updatedAt?: string;
|
|
39
39
|
}
|
|
40
|
-
export type NotificationType
|
|
41
|
-
export interface UserNotification {
|
|
42
|
-
id: string;
|
|
43
|
-
title: string;
|
|
44
|
-
message: string;
|
|
45
|
-
type: NotificationType;
|
|
46
|
-
isRead: boolean;
|
|
47
|
-
createdAt: string;
|
|
48
|
-
href?: string;
|
|
49
|
-
}
|
|
40
|
+
export type { NotificationType } from "../../admin/schemas/firestore";
|
|
50
41
|
export type OfferStatus = "pending" | "countered" | "accepted" | "paid" | "declined" | "expired" | "withdrawn";
|
|
51
42
|
export interface UserOffer {
|
|
52
43
|
id: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NotificationDocument, NotificationCreateInput } from "../schemas";
|
|
2
|
+
import { type NotificationAudience } from "../../../_internal/shared/features/notifications/action-url";
|
|
2
3
|
export declare function markNotificationRead(id: string): Promise<void>;
|
|
3
4
|
export declare function markAllNotificationsRead(userId: string): Promise<number>;
|
|
4
5
|
export declare function deleteNotification(id: string): Promise<void>;
|
|
@@ -16,6 +17,17 @@ export interface SendNotificationInput extends NotificationCreateInput {
|
|
|
16
17
|
emailHtml?: string;
|
|
17
18
|
/** Buyer paid the ₹10 WhatsApp order-updates addon — enables WhatsApp channel for this notification even when the user has no standing WhatsApp subscription. */
|
|
18
19
|
orderWhatsappAddonPaid?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Which portal the recipient reads this in — decides where `actionUrl`
|
|
22
|
+
* points when the caller does not supply one.
|
|
23
|
+
*
|
|
24
|
+
* Defaults from the notification TYPE, not from the recipient's role: a
|
|
25
|
+
* seller is also a buyer, and `order_shipped` belongs in `/user` while the
|
|
26
|
+
* payout for that same order belongs in `/store`. Pass it explicitly only
|
|
27
|
+
* when one type genuinely reaches two audiences — `offer_received` goes to
|
|
28
|
+
* the seller, `offer_responded` back to the buyer.
|
|
29
|
+
*/
|
|
30
|
+
audience?: NotificationAudience;
|
|
19
31
|
}
|
|
20
32
|
export interface SendNotificationResult {
|
|
21
33
|
notification: NotificationDocument;
|