@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
|
@@ -7,6 +7,30 @@ import { isChannelHealthy, withChannelRetry } from "../../../_internal/server/no
|
|
|
7
7
|
import { enqueueJob } from "../../jobs/actions/enqueue-job";
|
|
8
8
|
import { serverLogger } from "../../../monitoring";
|
|
9
9
|
import { decryptPii } from "../../../security/index";
|
|
10
|
+
import { resolveNotificationActionUrl, } from "../../../_internal/shared/features/notifications/action-url";
|
|
11
|
+
import { renderNotificationEmail } from "../../email/notification-templates";
|
|
12
|
+
/**
|
|
13
|
+
* A notification's `actionUrl` is a site-relative path — correct for the
|
|
14
|
+
* in-app bell, useless in an email, where a bare `/user/orders/x` is not a
|
|
15
|
+
* link at all.
|
|
16
|
+
*
|
|
17
|
+
* `NEXT_PUBLIC_APP_URL` is the same source `ApiClient` uses for its own
|
|
18
|
+
* server-side base, so there is one answer to "what host is this" rather than
|
|
19
|
+
* a second one on `siteSettings` to drift from it.
|
|
20
|
+
*
|
|
21
|
+
* Falls back to returning the path unchanged rather than guessing a host: a
|
|
22
|
+
* wrong absolute URL sends the reader to someone else's site, while a
|
|
23
|
+
* relative one merely fails to be clickable. The template also drops the
|
|
24
|
+
* button entirely when this is undefined.
|
|
25
|
+
*/
|
|
26
|
+
function absoluteActionUrl(path) {
|
|
27
|
+
if (!path)
|
|
28
|
+
return undefined;
|
|
29
|
+
if (/^https?:\/\//i.test(path))
|
|
30
|
+
return path;
|
|
31
|
+
const base = process.env.NEXT_PUBLIC_APP_URL?.replace(/\/+$/, "");
|
|
32
|
+
return base ? `${base}${path}` : path;
|
|
33
|
+
}
|
|
10
34
|
import { DEFAULT_NOTIFICATION_CHANNELS, meetsMinPriority, } from "../schemas/firestore";
|
|
11
35
|
/**
|
|
12
36
|
* Notification types whose WhatsApp send is gated behind the buyer's
|
|
@@ -24,7 +48,27 @@ const ORDER_GATED_TYPES = new Set([
|
|
|
24
48
|
"order_cancelled",
|
|
25
49
|
"refund_initiated",
|
|
26
50
|
]);
|
|
27
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Notification type → the flat `SiteSettingsCredentials` field holding its
|
|
53
|
+
* approved Meta template name.
|
|
54
|
+
*
|
|
55
|
+
* 🛑 **`Partial` here is CORRECT and is not the `typeToPrefsKey` mistake.**
|
|
56
|
+
*
|
|
57
|
+
* The distinction is what a missing key MEANS. A missing `typeToPrefsKey`
|
|
58
|
+
* entry silently ignored a user's opt-out — a real setting that read as
|
|
59
|
+
* honoured and was not. A missing entry here means "no Meta-approved template
|
|
60
|
+
* exists for this type", which is a fact about the Meta account, not a bug:
|
|
61
|
+
* six of the 28 types have one, and the rest legitimately fall back to
|
|
62
|
+
* free-form (which Meta accepts inside the 24-hour customer-service window
|
|
63
|
+
* and rejects outside it — the runner already logs that rejection).
|
|
64
|
+
*
|
|
65
|
+
* Getting six approved templates is a Meta Business review process, not a
|
|
66
|
+
* code change, so listing all 28 here with empty values would be a
|
|
67
|
+
* `Record<…>` that lies about coverage.
|
|
68
|
+
*
|
|
69
|
+
* The six that ARE approved are the order lifecycle, which is also the set
|
|
70
|
+
* gated behind the buyer's ₹10 addon above — the two lists agree on purpose.
|
|
71
|
+
*/
|
|
28
72
|
const WHATSAPP_TEMPLATE_FIELD = {
|
|
29
73
|
order_placed: "whatsappTemplateOrderPlaced",
|
|
30
74
|
order_confirmed: "whatsappTemplateOrderConfirmed",
|
|
@@ -56,6 +100,46 @@ export async function listNotifications(userId, limit = 20) {
|
|
|
56
100
|
export async function getUnreadNotificationCount(userId) {
|
|
57
101
|
return notificationRepository.getUnreadCount(userId);
|
|
58
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Which portal each notification type belongs to.
|
|
105
|
+
*
|
|
106
|
+
* `Record<NotificationType, …>`, so a new type cannot compile without an
|
|
107
|
+
* answer — the same discipline `typeToPrefsKey` needed after it silently
|
|
108
|
+
* omitted three types and ignored those users' opt-outs.
|
|
109
|
+
*/
|
|
110
|
+
const TYPE_AUDIENCE = {
|
|
111
|
+
// Buyer-facing: the order lifecycle they are waiting on.
|
|
112
|
+
order_placed: "buyer",
|
|
113
|
+
order_confirmed: "buyer",
|
|
114
|
+
order_shipped: "buyer",
|
|
115
|
+
order_delivered: "buyer",
|
|
116
|
+
order_cancelled: "buyer",
|
|
117
|
+
refund_initiated: "buyer",
|
|
118
|
+
payment_review: "buyer",
|
|
119
|
+
emi_installment_due_soon: "buyer",
|
|
120
|
+
emi_installment_overdue: "buyer",
|
|
121
|
+
prize_won: "buyer",
|
|
122
|
+
prize_reveal_expired: "buyer",
|
|
123
|
+
product_available: "buyer",
|
|
124
|
+
bid_placed: "buyer",
|
|
125
|
+
bid_outbid: "buyer",
|
|
126
|
+
bid_won: "buyer",
|
|
127
|
+
bid_lost: "buyer",
|
|
128
|
+
offer_responded: "buyer",
|
|
129
|
+
offer_counter_accepted: "buyer",
|
|
130
|
+
offer_expired: "buyer",
|
|
131
|
+
review_replied: "buyer",
|
|
132
|
+
catalogue_images_stale: "buyer",
|
|
133
|
+
// Seller-facing.
|
|
134
|
+
auction_ended: "seller", // "your auction closed with no winner"
|
|
135
|
+
offer_received: "seller",
|
|
136
|
+
review_approved: "seller",
|
|
137
|
+
// Account-level, which lives in the user portal for everyone.
|
|
138
|
+
welcome: "buyer",
|
|
139
|
+
account_action: "buyer",
|
|
140
|
+
system: "buyer",
|
|
141
|
+
promotion: "buyer",
|
|
142
|
+
};
|
|
59
143
|
/**
|
|
60
144
|
* Create an in-app notification and fan out to enabled external channels.
|
|
61
145
|
*
|
|
@@ -67,9 +151,26 @@ export async function getUnreadNotificationCount(userId) {
|
|
|
67
151
|
* 5. The required credentials are set in siteSettings.credentials
|
|
68
152
|
*/
|
|
69
153
|
export async function sendNotification(input) {
|
|
70
|
-
const { userEmail, userPhone, emailHtml, ...notifInput } = input;
|
|
154
|
+
const { userEmail, userPhone, emailHtml, audience, ...notifInput } = input;
|
|
155
|
+
/*
|
|
156
|
+
* Fill `actionUrl` when the caller did not.
|
|
157
|
+
*
|
|
158
|
+
* It was set by 5 of 40 call sites, so 35 notifications arrived in the bell
|
|
159
|
+
* as text with nothing to click. Resolving it HERE rather than asking 35
|
|
160
|
+
* writers to remember means the destination is right by construction, and
|
|
161
|
+
* changing where a `bid` notification lands is one edit rather than a sweep.
|
|
162
|
+
*
|
|
163
|
+
* A caller-supplied value always wins — a few writers legitimately point at
|
|
164
|
+
* something the relatedType cannot express (the payment-proof upload page
|
|
165
|
+
* rather than the order page).
|
|
166
|
+
*/
|
|
167
|
+
const resolvedActionUrl = notifInput.actionUrl ??
|
|
168
|
+
resolveNotificationActionUrl(notifInput.relatedType, notifInput.relatedId, audience ?? TYPE_AUDIENCE[notifInput.type] ?? "buyer");
|
|
71
169
|
// Always write the in-app notification first.
|
|
72
|
-
const notification = await notificationRepository.create(
|
|
170
|
+
const notification = await notificationRepository.create({
|
|
171
|
+
...notifInput,
|
|
172
|
+
...(resolvedActionUrl ? { actionUrl: resolvedActionUrl } : {}),
|
|
173
|
+
});
|
|
73
174
|
// Load channel config + credentials (one Firestore read).
|
|
74
175
|
let settings;
|
|
75
176
|
try {
|
|
@@ -102,6 +203,20 @@ export async function sendNotification(input) {
|
|
|
102
203
|
const resolvedPhone = userPhone ??
|
|
103
204
|
(fetchedUserDoc?.phoneNumber ? decryptPii(fetchedUserDoc.phoneNumber) : undefined);
|
|
104
205
|
// Map NotificationType to a NotificationTypePrefs key.
|
|
206
|
+
/*
|
|
207
|
+
* 🛑 COMPLETE and compile-checked: `Record<NotificationType, …>`, not
|
|
208
|
+
* `Partial<Record<string, …>>`.
|
|
209
|
+
*
|
|
210
|
+
* The partial version silently omitted `emi_installment_due_soon`,
|
|
211
|
+
* `emi_installment_overdue` and `payment_review` — and a missing key means
|
|
212
|
+
* `typeKey` is undefined, so the opt-out check below never fires and the
|
|
213
|
+
* user's preference is IGNORED. A silently-ignored opt-out is worse than a
|
|
214
|
+
* missing feature: the toggle is right there, it reads as honoured, and
|
|
215
|
+
* mail keeps arriving.
|
|
216
|
+
*
|
|
217
|
+
* Typed on the union so the next type added cannot compile without an
|
|
218
|
+
* answer to "which toggle silences this".
|
|
219
|
+
*/
|
|
105
220
|
const typeToPrefsKey = {
|
|
106
221
|
order_placed: "orderUpdates", order_confirmed: "orderUpdates",
|
|
107
222
|
order_shipped: "orderUpdates", order_delivered: "orderUpdates",
|
|
@@ -116,6 +231,19 @@ export async function sendNotification(input) {
|
|
|
116
231
|
product_available: "system",
|
|
117
232
|
prize_won: "orderUpdates",
|
|
118
233
|
prize_reveal_expired: "orderUpdates",
|
|
234
|
+
// Money owed on an order. Under orderUpdates rather than a new toggle:
|
|
235
|
+
// an EMI installment IS an order update, and inventing an eighth
|
|
236
|
+
// preference key means an existing user's saved prefs have no value for
|
|
237
|
+
// it — which reads as "off" or "on" depending on the default and is a
|
|
238
|
+
// migration nobody asked for.
|
|
239
|
+
emi_installment_due_soon: "orderUpdates",
|
|
240
|
+
emi_installment_overdue: "orderUpdates",
|
|
241
|
+
// The manual-payment review queue — the buyer's proof was verified,
|
|
242
|
+
// rejected, or needs re-uploading. Squarely an order update.
|
|
243
|
+
payment_review: "orderUpdates",
|
|
244
|
+
// "Your catalogue photos are going stale" — housekeeping about the user's
|
|
245
|
+
// own account, which is what `system` covers.
|
|
246
|
+
catalogue_images_stale: "system",
|
|
119
247
|
};
|
|
120
248
|
const typeKey = typeToPrefsKey[type];
|
|
121
249
|
// If the user has explicitly disabled this notification type, skip all external channels.
|
|
@@ -138,7 +266,24 @@ export async function sendNotification(input) {
|
|
|
138
266
|
await withChannelRetry("email", () => emailProvider.send({
|
|
139
267
|
to: resolvedEmail,
|
|
140
268
|
subject: title,
|
|
141
|
-
|
|
269
|
+
/*
|
|
270
|
+
* A real template, not `<p>{message}</p>`.
|
|
271
|
+
*
|
|
272
|
+
* `emailHtml` existed on the input from the day the channel was
|
|
273
|
+
* built and NOT ONE of the 40 call sites passed it, so all 28
|
|
274
|
+
* types shipped a bare paragraph — no framing, no branding,
|
|
275
|
+
* nothing to click. Rendering here rather than asking 40 writers
|
|
276
|
+
* to remember is what makes that structurally impossible to
|
|
277
|
+
* regress: a caller-supplied `emailHtml` still wins, for the
|
|
278
|
+
* handful of flows with genuinely bespoke mail.
|
|
279
|
+
*/
|
|
280
|
+
html: emailHtml ??
|
|
281
|
+
renderNotificationEmail(type, {
|
|
282
|
+
title,
|
|
283
|
+
message,
|
|
284
|
+
siteName: fromName,
|
|
285
|
+
actionUrl: absoluteActionUrl(resolvedActionUrl),
|
|
286
|
+
}),
|
|
142
287
|
text: message,
|
|
143
288
|
}));
|
|
144
289
|
emailStatus = "sent";
|
|
@@ -4,7 +4,18 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { AboutContentDocument } from "../../about/schemas/firestore";
|
|
6
6
|
export declare const ADMIN_CHECKOUT_BYPASS_FLAG_KEY: "adminCheckoutBypass";
|
|
7
|
-
export
|
|
7
|
+
export declare const NOTIFICATION_TYPE_VALUES: 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"];
|
|
8
|
+
export type NotificationType = (typeof NOTIFICATION_TYPE_VALUES)[number];
|
|
9
|
+
/**
|
|
10
|
+
* What a notification points AT. Drives the shared `actionUrl` resolver, so a
|
|
11
|
+
* value here without a per-record page in some role is a notification that
|
|
12
|
+
* lands nowhere.
|
|
13
|
+
*
|
|
14
|
+
* Runtime array for the same reason as the type union above — and
|
|
15
|
+
* `catalogueItem` was being sent with an `as never` cast rather than added.
|
|
16
|
+
*/
|
|
17
|
+
export declare const NOTIFICATION_RELATED_TYPE_VALUES: readonly ["order", "product", "bid", "review", "blog", "user", "offer", "support_ticket", "scammer", "catalogueItem", "payout", "store"];
|
|
18
|
+
export type NotificationRelatedType = (typeof NOTIFICATION_RELATED_TYPE_VALUES)[number];
|
|
8
19
|
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
9
20
|
export type NotificationPriority = "low" | "normal" | "high";
|
|
10
21
|
export interface NotificationDocument extends BaseDocument {
|
|
@@ -19,7 +30,7 @@ export interface NotificationDocument extends BaseDocument {
|
|
|
19
30
|
isRead: boolean;
|
|
20
31
|
readAt?: Date;
|
|
21
32
|
relatedId?: string;
|
|
22
|
-
relatedType?:
|
|
33
|
+
relatedType?: NotificationRelatedType;
|
|
23
34
|
/** Outcome of the async whatsappNotify job for this notification's WhatsApp send, if one was enqueued. */
|
|
24
35
|
whatsappStatus?: "queued" | "sent" | "failed" | "skipped";
|
|
25
36
|
/** jobs/{id} doc that ran (or is running) the WhatsApp send for this notification. */
|
|
@@ -43,32 +54,16 @@ export declare const NOTIFICATION_FIELDS: {
|
|
|
43
54
|
readonly RELATED_TYPE: "relatedType";
|
|
44
55
|
readonly CREATED_AT: "createdAt";
|
|
45
56
|
readonly UPDATED_AT: "updatedAt";
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
readonly AUCTION_ENDED: NotificationType;
|
|
57
|
-
readonly REVIEW_APPROVED: NotificationType;
|
|
58
|
-
readonly REVIEW_REPLIED: NotificationType;
|
|
59
|
-
readonly PRODUCT_AVAILABLE: NotificationType;
|
|
60
|
-
readonly PROMOTION: NotificationType;
|
|
61
|
-
readonly SYSTEM: NotificationType;
|
|
62
|
-
readonly WELCOME: NotificationType;
|
|
63
|
-
readonly OFFER_RECEIVED: NotificationType;
|
|
64
|
-
readonly OFFER_RESPONDED: NotificationType;
|
|
65
|
-
readonly OFFER_EXPIRED: NotificationType;
|
|
66
|
-
readonly OFFER_COUNTER_ACCEPTED: NotificationType;
|
|
67
|
-
readonly REFUND_INITIATED: NotificationType;
|
|
68
|
-
readonly ACCOUNT_ACTION: NotificationType;
|
|
69
|
-
readonly PRIZE_WON: NotificationType;
|
|
70
|
-
readonly PRIZE_REVEAL_EXPIRED: NotificationType;
|
|
71
|
-
};
|
|
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; };
|
|
72
67
|
readonly PRIORITY_VALUES: {
|
|
73
68
|
readonly LOW: NotificationPriority;
|
|
74
69
|
readonly NORMAL: NotificationPriority;
|
|
@@ -82,7 +77,7 @@ export type NotificationUpdateInput = Partial<Pick<NotificationDocument, "isRead
|
|
|
82
77
|
export declare const notificationQueryHelpers: {
|
|
83
78
|
readonly byUser: (userId: string) => readonly ["userId", "==", string];
|
|
84
79
|
readonly unread: (userId: string) => readonly [readonly ["userId", "==", string], readonly ["isRead", "==", false]];
|
|
85
|
-
readonly byType: (type: NotificationType) => readonly ["type", "==",
|
|
80
|
+
readonly byType: (type: NotificationType) => readonly ["type", "==", "system" | "welcome" | "promotion" | "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" | "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"];
|
|
86
81
|
};
|
|
87
82
|
export interface ChatRoomDocument extends BaseDocument {
|
|
88
83
|
buyerId: string;
|
|
@@ -8,6 +8,85 @@ import { DEFAULT_AUCTION_BID_INCREMENT_TIERS } from "../../../_internal/shared/f
|
|
|
8
8
|
// single occurrence in schemas. Splitting the literal makes the substring
|
|
9
9
|
// audit-invisible while keeping the runtime key correct.
|
|
10
10
|
export const ADMIN_CHECKOUT_BYPASS_FLAG_KEY = "adminCheckoutBypass";
|
|
11
|
+
/*
|
|
12
|
+
* 🛑 The ONE notification-type union. Four existed until 2026-08-26:
|
|
13
|
+
*
|
|
14
|
+
* · this one (27) — what `NotificationDocument.type` actually holds
|
|
15
|
+
* · `constants/notification-types.ts` (9) — fed the admin filter chips and
|
|
16
|
+
* the per-channel allow-list, so the admin list could not filter 18 of
|
|
17
|
+
* the 27 real types and an admin literally could not allow-list
|
|
18
|
+
* `offer_received` or `payment_review` for email or WhatsApp
|
|
19
|
+
* · `features/account/types/index.ts` (5: order|offer|promo|system|message)
|
|
20
|
+
* — a different vocabulary entirely, on a `UserNotification` type with
|
|
21
|
+
* zero real consumers
|
|
22
|
+
* · `seed/factories/notification.factory.ts` (9) — and THIS is the one
|
|
23
|
+
* `appkit/src/index.ts` exported publicly as `NotificationType`, so every
|
|
24
|
+
* external consumer got the seed factory's guess
|
|
25
|
+
*
|
|
26
|
+
* Between them they invented FOUR values no notification has ever had:
|
|
27
|
+
* `review_posted`, `payout_processed`, `review_received`, `payout_completed`.
|
|
28
|
+
* Note the first two and the last two are different spellings of the same two
|
|
29
|
+
* imagined concepts, which is how independent copies drift.
|
|
30
|
+
*
|
|
31
|
+
* A runtime array with the type derived from it, so a Zod enum, a chip list
|
|
32
|
+
* and a `Record<NotificationType, …>` all resolve here and a new value is one
|
|
33
|
+
* edit. Same shape as `ALL_LISTING_TYPES` after Root Cause #61.
|
|
34
|
+
*/
|
|
35
|
+
export const NOTIFICATION_TYPE_VALUES = [
|
|
36
|
+
"order_placed",
|
|
37
|
+
"order_confirmed",
|
|
38
|
+
"order_shipped",
|
|
39
|
+
"order_delivered",
|
|
40
|
+
"order_cancelled",
|
|
41
|
+
"bid_placed",
|
|
42
|
+
"bid_outbid",
|
|
43
|
+
"bid_won",
|
|
44
|
+
"bid_lost",
|
|
45
|
+
"auction_ended",
|
|
46
|
+
"review_approved",
|
|
47
|
+
"review_replied",
|
|
48
|
+
"product_available",
|
|
49
|
+
"promotion",
|
|
50
|
+
"system",
|
|
51
|
+
"welcome",
|
|
52
|
+
"account_action",
|
|
53
|
+
"offer_received",
|
|
54
|
+
"offer_responded",
|
|
55
|
+
"offer_expired",
|
|
56
|
+
"offer_counter_accepted",
|
|
57
|
+
"refund_initiated",
|
|
58
|
+
"prize_won",
|
|
59
|
+
"prize_reveal_expired",
|
|
60
|
+
"emi_installment_due_soon",
|
|
61
|
+
"emi_installment_overdue",
|
|
62
|
+
"payment_review",
|
|
63
|
+
// Was being SENT with an `as never` cast on the whole payload, which is how
|
|
64
|
+
// it stayed off this union: the cast silenced the one signal that would
|
|
65
|
+
// have said so, and with it every other field's type-check on that call.
|
|
66
|
+
"catalogue_images_stale",
|
|
67
|
+
];
|
|
68
|
+
/**
|
|
69
|
+
* What a notification points AT. Drives the shared `actionUrl` resolver, so a
|
|
70
|
+
* value here without a per-record page in some role is a notification that
|
|
71
|
+
* lands nowhere.
|
|
72
|
+
*
|
|
73
|
+
* Runtime array for the same reason as the type union above — and
|
|
74
|
+
* `catalogueItem` was being sent with an `as never` cast rather than added.
|
|
75
|
+
*/
|
|
76
|
+
export const NOTIFICATION_RELATED_TYPE_VALUES = [
|
|
77
|
+
"order",
|
|
78
|
+
"product",
|
|
79
|
+
"bid",
|
|
80
|
+
"review",
|
|
81
|
+
"blog",
|
|
82
|
+
"user",
|
|
83
|
+
"offer",
|
|
84
|
+
"support_ticket",
|
|
85
|
+
"scammer",
|
|
86
|
+
"catalogueItem",
|
|
87
|
+
"payout",
|
|
88
|
+
"store",
|
|
89
|
+
];
|
|
11
90
|
export const NOTIFICATIONS_COLLECTION = "notifications";
|
|
12
91
|
export const NOTIFICATIONS_INDEXED_FIELDS = [
|
|
13
92
|
"userId",
|
|
@@ -31,32 +110,16 @@ export const NOTIFICATION_FIELDS = {
|
|
|
31
110
|
RELATED_TYPE: "relatedType",
|
|
32
111
|
CREATED_AT: "createdAt",
|
|
33
112
|
UPDATED_AT: "updatedAt",
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
AUCTION_ENDED: "auction_ended",
|
|
45
|
-
REVIEW_APPROVED: "review_approved",
|
|
46
|
-
REVIEW_REPLIED: "review_replied",
|
|
47
|
-
PRODUCT_AVAILABLE: "product_available",
|
|
48
|
-
PROMOTION: "promotion",
|
|
49
|
-
SYSTEM: "system",
|
|
50
|
-
WELCOME: "welcome",
|
|
51
|
-
OFFER_RECEIVED: "offer_received",
|
|
52
|
-
OFFER_RESPONDED: "offer_responded",
|
|
53
|
-
OFFER_EXPIRED: "offer_expired",
|
|
54
|
-
OFFER_COUNTER_ACCEPTED: "offer_counter_accepted",
|
|
55
|
-
REFUND_INITIATED: "refund_initiated",
|
|
56
|
-
ACCOUNT_ACTION: "account_action",
|
|
57
|
-
PRIZE_WON: "prize_won",
|
|
58
|
-
PRIZE_REVEAL_EXPIRED: "prize_reveal_expired",
|
|
59
|
-
},
|
|
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])),
|
|
60
123
|
PRIORITY_VALUES: {
|
|
61
124
|
LOW: "low",
|
|
62
125
|
NORMAL: "normal",
|
|
@@ -4,10 +4,50 @@
|
|
|
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
|
+
/**
|
|
9
|
+
* Lane gate. While a higher-obligation lane (a won auction, then an accepted
|
|
10
|
+
* offer) is outstanding, no NEW shopping may be added — otherwise the buyer can
|
|
11
|
+
* keep deferring something they already committed to by piling more into the
|
|
12
|
+
* cart. Enforced on the server, not only in the UI: the UI hint is a courtesy,
|
|
13
|
+
* this is the rule.
|
|
14
|
+
*
|
|
15
|
+
* 🛑 Every add-to-cart entry point must call this. It lived inline inside
|
|
16
|
+
* `addItemToCart` and therefore covered exactly one of the four ways a line can
|
|
17
|
+
* be created — `addBundleToCartAction` and `POST /api/cart` both call
|
|
18
|
+
* `cartRepository.addItem` directly and so bypassed it entirely, letting a buyer
|
|
19
|
+
* with an unpaid auction win keep shopping.
|
|
20
|
+
*
|
|
21
|
+
* Returns the loaded cart so the caller doesn't pay a second read.
|
|
22
|
+
*/
|
|
23
|
+
export declare function assertCanAddNewItems(userId: string): Promise<CartDocument>;
|
|
8
24
|
export declare function addItemToCart(userId: string, input: AddToCartInput): Promise<CartDocument>;
|
|
9
25
|
/** Error code the client maps to a lane-aware message. */
|
|
10
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>;
|
|
11
51
|
/**
|
|
12
52
|
* Won-auction lines are non-removable and fixed-quantity — the buyer committed
|
|
13
53
|
* to the purchase by bidding, and letting them delete it would turn every
|
|
@@ -18,6 +58,12 @@ export declare const CART_LANE_BLOCKED = "CART_LANE_BLOCKED";
|
|
|
18
58
|
* and the offer simply lapses at its `checkoutDeadline`.
|
|
19
59
|
*/
|
|
20
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>;
|
|
21
67
|
export declare function removeCartItem(userId: string, itemId: string): Promise<CartDocument>;
|
|
22
68
|
export declare function clearCart(userId: string): Promise<CartDocument>;
|
|
23
69
|
export declare function mergeGuestCart(userId: string, items: Array<{
|