@mohasinac/appkit 4.63.4 → 4.63.5
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/.build-stamp.json +3 -3
- package/dist/_internal/server/features/checkout/actions.d.ts +2 -0
- package/dist/_internal/server/features/checkout/actions.js +22 -5
- package/dist/_internal/server/features/orders/adapters.js +3 -1
- package/dist/_internal/server/features/site-settings/adapters.js +20 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +1 -0
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +5 -2
- package/dist/_internal/server/jobs/core/jobRunners.js +5 -0
- package/dist/_internal/server/jobs/core/onOrderCreate.js +104 -10
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +2 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportCreate.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportRejected.js +1 -0
- package/dist/_internal/server/jobs/core/onScamReportVerified.js +1 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/pendingOrderTimeout.js +1 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.d.ts +35 -0
- package/dist/_internal/server/jobs/core/whatsappNotify.js +140 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +8 -6
- package/dist/_internal/server/notifications/channel-health.js +2 -2
- package/dist/_internal/server/notifications/send-guard.d.ts +1 -1
- package/dist/_internal/server/notifications/send-guard.js +4 -2
- package/dist/_internal/shared/actions/action-registry.js +28 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +1 -1
- package/dist/_internal/shared/checkout/order-math.js +1 -1
- package/dist/_internal/shared/features/checkout/config.d.ts +2 -0
- package/dist/_internal/shared/features/checkout/config.js +2 -0
- package/dist/_internal/shared/features/messaging/config.d.ts +5 -7
- package/dist/_internal/shared/features/messaging/config.js +4 -1
- package/dist/_internal/shared/features/site-settings/fees.d.ts +2 -0
- package/dist/_internal/shared/features/site-settings/fees.js +2 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +27 -2
- package/dist/_internal/shared/fees/calculator.d.ts +8 -0
- package/dist/_internal/shared/fees/calculator.js +7 -0
- package/dist/cli/index.js +2 -0
- package/dist/client.d.ts +2 -2
- package/dist/client.js +1 -1
- package/dist/constants/api-endpoints.d.ts +15 -0
- package/dist/constants/api-endpoints.js +8 -0
- package/dist/constants/notification-types.d.ts +1 -1
- package/dist/constants/notification-types.js +1 -1
- package/dist/core/integration-keys.d.ts +5 -0
- package/dist/core/integration-keys.js +10 -0
- package/dist/features/about/components/FeesView.js +6 -0
- package/dist/features/account/components/BuyerShoppingGuideView.js +1 -1
- package/dist/features/account/components/NotificationPreferencesPanel.js +4 -3
- package/dist/features/account/types/index.d.ts +2 -0
- package/dist/features/admin/actions/notification-actions.d.ts +17 -2
- package/dist/features/admin/actions/notification-actions.js +154 -9
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +1 -1
- package/dist/features/admin/components/AdminOrdersView.js +2 -0
- package/dist/features/admin/components/AdminSiteSettingsView.js +66 -6
- package/dist/features/admin/components/AdminStoreEditorView.js +1 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +12 -7
- package/dist/features/admin/constants/site-settings-tabs.js +12 -6
- package/dist/features/admin/repository/site-settings.repository.js +1 -1
- package/dist/features/admin/schemas/admin-user-form.d.ts +61 -0
- package/dist/features/admin/schemas/admin-user-form.js +44 -3
- package/dist/features/admin/schemas/firestore.d.ts +73 -0
- package/dist/features/admin/schemas/firestore.js +8 -3
- package/dist/features/auth/checkout-value-otp.d.ts +1 -1
- package/dist/features/auth/checkout-value-otp.js +1 -1
- package/dist/features/auth/permissions/constants.d.ts +2 -2
- package/dist/features/auth/permissions/constants.js +2 -1
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +1 -1
- package/dist/features/cart/components/StoreAddonsPicker.d.ts +5 -2
- package/dist/features/cart/components/StoreAddonsPicker.js +5 -4
- package/dist/features/cart/repository/cart.repository.js +2 -1
- package/dist/features/cart/schemas/firestore.d.ts +2 -1
- package/dist/features/checkout/actions/checkout-value-otp-actions.d.ts +5 -10
- package/dist/features/checkout/actions/checkout-value-otp-actions.js +88 -2
- package/dist/features/homepage/lib/section-renderer.js +1 -1
- package/dist/features/orders/actions/refund-actions.js +1 -0
- package/dist/features/orders/components/OrderAddonBadges.d.ts +3 -1
- package/dist/features/orders/components/OrderAddonBadges.js +4 -3
- package/dist/features/orders/repository/orders.repository.d.ts +8 -0
- package/dist/features/orders/repository/orders.repository.js +2 -0
- package/dist/features/orders/schemas/firestore.d.ts +4 -0
- package/dist/features/orders/types/index.d.ts +5 -2
- package/dist/features/search/action-index/settings-entries.js +9 -0
- package/dist/features/store-extensions/repository/store-extensions.repositories.d.ts +6 -1
- package/dist/features/store-extensions/repository/store-extensions.repositories.js +10 -1
- package/dist/features/store-extensions/schemas/firestore.d.ts +17 -1
- package/dist/features/store-extensions/schemas/firestore.js +10 -0
- package/dist/features/stores/components/StoreCapabilitiesGuideView.js +1 -0
- package/dist/features/stores/components/StoreGuideHubView.js +1 -1
- package/dist/features/stores/components/StoreSettingsGuideView.js +6 -2
- package/dist/features/stores/repository/store.repository.d.ts +23 -1
- package/dist/features/stores/repository/store.repository.js +29 -1
- package/dist/features/stores/schemas/firestore.d.ts +16 -6
- package/dist/features/stores/schemas/firestore.js +3 -6
- package/dist/features/stores/schemas/index.d.ts +86 -0
- package/dist/features/stores/schemas/index.js +14 -0
- package/dist/features/whatsapp-bot/api/send-status/route.d.ts +12 -0
- package/dist/features/whatsapp-bot/api/send-status/route.js +38 -0
- package/dist/features/whatsapp-bot/api/webhook/route.d.ts +12 -0
- package/dist/features/whatsapp-bot/api/webhook/route.js +43 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.d.ts +57 -0
- package/dist/features/whatsapp-bot/components/SellerWhatsAppSettingsView.js +76 -0
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.d.ts +9 -0
- package/dist/features/whatsapp-bot/components/WhatsAppChatButton.js +7 -0
- package/dist/features/whatsapp-bot/components/index.d.ts +3 -0
- package/dist/features/whatsapp-bot/components/index.js +2 -0
- package/dist/features/whatsapp-bot/helpers/whatsapp.d.ts +52 -0
- package/dist/features/whatsapp-bot/helpers/whatsapp.js +288 -0
- package/dist/features/whatsapp-bot/index.d.ts +3 -0
- package/dist/features/whatsapp-bot/index.js +3 -0
- package/dist/features/whatsapp-bot/manifest.d.ts +2 -0
- package/dist/features/whatsapp-bot/manifest.js +13 -0
- package/dist/features/whatsapp-bot/messages/en.json +7 -0
- package/dist/features/whatsapp-bot/server.d.ts +7 -0
- package/dist/features/whatsapp-bot/server.js +7 -0
- package/dist/features/whatsapp-bot/types/index.d.ts +126 -0
- package/dist/features/whatsapp-bot/types/index.js +1 -0
- package/dist/index.d.ts +23 -6
- package/dist/index.js +38 -12
- package/dist/next/routing/route-map.d.ts +3 -0
- package/dist/next/routing/route-map.js +1 -0
- package/dist/providers/db-firebase/base.repository.d.ts +1 -1
- package/dist/providers/db-firebase/base.repository.js +1 -1
- package/dist/repositories/index.d.ts +1 -1
- package/dist/repositories/index.js +1 -1
- package/dist/schemas/registry.d.ts +105 -0
- package/dist/schemas/registry.js +2 -1
- package/dist/security/pii-encrypt.js +1 -1
- package/dist/server.d.ts +10 -2
- package/dist/server.js +22 -2
- package/dist/styles.css +1 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/NavigationLoader.d.ts +14 -1
- package/dist/ui/components/NavigationLoader.js +79 -30
- package/package.json +1 -1
- package/dist/features/orders/utils/order-announcement-message.d.ts +0 -18
- package/dist/features/orders/utils/order-announcement-message.js +0 -16
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core: async WhatsApp notification dispatch (`whatsappNotify` job).
|
|
3
|
+
*
|
|
4
|
+
* `sendNotification()` (appkit/src/features/admin/actions/notification-actions.ts)
|
|
5
|
+
* no longer calls the Meta Cloud API synchronously in-request — it enqueues
|
|
6
|
+
* this job instead (CLAUDE.md Rule #6 async job primitive) and returns
|
|
7
|
+
* immediately. This runner does the actual send, with a bounded retry, and
|
|
8
|
+
* writes the outcome back onto `notifications/{notificationId}` so admins
|
|
9
|
+
* have visibility beyond the `jobs/{jobId}` doc itself.
|
|
10
|
+
*
|
|
11
|
+
* Retry note: `sendWhatsAppTemplateMessage`/`sendWhatsAppBusinessMessage`
|
|
12
|
+
* return a plain boolean (not a status code), so this runner cannot cleanly
|
|
13
|
+
* distinguish a transient 5xx from a terminal 4xx (bad token, unknown
|
|
14
|
+
* template) the way a status-code-aware client could. It retries any
|
|
15
|
+
* failure up to MAX_ATTEMPTS with a short backoff — an acceptable
|
|
16
|
+
* simplification since this runs off the critical path, well inside the
|
|
17
|
+
* Firebase Function's 300s budget, and a terminal error just burns two
|
|
18
|
+
* extra fast failures before giving up.
|
|
19
|
+
*/
|
|
20
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
21
|
+
import { notificationRepository } from "../../../../features/admin/repository/notification.repository.js";
|
|
22
|
+
import { sendWhatsAppTemplateMessage, sendWhatsAppBusinessMessage, } from "../../../../features/whatsapp-bot/helpers/whatsapp.js";
|
|
23
|
+
import { isChannelHealthy, recordChannelOutcome } from "../../notifications/channel-health.js";
|
|
24
|
+
import { guardSend, logSuppressedSend } from "../../notifications/send-guard.js";
|
|
25
|
+
const MAX_ATTEMPTS = 3;
|
|
26
|
+
const RETRY_DELAY_MS = 1500;
|
|
27
|
+
function delay(ms) {
|
|
28
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
}
|
|
30
|
+
export async function runWhatsAppNotify(payload, ctx) {
|
|
31
|
+
const { toPhone, title, message, type, templateName, templateLanguage, notificationId, phoneNumberId, accessToken } = payload;
|
|
32
|
+
/*
|
|
33
|
+
* The messaging guard, applied HERE rather than inside
|
|
34
|
+
* `sendWhatsAppBusinessMessage` itself.
|
|
35
|
+
*
|
|
36
|
+
* 🛑 That placement is deliberate and must not be "improved" by pushing a
|
|
37
|
+
* required guard parameter down into the sender. `features/whatsapp-bot/helpers/whatsapp.ts`
|
|
38
|
+
* is re-exported from `appkit/src/index.ts` (via `isAdminNumber`), and
|
|
39
|
+
* `guardSend` reaches `firebase-admin` through the settings and budget
|
|
40
|
+
* repositories. A static import of it from that file would put
|
|
41
|
+
* `firebase-admin` in the main entry's import graph, which is precisely the
|
|
42
|
+
* Turbopack client-bundle trap in Root Cause #6/#24 — it builds fine locally
|
|
43
|
+
* on webpack and fails every page in production.
|
|
44
|
+
*
|
|
45
|
+
* So the senders stay pure and each of the three real call sites guards
|
|
46
|
+
* itself; `scripts/audit-unguarded-send.mjs` enforces that a file calling a
|
|
47
|
+
* WhatsApp sender also calls `guardSend`.
|
|
48
|
+
*
|
|
49
|
+
* Note this job is only ever reached for an email/WhatsApp-ELIGIBLE type —
|
|
50
|
+
* `sendNotification` returns before enqueuing otherwise — so this is the
|
|
51
|
+
* ceiling and kill-switch check, not the type check.
|
|
52
|
+
*/
|
|
53
|
+
const guard = {
|
|
54
|
+
channel: "whatsapp",
|
|
55
|
+
feature: type,
|
|
56
|
+
audience: "user",
|
|
57
|
+
};
|
|
58
|
+
const decision = await guardSend(guard);
|
|
59
|
+
if (!decision.allow) {
|
|
60
|
+
logSuppressedSend(guard, decision);
|
|
61
|
+
await notificationRepository
|
|
62
|
+
.update(notificationId, { whatsappStatus: "skipped" })
|
|
63
|
+
.catch((err) => {
|
|
64
|
+
void normalizeError(err);
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
summary: { total: 1, succeeded: 0, skipped: 1, failed: 0 },
|
|
68
|
+
succeeded: [],
|
|
69
|
+
skipped: [notificationId],
|
|
70
|
+
failed: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
if (!(await isChannelHealthy("whatsapp"))) {
|
|
74
|
+
ctx.logger.warn("whatsappNotify: WhatsApp channel circuit is open — skipping send", { notificationId });
|
|
75
|
+
await notificationRepository
|
|
76
|
+
.update(notificationId, { whatsappStatus: "skipped" })
|
|
77
|
+
.catch((err) => {
|
|
78
|
+
void normalizeError(err);
|
|
79
|
+
});
|
|
80
|
+
return {
|
|
81
|
+
summary: { total: 1, succeeded: 0, skipped: 1, failed: 0 },
|
|
82
|
+
succeeded: [],
|
|
83
|
+
skipped: [notificationId],
|
|
84
|
+
failed: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
let sent = false;
|
|
88
|
+
let lastError;
|
|
89
|
+
for (let attempt = 1; attempt <= MAX_ATTEMPTS && !sent; attempt++) {
|
|
90
|
+
try {
|
|
91
|
+
sent = templateName
|
|
92
|
+
? await sendWhatsAppTemplateMessage({
|
|
93
|
+
toPhone,
|
|
94
|
+
phoneNumberId,
|
|
95
|
+
accessToken,
|
|
96
|
+
templateName,
|
|
97
|
+
languageCode: templateLanguage ?? "en",
|
|
98
|
+
bodyParams: [title, message],
|
|
99
|
+
})
|
|
100
|
+
: await sendWhatsAppBusinessMessage({
|
|
101
|
+
toPhone,
|
|
102
|
+
phoneNumberId,
|
|
103
|
+
accessToken,
|
|
104
|
+
message: `*${title}*\n${message}`,
|
|
105
|
+
});
|
|
106
|
+
if (!sent)
|
|
107
|
+
lastError = "Meta WhatsApp API returned a non-OK response";
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
void normalizeError(err);
|
|
111
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
112
|
+
}
|
|
113
|
+
if (!sent && attempt < MAX_ATTEMPTS) {
|
|
114
|
+
ctx.logger.warn("whatsappNotify: send attempt failed, retrying", { notificationId, attempt, error: lastError ?? "" });
|
|
115
|
+
await delay(RETRY_DELAY_MS * attempt);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (!templateName) {
|
|
119
|
+
ctx.logger.warn("whatsappNotify: no approved template configured for this notification type — sent as free-form text, which Meta rejects outside the 24h customer-service window", { notificationId });
|
|
120
|
+
}
|
|
121
|
+
await recordChannelOutcome("whatsapp", sent);
|
|
122
|
+
await notificationRepository
|
|
123
|
+
.update(notificationId, { whatsappStatus: sent ? "sent" : "failed" })
|
|
124
|
+
.catch((err) => {
|
|
125
|
+
void normalizeError(err);
|
|
126
|
+
ctx.logger.warn("whatsappNotify: failed to write back whatsappStatus onto notification doc", {
|
|
127
|
+
notificationId,
|
|
128
|
+
error: err instanceof Error ? err.message : String(err),
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
if (!sent) {
|
|
132
|
+
throw new Error(lastError ?? "WhatsApp send failed after retries");
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
summary: { total: 1, succeeded: 1, skipped: 0, failed: 0 },
|
|
136
|
+
succeeded: [notificationId],
|
|
137
|
+
skipped: [],
|
|
138
|
+
failed: [],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Circuit breaker for outbound notification channels.
|
|
2
|
+
* Circuit breaker for outbound notification channels (email, WhatsApp).
|
|
3
3
|
*
|
|
4
4
|
* Persisted as a singleton Firestore doc `system/channelHealth` so the
|
|
5
5
|
* "is this channel currently known-bad" state survives across requests and
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* its own outcome decides whether the circuit re-closes or re-opens.
|
|
11
11
|
*/
|
|
12
12
|
/**
|
|
13
|
-
* Email
|
|
14
|
-
*
|
|
15
|
-
* integration
|
|
13
|
+
* Email and WhatsApp are the outbound *notification* channels with a
|
|
14
|
+
* transport. The WhatsApp arm was removed 2026-10-20 with the Cloud API
|
|
15
|
+
* integration and restored 2026-10-23; it stays inert until
|
|
16
|
+
* `siteSettings.whatsapp.enabled` is on AND a number is registered on the
|
|
17
|
+
* Cloud API, so a breaker on it reports a channel nobody is using until then.
|
|
16
18
|
*
|
|
17
19
|
* ## 🛑 Phase 8 widened this to the RECONCILERS' reads — and only those
|
|
18
20
|
*
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
* backing off costs only freshness. Anything that moves money is not a reader
|
|
37
39
|
* and does not belong in this union.
|
|
38
40
|
*/
|
|
39
|
-
export type NotificationChannel = "email" | "razorpay_read" | "shiprocket_read";
|
|
41
|
+
export type NotificationChannel = "email" | "whatsapp" | "razorpay_read" | "shiprocket_read";
|
|
40
42
|
/**
|
|
41
43
|
* Returns false only when the circuit is open and still within its cooldown
|
|
42
44
|
* window. Any other state (closed, or open-but-cooldown-elapsed) is treated
|
|
@@ -47,7 +49,7 @@ export declare function recordChannelOutcome(channel: NotificationChannel, succe
|
|
|
47
49
|
/**
|
|
48
50
|
* Runs `fn` up to 3 times (500ms then 1.5s between attempts) and records the
|
|
49
51
|
* final outcome against the channel's health state. `fn` must throw on
|
|
50
|
-
* failure — for a helper that returns a boolean instead (e.g.
|
|
52
|
+
* failure — for a helper that returns a boolean instead (e.g. the WhatsApp
|
|
51
53
|
* Cloud API helpers), convert `false` into a thrown error at the call site
|
|
52
54
|
* before passing it in.
|
|
53
55
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Circuit breaker for outbound notification channels.
|
|
2
|
+
* Circuit breaker for outbound notification channels (email, WhatsApp).
|
|
3
3
|
*
|
|
4
4
|
* Persisted as a singleton Firestore doc `system/channelHealth` so the
|
|
5
5
|
* "is this channel currently known-bad" state survives across requests and
|
|
@@ -72,7 +72,7 @@ function delay(ms) {
|
|
|
72
72
|
/**
|
|
73
73
|
* Runs `fn` up to 3 times (500ms then 1.5s between attempts) and records the
|
|
74
74
|
* final outcome against the channel's health state. `fn` must throw on
|
|
75
|
-
* failure — for a helper that returns a boolean instead (e.g.
|
|
75
|
+
* failure — for a helper that returns a boolean instead (e.g. the WhatsApp
|
|
76
76
|
* Cloud API helpers), convert `false` into a thrown error at the call site
|
|
77
77
|
* before passing it in.
|
|
78
78
|
*/
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* message.
|
|
16
16
|
*
|
|
17
17
|
* The guarantee that nothing bypasses this is STRUCTURAL instead: the guard
|
|
18
|
-
* argument is REQUIRED on `sendEmail
|
|
18
|
+
* argument is REQUIRED on `sendEmail` and on both WhatsApp senders, so an
|
|
19
19
|
* unguarded send does not typecheck, and `scripts/audit-unguarded-send.mjs`
|
|
20
20
|
* fails the build on a raw `createResendProvider().send()`. Make the wrong
|
|
21
21
|
* thing unrepresentable rather than adding a second layer of defence that has
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* message.
|
|
16
16
|
*
|
|
17
17
|
* The guarantee that nothing bypasses this is STRUCTURAL instead: the guard
|
|
18
|
-
* argument is REQUIRED on `sendEmail
|
|
18
|
+
* argument is REQUIRED on `sendEmail` and on both WhatsApp senders, so an
|
|
19
19
|
* unguarded send does not typecheck, and `scripts/audit-unguarded-send.mjs`
|
|
20
20
|
* fails the build on a raw `createResendProvider().send()`. Make the wrong
|
|
21
21
|
* thing unrepresentable rather than adding a second layer of defence that has
|
|
@@ -69,7 +69,9 @@ export async function guardSend(ctx) {
|
|
|
69
69
|
if (ctx.audience === "user" && !ctx.transactional) {
|
|
70
70
|
const channelEnabled = ctx.channel === "email"
|
|
71
71
|
? (messaging?.emailEnabled ?? MESSAGE_BUDGET_DEFAULTS.emailEnabled)
|
|
72
|
-
:
|
|
72
|
+
: ctx.channel === "whatsapp"
|
|
73
|
+
? (messaging?.whatsappEnabled ?? MESSAGE_BUDGET_DEFAULTS.whatsappEnabled)
|
|
74
|
+
: false; // sms has no sender; if one appears it starts disabled.
|
|
73
75
|
if (!channelEnabled) {
|
|
74
76
|
return {
|
|
75
77
|
allow: false,
|
|
@@ -919,6 +919,34 @@ export const ACTIONS = {
|
|
|
919
919
|
kind: "ghost",
|
|
920
920
|
permissions: ["seller", "admin"],
|
|
921
921
|
},
|
|
922
|
+
// ── WhatsApp integration ────────────────────────────────────────────
|
|
923
|
+
"whatsapp-connect": {
|
|
924
|
+
iconKey: "link",
|
|
925
|
+
id: "store.whatsapp-connect",
|
|
926
|
+
label: "Save & Connect",
|
|
927
|
+
ariaLabel: "Save WhatsApp credentials and connect",
|
|
928
|
+
description: "Save WhatsApp Business credentials and establish connection.",
|
|
929
|
+
kind: "primary",
|
|
930
|
+
permissions: ["seller", "admin"],
|
|
931
|
+
},
|
|
932
|
+
"whatsapp-catalog-sync": {
|
|
933
|
+
iconKey: "retry",
|
|
934
|
+
id: "store.whatsapp-catalog-sync",
|
|
935
|
+
label: "Push to WhatsApp",
|
|
936
|
+
ariaLabel: "Sync products to WhatsApp catalog",
|
|
937
|
+
description: "Push published standard products to WhatsApp Business Catalog.",
|
|
938
|
+
kind: "secondary",
|
|
939
|
+
permissions: ["seller", "admin"],
|
|
940
|
+
},
|
|
941
|
+
"whatsapp-catalog-import": {
|
|
942
|
+
iconKey: "upload",
|
|
943
|
+
id: "store.whatsapp-catalog-import",
|
|
944
|
+
label: "Import from WhatsApp",
|
|
945
|
+
ariaLabel: "Import products from WhatsApp catalog",
|
|
946
|
+
description: "Import products from WhatsApp Catalog as drafts.",
|
|
947
|
+
kind: "secondary",
|
|
948
|
+
permissions: ["seller", "admin"],
|
|
949
|
+
},
|
|
922
950
|
// ── Google Reviews integration ──────────────────────────────────────
|
|
923
951
|
"google-reviews-sync": {
|
|
924
952
|
iconKey: "retry",
|
|
@@ -227,7 +227,7 @@ export interface PayableOrderLike {
|
|
|
227
227
|
* money bug in all four payout paths (`autoPayoutEligibility`,
|
|
228
228
|
* `weeklyPayoutEligibility`, and both `seller-actions` entry points). That
|
|
229
229
|
* field is the GRAND TOTAL — `checkout/actions.ts` builds it as
|
|
230
|
-
* `goods − coupon + shipping + codHandling + giftWrap +
|
|
230
|
+
* `goods − coupon + shipping + codHandling + whatsappNotify + giftWrap +
|
|
231
231
|
* shipmentProtection + platformFee + platformFeeGst + emiSurcharge + gstAmount`
|
|
232
232
|
* — so using it as the payout base did three wrong things at once:
|
|
233
233
|
*
|
|
@@ -412,7 +412,7 @@ export function allocateAcrossMembers(total, weights) {
|
|
|
412
412
|
* money bug in all four payout paths (`autoPayoutEligibility`,
|
|
413
413
|
* `weeklyPayoutEligibility`, and both `seller-actions` entry points). That
|
|
414
414
|
* field is the GRAND TOTAL — `checkout/actions.ts` builds it as
|
|
415
|
-
* `goods − coupon + shipping + codHandling + giftWrap +
|
|
415
|
+
* `goods − coupon + shipping + codHandling + whatsappNotify + giftWrap +
|
|
416
416
|
* shipmentProtection + platformFee + platformFeeGst + emiSurcharge + gstAmount`
|
|
417
417
|
* — so using it as the payout base did three wrong things at once:
|
|
418
418
|
*
|
|
@@ -6,6 +6,8 @@ export declare const CHECKOUT_DEFAULT_COMMISSIONS: {
|
|
|
6
6
|
readonly codDepositPercent: 10;
|
|
7
7
|
readonly codHandlingFeeCap: 200;
|
|
8
8
|
readonly codHandlingFeePercent: 10;
|
|
9
|
+
readonly whatsappNotifyFeeEnabled: true;
|
|
10
|
+
readonly whatsappNotifyFee: 10;
|
|
9
11
|
readonly giftWrapFeeEnabled: true;
|
|
10
12
|
readonly giftWrapFee: 49;
|
|
11
13
|
readonly shipmentProtectionFeeEnabled: true;
|
|
@@ -13,6 +13,8 @@ export const CHECKOUT_DEFAULT_COMMISSIONS = {
|
|
|
13
13
|
// Mirrors the SiteSettingsDocument defaults, which enable every add-on.
|
|
14
14
|
// Consequence of the mirror: if the settings read fails, an add-on already
|
|
15
15
|
// ticked on the cart document is still billed rather than silently dropped.
|
|
16
|
+
whatsappNotifyFeeEnabled: true,
|
|
17
|
+
whatsappNotifyFee: 10,
|
|
16
18
|
giftWrapFeeEnabled: true,
|
|
17
19
|
giftWrapFee: 49,
|
|
18
20
|
shipmentProtectionFeeEnabled: true,
|
|
@@ -16,12 +16,7 @@
|
|
|
16
16
|
* and find the budget already covers them. A channel that is born capped
|
|
17
17
|
* cannot repeat what email did.
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
* `whatsapp` was removed 2026-10-20 with the Cloud API integration. `sms`
|
|
21
|
-
* stays declared with no sender — the ceiling exists so one cannot ship
|
|
22
|
-
* uncapped — and `email` is the only channel with a live transport.
|
|
23
|
-
*/
|
|
24
|
-
export type MessageChannel = "email" | "sms";
|
|
19
|
+
export type MessageChannel = "email" | "whatsapp" | "sms";
|
|
25
20
|
/**
|
|
26
21
|
* Who the message is FOR — which is what the kill switch keys on.
|
|
27
22
|
*
|
|
@@ -81,12 +76,13 @@ export type SendDecision = {
|
|
|
81
76
|
/**
|
|
82
77
|
* Defaults, mirrored into `DEFAULT_SITE_SETTINGS_DATA.messaging`.
|
|
83
78
|
*
|
|
84
|
-
* 🛑 `emailEnabled`
|
|
79
|
+
* 🛑 `emailEnabled` and `whatsappEnabled` default to FALSE. An install that
|
|
85
80
|
* has not made a decision should be quiet, not loud — the opposite default is
|
|
86
81
|
* how a fresh deploy mails real people from a half-configured site.
|
|
87
82
|
*/
|
|
88
83
|
export declare const MESSAGE_BUDGET_DEFAULTS: {
|
|
89
84
|
readonly emailEnabled: false;
|
|
85
|
+
readonly whatsappEnabled: false;
|
|
90
86
|
readonly dailyCeiling: {
|
|
91
87
|
/**
|
|
92
88
|
* Resend's free tier is 100/day. 80 is not timidity — the counter is
|
|
@@ -96,6 +92,8 @@ export declare const MESSAGE_BUDGET_DEFAULTS: {
|
|
|
96
92
|
* headroom is what stops "at the limit" from becoming "over it".
|
|
97
93
|
*/
|
|
98
94
|
readonly email: 80;
|
|
95
|
+
/** Meta's pricing is per-conversation, not per-message; this is a sanity bound, not a tier limit. */
|
|
96
|
+
readonly whatsapp: 200;
|
|
99
97
|
/** No sender exists yet. Present so one cannot ship uncapped. */
|
|
100
98
|
readonly sms: 50;
|
|
101
99
|
};
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
/**
|
|
10
10
|
* Defaults, mirrored into `DEFAULT_SITE_SETTINGS_DATA.messaging`.
|
|
11
11
|
*
|
|
12
|
-
* 🛑 `emailEnabled`
|
|
12
|
+
* 🛑 `emailEnabled` and `whatsappEnabled` default to FALSE. An install that
|
|
13
13
|
* has not made a decision should be quiet, not loud — the opposite default is
|
|
14
14
|
* how a fresh deploy mails real people from a half-configured site.
|
|
15
15
|
*/
|
|
16
16
|
export const MESSAGE_BUDGET_DEFAULTS = {
|
|
17
17
|
emailEnabled: false,
|
|
18
|
+
whatsappEnabled: false,
|
|
18
19
|
dailyCeiling: {
|
|
19
20
|
/**
|
|
20
21
|
* Resend's free tier is 100/day. 80 is not timidity — the counter is
|
|
@@ -24,6 +25,8 @@ export const MESSAGE_BUDGET_DEFAULTS = {
|
|
|
24
25
|
* headroom is what stops "at the limit" from becoming "over it".
|
|
25
26
|
*/
|
|
26
27
|
email: 80,
|
|
28
|
+
/** Meta's pricing is per-conversation, not per-message; this is a sanity bound, not a tier limit. */
|
|
29
|
+
whatsapp: 200,
|
|
27
30
|
/** No sender exists yet. Present so one cannot ship uncapped. */
|
|
28
31
|
sms: 50,
|
|
29
32
|
},
|
|
@@ -27,6 +27,8 @@ import type { EmiSettings } from "../emi/schedule";
|
|
|
27
27
|
* and each `compute*Fee` helper carries its own default.
|
|
28
28
|
*/
|
|
29
29
|
export interface BuyerFacingFees {
|
|
30
|
+
whatsappNotifyFeeEnabled?: boolean;
|
|
31
|
+
whatsappNotifyFee?: number;
|
|
30
32
|
giftWrapFeeEnabled?: boolean;
|
|
31
33
|
giftWrapFee?: number;
|
|
32
34
|
shipmentProtectionFeeEnabled?: boolean;
|
|
@@ -13,6 +13,8 @@ export function toBuyerFacingFees(commissions) {
|
|
|
13
13
|
if (!commissions)
|
|
14
14
|
return null;
|
|
15
15
|
return {
|
|
16
|
+
whatsappNotifyFeeEnabled: commissions.whatsappNotifyFeeEnabled,
|
|
17
|
+
whatsappNotifyFee: commissions.whatsappNotifyFee,
|
|
16
18
|
giftWrapFeeEnabled: commissions.giftWrapFeeEnabled,
|
|
17
19
|
giftWrapFee: commissions.giftWrapFee,
|
|
18
20
|
shipmentProtectionFeeEnabled: commissions.shipmentProtectionFeeEnabled,
|
|
@@ -12,8 +12,20 @@
|
|
|
12
12
|
* `scripts/audit-public-projection-parity.mjs`. Adding a field here without a
|
|
13
13
|
* proven client reader is how the endpoint drifted in the first place.
|
|
14
14
|
*/
|
|
15
|
-
import type {
|
|
15
|
+
import type { SiteSettingsDocument } from "../../../../features/admin/schemas/firestore";
|
|
16
16
|
import type { MediaVideoWatermark } from "../../../../features/media/MediaVideo";
|
|
17
|
+
/** The only notification-channel facts a browser is given: is each channel on. */
|
|
18
|
+
export interface PublicNotificationChannels {
|
|
19
|
+
email: {
|
|
20
|
+
enabled: boolean;
|
|
21
|
+
};
|
|
22
|
+
whatsapp: {
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
};
|
|
25
|
+
sms: {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
17
29
|
export interface PublicSiteSettings {
|
|
18
30
|
/** Support channels shown to buyers. Deliberately NOT the full `contact` — `address` and `upiVpa` stay server-side. */
|
|
19
31
|
contact: {
|
|
@@ -64,7 +76,20 @@ export interface PublicSiteSettings {
|
|
|
64
76
|
search?: {
|
|
65
77
|
indexEnabled: boolean;
|
|
66
78
|
};
|
|
67
|
-
|
|
79
|
+
/**
|
|
80
|
+
* 🛑 NARROWER than `NotificationChannelConfig` on purpose, and it must stay
|
|
81
|
+
* that way. The full config carries `minPriority` and a `types[]` allow-list
|
|
82
|
+
* naming exactly which notification types each channel carries — operational
|
|
83
|
+
* routing detail that no browser reads and that an anonymous caller has no
|
|
84
|
+
* business enumerating.
|
|
85
|
+
*
|
|
86
|
+
* `inApp` is deliberately absent too: it is always on and nothing renders a
|
|
87
|
+
* toggle for it.
|
|
88
|
+
*
|
|
89
|
+
* Widening this back to `NotificationChannelConfig` re-publishes the lot by
|
|
90
|
+
* the act of widening, which is the failure Root Cause #70 records.
|
|
91
|
+
*/
|
|
92
|
+
notificationChannels?: PublicNotificationChannels;
|
|
68
93
|
announcementBar?: SiteSettingsDocument["announcementBar"];
|
|
69
94
|
navConfig?: SiteSettingsDocument["navConfig"];
|
|
70
95
|
actionConfig?: SiteSettingsDocument["actionConfig"];
|
|
@@ -175,6 +175,14 @@ export interface CodHandlingFeeRates {
|
|
|
175
175
|
* changes no number on its own.
|
|
176
176
|
*/
|
|
177
177
|
export declare function computeCodHandlingFee(subtotal: number, rates: CodHandlingFeeRates): number;
|
|
178
|
+
export interface WhatsAppNotifyFeeRates {
|
|
179
|
+
/** Admin master toggle — the addon isn't charged (even if the buyer selected it) unless this is true. */
|
|
180
|
+
whatsappNotifyFeeEnabled?: boolean;
|
|
181
|
+
/** Flat rupee fee charged when the buyer opts in. Falls back to ₹10. */
|
|
182
|
+
whatsappNotifyFee?: number;
|
|
183
|
+
}
|
|
184
|
+
/** Flat WhatsApp order-updates addon fee — charged only when the buyer opted in AND the admin has the addon enabled. */
|
|
185
|
+
export declare function computeWhatsAppNotifyFee(addonSelected: boolean, rates: WhatsAppNotifyFeeRates): number;
|
|
178
186
|
export interface GiftWrapFeeRates {
|
|
179
187
|
/** Admin master toggle — the addon isn't charged (even if the buyer selected it) unless this is true. */
|
|
180
188
|
giftWrapFeeEnabled?: boolean;
|
|
@@ -202,6 +202,13 @@ export function computeCodHandlingFee(subtotal, rates) {
|
|
|
202
202
|
const percentFee = roundRupees(subtotal * (percent / 100));
|
|
203
203
|
return Math.min(percentFee, cap);
|
|
204
204
|
}
|
|
205
|
+
const DEFAULT_WHATSAPP_NOTIFY_FEE = 10;
|
|
206
|
+
/** Flat WhatsApp order-updates addon fee — charged only when the buyer opted in AND the admin has the addon enabled. */
|
|
207
|
+
export function computeWhatsAppNotifyFee(addonSelected, rates) {
|
|
208
|
+
if (!addonSelected || !rates.whatsappNotifyFeeEnabled)
|
|
209
|
+
return 0;
|
|
210
|
+
return rates.whatsappNotifyFee ?? DEFAULT_WHATSAPP_NOTIFY_FEE;
|
|
211
|
+
}
|
|
205
212
|
const DEFAULT_GIFT_WRAP_FEE = 49;
|
|
206
213
|
/** Flat gift-wrap addon fee — charged only when the buyer opted in AND the admin has the addon enabled. */
|
|
207
214
|
export function computeGiftWrapFee(addonSelected, rates) {
|
package/dist/cli/index.js
CHANGED
|
@@ -59,6 +59,7 @@ export const FEATURE_SUBPATH_MAP = {
|
|
|
59
59
|
"before-after": "@mohasinac/appkit/features/before-after",
|
|
60
60
|
collections: "@mohasinac/appkit/features/collections",
|
|
61
61
|
preorders: "@mohasinac/appkit/features/pre-orders",
|
|
62
|
+
"whatsapp-bot": "@mohasinac/appkit/features/whatsapp-bot",
|
|
62
63
|
};
|
|
63
64
|
/**
|
|
64
65
|
* Wraps a Next.js config object. With appkit, only `@mohasinac/appkit` needs to
|
|
@@ -109,6 +110,7 @@ const FEATURE_NAMESPACE_MAP = {
|
|
|
109
110
|
corporate: "corporate",
|
|
110
111
|
"before-after": "beforeAfter",
|
|
111
112
|
collections: "collections",
|
|
113
|
+
"whatsapp-bot": "whatsappBot",
|
|
112
114
|
};
|
|
113
115
|
// Opaque dynamic import — prevents bundlers from statically analysing the path
|
|
114
116
|
const _dynamicImport = new Function("modulePath", "return import(modulePath)");
|
package/dist/client.d.ts
CHANGED
|
@@ -545,7 +545,7 @@ export type { FeeDerivation, FeeDerivations } from "./_internal/shared/fees/expl
|
|
|
545
545
|
export type { BuyerFacingFees, BuyerEmiSettings, } from "./_internal/shared/features/site-settings/fees";
|
|
546
546
|
export type { PublicSiteSettings } from "./_internal/shared/features/site-settings/types";
|
|
547
547
|
export { computeCodHandlingFee } from "./_internal/shared/fees/calculator";
|
|
548
|
-
export type { CodHandlingFeeRates, GiftWrapFeeRates, ShipmentProtectionFeeRates, } from "./_internal/shared/fees/calculator";
|
|
548
|
+
export type { CodHandlingFeeRates, WhatsAppNotifyFeeRates, GiftWrapFeeRates, ShipmentProtectionFeeRates, } from "./_internal/shared/fees/calculator";
|
|
549
549
|
export { FAQPageContent } from "./features/faq/index";
|
|
550
550
|
export type { FAQCategory, FAQCategoryItem, FAQSortOption } from "./features/faq/index";
|
|
551
551
|
export { CategoryInlineSelect } from "./features/seller/components/CategoryInlineSelect";
|
|
@@ -571,7 +571,7 @@ export type { AdCreativeFormValues } from "./features/ads/schemas/creative-form"
|
|
|
571
571
|
export { adminStoreUpdateSchema, announcementBarSchema, } from "./features/admin/schemas/admin-editor-forms";
|
|
572
572
|
export { adminOrderUpdateSchema, employeeInviteSchema, hardBanReasonSchema, featureFlagsUpdateSchema, adminAdFormSchema, } from "./features/admin/schemas/admin-ops-forms";
|
|
573
573
|
export { changeEmailSchema } from "./features/account/schemas/index";
|
|
574
|
-
export { adminUserUpdateSchema, lotteryPullSchema, } from "./features/admin/schemas/admin-user-form";
|
|
574
|
+
export { adminUserUpdateSchema, whatsappSettingsSchema, lotteryPullSchema, } from "./features/admin/schemas/admin-user-form";
|
|
575
575
|
export { siteSettingsFormSchema } from "./features/admin/schemas/site-settings-form";
|
|
576
576
|
export { refineAddressPostal, addressFormSchema, addressUpdateSchema, adminAddressCreateSchema, adminAddressUpdateSchema, addressUnbanRequestSchema, } from "./features/addresses/schemas/address-form";
|
|
577
577
|
export type { AddressFormValues, AddressUpdateValues, AdminAddressCreateValues, } from "./features/addresses/schemas/address-form";
|
package/dist/client.js
CHANGED
|
@@ -619,7 +619,7 @@ export { adCreativeFormSchema } from "./features/ads/schemas/creative-form.js";
|
|
|
619
619
|
export { adminStoreUpdateSchema, announcementBarSchema, } from "./features/admin/schemas/admin-editor-forms.js";
|
|
620
620
|
export { adminOrderUpdateSchema, employeeInviteSchema, hardBanReasonSchema, featureFlagsUpdateSchema, adminAdFormSchema, } from "./features/admin/schemas/admin-ops-forms.js";
|
|
621
621
|
export { changeEmailSchema } from "./features/account/schemas/index.js";
|
|
622
|
-
export { adminUserUpdateSchema, lotteryPullSchema, } from "./features/admin/schemas/admin-user-form.js";
|
|
622
|
+
export { adminUserUpdateSchema, whatsappSettingsSchema, lotteryPullSchema, } from "./features/admin/schemas/admin-user-form.js";
|
|
623
623
|
export { siteSettingsFormSchema } from "./features/admin/schemas/site-settings-form.js";
|
|
624
624
|
// The ONE address shape and the ONE postal rule (W5 / D19). Eleven field
|
|
625
625
|
// shapes and fifteen disagreeing postal rules collapsed to these.
|
|
@@ -626,6 +626,11 @@ export declare const SCAMMER_ENDPOINTS: {
|
|
|
626
626
|
export declare const BEFORE_AFTER_ENDPOINTS: {
|
|
627
627
|
readonly LIST: "/api/before-after";
|
|
628
628
|
};
|
|
629
|
+
export declare const WHATSAPP_SELLER_ENDPOINTS: {
|
|
630
|
+
readonly SETTINGS: "/api/store/whatsapp-settings";
|
|
631
|
+
readonly CATALOG_SYNC: "/api/store/whatsapp-settings/catalog-sync";
|
|
632
|
+
readonly CATALOG_IMPORT: "/api/store/whatsapp-settings/catalog-import";
|
|
633
|
+
};
|
|
629
634
|
export declare const API_ENDPOINTS: {
|
|
630
635
|
readonly CONFIG: {};
|
|
631
636
|
readonly LOGS: {
|
|
@@ -1178,6 +1183,11 @@ export declare const API_ENDPOINTS: {
|
|
|
1178
1183
|
readonly PRODUCTS: (sellerId: string) => string;
|
|
1179
1184
|
readonly REVIEWS: (userId: string) => string;
|
|
1180
1185
|
};
|
|
1186
|
+
readonly WHATSAPP_SELLER: {
|
|
1187
|
+
readonly SETTINGS: "/api/store/whatsapp-settings";
|
|
1188
|
+
readonly CATALOG_SYNC: "/api/store/whatsapp-settings/catalog-sync";
|
|
1189
|
+
readonly CATALOG_IMPORT: "/api/store/whatsapp-settings/catalog-import";
|
|
1190
|
+
};
|
|
1181
1191
|
readonly SUPPORT: {
|
|
1182
1192
|
readonly TICKETS: "/api/support/tickets";
|
|
1183
1193
|
readonly TICKET_BY_ID: (id: string) => string;
|
|
@@ -1764,6 +1774,11 @@ export declare const API_ROUTES: {
|
|
|
1764
1774
|
readonly PRODUCTS: (sellerId: string) => string;
|
|
1765
1775
|
readonly REVIEWS: (userId: string) => string;
|
|
1766
1776
|
};
|
|
1777
|
+
readonly WHATSAPP_SELLER: {
|
|
1778
|
+
readonly SETTINGS: "/api/store/whatsapp-settings";
|
|
1779
|
+
readonly CATALOG_SYNC: "/api/store/whatsapp-settings/catalog-sync";
|
|
1780
|
+
readonly CATALOG_IMPORT: "/api/store/whatsapp-settings/catalog-import";
|
|
1781
|
+
};
|
|
1767
1782
|
readonly SUPPORT: {
|
|
1768
1783
|
readonly TICKETS: "/api/support/tickets";
|
|
1769
1784
|
readonly TICKET_BY_ID: (id: string) => string;
|
|
@@ -829,6 +829,13 @@ export const BEFORE_AFTER_ENDPOINTS = {
|
|
|
829
829
|
LIST: "/api/before-after",
|
|
830
830
|
};
|
|
831
831
|
// ---------------------------------------------------------------------------
|
|
832
|
+
// WhatsApp Business (store-level settings + catalog sync)
|
|
833
|
+
// ---------------------------------------------------------------------------
|
|
834
|
+
export const WHATSAPP_SELLER_ENDPOINTS = {
|
|
835
|
+
SETTINGS: "/api/store/whatsapp-settings",
|
|
836
|
+
CATALOG_SYNC: "/api/store/whatsapp-settings/catalog-sync",
|
|
837
|
+
CATALOG_IMPORT: "/api/store/whatsapp-settings/catalog-import",
|
|
838
|
+
};
|
|
832
839
|
// ---------------------------------------------------------------------------
|
|
833
840
|
// Aggregate namespace export
|
|
834
841
|
// ---------------------------------------------------------------------------
|
|
@@ -873,6 +880,7 @@ export const API_ENDPOINTS = {
|
|
|
873
880
|
SUBLISTING_CATEGORIES: SUBLISTING_CATEGORY_ENDPOINTS,
|
|
874
881
|
WISHLIST: WISHLIST_ENDPOINTS,
|
|
875
882
|
PROFILE_STATS: PROFILE_STATS_ENDPOINTS,
|
|
883
|
+
WHATSAPP_SELLER: WHATSAPP_SELLER_ENDPOINTS,
|
|
876
884
|
SUPPORT: SUPPORT_ENDPOINTS,
|
|
877
885
|
SCAMMERS: SCAMMER_ENDPOINTS,
|
|
878
886
|
REPORTS: REPORT_ENDPOINTS,
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* **18 of the 27 real types** and was offered two that never appear.
|
|
13
13
|
* · `AdminSiteSettingsView`'s per-channel allow-list, so an admin literally
|
|
14
14
|
* **could not allow-list `offer_received` or `payment_review`** for email
|
|
15
|
-
* — the toggle did not exist.
|
|
15
|
+
* or WhatsApp — the toggle did not exist.
|
|
16
16
|
*
|
|
17
17
|
* Everything here now derives from `NOTIFICATION_TYPE_VALUES`, so a new
|
|
18
18
|
* notification type is one edit and cannot be missing from either surface.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* **18 of the 27 real types** and was offered two that never appear.
|
|
13
13
|
* · `AdminSiteSettingsView`'s per-channel allow-list, so an admin literally
|
|
14
14
|
* **could not allow-list `offer_received` or `payment_review`** for email
|
|
15
|
-
* — the toggle did not exist.
|
|
15
|
+
* or WhatsApp — the toggle did not exist.
|
|
16
16
|
*
|
|
17
17
|
* Everything here now derives from `NOTIFICATION_TYPE_VALUES`, so a new
|
|
18
18
|
* notification type is one edit and cannot be missing from either surface.
|
|
@@ -12,6 +12,11 @@ export interface ResolvedKeys {
|
|
|
12
12
|
/** Webhook signing secret. Optional in practice: without it `verifyWebhook()` fails closed and the buyer-present /verify path still works. */
|
|
13
13
|
razorpayWebhookSecret: string;
|
|
14
14
|
resendApiKey: string;
|
|
15
|
+
whatsappApiKey: string;
|
|
16
|
+
whatsappPhoneNumberId: string;
|
|
17
|
+
whatsappCloudApiToken: string;
|
|
18
|
+
/** Comma-separated digits-only numbers, e.g. "919876543210,918765432109" */
|
|
19
|
+
whatsappAdminNotifyNumbers: string;
|
|
15
20
|
}
|
|
16
21
|
/** Drops the in-process cache. Call this after admin saves new credentials. */
|
|
17
22
|
export declare function invalidateIntegrationKeysCache(): void;
|
|
@@ -32,6 +32,10 @@ const EMPTY_KEYS = {
|
|
|
32
32
|
razorpayKeySecret: "",
|
|
33
33
|
razorpayWebhookSecret: "",
|
|
34
34
|
resendApiKey: "",
|
|
35
|
+
whatsappApiKey: "",
|
|
36
|
+
whatsappPhoneNumberId: "",
|
|
37
|
+
whatsappCloudApiToken: "",
|
|
38
|
+
whatsappAdminNotifyNumbers: "",
|
|
35
39
|
};
|
|
36
40
|
let _cache = null;
|
|
37
41
|
const CACHE_TTL_MS = 60000; // 1 minute
|
|
@@ -79,6 +83,12 @@ export async function resolveKeys() {
|
|
|
79
83
|
phonepeWebhookUsername: db.phonepeWebhookUsername || process.env.PHONEPE_WEBHOOK_USERNAME || "",
|
|
80
84
|
phonepeWebhookPassword: db.phonepeWebhookPassword || process.env.PHONEPE_WEBHOOK_PASSWORD || "",
|
|
81
85
|
resendApiKey: db.resendApiKey || process.env.RESEND_API_KEY || "",
|
|
86
|
+
whatsappApiKey: db.whatsappApiKey || process.env.WHATSAPP_API_KEY || "",
|
|
87
|
+
whatsappPhoneNumberId: db.whatsappPhoneNumberId || process.env.WHATSAPP_PHONE_NUMBER_ID || "",
|
|
88
|
+
whatsappCloudApiToken: db.whatsappCloudApiToken || process.env.WHATSAPP_CLOUD_API_TOKEN || "",
|
|
89
|
+
whatsappAdminNotifyNumbers: db.whatsappAdminNotifyNumbers ||
|
|
90
|
+
process.env.WHATSAPP_ADMIN_NOTIFY_NUMBERS ||
|
|
91
|
+
"",
|
|
82
92
|
};
|
|
83
93
|
_cache = { value, expiresAt: Date.now() + CACHE_TTL_MS };
|
|
84
94
|
return value;
|
|
@@ -66,6 +66,12 @@ export async function FeesView({} = {}) {
|
|
|
66
66
|
who: t("paidByBuyer"),
|
|
67
67
|
note: t("codHandlingFeeNote"),
|
|
68
68
|
},
|
|
69
|
+
{
|
|
70
|
+
category: t("whatsappNotifyFeeTitle"),
|
|
71
|
+
rate: t("whatsappNotifyFeeRate"),
|
|
72
|
+
who: t("paidByBuyer"),
|
|
73
|
+
note: t("whatsappNotifyFeeNote"),
|
|
74
|
+
},
|
|
69
75
|
{
|
|
70
76
|
category: t("giftWrapFeeTitle"),
|
|
71
77
|
rate: t("giftWrapFeeRate"),
|