@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
|
@@ -822,6 +822,10 @@ export interface OrderDocument extends BaseDocument {
|
|
|
822
822
|
codRemainingAmount?: number;
|
|
823
823
|
/** COD handling fee charged to the buyer: min(subtotal × codHandlingFeePercent / 100, codHandlingFeeCap). Only set when paymentMethod === "cod". */
|
|
824
824
|
codHandlingFee?: number;
|
|
825
|
+
/** Buyer opted into WhatsApp order-update messages at checkout (unchecked by default). */
|
|
826
|
+
whatsappNotifyAddon?: boolean;
|
|
827
|
+
/** Fee charged for the WhatsApp addon, decimal rupees — snapshot of siteSettings.commissions.whatsappNotifyFee at order time. */
|
|
828
|
+
whatsappNotifyFee?: number;
|
|
825
829
|
/** Buyer opted into gift wrapping at checkout (unchecked by default). */
|
|
826
830
|
giftWrapAddon?: boolean;
|
|
827
831
|
/** Fee charged for gift wrap, decimal rupees — snapshot of siteSettings.commissions.giftWrapFee at order time. */
|
|
@@ -231,12 +231,15 @@ export interface Order {
|
|
|
231
231
|
/**
|
|
232
232
|
* Paid add-ons the buyer opted into for THIS order (= this store).
|
|
233
233
|
*
|
|
234
|
-
* These are operational flags, not just billing records:
|
|
235
|
-
*
|
|
234
|
+
* These are operational flags, not just billing records: `whatsappNotifyAddon`
|
|
235
|
+
* is what the status-change notifier checks to decide whether to send a
|
|
236
|
+
* WhatsApp message, and `giftWrapAddon`/`shipmentProtectionAddon` are what
|
|
236
237
|
* the packer needs to see before dispatch. They live on the order itself and
|
|
237
238
|
* are filterable (`orders` SIEVE_FIELDS) precisely so nobody has to keep a
|
|
238
239
|
* separate list of who asked for what.
|
|
239
240
|
*/
|
|
241
|
+
whatsappNotifyAddon?: boolean;
|
|
242
|
+
whatsappNotifyFee?: number;
|
|
240
243
|
giftWrapAddon?: boolean;
|
|
241
244
|
giftWrapFee?: number;
|
|
242
245
|
/** The buyer's gift message — the packer has to physically include this. */
|
|
@@ -169,6 +169,15 @@ export const SITE_SETTINGS_ENTRIES = [
|
|
|
169
169
|
tab: "notifications",
|
|
170
170
|
kind: "toggle",
|
|
171
171
|
},
|
|
172
|
+
{
|
|
173
|
+
slug: "whatsapp-notifications",
|
|
174
|
+
anchored: true,
|
|
175
|
+
label: "WhatsApp notifications",
|
|
176
|
+
description: "Send order updates over WhatsApp.",
|
|
177
|
+
keywords: ["whatsapp", "order updates", "meta"],
|
|
178
|
+
tab: "notifications",
|
|
179
|
+
kind: "toggle",
|
|
180
|
+
},
|
|
172
181
|
{
|
|
173
182
|
slug: "daily-digest",
|
|
174
183
|
anchored: true,
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { BaseRepository } from "../../../providers/db-firebase";
|
|
16
16
|
import type { DocumentSnapshot } from "firebase-admin/firestore";
|
|
17
|
-
import { type AnalyticsAlertDocument, type AnalyticsCardDocument, type ItemRequestDocument, type ItemRequestReply, type ListingTemplateDocument, type ModerationQueueDocument, type PayoutMethodDocument, type ReportDocument, type ShippingConfigDocument, type StoreCategoryDocument, type StoreGoogleConfigDocument } from "../schemas/firestore";
|
|
17
|
+
import { type AnalyticsAlertDocument, type AnalyticsCardDocument, type ItemRequestDocument, type ItemRequestReply, type ListingTemplateDocument, type ModerationQueueDocument, type PayoutMethodDocument, type ReportDocument, type ShippingConfigDocument, type StoreCategoryDocument, type StoreGoogleConfigDocument, type StoreWhatsAppConfigDocument } from "../schemas/firestore";
|
|
18
18
|
export declare class PayoutMethodsRepository extends BaseRepository<PayoutMethodDocument> {
|
|
19
19
|
/**
|
|
20
20
|
* Bank/UPI details, encrypted at rest by BaseRepository's write hooks.
|
|
@@ -103,6 +103,10 @@ export declare class ItemRequestsRepository extends BaseRepository<ItemRequestDo
|
|
|
103
103
|
*/
|
|
104
104
|
appendReply(id: string, reply: ItemRequestReply): Promise<void>;
|
|
105
105
|
}
|
|
106
|
+
export declare class StoreWhatsAppConfigRepository extends BaseRepository<StoreWhatsAppConfigDocument> {
|
|
107
|
+
constructor();
|
|
108
|
+
getByStore(storeId: string): Promise<StoreWhatsAppConfigDocument | null>;
|
|
109
|
+
}
|
|
106
110
|
export declare class StoreGoogleConfigRepository extends BaseRepository<StoreGoogleConfigDocument> {
|
|
107
111
|
constructor();
|
|
108
112
|
getByStore(storeId: string): Promise<StoreGoogleConfigDocument | null>;
|
|
@@ -116,4 +120,5 @@ export declare const listingTemplatesRepository: ListingTemplatesRepository;
|
|
|
116
120
|
export declare const moderationQueueRepository: ModerationQueueRepository;
|
|
117
121
|
export declare const reportsRepository: ReportsRepository;
|
|
118
122
|
export declare const itemRequestsRepository: ItemRequestsRepository;
|
|
123
|
+
export declare const storeWhatsAppConfigRepository: StoreWhatsAppConfigRepository;
|
|
119
124
|
export declare const storeGoogleConfigRepository: StoreGoogleConfigRepository;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { BaseRepository } from "../../../providers/db-firebase/index.js";
|
|
16
16
|
import { arrayUnion, increment } from "../../../contracts/field-ops.js";
|
|
17
17
|
import { decryptPiiFields } from "../../../security/pii-encrypt.js";
|
|
18
|
-
import { ANALYTICS_ALERTS_COLLECTION, ANALYTICS_CARDS_COLLECTION, ITEM_REQUESTS_COLLECTION, LISTING_TEMPLATES_COLLECTION, MODERATION_QUEUE_COLLECTION, PAYOUT_METHODS_COLLECTION, REPORTS_COLLECTION, SHIPPING_CONFIGS_COLLECTION, STORE_CATEGORIES_COLLECTION, STORE_GOOGLE_CONFIG_COLLECTION, } from "../schemas/firestore.js";
|
|
18
|
+
import { ANALYTICS_ALERTS_COLLECTION, ANALYTICS_CARDS_COLLECTION, ITEM_REQUESTS_COLLECTION, LISTING_TEMPLATES_COLLECTION, MODERATION_QUEUE_COLLECTION, PAYOUT_METHODS_COLLECTION, REPORTS_COLLECTION, SHIPPING_CONFIGS_COLLECTION, STORE_CATEGORIES_COLLECTION, STORE_GOOGLE_CONFIG_COLLECTION, STORE_WHATSAPP_CONFIG_COLLECTION, } from "../schemas/firestore.js";
|
|
19
19
|
export class PayoutMethodsRepository extends BaseRepository {
|
|
20
20
|
constructor() {
|
|
21
21
|
super(PAYOUT_METHODS_COLLECTION);
|
|
@@ -149,6 +149,14 @@ export class ItemRequestsRepository extends BaseRepository {
|
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
|
+
export class StoreWhatsAppConfigRepository extends BaseRepository {
|
|
153
|
+
constructor() {
|
|
154
|
+
super(STORE_WHATSAPP_CONFIG_COLLECTION);
|
|
155
|
+
}
|
|
156
|
+
async getByStore(storeId) {
|
|
157
|
+
return this.findOneBy("storeId", storeId);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
152
160
|
export class StoreGoogleConfigRepository extends BaseRepository {
|
|
153
161
|
constructor() {
|
|
154
162
|
super(STORE_GOOGLE_CONFIG_COLLECTION);
|
|
@@ -166,4 +174,5 @@ export const listingTemplatesRepository = new ListingTemplatesRepository();
|
|
|
166
174
|
export const moderationQueueRepository = new ModerationQueueRepository();
|
|
167
175
|
export const reportsRepository = new ReportsRepository();
|
|
168
176
|
export const itemRequestsRepository = new ItemRequestsRepository();
|
|
177
|
+
export const storeWhatsAppConfigRepository = new StoreWhatsAppConfigRepository();
|
|
169
178
|
export const storeGoogleConfigRepository = new StoreGoogleConfigRepository();
|
|
@@ -18,6 +18,7 @@ import type { BaseDocument } from "../../../_internal/shared/types/base-document
|
|
|
18
18
|
* 7. moderationQueue — video / media moderation queue items
|
|
19
19
|
* 8. reports — buyer-submitted reports against listings/stores
|
|
20
20
|
* 9. itemRequests — community item request bulletin board
|
|
21
|
+
* 10. storeWhatsAppConfig — per-store WhatsApp business catalogue + auto-replies
|
|
21
22
|
* 11. storeGoogleConfig — per-store Google Business reviews integration
|
|
22
23
|
*/
|
|
23
24
|
export type PayoutMethodType = "upi" | "bank" | "card" | "other";
|
|
@@ -88,7 +89,7 @@ export interface AnalyticsAlertDocument extends BaseDocument {
|
|
|
88
89
|
threshold: number;
|
|
89
90
|
windowHours: number;
|
|
90
91
|
isActive: boolean;
|
|
91
|
-
notifyChannels: Array<"in-app" | "email">;
|
|
92
|
+
notifyChannels: Array<"in-app" | "email" | "whatsapp">;
|
|
92
93
|
/**
|
|
93
94
|
* When this alert last fired.
|
|
94
95
|
*
|
|
@@ -220,6 +221,21 @@ export interface ItemRequestDocument extends BaseDocument {
|
|
|
220
221
|
export declare const ITEM_REQUESTS_COLLECTION: "itemRequests";
|
|
221
222
|
export declare const ITEM_REQUEST_INDEXED_FIELDS: readonly ["opUserId", "status", "category", "createdAt"];
|
|
222
223
|
export declare const DEFAULT_ITEM_REQUEST_DATA: Partial<ItemRequestDocument>;
|
|
224
|
+
export interface StoreWhatsAppConfigDocument extends BaseDocument {
|
|
225
|
+
storeId: string;
|
|
226
|
+
isConnected: boolean;
|
|
227
|
+
isPaid: boolean;
|
|
228
|
+
phoneNumber?: string;
|
|
229
|
+
businessProfileName?: string;
|
|
230
|
+
catalogUrl?: string;
|
|
231
|
+
autoReply?: string;
|
|
232
|
+
welcomeMessage?: string;
|
|
233
|
+
/** Status of pending WhatsApp Business onboarding (Meta Cloud API). */
|
|
234
|
+
onboardingStatus?: "pending" | "approved" | "rejected";
|
|
235
|
+
}
|
|
236
|
+
export declare const STORE_WHATSAPP_CONFIG_COLLECTION: "storeWhatsAppConfig";
|
|
237
|
+
export declare const STORE_WHATSAPP_CONFIG_INDEXED_FIELDS: readonly ["storeId", "isConnected", "isPaid"];
|
|
238
|
+
export declare const DEFAULT_STORE_WHATSAPP_CONFIG_DATA: Partial<StoreWhatsAppConfigDocument>;
|
|
223
239
|
export interface StoreGoogleConfigDocument extends BaseDocument {
|
|
224
240
|
storeId: string;
|
|
225
241
|
isConnected: boolean;
|
|
@@ -182,6 +182,16 @@ export const DEFAULT_ITEM_REQUEST_DATA = {
|
|
|
182
182
|
replies: [],
|
|
183
183
|
replyCount: 0,
|
|
184
184
|
};
|
|
185
|
+
export const STORE_WHATSAPP_CONFIG_COLLECTION = "storeWhatsAppConfig";
|
|
186
|
+
export const STORE_WHATSAPP_CONFIG_INDEXED_FIELDS = [
|
|
187
|
+
"storeId",
|
|
188
|
+
"isConnected",
|
|
189
|
+
"isPaid",
|
|
190
|
+
];
|
|
191
|
+
export const DEFAULT_STORE_WHATSAPP_CONFIG_DATA = {
|
|
192
|
+
isConnected: false,
|
|
193
|
+
isPaid: false,
|
|
194
|
+
};
|
|
185
195
|
export const STORE_GOOGLE_CONFIG_COLLECTION = "storeGoogleConfig";
|
|
186
196
|
export const STORE_GOOGLE_CONFIG_INDEXED_FIELDS = [
|
|
187
197
|
"storeId",
|
|
@@ -45,6 +45,7 @@ const CAPABILITY_GROUPS = [
|
|
|
45
45
|
{ cap: "lower_commission_rate", unlocks: "Negotiated reduced platform fee. Requires a signed agreement and minimum volume.", defaultOn: false },
|
|
46
46
|
{ cap: "early_access_features", unlocks: "Access to beta features before they are publicly available.", defaultOn: false },
|
|
47
47
|
{ cap: "advanced_analytics", unlocks: "Cohort retention, basket analysis, and top-buyer breakdown (beyond summary stats).", defaultOn: false },
|
|
48
|
+
{ cap: "whatsapp_catalog_sync", unlocks: "Sync your store products to your WhatsApp Business Catalog automatically.", defaultOn: false },
|
|
48
49
|
],
|
|
49
50
|
},
|
|
50
51
|
];
|
|
@@ -27,7 +27,7 @@ const GUIDE_CARDS = [
|
|
|
27
27
|
{
|
|
28
28
|
Icon: Settings,
|
|
29
29
|
title: "Settings",
|
|
30
|
-
description: "Store profile, shipping config, return policy, and
|
|
30
|
+
description: "Store profile, shipping config, return policy, vacation mode, and WhatsApp integration.",
|
|
31
31
|
href: String(ROUTES.STORE.GUIDE_SETTINGS),
|
|
32
32
|
},
|
|
33
33
|
{
|
|
@@ -26,7 +26,11 @@ function ReturnPolicySection() {
|
|
|
26
26
|
function VacationModeSection() {
|
|
27
27
|
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "What vacation mode does" }), _jsxs(Ul, { spacing: "tight", indent: "lg", children: [_jsx(Li, { className: LIST_DISC, children: "All your published listings are hidden from search results and browse pages." }), _jsx(Li, { className: LIST_DISC, children: "Your store profile page shows a \"On Vacation\" banner." }), _jsx(Li, { className: LIST_DISC, children: "Buyers cannot add your items to cart or checkout while on vacation." }), _jsx(Li, { className: LIST_DISC, children: "Existing orders that were placed before vacation mode must still be fulfilled." })] })] }), _jsxs(Div, { children: [_jsx(Text, { className: SUBHEADING, children: "How to enable" }), _jsx(Text, { className: "leading-relaxed", children: "Go to your Store Dashboard \u2192 Settings \u2192 Storefront tab. Toggle \"Vacation mode\" on. Your listings re-appear instantly when you toggle it off." })] }), _jsx(Alert, { variant: "warning", title: "Ongoing orders are not paused", children: "Enabling vacation mode does not pause or cancel any existing orders. You are still responsible for fulfilling orders placed before vacation mode was enabled." })] }));
|
|
28
28
|
}
|
|
29
|
-
// -- Section 5:
|
|
29
|
+
// -- Section 5: WhatsApp integration ------------------------------------------
|
|
30
|
+
function WhatsAppSection() {
|
|
31
|
+
return (_jsxs(Div, { className: SECTION_BODY, children: [_jsxs(Text, { className: "leading-relaxed", children: ["The WhatsApp integration allows your customers to contact you via WhatsApp from your store profile and lets you sync your product catalog to your WhatsApp Business Catalog (requires the", _jsx(Code, { className: "mx-1", size: "xs", padding: "xs", rounded: "default", surface: "subtle", children: "whatsapp_catalog_sync" }), " capability)."] }), _jsxs(Text, { className: "leading-relaxed", children: ["Full settings and the catalog sync toggle are at", " ", _jsx(TextLink, { variant: "bare", href: String(ROUTES.STORE.WHATSAPP), className: "underline text-[var(--appkit-color-primary)] hover:opacity-80", children: "Settings \u2192 WhatsApp" }), ". See the WhatsApp settings page for step-by-step setup instructions."] })] }));
|
|
32
|
+
}
|
|
33
|
+
// -- Section 6: Visibility states ---------------------------------------------
|
|
30
34
|
function VisibilityStatesSection() {
|
|
31
35
|
const STATES = [
|
|
32
36
|
{
|
|
@@ -49,5 +53,5 @@ function VisibilityStatesSection() {
|
|
|
49
53
|
return (_jsx(Stack, { gap: "3", children: STATES.map(({ state, note }) => (_jsxs(Row, { textSize: "sm", gap: "3", children: [_jsx(Text, { className: "shrink-0 w-32 text-[var(--appkit-color-text)]", weight: "semibold", children: state }), _jsx(Text, { className: "text-[var(--appkit-color-text-muted)] leading-relaxed", children: note })] }, state))) }));
|
|
50
54
|
}
|
|
51
55
|
export function StoreSettingsGuideView(_props) {
|
|
52
|
-
return (_jsxs(Stack, { gap: "lg", padding: "b-2xl", children: [_jsxs(TextLink, { variant: "bare", href: String(ROUTES.STORE.GUIDE), size: "sm", layout: "inline-flex", align: "center", gap: "sm", className: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)] transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back to Seller Guide"] }), _jsxs(Div, { children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "2xl", weight: "bold", children: "Settings Guide" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", size: "sm", children: "Store profile, shipping, returns, vacation mode, and visibility." })] }), _jsx(GuideSection, { title: "1. Store profile", children: _jsx(StoreProfileSection, {}) }), _jsx(GuideSection, { title: "2. Shipping configuration", children: _jsx(ShippingSection, {}) }), _jsx(GuideSection, { title: "3. Return policy", children: _jsx(ReturnPolicySection, {}) }), _jsx(GuideSection, { title: "4. Vacation mode", children: _jsx(VacationModeSection, {}) }), _jsx(GuideSection, { title: "5. Visibility states", children: _jsx(VisibilityStatesSection, {}) })] }));
|
|
56
|
+
return (_jsxs(Stack, { gap: "lg", padding: "b-2xl", children: [_jsxs(TextLink, { variant: "bare", href: String(ROUTES.STORE.GUIDE), size: "sm", layout: "inline-flex", align: "center", gap: "sm", className: "text-[var(--appkit-color-text-muted)] hover:text-[var(--appkit-color-text)] transition-colors", children: [_jsx(ArrowLeft, { className: "h-4 w-4" }), "Back to Seller Guide"] }), _jsxs(Div, { children: [_jsx(Heading, { level: 1, className: "text-[var(--appkit-color-text)]", size: "2xl", weight: "bold", children: "Settings Guide" }), _jsx(Text, { className: "mt-1 text-[var(--appkit-color-text-muted)]", size: "sm", children: "Store profile, shipping, returns, vacation mode, WhatsApp, and visibility." })] }), _jsx(GuideSection, { title: "1. Store profile", children: _jsx(StoreProfileSection, {}) }), _jsx(GuideSection, { title: "2. Shipping configuration", children: _jsx(ShippingSection, {}) }), _jsx(GuideSection, { title: "3. Return policy", children: _jsx(ReturnPolicySection, {}) }), _jsx(GuideSection, { title: "4. Vacation mode", children: _jsx(VacationModeSection, {}) }), _jsx(GuideSection, { title: "5. WhatsApp integration", children: _jsx(WhatsAppSection, {}) }), _jsx(GuideSection, { title: "6. Visibility states", children: _jsx(VisibilityStatesSection, {}) })] }));
|
|
53
57
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Data access layer for the `stores` Firestore collection.
|
|
5
5
|
* One store per seller; identified by storeSlug (used as document ID).
|
|
6
6
|
*/
|
|
7
|
-
import { BaseRepository, type SieveModel, type FirebaseSieveResult } from "../../../providers/db-firebase";
|
|
7
|
+
import { BaseRepository, type SieveModel, type FirebaseSieveResult, type DocumentSnapshot } from "../../../providers/db-firebase";
|
|
8
8
|
import { StoreDocument } from "../schemas";
|
|
9
9
|
import { type HistoryActor } from "../../../_internal/shared/history/index";
|
|
10
10
|
import type { JsonValue } from "../../../schemas/types";
|
|
@@ -17,6 +17,28 @@ export interface StoreWriteContext {
|
|
|
17
17
|
}
|
|
18
18
|
export declare class StoreRepository extends BaseRepository<StoreDocument> {
|
|
19
19
|
constructor();
|
|
20
|
+
protected mapDoc<D = StoreDocument>(snap: DocumentSnapshot): D;
|
|
21
|
+
/**
|
|
22
|
+
* LIST reads never carry the WhatsApp token — not even encrypted.
|
|
23
|
+
*
|
|
24
|
+
* `mapDoc` above decrypts it, and `listStores(model, activeOnly)` backs the
|
|
25
|
+
* PUBLIC `/stores` page. Once `sieveQuery` started routing list rows through
|
|
26
|
+
* `mapDoc` (it previously mapped documents itself and skipped every
|
|
27
|
+
* override), that decrypt would have put a plaintext Meta OAuth bearer token
|
|
28
|
+
* on a public listing — the exact shape of Root Cause #70.
|
|
29
|
+
*
|
|
30
|
+
* So the list path deliberately does NOT call `decryptSecrets`, and drops
|
|
31
|
+
* `accessToken` outright: a list has no legitimate use for it, and a value
|
|
32
|
+
* that is never in the payload cannot be leaked by a projection someone
|
|
33
|
+
* forgets to write.
|
|
34
|
+
*
|
|
35
|
+
* Detail reads are unchanged — `findBySlug`/`findById` still decrypt, so the
|
|
36
|
+
* WhatsApp settings and catalog-sync routes keep working exactly as before.
|
|
37
|
+
* Removing the decrypt from `mapDoc` itself is a separate, riskier change
|
|
38
|
+
* that CLAUDE.md explicitly defers; this closes the public-exposure half
|
|
39
|
+
* without touching the encryption round-trip at all.
|
|
40
|
+
*/
|
|
41
|
+
update(id: string, data: Partial<StoreDocument>): Promise<StoreDocument>;
|
|
20
42
|
/**
|
|
21
43
|
* Create a new store.
|
|
22
44
|
* The document ID is set to storeSlug for easy URL-based lookups.
|
|
@@ -16,6 +16,32 @@ export class StoreRepository extends BaseRepository {
|
|
|
16
16
|
constructor() {
|
|
17
17
|
super(STORE_COLLECTION);
|
|
18
18
|
}
|
|
19
|
+
mapDoc(snap) {
|
|
20
|
+
return super.mapDoc(snap);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* LIST reads never carry the WhatsApp token — not even encrypted.
|
|
24
|
+
*
|
|
25
|
+
* `mapDoc` above decrypts it, and `listStores(model, activeOnly)` backs the
|
|
26
|
+
* PUBLIC `/stores` page. Once `sieveQuery` started routing list rows through
|
|
27
|
+
* `mapDoc` (it previously mapped documents itself and skipped every
|
|
28
|
+
* override), that decrypt would have put a plaintext Meta OAuth bearer token
|
|
29
|
+
* on a public listing — the exact shape of Root Cause #70.
|
|
30
|
+
*
|
|
31
|
+
* So the list path deliberately does NOT call `decryptSecrets`, and drops
|
|
32
|
+
* `accessToken` outright: a list has no legitimate use for it, and a value
|
|
33
|
+
* that is never in the payload cannot be leaked by a projection someone
|
|
34
|
+
* forgets to write.
|
|
35
|
+
*
|
|
36
|
+
* Detail reads are unchanged — `findBySlug`/`findById` still decrypt, so the
|
|
37
|
+
* WhatsApp settings and catalog-sync routes keep working exactly as before.
|
|
38
|
+
* Removing the decrypt from `mapDoc` itself is a separate, riskier change
|
|
39
|
+
* that CLAUDE.md explicitly defers; this closes the public-exposure half
|
|
40
|
+
* without touching the encryption round-trip at all.
|
|
41
|
+
*/
|
|
42
|
+
async update(id, data) {
|
|
43
|
+
return super.update(id, data);
|
|
44
|
+
}
|
|
19
45
|
/**
|
|
20
46
|
* Create a new store.
|
|
21
47
|
* The document ID is set to storeSlug for easy URL-based lookups.
|
|
@@ -206,7 +232,9 @@ export class StoreRepository extends BaseRepository {
|
|
|
206
232
|
options: { maxPageSize: win.maxPageSize(100) },
|
|
207
233
|
fields: sieveFields,
|
|
208
234
|
// Called DIRECTLY, not through `sieveQuery`, so the mapper has to be
|
|
209
|
-
// passed by hand
|
|
235
|
+
// passed by hand — and it is the list mapper, never `mapDoc`. With
|
|
236
|
+
// `activeOnly` this is the public /stores listing; `mapDoc` decrypts the
|
|
237
|
+
// WhatsApp bearer token and a list has no use for it.
|
|
210
238
|
mapDoc: (snap) => this.mapDocForList(snap),
|
|
211
239
|
});
|
|
212
240
|
const searched = win.finish(result);
|
|
@@ -107,6 +107,19 @@ export interface StoreDocument extends BaseDocument {
|
|
|
107
107
|
adminNotes?: string;
|
|
108
108
|
/** Admin "verified store" badge — distinct from `status`/`isPublic`. */
|
|
109
109
|
isVerified?: boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Does this store subscribe to WhatsApp order notifications?
|
|
112
|
+
*
|
|
113
|
+
* A monthly add-on priced by the admin (`commissions.whatsappNotifyMonthlyFee`),
|
|
114
|
+
* not a per-order charge: the platform owns one WhatsApp number, so the cost
|
|
115
|
+
* is standing and borne by the seller who benefits, rather than a line item
|
|
116
|
+
* a buyer opts into per order.
|
|
117
|
+
*
|
|
118
|
+
* 🛑 Absent means NOT subscribed, and the gate fails closed on it. Defaulting
|
|
119
|
+
* a missing flag to `true` would message customers on behalf of every seller
|
|
120
|
+
* who never signed up, and bill the platform for each template.
|
|
121
|
+
*/
|
|
122
|
+
whatsappNotifySubscribed?: boolean;
|
|
110
123
|
/**
|
|
111
124
|
* Paid at SHIPPING rather than at delivery.
|
|
112
125
|
*
|
|
@@ -370,12 +383,9 @@ export interface StoreDocument extends BaseDocument {
|
|
|
370
383
|
*/
|
|
371
384
|
export declare const STORE_TRACKED_FIELDS: readonly ["status", "isPublic", "isVerified", "isFeatured", "suspensionReason", "capabilities"];
|
|
372
385
|
/**
|
|
373
|
-
* PII on this document, for
|
|
374
|
-
*
|
|
375
|
-
*
|
|
376
|
-
* one was removed 2026-10-20: a scrub list is cheap, and a future secret
|
|
377
|
-
* reaching this document under that name should be scrubbed by default
|
|
378
|
-
* rather than by someone remembering to add it.
|
|
386
|
+
* PII on this document, for `withHistory`'s scrub. `whatsappConfig.accessToken`
|
|
387
|
+
* is a live Meta credential; `mapDoc` decrypts it on EVERY read, so it is
|
|
388
|
+
* present in memory whenever a diff runs.
|
|
379
389
|
*/
|
|
380
390
|
export declare const STORE_HISTORY_PII_FIELDS: readonly ["accessToken", "payoutDetails", "upiVpa", "accountNumber"];
|
|
381
391
|
export type { StoreCapability } from "../../auth/permissions/constants";
|
|
@@ -33,12 +33,9 @@ export const STORE_TRACKED_FIELDS = [
|
|
|
33
33
|
"capabilities",
|
|
34
34
|
];
|
|
35
35
|
/**
|
|
36
|
-
* PII on this document, for
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* one was removed 2026-10-20: a scrub list is cheap, and a future secret
|
|
40
|
-
* reaching this document under that name should be scrubbed by default
|
|
41
|
-
* rather than by someone remembering to add it.
|
|
36
|
+
* PII on this document, for `withHistory`'s scrub. `whatsappConfig.accessToken`
|
|
37
|
+
* is a live Meta credential; `mapDoc` decrypts it on EVERY read, so it is
|
|
38
|
+
* present in memory whenever a diff runs.
|
|
42
39
|
*/
|
|
43
40
|
export const STORE_HISTORY_PII_FIELDS = [
|
|
44
41
|
"accessToken",
|
|
@@ -347,6 +347,60 @@ export declare const storeFirestoreSchema: z.ZodObject<{
|
|
|
347
347
|
defaultProviderId: string;
|
|
348
348
|
}>>;
|
|
349
349
|
emiEnabled: z.ZodOptional<z.ZodBoolean>;
|
|
350
|
+
whatsappConfig: z.ZodOptional<z.ZodObject<{
|
|
351
|
+
phoneNumber: z.ZodOptional<z.ZodString>;
|
|
352
|
+
wabaId: z.ZodOptional<z.ZodString>;
|
|
353
|
+
catalogId: z.ZodOptional<z.ZodString>;
|
|
354
|
+
accessToken: z.ZodOptional<z.ZodString>;
|
|
355
|
+
catalogSyncEnabled: z.ZodBoolean;
|
|
356
|
+
lastCatalogSyncAt: z.ZodOptional<z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
|
|
357
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
358
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
359
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
360
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
361
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
362
|
+
}, z.ZodTypeAny, "passthrough">>]>>;
|
|
363
|
+
lastSyncCount: z.ZodOptional<z.ZodNumber>;
|
|
364
|
+
lastSyncStatus: z.ZodOptional<z.ZodEnum<["success", "partial", "failed"]>>;
|
|
365
|
+
connected: z.ZodBoolean;
|
|
366
|
+
connectedAt: z.ZodOptional<z.ZodUnion<[z.ZodDate, z.ZodString, z.ZodObject<{
|
|
367
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
368
|
+
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
369
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
370
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
371
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
372
|
+
}, z.ZodTypeAny, "passthrough">>]>>;
|
|
373
|
+
}, "strip", z.ZodTypeAny, {
|
|
374
|
+
catalogSyncEnabled: boolean;
|
|
375
|
+
connected: boolean;
|
|
376
|
+
phoneNumber?: string | undefined;
|
|
377
|
+
accessToken?: string | undefined;
|
|
378
|
+
wabaId?: string | undefined;
|
|
379
|
+
catalogId?: string | undefined;
|
|
380
|
+
lastCatalogSyncAt?: string | Date | z.objectOutputType<{
|
|
381
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
382
|
+
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
383
|
+
lastSyncCount?: number | undefined;
|
|
384
|
+
lastSyncStatus?: "success" | "failed" | "partial" | undefined;
|
|
385
|
+
connectedAt?: string | Date | z.objectOutputType<{
|
|
386
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
387
|
+
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
388
|
+
}, {
|
|
389
|
+
catalogSyncEnabled: boolean;
|
|
390
|
+
connected: boolean;
|
|
391
|
+
phoneNumber?: string | undefined;
|
|
392
|
+
accessToken?: string | undefined;
|
|
393
|
+
wabaId?: string | undefined;
|
|
394
|
+
catalogId?: string | undefined;
|
|
395
|
+
lastCatalogSyncAt?: string | Date | z.objectInputType<{
|
|
396
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
397
|
+
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
398
|
+
lastSyncCount?: number | undefined;
|
|
399
|
+
lastSyncStatus?: "success" | "failed" | "partial" | undefined;
|
|
400
|
+
connectedAt?: string | Date | z.objectInputType<{
|
|
401
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
402
|
+
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
403
|
+
}>>;
|
|
350
404
|
}, "strip", z.ZodTypeAny, {
|
|
351
405
|
status: "pending" | "active" | "rejected" | "suspended";
|
|
352
406
|
id: string;
|
|
@@ -416,6 +470,22 @@ export declare const storeFirestoreSchema: z.ZodObject<{
|
|
|
416
470
|
maxReviews?: number | undefined;
|
|
417
471
|
minRating?: number | undefined;
|
|
418
472
|
} | undefined;
|
|
473
|
+
whatsappConfig?: {
|
|
474
|
+
catalogSyncEnabled: boolean;
|
|
475
|
+
connected: boolean;
|
|
476
|
+
phoneNumber?: string | undefined;
|
|
477
|
+
accessToken?: string | undefined;
|
|
478
|
+
wabaId?: string | undefined;
|
|
479
|
+
catalogId?: string | undefined;
|
|
480
|
+
lastCatalogSyncAt?: string | Date | z.objectOutputType<{
|
|
481
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
482
|
+
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
483
|
+
lastSyncCount?: number | undefined;
|
|
484
|
+
lastSyncStatus?: "success" | "failed" | "partial" | undefined;
|
|
485
|
+
connectedAt?: string | Date | z.objectOutputType<{
|
|
486
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
487
|
+
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
488
|
+
} | undefined;
|
|
419
489
|
}, {
|
|
420
490
|
status: "pending" | "active" | "rejected" | "suspended";
|
|
421
491
|
id: string;
|
|
@@ -485,6 +555,22 @@ export declare const storeFirestoreSchema: z.ZodObject<{
|
|
|
485
555
|
maxReviews?: number | undefined;
|
|
486
556
|
minRating?: number | undefined;
|
|
487
557
|
} | undefined;
|
|
558
|
+
whatsappConfig?: {
|
|
559
|
+
catalogSyncEnabled: boolean;
|
|
560
|
+
connected: boolean;
|
|
561
|
+
phoneNumber?: string | undefined;
|
|
562
|
+
accessToken?: string | undefined;
|
|
563
|
+
wabaId?: string | undefined;
|
|
564
|
+
catalogId?: string | undefined;
|
|
565
|
+
lastCatalogSyncAt?: string | Date | z.objectInputType<{
|
|
566
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
567
|
+
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
568
|
+
lastSyncCount?: number | undefined;
|
|
569
|
+
lastSyncStatus?: "success" | "failed" | "partial" | undefined;
|
|
570
|
+
connectedAt?: string | Date | z.objectInputType<{
|
|
571
|
+
toDate: z.ZodFunction<z.ZodTuple<[], z.ZodUnknown>, z.ZodDate>;
|
|
572
|
+
}, z.ZodTypeAny, "passthrough"> | undefined;
|
|
573
|
+
} | undefined;
|
|
488
574
|
}>;
|
|
489
575
|
export type StoreFromSchema = z.infer<typeof storeFirestoreSchema>;
|
|
490
576
|
/** Zod schema for store status — use instead of inline `z.enum(["pending",...])`. */
|
|
@@ -92,6 +92,20 @@ export const storeFirestoreSchema = z.object({
|
|
|
92
92
|
.optional(),
|
|
93
93
|
shippingConfig: storeShippingConfigSchema.optional(),
|
|
94
94
|
emiEnabled: z.boolean().optional(),
|
|
95
|
+
whatsappConfig: z
|
|
96
|
+
.object({
|
|
97
|
+
phoneNumber: z.string().optional(),
|
|
98
|
+
wabaId: z.string().optional(),
|
|
99
|
+
catalogId: z.string().optional(),
|
|
100
|
+
accessToken: z.string().optional(),
|
|
101
|
+
catalogSyncEnabled: z.boolean(),
|
|
102
|
+
lastCatalogSyncAt: firestoreDateSchema.optional(),
|
|
103
|
+
lastSyncCount: z.number().int().optional(),
|
|
104
|
+
lastSyncStatus: z.enum(["success", "partial", "failed"]).optional(),
|
|
105
|
+
connected: z.boolean(),
|
|
106
|
+
connectedAt: firestoreDateSchema.optional(),
|
|
107
|
+
})
|
|
108
|
+
.optional(),
|
|
95
109
|
...auditTimestampsShape,
|
|
96
110
|
});
|
|
97
111
|
/** Zod schema for store status — use instead of inline `z.enum(["pending",...])`. */
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feat-whatsapp-bot — POST /api/whatsapp/send-status
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp message delivery status update.
|
|
5
|
+
*
|
|
6
|
+
* Consumer stub:
|
|
7
|
+
* ```ts
|
|
8
|
+
* export { whatsappSendStatusPOST as POST } from "@mohasinac/feat-whatsapp-bot";
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
import { NextResponse } from "next/server.js";
|
|
12
|
+
export declare function POST(request: Request): Promise<NextResponse>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feat-whatsapp-bot — POST /api/whatsapp/send-status
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp message delivery status update.
|
|
5
|
+
*
|
|
6
|
+
* Consumer stub:
|
|
7
|
+
* ```ts
|
|
8
|
+
* export { whatsappSendStatusPOST as POST } from "@mohasinac/feat-whatsapp-bot";
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
import { NextResponse } from "next/server.js";
|
|
12
|
+
import { getProviders } from "../../../../contracts/index.js";
|
|
13
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
14
|
+
export async function POST(request) {
|
|
15
|
+
try {
|
|
16
|
+
const body = await request.json();
|
|
17
|
+
const { db } = getProviders();
|
|
18
|
+
if (!db)
|
|
19
|
+
return NextResponse.json({ success: false, error: "DB not configured" }, { status: 503 });
|
|
20
|
+
// Update message status in database
|
|
21
|
+
const repo = db.getRepository("whatsapp_message_status");
|
|
22
|
+
const updated = await repo.create({
|
|
23
|
+
...body,
|
|
24
|
+
updatedAt: new Date(),
|
|
25
|
+
});
|
|
26
|
+
return NextResponse.json({
|
|
27
|
+
success: true,
|
|
28
|
+
data: updated,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
void normalizeError(error);
|
|
33
|
+
return NextResponse.json({
|
|
34
|
+
success: false,
|
|
35
|
+
error: error instanceof Error ? error.message : "Internal server error",
|
|
36
|
+
}, { status: 500 });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feat-whatsapp-bot — POST /api/whatsapp/webhook
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp webhook receiver for incoming messages.
|
|
5
|
+
*
|
|
6
|
+
* Consumer stub:
|
|
7
|
+
* ```ts
|
|
8
|
+
* export { whatsappWebhookPOST as POST } from "@mohasinac/feat-whatsapp-bot";
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
import { NextResponse } from "next/server.js";
|
|
12
|
+
export declare function POST(request: Request): Promise<NextResponse>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* feat-whatsapp-bot — POST /api/whatsapp/webhook
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp webhook receiver for incoming messages.
|
|
5
|
+
*
|
|
6
|
+
* Consumer stub:
|
|
7
|
+
* ```ts
|
|
8
|
+
* export { whatsappWebhookPOST as POST } from "@mohasinac/feat-whatsapp-bot";
|
|
9
|
+
* ```
|
|
10
|
+
*/
|
|
11
|
+
import { NextResponse } from "next/server.js";
|
|
12
|
+
import { getProviders } from "../../../../contracts/index.js";
|
|
13
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
14
|
+
export async function POST(request) {
|
|
15
|
+
try {
|
|
16
|
+
const body = await request.json();
|
|
17
|
+
const { db, eventBus } = getProviders();
|
|
18
|
+
if (!db)
|
|
19
|
+
return NextResponse.json({ success: false, error: "DB not configured" }, { status: 503 });
|
|
20
|
+
// Store webhook payload for processing
|
|
21
|
+
const repo = db.getRepository("whatsapp_messages");
|
|
22
|
+
const created = await repo.create({
|
|
23
|
+
...body,
|
|
24
|
+
receivedAt: new Date(),
|
|
25
|
+
processed: false,
|
|
26
|
+
});
|
|
27
|
+
// Emit event for async processing if eventBus available
|
|
28
|
+
if (eventBus) {
|
|
29
|
+
await eventBus.emit("whatsapp.message.received", created);
|
|
30
|
+
}
|
|
31
|
+
return NextResponse.json({
|
|
32
|
+
success: true,
|
|
33
|
+
data: created,
|
|
34
|
+
}, { status: 201 });
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
void normalizeError(error);
|
|
38
|
+
return NextResponse.json({
|
|
39
|
+
success: false,
|
|
40
|
+
error: error instanceof Error ? error.message : "Internal server error",
|
|
41
|
+
}, { status: 500 });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHY: a seller's WhatsApp settings are now ONE decision — do they pay the
|
|
3
|
+
* monthly fee to have order updates sent to their buyers on the
|
|
4
|
+
* platform's WhatsApp number.
|
|
5
|
+
*
|
|
6
|
+
* 🛑 This replaced a 574-line credential form that asked every seller for a
|
|
7
|
+
* WhatsApp Business Account ID, a Catalogue ID and a **System User Access
|
|
8
|
+
* Token**, then stored that token encrypted on their store document.
|
|
9
|
+
*
|
|
10
|
+
* Three things were wrong with it, and they are why none of it is coming back:
|
|
11
|
+
*
|
|
12
|
+
* 1. Almost no small seller will create a Meta app and mint a system user
|
|
13
|
+
* token. A setting nobody can complete is a feature nobody has.
|
|
14
|
+
* 2. It made LetItRip the custodian of a live Meta credential per seller.
|
|
15
|
+
* `StoreRepository.mapDoc` decrypted that token on EVERY read, so any
|
|
16
|
+
* code path that forgot to project was handling plaintext — a gap
|
|
17
|
+
* CLAUDE.md flags as deserving its own session.
|
|
18
|
+
* 3. The catalogue half it existed for is gone: `/whatsapp/{slug}/feed.csv`
|
|
19
|
+
* is a public feed the seller points their own Commerce Manager at, so
|
|
20
|
+
* no credential crosses the boundary at all.
|
|
21
|
+
*
|
|
22
|
+
* WHAT: subscription status, the price, and the toggle.
|
|
23
|
+
*
|
|
24
|
+
* ## Presentational on purpose
|
|
25
|
+
*
|
|
26
|
+
* Takes the price and the current state as PROPS rather than reading site
|
|
27
|
+
* settings itself. Site settings are server-read and passed down — the
|
|
28
|
+
* monthly fee lives beside the platform's Meta credentials in a document that
|
|
29
|
+
* must never reach a browser, and a client hook here would be a second reader
|
|
30
|
+
* of it (Root Cause #70).
|
|
31
|
+
*
|
|
32
|
+
* EXPORTS: SellerWhatsAppSettingsView, SellerWhatsAppSettingsViewProps
|
|
33
|
+
*
|
|
34
|
+
* @tag domain:whatsapp
|
|
35
|
+
* @tag layer:component
|
|
36
|
+
* @tag access:client
|
|
37
|
+
*/
|
|
38
|
+
import React from "react";
|
|
39
|
+
export interface SellerWhatsAppSettingsViewProps {
|
|
40
|
+
/** Is this store currently subscribed? Server-resolved from the store document. */
|
|
41
|
+
subscribed: boolean;
|
|
42
|
+
/** Admin-set monthly price, in rupees. Server-resolved from site settings. */
|
|
43
|
+
monthlyFee: number;
|
|
44
|
+
/** Currency for the price label. */
|
|
45
|
+
currency?: string;
|
|
46
|
+
/**
|
|
47
|
+
* Is the channel switched on platform-wide? When false the toggle is
|
|
48
|
+
* disabled and says so — a seller who subscribes into a disabled channel
|
|
49
|
+
* pays for silence.
|
|
50
|
+
*/
|
|
51
|
+
channelEnabled: boolean;
|
|
52
|
+
/** Persists the change. Resolves to the new state. */
|
|
53
|
+
onToggleSubscription: (next: boolean) => Promise<void>;
|
|
54
|
+
/** Gate from the store's capabilities; false renders a read-only explanation. */
|
|
55
|
+
hasCapability?: boolean;
|
|
56
|
+
}
|
|
57
|
+
export declare function SellerWhatsAppSettingsView({ subscribed, monthlyFee, currency, channelEnabled, onToggleSubscription, hasCapability, }: SellerWhatsAppSettingsViewProps): React.JSX.Element;
|