@mohasinac/appkit 4.63.3 → 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/promotions/adapters.d.ts +25 -0
- package/dist/_internal/server/features/promotions/adapters.js +43 -0
- package/dist/_internal/server/features/promotions/data.d.ts +26 -0
- package/dist/_internal/server/features/promotions/data.js +45 -0
- package/dist/_internal/server/features/promotions/index.d.ts +2 -1
- package/dist/_internal/server/features/promotions/index.js +2 -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/triggerEventRaffle.js +45 -14
- 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/field-names.d.ts +4 -0
- package/dist/constants/field-names.js +4 -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/components/UserSidebar.js +1 -1
- 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/AdminBlogEditorView.js +10 -2
- 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/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +14 -0
- package/dist/features/blog/schemas/firestore.d.ts +17 -0
- package/dist/features/blog/schemas/firestore.js +1 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +3 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- 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/events/components/AdminEventEditorView.js +64 -9
- package/dist/features/events/components/EventOfferCard.js +3 -18
- package/dist/features/events/components/EventRaffleWinnerView.d.ts +6 -0
- package/dist/features/events/components/EventRaffleWinnerView.js +11 -1
- package/dist/features/events/schemas/event-form.d.ts +10 -0
- package/dist/features/events/schemas/event-form.js +19 -0
- package/dist/features/events/schemas/firestore.d.ts +68 -0
- package/dist/features/events/schemas/firestore.js +25 -0
- package/dist/features/events/schemas/index.d.ts +7 -0
- package/dist/features/events/schemas/index.js +5 -0
- package/dist/features/events/types/index.d.ts +4 -0
- 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/promotions/components/AttachedCouponsSection.d.ts +30 -0
- package/dist/features/promotions/components/AttachedCouponsSection.js +99 -0
- package/dist/features/promotions/components/CouponCodeCopyRow.d.ts +19 -0
- package/dist/features/promotions/components/CouponCodeCopyRow.js +39 -0
- package/dist/features/promotions/components/index.d.ts +4 -0
- package/dist/features/promotions/components/index.js +2 -0
- package/dist/features/promotions/constants/attached-coupons.d.ts +55 -0
- package/dist/features/promotions/constants/attached-coupons.js +24 -0
- package/dist/features/promotions/repository/coupons.repository.d.ts +20 -0
- package/dist/features/promotions/repository/coupons.repository.js +39 -0
- package/dist/features/promotions/schemas/attached-coupons.d.ts +39 -0
- package/dist/features/promotions/schemas/attached-coupons.js +66 -0
- 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 +121 -0
- package/dist/schemas/registry.js +2 -1
- package/dist/security/pii-encrypt.js +1 -1
- package/dist/server-entry.d.ts +1 -0
- package/dist/server-entry.js +2 -0
- package/dist/server.d.ts +11 -2
- package/dist/server.js +26 -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
|
@@ -32,6 +32,10 @@ export interface NotificationDocument extends BaseDocument {
|
|
|
32
32
|
readAt?: Date;
|
|
33
33
|
relatedId?: string;
|
|
34
34
|
relatedType?: NotificationRelatedType;
|
|
35
|
+
/** Outcome of the async whatsappNotify job for this notification's WhatsApp send, if one was enqueued. */
|
|
36
|
+
whatsappStatus?: "queued" | "sent" | "failed" | "skipped";
|
|
37
|
+
/** jobs/{id} doc that ran (or is running) the WhatsApp send for this notification. */
|
|
38
|
+
whatsappJobId?: string;
|
|
35
39
|
}
|
|
36
40
|
export declare const NOTIFICATIONS_COLLECTION: "notifications";
|
|
37
41
|
export declare const NOTIFICATIONS_INDEXED_FIELDS: readonly ["userId", "isRead", "createdAt", "type"];
|
|
@@ -110,6 +114,7 @@ export interface SiteSettingsCredentials {
|
|
|
110
114
|
/** Webhook signing secret from Razorpay's dashboard → Webhooks. Without it `verifyWebhook()` fails closed. */
|
|
111
115
|
razorpayWebhookSecret?: string;
|
|
112
116
|
resendApiKey?: string;
|
|
117
|
+
whatsappApiKey?: string;
|
|
113
118
|
metaAppId?: string;
|
|
114
119
|
metaAppSecret?: string;
|
|
115
120
|
metaPageAccessToken?: string;
|
|
@@ -124,6 +129,28 @@ export interface SiteSettingsCredentials {
|
|
|
124
129
|
/** Google Places API for Google Business Reviews (HS4) */
|
|
125
130
|
googleMapsApiKey?: string;
|
|
126
131
|
googlePlaceId?: string;
|
|
132
|
+
/** WhatsApp Business Cloud API — platform level (for admin purchase announcements) */
|
|
133
|
+
whatsappPhoneNumberId?: string;
|
|
134
|
+
whatsappCloudApiToken?: string;
|
|
135
|
+
/** Comma-separated digits-only phone numbers that receive order announcements */
|
|
136
|
+
whatsappAdminNotifyNumbers?: string;
|
|
137
|
+
/**
|
|
138
|
+
* Approved Meta message-template names, one flat string field per
|
|
139
|
+
* order-lifecycle notification type — every `SiteSettingsCredentials`
|
|
140
|
+
* field is stored as an individually-encrypted string (see
|
|
141
|
+
* `getDecryptedCredentials`/`getCredentialsMasked`, which iterate + mask
|
|
142
|
+
* every field as a string), so this can't be a `Record<string,string>`
|
|
143
|
+
* map. Business-initiated WhatsApp sends outside the 24h customer-service
|
|
144
|
+
* window require a pre-approved template.
|
|
145
|
+
*/
|
|
146
|
+
whatsappTemplateOrderPlaced?: string;
|
|
147
|
+
whatsappTemplateOrderConfirmed?: string;
|
|
148
|
+
whatsappTemplateOrderShipped?: string;
|
|
149
|
+
whatsappTemplateOrderDelivered?: string;
|
|
150
|
+
whatsappTemplateOrderCancelled?: string;
|
|
151
|
+
whatsappTemplateRefundInitiated?: string;
|
|
152
|
+
/** BCP-47 language code the templates above were approved in, e.g. "en" or "en_US". */
|
|
153
|
+
whatsappTemplateLanguage?: string;
|
|
127
154
|
}
|
|
128
155
|
export interface SiteSettingsCredentialsMasked {
|
|
129
156
|
phonepeClientId?: string;
|
|
@@ -136,6 +163,7 @@ export interface SiteSettingsCredentialsMasked {
|
|
|
136
163
|
razorpayKeySecret?: string;
|
|
137
164
|
razorpayWebhookSecret?: string;
|
|
138
165
|
resendApiKey?: string;
|
|
166
|
+
whatsappApiKey?: string;
|
|
139
167
|
metaAppId?: string;
|
|
140
168
|
metaAppSecret?: string;
|
|
141
169
|
metaPageAccessToken?: string;
|
|
@@ -147,6 +175,16 @@ export interface SiteSettingsCredentialsMasked {
|
|
|
147
175
|
deviantartClientSecret?: string;
|
|
148
176
|
googleMapsApiKey?: string;
|
|
149
177
|
googlePlaceId?: string;
|
|
178
|
+
whatsappPhoneNumberId?: string;
|
|
179
|
+
whatsappCloudApiToken?: string;
|
|
180
|
+
whatsappAdminNotifyNumbers?: string;
|
|
181
|
+
whatsappTemplateOrderPlaced?: string;
|
|
182
|
+
whatsappTemplateOrderConfirmed?: string;
|
|
183
|
+
whatsappTemplateOrderShipped?: string;
|
|
184
|
+
whatsappTemplateOrderDelivered?: string;
|
|
185
|
+
whatsappTemplateOrderCancelled?: string;
|
|
186
|
+
whatsappTemplateRefundInitiated?: string;
|
|
187
|
+
whatsappTemplateLanguage?: string;
|
|
150
188
|
}
|
|
151
189
|
export interface NotificationChannelConfig {
|
|
152
190
|
inApp: {
|
|
@@ -162,6 +200,15 @@ export interface NotificationChannelConfig {
|
|
|
162
200
|
/** Subset of NotificationTypes to email — absent means all types. */
|
|
163
201
|
types?: NotificationType[];
|
|
164
202
|
};
|
|
203
|
+
whatsapp: {
|
|
204
|
+
enabled: boolean;
|
|
205
|
+
/** Minimum priority that triggers a WhatsApp message (default: "high"). */
|
|
206
|
+
minPriority?: NotificationPriority;
|
|
207
|
+
/** Subset of NotificationTypes to WhatsApp — absent means all types. */
|
|
208
|
+
types?: NotificationType[];
|
|
209
|
+
/** When true, WhatsApp OTP is offered as an alternative to email OTP at checkout. */
|
|
210
|
+
otpEnabled?: boolean;
|
|
211
|
+
};
|
|
165
212
|
sms: {
|
|
166
213
|
enabled: boolean;
|
|
167
214
|
/** Minimum priority that triggers an SMS (default: "high"). */
|
|
@@ -452,6 +499,29 @@ export interface SiteSettingsDocument extends BaseDocument {
|
|
|
452
499
|
* ₹2,000 of "handling" on a ₹20,000 order — see `computeCodHandlingFee`.
|
|
453
500
|
*/ codHandlingFeeCap: number;
|
|
454
501
|
codHandlingFeePercent: number;
|
|
502
|
+
/**
|
|
503
|
+
* Admin master toggle for the per-order, BUYER-paid WhatsApp addon.
|
|
504
|
+
*
|
|
505
|
+
* 🛑 Defaults to FALSE. The cost of WhatsApp moved to a standing monthly
|
|
506
|
+
* charge on the SELLER (`whatsappNotifyMonthlyFee` below) — the platform
|
|
507
|
+
* owns one number, so the expense is standing rather than per order, and a
|
|
508
|
+
* buyer should not have to purchase their own delivery updates. The
|
|
509
|
+
* checkout path that charges it is left intact rather than cut out again:
|
|
510
|
+
* it is woven through the order-total arithmetic that carries D42's
|
|
511
|
+
* round-once fix, and disabling it makes the fee resolve to 0 without
|
|
512
|
+
* touching that money code.
|
|
513
|
+
*/
|
|
514
|
+
whatsappNotifyFeeEnabled: boolean;
|
|
515
|
+
/** Flat rupee fee charged to the buyer when they opt into the per-order addon. Unreachable while `whatsappNotifyFeeEnabled` is false. */
|
|
516
|
+
whatsappNotifyFee: number;
|
|
517
|
+
/**
|
|
518
|
+
* What a seller pays per month to receive WhatsApp order notifications —
|
|
519
|
+
* the admin-set price of `StoreDocument.whatsappNotifySubscribed`.
|
|
520
|
+
*
|
|
521
|
+
* Rupees per month, not per message: Meta bills us per template, but a
|
|
522
|
+
* seller cannot predict their own order count, and a per-message price
|
|
523
|
+
* would make their bill a function of how well they sold.
|
|
524
|
+
*/ whatsappNotifyMonthlyFee: number;
|
|
455
525
|
/** Admin master toggle — whether the gift-wrap addon is offered at checkout at all. */
|
|
456
526
|
giftWrapFeeEnabled: boolean;
|
|
457
527
|
/** Flat rupee fee charged to the buyer when they opt into gift wrap. */
|
|
@@ -537,6 +607,8 @@ export interface SiteSettingsDocument extends BaseDocument {
|
|
|
537
607
|
messaging?: {
|
|
538
608
|
/** User-facing email. Staff mail (daily digest, payout summary) ignores this. */
|
|
539
609
|
emailEnabled: boolean;
|
|
610
|
+
/** User-facing WhatsApp. Same exemption for staff. */
|
|
611
|
+
whatsappEnabled: boolean;
|
|
540
612
|
/**
|
|
541
613
|
* Reserved sends allowed per channel per day. Applies to staff and
|
|
542
614
|
* transactional mail too — the ceiling exists to stay inside the
|
|
@@ -544,6 +616,7 @@ export interface SiteSettingsDocument extends BaseDocument {
|
|
|
544
616
|
*/
|
|
545
617
|
dailyCeiling: {
|
|
546
618
|
email: number;
|
|
619
|
+
whatsapp: number;
|
|
547
620
|
sms: number;
|
|
548
621
|
};
|
|
549
622
|
};
|
|
@@ -15,7 +15,7 @@ export const ADMIN_CHECKOUT_BYPASS_FLAG_KEY = "adminCheckoutBypass";
|
|
|
15
15
|
* · `constants/notification-types.ts` (9) — fed the admin filter chips and
|
|
16
16
|
* the per-channel allow-list, so the admin list could not filter 18 of
|
|
17
17
|
* the 27 real types and an admin literally could not allow-list
|
|
18
|
-
* `offer_received` or `payment_review` for email
|
|
18
|
+
* `offer_received` or `payment_review` for email or WhatsApp
|
|
19
19
|
* · `features/account/types/index.ts` (5: order|offer|promo|system|message)
|
|
20
20
|
* — a different vocabulary entirely, on a `UserNotification` type with
|
|
21
21
|
* zero real consumers
|
|
@@ -195,6 +195,7 @@ export const notificationQueryHelpers = {
|
|
|
195
195
|
export const DEFAULT_NOTIFICATION_CHANNELS = {
|
|
196
196
|
inApp: { enabled: true, readOnly: true },
|
|
197
197
|
email: { enabled: false, minPriority: "normal" },
|
|
198
|
+
whatsapp: { enabled: false, minPriority: "high", otpEnabled: false },
|
|
198
199
|
sms: { enabled: false, minPriority: "high" },
|
|
199
200
|
};
|
|
200
201
|
const PRIORITY_ORDER = {
|
|
@@ -254,7 +255,8 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
|
|
|
254
255
|
// half-configured site.
|
|
255
256
|
messaging: {
|
|
256
257
|
emailEnabled: false,
|
|
257
|
-
|
|
258
|
+
whatsappEnabled: false,
|
|
259
|
+
dailyCeiling: { email: 80, whatsapp: 200, sms: 50 },
|
|
258
260
|
},
|
|
259
261
|
emi: {
|
|
260
262
|
enabled: false,
|
|
@@ -277,9 +279,12 @@ export const DEFAULT_SITE_SETTINGS_DATA = {
|
|
|
277
279
|
// The buyer add-ons ship ENABLED. They were `false`, which made
|
|
278
280
|
// StoreAddonsPicker return null on both the cart and checkout, so a fresh
|
|
279
281
|
// install had no add-ons at all and no indication why. `undefined` still
|
|
280
|
-
// means disabled everywhere (
|
|
282
|
+
// means disabled everywhere (computeWhatsAppNotifyFee et al. read
|
|
281
283
|
// `if (!rates.XEnabled) return 0`) — only the DEFAULT changed, so an
|
|
282
284
|
// existing siteSettings document keeps whatever it already stored.
|
|
285
|
+
whatsappNotifyFeeEnabled: false,
|
|
286
|
+
whatsappNotifyFee: 10,
|
|
287
|
+
whatsappNotifyMonthlyFee: 199,
|
|
283
288
|
giftWrapFeeEnabled: true,
|
|
284
289
|
giftWrapFee: 49,
|
|
285
290
|
shipmentProtectionFeeEnabled: true,
|
|
@@ -44,6 +44,6 @@ export interface CheckoutValueOtpDoc {
|
|
|
44
44
|
* making the whole feature look broken.
|
|
45
45
|
*/
|
|
46
46
|
export declare function enforceCheckoutValueOtpRateLimit(db: Db, uid: string): Promise<void>;
|
|
47
|
-
/** Build the checkout-value OTP message body. */
|
|
47
|
+
/** Build the checkout-value OTP WhatsApp/SMS message body. */
|
|
48
48
|
export declare function buildCheckoutValueOtpMessage(code: string, siteName: string): string;
|
|
49
49
|
export {};
|
|
@@ -75,7 +75,7 @@ export async function enforceCheckoutValueOtpRateLimit(db, uid) {
|
|
|
75
75
|
tx.set(metaRef, { lastSentAt: new Date() }, { merge: true });
|
|
76
76
|
});
|
|
77
77
|
}
|
|
78
|
-
/** Build the checkout-value OTP message body. */
|
|
78
|
+
/** Build the checkout-value OTP WhatsApp/SMS message body. */
|
|
79
79
|
export function buildCheckoutValueOtpMessage(code, siteName) {
|
|
80
80
|
return (`*${siteName} — Order Verification*\n\n` +
|
|
81
81
|
`Your verification code for this high-value order is: *${code}*\n\n` +
|
|
@@ -83,7 +83,7 @@ export declare const ROUTE_PERMISSION_MAP: Record<string, Permission>;
|
|
|
83
83
|
*
|
|
84
84
|
* Defaults on store creation: ["suggest_brands", "create_coupons"]
|
|
85
85
|
*/
|
|
86
|
-
export type StoreCapability = "advanced_listing_types" | "host_auctions" | "host_preorders" | "create_categories" | "suggest_brands" | "create_coupons" | "bulk_listing_import" | "extended_return_window" | "verified_seller" | "featured_placement" | "promotional_banner" | "priority_support" | "multiple_stores" | "custom_store_slug" | "api_access" | "lower_commission_rate" | "early_access_features" | "advanced_analytics";
|
|
86
|
+
export type StoreCapability = "advanced_listing_types" | "host_auctions" | "host_preorders" | "create_categories" | "suggest_brands" | "create_coupons" | "bulk_listing_import" | "extended_return_window" | "verified_seller" | "featured_placement" | "promotional_banner" | "priority_support" | "multiple_stores" | "custom_store_slug" | "api_access" | "lower_commission_rate" | "early_access_features" | "advanced_analytics" | "whatsapp_catalog_sync";
|
|
87
87
|
export declare const ALL_STORE_CAPABILITIES: StoreCapability[];
|
|
88
88
|
/** Narrow an untrusted string to a real capability. */
|
|
89
89
|
export declare function isStoreCapability(value: unknown): value is StoreCapability;
|
|
@@ -118,7 +118,7 @@ export declare function hasCapability(capabilities: StoreCapability[], required:
|
|
|
118
118
|
*
|
|
119
119
|
* 🛑 The PURE half of `getStoreCapabilities`, extracted so a caller that
|
|
120
120
|
* already holds the store document does not pay a second Firestore read to ask
|
|
121
|
-
* what it is entitled to (Rule #6).
|
|
121
|
+
* what it is entitled to (Rule #6). Three WhatsApp routes were reading
|
|
122
122
|
* `store.capabilities ?? []` inline for exactly that reason, which made them a
|
|
123
123
|
* second, divergent reader of the field — they skipped the default merge, so
|
|
124
124
|
* they and `getStoreCapabilities` could disagree about the same store.
|
|
@@ -390,6 +390,7 @@ const STORE_CAPABILITY_MAP = {
|
|
|
390
390
|
lower_commission_rate: true,
|
|
391
391
|
early_access_features: true,
|
|
392
392
|
advanced_analytics: true,
|
|
393
|
+
whatsapp_catalog_sync: true,
|
|
393
394
|
};
|
|
394
395
|
export const ALL_STORE_CAPABILITIES = Object.keys(STORE_CAPABILITY_MAP);
|
|
395
396
|
/** Narrow an untrusted string to a real capability. */
|
|
@@ -441,7 +442,7 @@ export function hasCapability(capabilities, required) {
|
|
|
441
442
|
*
|
|
442
443
|
* 🛑 The PURE half of `getStoreCapabilities`, extracted so a caller that
|
|
443
444
|
* already holds the store document does not pay a second Firestore read to ask
|
|
444
|
-
* what it is entitled to (Rule #6).
|
|
445
|
+
* what it is entitled to (Rule #6). Three WhatsApp routes were reading
|
|
445
446
|
* `store.capabilities ?? []` inline for exactly that reason, which made them a
|
|
446
447
|
* second, divergent reader of the field — they skipped the default merge, so
|
|
447
448
|
* they and `getStoreCapabilities` could disagree about the same store.
|
|
@@ -48,6 +48,7 @@ export declare const blogDraftSchema: z.ZodObject<{
|
|
|
48
48
|
authorName: z.ZodString;
|
|
49
49
|
isFeatured: z.ZodBoolean;
|
|
50
50
|
isPinned: z.ZodBoolean;
|
|
51
|
+
couponIds: z.ZodType<string[] | undefined, z.ZodTypeDef, string[] | undefined>;
|
|
51
52
|
}, "strip", z.ZodTypeAny, {
|
|
52
53
|
status: "published" | "draft" | "archived";
|
|
53
54
|
title: string;
|
|
@@ -64,6 +65,7 @@ export declare const blogDraftSchema: z.ZodObject<{
|
|
|
64
65
|
isPinned: boolean;
|
|
65
66
|
metaTitle: string;
|
|
66
67
|
metaDescription: string;
|
|
68
|
+
couponIds?: string[] | undefined;
|
|
67
69
|
}, {
|
|
68
70
|
status: "published" | "draft" | "archived";
|
|
69
71
|
title: string;
|
|
@@ -80,4 +82,5 @@ export declare const blogDraftSchema: z.ZodObject<{
|
|
|
80
82
|
isPinned: boolean;
|
|
81
83
|
metaTitle: string;
|
|
82
84
|
metaDescription: string;
|
|
85
|
+
couponIds?: string[] | undefined;
|
|
83
86
|
}>;
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
import { z } from "zod";
|
|
21
21
|
import { annotate } from "../../shell/field-ui-meta.js";
|
|
22
22
|
import { blogPostCategorySchema } from "./index.js";
|
|
23
|
+
import { attachedCouponIdsSchema } from "../../promotions/schemas/attached-coupons.js";
|
|
23
24
|
/**
|
|
24
25
|
* The blog form's own schema. Distinct from `blogPostSchema` (blog/schemas),
|
|
25
26
|
* which requires an `id` this draft cannot have before its first save and
|
|
@@ -98,4 +99,17 @@ export const blogDraftSchema = z.object({
|
|
|
98
99
|
authorName: annotate(z.string(), { section: "publish", order: 3, row: "pair" }),
|
|
99
100
|
isFeatured: annotate(z.boolean(), { section: "publish", order: 4, row: "quarter" }),
|
|
100
101
|
isPinned: annotate(z.boolean(), { section: "publish", order: 5, row: "quarter" }),
|
|
102
|
+
/*
|
|
103
|
+
* 🛑 Declared here even though the picker enforces the cap in the UI, and
|
|
104
|
+
* this file's own header says why: `z.object()` STRIPS unknown keys, so a
|
|
105
|
+
* field missing from this schema is silently dropped on every save — the
|
|
106
|
+
* form would show the coupons, accept the edit, and persist nothing. Same
|
|
107
|
+
* shape as the grouped-listings PATCH in Root Cause #76(b).
|
|
108
|
+
*
|
|
109
|
+
* The cap is `attachedCouponIdsSchema`'s, imported rather than re-stated, so
|
|
110
|
+
* the form and the route cannot disagree about what "too many" means.
|
|
111
|
+
*/
|
|
112
|
+
couponIds: annotate(attachedCouponIdsSchema, {
|
|
113
|
+
section: "publish", order: 6, row: "full", kind: "list",
|
|
114
|
+
}),
|
|
101
115
|
});
|
|
@@ -28,6 +28,22 @@ export interface BlogPostDocument extends BaseDocument {
|
|
|
28
28
|
youtubeId?: string;
|
|
29
29
|
category: BlogPostCategory;
|
|
30
30
|
tags: string[];
|
|
31
|
+
/**
|
|
32
|
+
* PROMOTIONAL coupons shown below the article for a reader to copy.
|
|
33
|
+
*
|
|
34
|
+
* Mirrors `EventDocument.couponIds` exactly — same name, same shape, same
|
|
35
|
+
* `<AttachedCouponsSection>` renderer, same `getAttachedCoupons` resolver.
|
|
36
|
+
* That symmetry is the point: "attach coupons to content" is one feature
|
|
37
|
+
* with two hosts, not two features that happen to look alike.
|
|
38
|
+
*
|
|
39
|
+
* 🛑 OPTIONAL, unlike `isPinned` above, and for a reason worth keeping
|
|
40
|
+
* straight: `isPinned` had to be required because it is an `orderBy` field
|
|
41
|
+
* and Firestore excludes a document that lacks one (Root Cause #100).
|
|
42
|
+
* `couponIds` is never sorted or filtered on — it is read only after a post
|
|
43
|
+
* has already been fetched by slug — so an absent field costs nothing and
|
|
44
|
+
* needs no backfill across the 21 existing posts.
|
|
45
|
+
*/
|
|
46
|
+
couponIds?: string[];
|
|
31
47
|
isFeatured: boolean;
|
|
32
48
|
/**
|
|
33
49
|
* Keeps the post at the top of the blog index and the homepage
|
|
@@ -90,6 +106,7 @@ export declare const BLOG_POST_FIELDS: {
|
|
|
90
106
|
readonly YOUTUBE_ID: "youtubeId";
|
|
91
107
|
readonly CATEGORY: "category";
|
|
92
108
|
readonly TAGS: "tags";
|
|
109
|
+
readonly COUPON_IDS: "couponIds";
|
|
93
110
|
readonly IS_FEATURED: "isFeatured";
|
|
94
111
|
readonly IS_PINNED: "isPinned";
|
|
95
112
|
readonly STATUS: "status";
|
|
@@ -98,6 +98,7 @@ export declare const blogPostFirestoreSchema: z.ZodObject<{
|
|
|
98
98
|
}>, "many">>;
|
|
99
99
|
category: z.ZodString;
|
|
100
100
|
tags: z.ZodArray<z.ZodString, "many">;
|
|
101
|
+
couponIds: z.ZodType<string[] | undefined, z.ZodTypeDef, string[] | undefined>;
|
|
101
102
|
isFeatured: z.ZodBoolean;
|
|
102
103
|
isPinned: z.ZodBoolean;
|
|
103
104
|
status: z.ZodEnum<["draft", "published", "archived"]>;
|
|
@@ -139,6 +140,7 @@ export declare const blogPostFirestoreSchema: z.ZodObject<{
|
|
|
139
140
|
publishedAt?: string | Date | z.objectOutputType<{
|
|
140
141
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
141
142
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
143
|
+
couponIds?: string[] | undefined;
|
|
142
144
|
coverImage?: {
|
|
143
145
|
type: "video" | "image" | "file";
|
|
144
146
|
url: string;
|
|
@@ -193,6 +195,7 @@ export declare const blogPostFirestoreSchema: z.ZodObject<{
|
|
|
193
195
|
publishedAt?: string | Date | z.objectInputType<{
|
|
194
196
|
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
195
197
|
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
198
|
+
couponIds?: string[] | undefined;
|
|
196
199
|
coverImage?: {
|
|
197
200
|
type: "video" | "image" | "file";
|
|
198
201
|
url: string;
|
|
@@ -345,6 +348,7 @@ export declare const blogPostSchema: z.ZodObject<{
|
|
|
345
348
|
}>, "many">>>;
|
|
346
349
|
category: z.ZodEnum<["news", "tips", "guides", "updates", "community"]>;
|
|
347
350
|
tags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
351
|
+
couponIds: z.ZodType<string[] | undefined, z.ZodTypeDef, string[] | undefined>;
|
|
348
352
|
isFeatured: z.ZodOptional<z.ZodBoolean>;
|
|
349
353
|
isPinned: z.ZodOptional<z.ZodBoolean>;
|
|
350
354
|
status: z.ZodEnum<["draft", "published", "archived"]>;
|
|
@@ -400,6 +404,7 @@ export declare const blogPostSchema: z.ZodObject<{
|
|
|
400
404
|
publishedAt?: string | undefined;
|
|
401
405
|
views?: number | undefined;
|
|
402
406
|
tags?: string[] | undefined;
|
|
407
|
+
couponIds?: string[] | undefined;
|
|
403
408
|
isFeatured?: boolean | undefined;
|
|
404
409
|
excerpt?: string | undefined;
|
|
405
410
|
authorId?: string | undefined;
|
|
@@ -421,6 +426,7 @@ export declare const blogPostSchema: z.ZodObject<{
|
|
|
421
426
|
publishedAt?: string | undefined;
|
|
422
427
|
views?: number | undefined;
|
|
423
428
|
tags?: string[] | undefined;
|
|
429
|
+
couponIds?: string[] | undefined;
|
|
424
430
|
isFeatured?: boolean | undefined;
|
|
425
431
|
excerpt?: string | undefined;
|
|
426
432
|
coverImage?: string | {
|
|
@@ -3,6 +3,7 @@ import { z } from "zod";
|
|
|
3
3
|
import { mediaUrlSchema } from "../../../validation/schemas.js";
|
|
4
4
|
import { mediaFieldSchema } from "../../media/types/index.js";
|
|
5
5
|
import { auditTimestampsShape, firestoreDateSchema } from "../../../schemas/firestore-helpers.js";
|
|
6
|
+
import { attachedCouponIdsSchema } from "../../promotions/schemas/attached-coupons.js";
|
|
6
7
|
// ─── Firestore document schema (W2) ───────────────────────────────────────────
|
|
7
8
|
// Mirrors BlogPostDocument in ./firestore.ts. Registered into SCHEMAS.firestore.blogPosts.
|
|
8
9
|
export const blogPostStatusEnumSchema = z.enum(["draft", "published", "archived"]);
|
|
@@ -17,6 +18,7 @@ export const blogPostFirestoreSchema = z.object({
|
|
|
17
18
|
additionalImages: z.array(mediaFieldSchema).optional(),
|
|
18
19
|
category: z.string(),
|
|
19
20
|
tags: z.array(z.string()),
|
|
21
|
+
couponIds: attachedCouponIdsSchema,
|
|
20
22
|
isFeatured: z.boolean(),
|
|
21
23
|
isPinned: z.boolean(),
|
|
22
24
|
status: blogPostStatusEnumSchema,
|
|
@@ -72,6 +74,7 @@ export const blogPostSchema = z.object({
|
|
|
72
74
|
additionalImages: z.array(mediaFieldSchema).max(5).optional().default([]),
|
|
73
75
|
category: blogPostCategorySchema,
|
|
74
76
|
tags: z.array(z.string()).optional(),
|
|
77
|
+
couponIds: attachedCouponIdsSchema,
|
|
75
78
|
isFeatured: z.boolean().optional(),
|
|
76
79
|
isPinned: z.boolean().optional(),
|
|
77
80
|
status: blogPostStatusSchema,
|
|
@@ -14,6 +14,8 @@ export interface BlogPost {
|
|
|
14
14
|
youtubeId?: string;
|
|
15
15
|
category: BlogPostCategory;
|
|
16
16
|
tags?: string[];
|
|
17
|
+
/** Promotional coupons shown below the article — see `BlogPostDocument.couponIds`. */
|
|
18
|
+
couponIds?: string[];
|
|
17
19
|
isFeatured?: boolean;
|
|
18
20
|
/** Keeps the post above newer ones on the blog index and homepage. */
|
|
19
21
|
isPinned?: boolean;
|
|
@@ -22,6 +22,7 @@ export interface CartPriceBreakdownStore {
|
|
|
22
22
|
subtotal: number;
|
|
23
23
|
shippingFee: number;
|
|
24
24
|
codHandlingFee: number;
|
|
25
|
+
whatsappNotifyFee: number;
|
|
25
26
|
giftWrapFee: number;
|
|
26
27
|
shipmentProtectionFee: number;
|
|
27
28
|
saleProtectionFee: number;
|
|
@@ -44,6 +45,7 @@ export interface CartPriceBreakdownData {
|
|
|
44
45
|
subtotal: number;
|
|
45
46
|
shippingFee: number;
|
|
46
47
|
codHandlingFee: number;
|
|
48
|
+
whatsappNotifyFee: number;
|
|
47
49
|
giftWrapFee: number;
|
|
48
50
|
shipmentProtectionFee: number;
|
|
49
51
|
saleProtectionFee: number;
|
|
@@ -40,7 +40,7 @@ export function CartPriceBreakdown({ preview, itemCount, currency = "INR", unava
|
|
|
40
40
|
const subtotalLabel = itemCount !== undefined
|
|
41
41
|
? `Subtotal (${itemCount} item${itemCount !== 1 ? "s" : ""}${storeCount > 1 ? `, ${storeCount} stores` : ""})`
|
|
42
42
|
: "Subtotal";
|
|
43
|
-
return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee), derivation: derivations?.codHandlingFee })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.saleProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Sale protection", countCharged((s) => s.saleProtectionFee)), amount: money(preview.saleProtectionFee) })), preview.emiSurcharge > 0 && (_jsx(Line, { label: "EMI surcharge", amount: money(preview.emiSurcharge), derivation: derivations?.emiSurcharge })), preview.sharedPurchaseFee > 0 && (_jsx(Line, { label: withStoreCount("Split delivery", countCharged((s) => s.sharedPurchaseFee)), amount: money(preview.sharedPurchaseFee) })), preview.platformFee > 0 && (_jsx(Line, { label: "Platform fee", amount: money(preview.platformFee), derivation: derivations?.platformFee })), preview.gstOnFee > 0 && (_jsx(Line, { label: "GST on platform fee", amount: money(preview.gstOnFee), derivation: derivations?.gstOnFee })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: preview.pointsApplied <= 0 }) }), preview.pointsApplied > 0 && (_jsxs(_Fragment, { children: [_jsx(Line, { label: `Points applied (${preview.pointsApplied.toLocaleString("en-IN")} pts)`, amount: `−${money(preview.pointsApplied)}`, tone: "success" }), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Amount due", amount: money(preview.amountDue), tone: "primary", strong: true }) })] })), preview.pointsApplied > 0 && preview.pointsBalance > preview.pointsApplied && (_jsxs(Text, { size: "xs", color: "muted", children: [(preview.pointsBalance - preview.pointsApplied).toLocaleString("en-IN"), " points stay in your balance \u2014 up to half of an order can be paid with points."] })), preview.pointsApplied === 0 && preview.pointsBalance > 0 && (_jsx(Text, { size: "xs", color: "muted", children: preview.stores.some((s) => s.acceptsPoints)
|
|
43
|
+
return (_jsxs(Stack, { gap: "xs", className: className, children: [_jsx(Line, { label: subtotalLabel, amount: money(preview.subtotal) }), preview.shippingFee > 0 && (_jsx(Line, { label: withStoreCount("Shipping", storeCount), amount: money(preview.shippingFee) })), preview.codHandlingFee > 0 && (_jsx(Line, { label: "COD handling fee", amount: money(preview.codHandlingFee), derivation: derivations?.codHandlingFee })), preview.whatsappNotifyFee > 0 && (_jsx(Line, { label: withStoreCount("WhatsApp updates", countCharged((s) => s.whatsappNotifyFee)), amount: money(preview.whatsappNotifyFee) })), preview.giftWrapFee > 0 && (_jsx(Line, { label: withStoreCount("Gift wrap", countCharged((s) => s.giftWrapFee)), amount: money(preview.giftWrapFee) })), preview.shipmentProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Shipment protection", countCharged((s) => s.shipmentProtectionFee)), amount: money(preview.shipmentProtectionFee) })), preview.saleProtectionFee > 0 && (_jsx(Line, { label: withStoreCount("Sale protection", countCharged((s) => s.saleProtectionFee)), amount: money(preview.saleProtectionFee) })), preview.emiSurcharge > 0 && (_jsx(Line, { label: "EMI surcharge", amount: money(preview.emiSurcharge), derivation: derivations?.emiSurcharge })), preview.sharedPurchaseFee > 0 && (_jsx(Line, { label: withStoreCount("Split delivery", countCharged((s) => s.sharedPurchaseFee)), amount: money(preview.sharedPurchaseFee) })), preview.platformFee > 0 && (_jsx(Line, { label: "Platform fee", amount: money(preview.platformFee), derivation: derivations?.platformFee })), preview.gstOnFee > 0 && (_jsx(Line, { label: "GST on platform fee", amount: money(preview.gstOnFee), derivation: derivations?.gstOnFee })), preview.couponDiscount > 0 && (_jsx(Line, { label: "Coupon discount", amount: `−${money(preview.couponDiscount)}`, tone: "success" })), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Total", amount: money(preview.total), tone: "primary", strong: preview.pointsApplied <= 0 }) }), preview.pointsApplied > 0 && (_jsxs(_Fragment, { children: [_jsx(Line, { label: `Points applied (${preview.pointsApplied.toLocaleString("en-IN")} pts)`, amount: `−${money(preview.pointsApplied)}`, tone: "success" }), _jsx(Div, { border: "top-subtle", paddingY: "t-sm", children: _jsx(Line, { label: "Amount due", amount: money(preview.amountDue), tone: "primary", strong: true }) })] })), preview.pointsApplied > 0 && preview.pointsBalance > preview.pointsApplied && (_jsxs(Text, { size: "xs", color: "muted", children: [(preview.pointsBalance - preview.pointsApplied).toLocaleString("en-IN"), " points stay in your balance \u2014 up to half of an order can be paid with points."] })), preview.pointsApplied === 0 && preview.pointsBalance > 0 && (_jsx(Text, { size: "xs", color: "muted", children: preview.stores.some((s) => s.acceptsPoints)
|
|
44
44
|
? "Your points could not be applied to this order."
|
|
45
45
|
: storeCount > 1
|
|
46
46
|
? "None of these sellers accepts points."
|
|
@@ -3,12 +3,13 @@ import React from "react";
|
|
|
3
3
|
* Paid add-on checkboxes for ONE store.
|
|
4
4
|
*
|
|
5
5
|
* Add-on fees are billed per order group (= per store), so they are chosen per
|
|
6
|
-
* store: ticking "
|
|
6
|
+
* store: ticking "WhatsApp updates" for one seller must not bill it for every
|
|
7
7
|
* seller in the cart. Rendered inside that store's card in the cart, and inside
|
|
8
8
|
* that store's row of the checkout summary — Buy Now skips the cart entirely,
|
|
9
9
|
* so both surfaces have to be able to edit them.
|
|
10
10
|
*/
|
|
11
11
|
export interface StoreAddonsValue {
|
|
12
|
+
whatsappNotifyAddon?: boolean;
|
|
12
13
|
giftWrapAddon?: boolean;
|
|
13
14
|
giftWrapMessage?: string;
|
|
14
15
|
shipmentProtectionAddon?: boolean;
|
|
@@ -16,6 +17,8 @@ export interface StoreAddonsValue {
|
|
|
16
17
|
}
|
|
17
18
|
/** The subset of `siteSettings.commissions` that gates and prices the add-ons. */
|
|
18
19
|
export interface StoreAddonsRates {
|
|
20
|
+
whatsappNotifyFeeEnabled?: boolean;
|
|
21
|
+
whatsappNotifyFee?: number;
|
|
19
22
|
giftWrapFeeEnabled?: boolean;
|
|
20
23
|
giftWrapFee?: number;
|
|
21
24
|
shipmentProtectionFeeEnabled?: boolean;
|
|
@@ -56,7 +59,7 @@ export interface StoreProtectionContext {
|
|
|
56
59
|
* showing an explanatory line instead.
|
|
57
60
|
*
|
|
58
61
|
* 🛑 Deliberately asks only about TIER 1. A seller who supports neither
|
|
59
|
-
* protection still has gift wrap, and a store whose only
|
|
62
|
+
* protection still has WhatsApp updates and gift wrap, and a store whose only
|
|
60
63
|
* enabled add-ons are protections it declined must still render the picker —
|
|
61
64
|
* because that is where the risk notice lives, and a hidden notice is the
|
|
62
65
|
* tri-state failure in § `StoreProtectionContext`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Gift, ShieldAlert, ShieldCheck, ShieldOff } from "lucide-react";
|
|
3
|
+
import { Gift, MessageCircle, ShieldAlert, ShieldCheck, ShieldOff } from "lucide-react";
|
|
4
4
|
import { Div, Row, Stack, Text } from "../../../ui/index.js";
|
|
5
5
|
import { Disclaimer } from "../../../ui/components/Disclaimer.js";
|
|
6
6
|
import { IconBox } from "../../../ui/components/IconBox.js";
|
|
@@ -16,14 +16,15 @@ import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
|
16
16
|
* showing an explanatory line instead.
|
|
17
17
|
*
|
|
18
18
|
* 🛑 Deliberately asks only about TIER 1. A seller who supports neither
|
|
19
|
-
* protection still has gift wrap, and a store whose only
|
|
19
|
+
* protection still has WhatsApp updates and gift wrap, and a store whose only
|
|
20
20
|
* enabled add-ons are protections it declined must still render the picker —
|
|
21
21
|
* because that is where the risk notice lives, and a hidden notice is the
|
|
22
22
|
* tri-state failure in § `StoreProtectionContext`.
|
|
23
23
|
*/
|
|
24
24
|
export function hasAnyStoreAddon(rates) {
|
|
25
25
|
return (!!rates &&
|
|
26
|
-
!!(rates.
|
|
26
|
+
!!(rates.whatsappNotifyFeeEnabled ||
|
|
27
|
+
rates.giftWrapFeeEnabled ||
|
|
27
28
|
rates.shipmentProtectionFeeEnabled ||
|
|
28
29
|
rates.saleProtectionFeeEnabled));
|
|
29
30
|
}
|
|
@@ -51,7 +52,7 @@ export function StoreAddonsPicker({ storeId, storeSubtotal, value, onChange, rat
|
|
|
51
52
|
const shippingDeclined = !!rates.shipmentProtectionFeeEnabled && !protection?.supportsShippingProtection;
|
|
52
53
|
const saleDeclined = !!rates.saleProtectionFeeEnabled && !protection?.supportsSaleProtection;
|
|
53
54
|
const saleBlockedByFinalSale = !!rates.saleProtectionFeeEnabled && !!protection?.supportsSaleProtection && !!protection?.hasFinalSaleItem;
|
|
54
|
-
return (_jsxs(Stack, { gap: "xs", className: `min-w-0 ${className}`, children: [disabled && disabledReason && (_jsx(Text, { size: "xs", color: "muted", children: disabledReason })), rates.giftWrapFeeEnabled && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "brand", children: _jsx(Gift, { size: 14 }) }), _jsxs(Stack, { gap: "xs", className: "flex-1 min-w-0", children: [_jsx(FieldCheckbox, { name: `giftWrapAddon-${storeId}`, label: `Gift wrap (+${money(rates.giftWrapFee ?? 49)})`, checked: !!value.giftWrapAddon, onChange: (checked) => set({ giftWrapAddon: checked }), disabled: disabled }), showGiftMessage && value.giftWrapAddon && (_jsx(FieldTextarea, { name: `giftWrapMessage-${storeId}`, label: "Gift message (optional)", value: value.giftWrapMessage ?? "", onChange: (next) => set({ giftWrapMessage: next.slice(0, 500) }), rows: 2, maxLength: 500, showCharCount: true, placeholder: "Add a note for the recipient\u2026", disabled: disabled }))] })] })), shippingOffered && storeSubtotal > 0 && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "brand", children: _jsx(ShieldCheck, { size: 14 }) }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(FieldCheckbox, { name: `shipmentProtectionAddon-${storeId}`, label: `Shipment protection — loss or damage in transit (+${money(protectionFee)})`, checked: !!value.shipmentProtectionAddon, onChange: (checked) => set({ shipmentProtectionAddon: checked }), disabled: disabled }) })] })), saleOffered && storeSubtotal > 0 && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "brand", children: _jsx(ShieldAlert, { size: 14 }) }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(FieldCheckbox, { name: `saleProtectionAddon-${storeId}`, label: `Sale protection — item not as described (+${money(saleFee)})`, checked: !!value.saleProtectionAddon, onChange: (checked) => set({ saleProtectionAddon: checked }), disabled: disabled }) })] })), (shippingDeclined || saleDeclined) && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "muted", children: _jsx(ShieldOff, { size: 14 }) }), _jsx(Text, { size: "xs", color: "muted", className: "flex-1 min-w-0", children: shippingDeclined && saleDeclined
|
|
55
|
+
return (_jsxs(Stack, { gap: "xs", className: `min-w-0 ${className}`, children: [disabled && disabledReason && (_jsx(Text, { size: "xs", color: "muted", children: disabledReason })), rates.whatsappNotifyFeeEnabled && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "brand", children: _jsx(MessageCircle, { size: 14 }) }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(FieldCheckbox, { name: `whatsappNotifyAddon-${storeId}`, label: `WhatsApp order updates (+${money(rates.whatsappNotifyFee ?? 10)})`, checked: !!value.whatsappNotifyAddon, onChange: (checked) => set({ whatsappNotifyAddon: checked }), disabled: disabled }) })] })), rates.giftWrapFeeEnabled && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "brand", children: _jsx(Gift, { size: 14 }) }), _jsxs(Stack, { gap: "xs", className: "flex-1 min-w-0", children: [_jsx(FieldCheckbox, { name: `giftWrapAddon-${storeId}`, label: `Gift wrap (+${money(rates.giftWrapFee ?? 49)})`, checked: !!value.giftWrapAddon, onChange: (checked) => set({ giftWrapAddon: checked }), disabled: disabled }), showGiftMessage && value.giftWrapAddon && (_jsx(FieldTextarea, { name: `giftWrapMessage-${storeId}`, label: "Gift message (optional)", value: value.giftWrapMessage ?? "", onChange: (next) => set({ giftWrapMessage: next.slice(0, 500) }), rows: 2, maxLength: 500, showCharCount: true, placeholder: "Add a note for the recipient\u2026", disabled: disabled }))] })] })), shippingOffered && storeSubtotal > 0 && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "brand", children: _jsx(ShieldCheck, { size: 14 }) }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(FieldCheckbox, { name: `shipmentProtectionAddon-${storeId}`, label: `Shipment protection — loss or damage in transit (+${money(protectionFee)})`, checked: !!value.shipmentProtectionAddon, onChange: (checked) => set({ shipmentProtectionAddon: checked }), disabled: disabled }) })] })), saleOffered && storeSubtotal > 0 && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "brand", children: _jsx(ShieldAlert, { size: 14 }) }), _jsx(Div, { className: "flex-1 min-w-0", children: _jsx(FieldCheckbox, { name: `saleProtectionAddon-${storeId}`, label: `Sale protection — item not as described (+${money(saleFee)})`, checked: !!value.saleProtectionAddon, onChange: (checked) => set({ saleProtectionAddon: checked }), disabled: disabled }) })] })), (shippingDeclined || saleDeclined) && (_jsxs(Row, { gap: "sm", align: "start", className: "min-w-0", children: [_jsx(IconBox, { size: "sm", tone: "muted", children: _jsx(ShieldOff, { size: 14 }) }), _jsx(Text, { size: "xs", color: "muted", className: "flex-1 min-w-0", children: shippingDeclined && saleDeclined
|
|
55
56
|
? "This seller does not offer shipment or sale protection."
|
|
56
57
|
: shippingDeclined
|
|
57
58
|
? "This seller does not offer shipment protection."
|
|
@@ -577,7 +577,8 @@ export class CartRepository extends BaseRepository {
|
|
|
577
577
|
*/
|
|
578
578
|
async setStoreAddons(userId, storeId, addons) {
|
|
579
579
|
const isEmpty = !addons ||
|
|
580
|
-
(!addons.
|
|
580
|
+
(!addons.whatsappNotifyAddon &&
|
|
581
|
+
!addons.giftWrapAddon &&
|
|
581
582
|
!addons.shipmentProtectionAddon &&
|
|
582
583
|
!addons.saleProtectionAddon);
|
|
583
584
|
await this.db
|
|
@@ -272,11 +272,12 @@ export interface CartAppliedCoupon {
|
|
|
272
272
|
*
|
|
273
273
|
* Add-on fees have always been billed per order group (= per store) by
|
|
274
274
|
* `createOrderForGroup` / `previewCheckoutPricing`, but the selection used to be
|
|
275
|
-
* a single cart-wide boolean — so one tick on "
|
|
275
|
+
* a single cart-wide boolean — so one tick on "WhatsApp updates" quietly billed
|
|
276
276
|
* ₹10 × every store in the cart, with no way to opt in for just one seller.
|
|
277
277
|
* Keying the selection by storeId makes the choice match the billing.
|
|
278
278
|
*/
|
|
279
279
|
export interface CartStoreAddons {
|
|
280
|
+
whatsappNotifyAddon?: boolean;
|
|
280
281
|
giftWrapAddon?: boolean;
|
|
281
282
|
giftWrapMessage?: string;
|
|
282
283
|
/** Loss or damage IN TRANSIT. */
|
|
@@ -6,17 +6,12 @@
|
|
|
6
6
|
* purpose from `checkout-actions.ts`'s third-party shipping consent OTP —
|
|
7
7
|
* same crypto primitives, separate Firestore namespace and copy.
|
|
8
8
|
*/
|
|
9
|
+
export type CheckoutValueOtpChannel = "email" | "whatsapp";
|
|
9
10
|
/**
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* than deleted so the parameter stays self-documenting and a second channel
|
|
15
|
-
* has an obvious seam.
|
|
16
|
-
*/
|
|
17
|
-
export type CheckoutValueOtpChannel = "email";
|
|
18
|
-
/**
|
|
19
|
-
* Sends a checkout-value OTP by email.
|
|
11
|
+
* Sends a checkout-value OTP either by email (default) or WhatsApp
|
|
12
|
+
* (opt-in — the buyer must explicitly choose it in the UI). WhatsApp phone
|
|
13
|
+
* is always resolved server-side from the selected shipping address —
|
|
14
|
+
* never trust a client-supplied phone number.
|
|
20
15
|
*
|
|
21
16
|
* If the chosen channel (and, for email, its skip-worthy fallback) is
|
|
22
17
|
* currently circuit-broken or unconfigured, the OTP gate is treated as
|