@mohasinac/appkit 4.63.4 → 4.64.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
- package/dist/_internal/server/features/categories/adapters.js +13 -0
- 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/events/like-actions.d.ts +25 -0
- package/dist/_internal/server/features/events/like-actions.js +67 -0
- package/dist/_internal/server/features/events/likes.d.ts +65 -0
- package/dist/_internal/server/features/events/likes.js +146 -0
- package/dist/_internal/server/features/orders/adapters.js +3 -1
- package/dist/_internal/server/features/seo/index.d.ts +3 -0
- package/dist/_internal/server/features/seo/index.js +2 -0
- package/dist/_internal/server/features/seo/product-detail-types.d.ts +1 -0
- package/dist/_internal/server/features/seo/product-detail-types.js +26 -0
- package/dist/_internal/server/features/seo/robots.js +20 -1
- package/dist/_internal/server/features/seo/sitemap.js +125 -27
- package/dist/_internal/server/features/seo/static-params.d.ts +107 -0
- package/dist/_internal/server/features/seo/static-params.js +228 -0
- package/dist/_internal/server/features/seo/taxonomy-links.d.ts +44 -0
- package/dist/_internal/server/features/seo/taxonomy-links.js +85 -0
- package/dist/_internal/server/features/site-settings/adapters.js +20 -2
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +88 -1
- package/dist/_internal/server/functions/scheduled.d.ts +16 -1
- package/dist/_internal/server/functions/scheduled.js +23 -1
- 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/sellerStandingRollup.d.ts +63 -0
- package/dist/_internal/server/jobs/core/sellerStandingRollup.js +412 -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/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/sellerStandingRollup.d.ts +12 -0
- package/dist/_internal/server/jobs/handlers/sellerStandingRollup.js +14 -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 +89 -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/seller-standing/config.d.ts +130 -0
- package/dist/_internal/shared/features/seller-standing/config.js +153 -0
- 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 +39 -0
- package/dist/constants/api-endpoints.js +16 -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/about/components/HelpPageView.js +9 -1
- package/dist/features/account/components/BecomeSellerView.d.ts +15 -3
- package/dist/features/account/components/BecomeSellerView.js +2 -2
- 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.d.ts +12 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +31 -2
- package/dist/features/admin/components/AdminStoresGuideView.js +1 -1
- package/dist/features/admin/components/AdminStoresView.js +59 -2
- package/dist/features/admin/components/AdminTrustGuideView.js +10 -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-editor-forms.d.ts +10 -0
- package/dist/features/admin/schemas/admin-editor-forms.js +34 -0
- 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/components/BlogIndexPageView.js +2 -2
- package/dist/features/blog/components/BlogListView.js +2 -1
- package/dist/features/blog/components/BlogPostView.js +2 -1
- package/dist/features/blog/constants/category-badge.d.ts +33 -0
- package/dist/features/blog/constants/category-badge.js +40 -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/categories/components/TaxonomyLinkIndex.d.ts +39 -0
- package/dist/features/categories/components/TaxonomyLinkIndex.js +11 -0
- package/dist/features/categories/schemas/firestore.d.ts +25 -0
- package/dist/features/categories/utils/load-category-options.d.ts +33 -0
- package/dist/features/categories/utils/load-category-options.js +50 -0
- 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/EventCard.js +4 -12
- package/dist/features/events/components/EventInsights.d.ts +27 -0
- package/dist/features/events/components/EventInsights.js +10 -0
- package/dist/features/events/components/EventLikeButton.d.ts +21 -0
- package/dist/features/events/components/EventLikeButton.js +59 -0
- package/dist/features/events/components/EventLikersRail.d.ts +23 -0
- package/dist/features/events/components/EventLikersRail.js +54 -0
- package/dist/features/events/components/EventTypeBadge.d.ts +26 -0
- package/dist/features/events/components/EventTypeBadge.js +7 -0
- package/dist/features/events/components/EventsListPageView.js +2 -2
- package/dist/features/events/components/SpinWheel.d.ts +41 -0
- package/dist/features/events/components/SpinWheel.js +128 -0
- package/dist/features/events/components/SpinWheelView.js +36 -15
- package/dist/features/events/constants/event-type-meta.d.ts +54 -0
- package/dist/features/events/constants/event-type-meta.js +109 -0
- package/dist/features/events/repository/event-likes.repository.d.ts +57 -0
- package/dist/features/events/repository/event-likes.repository.js +151 -0
- package/dist/features/events/schemas/firestore.d.ts +111 -0
- package/dist/features/events/schemas/likes.d.ts +81 -0
- package/dist/features/events/schemas/likes.js +59 -0
- package/dist/features/events/types/index.d.ts +7 -0
- package/dist/features/events/utils/carousel-slide.d.ts +42 -0
- package/dist/features/events/utils/carousel-slide.js +174 -0
- package/dist/features/events/utils/event-charts.d.ts +65 -0
- package/dist/features/events/utils/event-charts.js +172 -0
- package/dist/features/faq/constants/help-guides.js +9 -0
- package/dist/features/faq/schemas/firestore.js +1 -0
- package/dist/features/faq/types/index.d.ts +19 -0
- package/dist/features/homepage/components/BrandsSection.js +25 -1
- package/dist/features/homepage/components/HeroCarousel.js +47 -10
- package/dist/features/homepage/lib/section-renderer.js +1 -1
- package/dist/features/homepage/types/index.d.ts +7 -0
- 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/InteractiveStoreCard.js +2 -1
- package/dist/features/stores/components/SellerStandingPanel.d.ts +31 -0
- package/dist/features/stores/components/SellerStandingPanel.js +25 -0
- package/dist/features/stores/components/SellerTierBadge.d.ts +57 -0
- package/dist/features/stores/components/SellerTierBadge.js +19 -0
- package/dist/features/stores/components/StoreCapabilitiesGuideView.js +33 -2
- package/dist/features/stores/components/StoreCreateForm.d.ts +46 -0
- package/dist/features/stores/components/StoreCreateForm.js +107 -0
- package/dist/features/stores/components/StoreGuideHubView.js +18 -5
- package/dist/features/stores/components/StoreHeader.js +2 -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 +52 -1
- package/dist/features/stores/schemas/firestore.d.ts +76 -7
- package/dist/features/stores/schemas/firestore.js +21 -6
- package/dist/features/stores/schemas/index.d.ts +88 -2
- package/dist/features/stores/schemas/index.js +14 -0
- package/dist/features/stores/schemas/store-create.d.ts +47 -0
- package/dist/features/stores/schemas/store-create.js +86 -0
- package/dist/features/stores/types/index.d.ts +42 -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 +6 -1
- package/dist/next/routing/route-map.js +14 -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 +115 -0
- package/dist/schemas/registry.js +2 -1
- package/dist/security/pii-encrypt.js +1 -1
- package/dist/server.d.ts +15 -2
- package/dist/server.js +32 -2
- package/dist/styles.css +36 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/HorizontalScroller.js +15 -2
- package/dist/ui/components/NavigationLoader.d.ts +14 -1
- package/dist/ui/components/NavigationLoader.js +79 -30
- package/dist/ui/components/PageHeroHeading.d.ts +40 -0
- package/dist/ui/components/PageHeroHeading.js +15 -0
- package/dist/ui/components/Typography.d.ts +3 -3
- package/dist/ui/components/Typography.js +59 -31
- package/dist/ui/components/Typography.style.css +35 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +1 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -2
- package/dist/features/orders/utils/order-announcement-message.d.ts +0 -18
- package/dist/features/orders/utils/order-announcement-message.js +0 -16
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ScheduleHandler } from "../runtime/types";
|
|
2
|
+
/**
|
|
3
|
+
* 🛑 No `force`. The schedule fires on days 28–31 and the core's month-end
|
|
4
|
+
* guard keeps exactly the real last day — see `isLastDayOfMonth` for why the
|
|
5
|
+
* cron cannot simply say "31".
|
|
6
|
+
*
|
|
7
|
+
* `ScheduleHandler` returns `void`, so the result object is dropped here. It
|
|
8
|
+
* is not lost: the core logs `granted`/`revoked`/`locked` counts, and it is
|
|
9
|
+
* returned for the admin recompute path and for verification, which call the
|
|
10
|
+
* core directly with `{ force: true }`.
|
|
11
|
+
*/
|
|
12
|
+
export declare const sellerStandingRollupHandler: ScheduleHandler;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { runSellerStandingRollup } from "../core/sellerStandingRollup.js";
|
|
2
|
+
/**
|
|
3
|
+
* 🛑 No `force`. The schedule fires on days 28–31 and the core's month-end
|
|
4
|
+
* guard keeps exactly the real last day — see `isLastDayOfMonth` for why the
|
|
5
|
+
* cron cannot simply say "31".
|
|
6
|
+
*
|
|
7
|
+
* `ScheduleHandler` returns `void`, so the result object is dropped here. It
|
|
8
|
+
* is not lost: the core logs `granted`/`revoked`/`locked` counts, and it is
|
|
9
|
+
* returned for the admin recompute path and for verification, which call the
|
|
10
|
+
* core directly with `{ force: true }`.
|
|
11
|
+
*/
|
|
12
|
+
export const sellerStandingRollupHandler = async (ctx) => {
|
|
13
|
+
await runSellerStandingRollup(ctx);
|
|
14
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Circuit breaker for outbound notification channels.
|
|
2
|
+
* Circuit breaker for outbound notification channels (email, WhatsApp).
|
|
3
3
|
*
|
|
4
4
|
* Persisted as a singleton Firestore doc `system/channelHealth` so the
|
|
5
5
|
* "is this channel currently known-bad" state survives across requests and
|
|
@@ -10,9 +10,11 @@
|
|
|
10
10
|
* its own outcome decides whether the circuit re-closes or re-opens.
|
|
11
11
|
*/
|
|
12
12
|
/**
|
|
13
|
-
* Email
|
|
14
|
-
*
|
|
15
|
-
* integration
|
|
13
|
+
* Email and WhatsApp are the outbound *notification* channels with a
|
|
14
|
+
* transport. The WhatsApp arm was removed 2026-10-20 with the Cloud API
|
|
15
|
+
* integration and restored 2026-10-23; it stays inert until
|
|
16
|
+
* `siteSettings.whatsapp.enabled` is on AND a number is registered on the
|
|
17
|
+
* Cloud API, so a breaker on it reports a channel nobody is using until then.
|
|
16
18
|
*
|
|
17
19
|
* ## 🛑 Phase 8 widened this to the RECONCILERS' reads — and only those
|
|
18
20
|
*
|
|
@@ -36,7 +38,7 @@
|
|
|
36
38
|
* backing off costs only freshness. Anything that moves money is not a reader
|
|
37
39
|
* and does not belong in this union.
|
|
38
40
|
*/
|
|
39
|
-
export type NotificationChannel = "email" | "razorpay_read" | "shiprocket_read";
|
|
41
|
+
export type NotificationChannel = "email" | "whatsapp" | "razorpay_read" | "shiprocket_read";
|
|
40
42
|
/**
|
|
41
43
|
* Returns false only when the circuit is open and still within its cooldown
|
|
42
44
|
* window. Any other state (closed, or open-but-cooldown-elapsed) is treated
|
|
@@ -47,7 +49,7 @@ export declare function recordChannelOutcome(channel: NotificationChannel, succe
|
|
|
47
49
|
/**
|
|
48
50
|
* Runs `fn` up to 3 times (500ms then 1.5s between attempts) and records the
|
|
49
51
|
* final outcome against the channel's health state. `fn` must throw on
|
|
50
|
-
* failure — for a helper that returns a boolean instead (e.g.
|
|
52
|
+
* failure — for a helper that returns a boolean instead (e.g. the WhatsApp
|
|
51
53
|
* Cloud API helpers), convert `false` into a thrown error at the call site
|
|
52
54
|
* before passing it in.
|
|
53
55
|
*/
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Circuit breaker for outbound notification channels.
|
|
2
|
+
* Circuit breaker for outbound notification channels (email, WhatsApp).
|
|
3
3
|
*
|
|
4
4
|
* Persisted as a singleton Firestore doc `system/channelHealth` so the
|
|
5
5
|
* "is this channel currently known-bad" state survives across requests and
|
|
@@ -72,7 +72,7 @@ function delay(ms) {
|
|
|
72
72
|
/**
|
|
73
73
|
* Runs `fn` up to 3 times (500ms then 1.5s between attempts) and records the
|
|
74
74
|
* final outcome against the channel's health state. `fn` must throw on
|
|
75
|
-
* failure — for a helper that returns a boolean instead (e.g.
|
|
75
|
+
* failure — for a helper that returns a boolean instead (e.g. the WhatsApp
|
|
76
76
|
* Cloud API helpers), convert `false` into a thrown error at the call site
|
|
77
77
|
* before passing it in.
|
|
78
78
|
*/
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* message.
|
|
16
16
|
*
|
|
17
17
|
* The guarantee that nothing bypasses this is STRUCTURAL instead: the guard
|
|
18
|
-
* argument is REQUIRED on `sendEmail
|
|
18
|
+
* argument is REQUIRED on `sendEmail` and on both WhatsApp senders, so an
|
|
19
19
|
* unguarded send does not typecheck, and `scripts/audit-unguarded-send.mjs`
|
|
20
20
|
* fails the build on a raw `createResendProvider().send()`. Make the wrong
|
|
21
21
|
* thing unrepresentable rather than adding a second layer of defence that has
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* message.
|
|
16
16
|
*
|
|
17
17
|
* The guarantee that nothing bypasses this is STRUCTURAL instead: the guard
|
|
18
|
-
* argument is REQUIRED on `sendEmail
|
|
18
|
+
* argument is REQUIRED on `sendEmail` and on both WhatsApp senders, so an
|
|
19
19
|
* unguarded send does not typecheck, and `scripts/audit-unguarded-send.mjs`
|
|
20
20
|
* fails the build on a raw `createResendProvider().send()`. Make the wrong
|
|
21
21
|
* thing unrepresentable rather than adding a second layer of defence that has
|
|
@@ -69,7 +69,9 @@ export async function guardSend(ctx) {
|
|
|
69
69
|
if (ctx.audience === "user" && !ctx.transactional) {
|
|
70
70
|
const channelEnabled = ctx.channel === "email"
|
|
71
71
|
? (messaging?.emailEnabled ?? MESSAGE_BUDGET_DEFAULTS.emailEnabled)
|
|
72
|
-
:
|
|
72
|
+
: ctx.channel === "whatsapp"
|
|
73
|
+
? (messaging?.whatsappEnabled ?? MESSAGE_BUDGET_DEFAULTS.whatsappEnabled)
|
|
74
|
+
: false; // sms has no sender; if one appears it starts disabled.
|
|
73
75
|
if (!channelEnabled) {
|
|
74
76
|
return {
|
|
75
77
|
allow: false,
|
|
@@ -771,6 +771,22 @@ export const ACTIONS = {
|
|
|
771
771
|
},
|
|
772
772
|
},
|
|
773
773
|
STORE: {
|
|
774
|
+
/*
|
|
775
|
+
* Open a storefront — the submit button for the store-creation form.
|
|
776
|
+
*
|
|
777
|
+
* No `confirmation`: creating a store is additive, reversible by the
|
|
778
|
+
* seller, and produces a `pending` / non-public document that no buyer can
|
|
779
|
+
* see until an admin approves it. A dialog here would be friction on the
|
|
780
|
+
* one step the seller came to perform.
|
|
781
|
+
*/
|
|
782
|
+
"create-store": {
|
|
783
|
+
iconKey: "create",
|
|
784
|
+
id: "store.create-store",
|
|
785
|
+
label: "Create my store",
|
|
786
|
+
ariaLabel: "Create your store",
|
|
787
|
+
description: "Create the seller's storefront. The store starts pending and private until an admin approves it.",
|
|
788
|
+
kind: "primary",
|
|
789
|
+
},
|
|
774
790
|
"follow": {
|
|
775
791
|
iconKey: "wishlist",
|
|
776
792
|
id: "store.follow",
|
|
@@ -919,6 +935,34 @@ export const ACTIONS = {
|
|
|
919
935
|
kind: "ghost",
|
|
920
936
|
permissions: ["seller", "admin"],
|
|
921
937
|
},
|
|
938
|
+
// ── WhatsApp integration ────────────────────────────────────────────
|
|
939
|
+
"whatsapp-connect": {
|
|
940
|
+
iconKey: "link",
|
|
941
|
+
id: "store.whatsapp-connect",
|
|
942
|
+
label: "Save & Connect",
|
|
943
|
+
ariaLabel: "Save WhatsApp credentials and connect",
|
|
944
|
+
description: "Save WhatsApp Business credentials and establish connection.",
|
|
945
|
+
kind: "primary",
|
|
946
|
+
permissions: ["seller", "admin"],
|
|
947
|
+
},
|
|
948
|
+
"whatsapp-catalog-sync": {
|
|
949
|
+
iconKey: "retry",
|
|
950
|
+
id: "store.whatsapp-catalog-sync",
|
|
951
|
+
label: "Push to WhatsApp",
|
|
952
|
+
ariaLabel: "Sync products to WhatsApp catalog",
|
|
953
|
+
description: "Push published standard products to WhatsApp Business Catalog.",
|
|
954
|
+
kind: "secondary",
|
|
955
|
+
permissions: ["seller", "admin"],
|
|
956
|
+
},
|
|
957
|
+
"whatsapp-catalog-import": {
|
|
958
|
+
iconKey: "upload",
|
|
959
|
+
id: "store.whatsapp-catalog-import",
|
|
960
|
+
label: "Import from WhatsApp",
|
|
961
|
+
ariaLabel: "Import products from WhatsApp catalog",
|
|
962
|
+
description: "Import products from WhatsApp Catalog as drafts.",
|
|
963
|
+
kind: "secondary",
|
|
964
|
+
permissions: ["seller", "admin"],
|
|
965
|
+
},
|
|
922
966
|
// ── Google Reviews integration ──────────────────────────────────────
|
|
923
967
|
"google-reviews-sync": {
|
|
924
968
|
iconKey: "retry",
|
|
@@ -2200,6 +2244,51 @@ export const ACTIONS = {
|
|
|
2200
2244
|
confirmKind: "danger",
|
|
2201
2245
|
},
|
|
2202
2246
|
},
|
|
2247
|
+
/*
|
|
2248
|
+
* 🛑 Approve/reject a seller's PENDING store — the step that was missing.
|
|
2249
|
+
*
|
|
2250
|
+
* A store is created `status: "pending"`, `isPublic: false`, and until
|
|
2251
|
+
* 2026-10-24 nothing in any admin UI could move it: `AdminStoresView` had
|
|
2252
|
+
* verify and suspend row actions only, and the one action named for this
|
|
2253
|
+
* job (`adminUpdateStoreStatus`) writes the USER-level `storeStatus`
|
|
2254
|
+
* mirror and never touches the store document — so wiring that would have
|
|
2255
|
+
* marked the applicant approved while leaving their store invisible, with
|
|
2256
|
+
* no error. Both of these go through the store PATCH route, i.e. through
|
|
2257
|
+
* `storeRepository.setStatus`, which is the only writer that keeps
|
|
2258
|
+
* `status` and `isPublic` in step (Root Cause #42).
|
|
2259
|
+
*
|
|
2260
|
+
* Approve carries a confirmation despite being `primary`: it is what makes
|
|
2261
|
+
* a storefront publicly visible, and that is outward-facing.
|
|
2262
|
+
*/
|
|
2263
|
+
"approve-store": {
|
|
2264
|
+
iconKey: "approve",
|
|
2265
|
+
id: "admin.approve-store",
|
|
2266
|
+
label: "Approve store",
|
|
2267
|
+
ariaLabel: "Approve this store and make it public",
|
|
2268
|
+
description: "Approve a pending store — sets it active and makes its listings visible to buyers.",
|
|
2269
|
+
kind: "primary",
|
|
2270
|
+
permissions: ["admin"],
|
|
2271
|
+
confirmation: {
|
|
2272
|
+
title: "Approve this store?",
|
|
2273
|
+
body: "The store becomes active and its listings become visible to buyers. The seller will be notified.",
|
|
2274
|
+
confirmLabel: "Approve store",
|
|
2275
|
+
},
|
|
2276
|
+
},
|
|
2277
|
+
"reject-store": {
|
|
2278
|
+
iconKey: "reject",
|
|
2279
|
+
id: "admin.reject-store",
|
|
2280
|
+
label: "Reject store",
|
|
2281
|
+
ariaLabel: "Reject this store application",
|
|
2282
|
+
description: "Reject a pending store application — the store stays hidden from buyers.",
|
|
2283
|
+
kind: "danger",
|
|
2284
|
+
permissions: ["admin"],
|
|
2285
|
+
confirmation: {
|
|
2286
|
+
title: "Reject this store application?",
|
|
2287
|
+
body: "The store will not be published and the seller will be notified. You can approve it later if they resolve the issue.",
|
|
2288
|
+
confirmLabel: "Reject store",
|
|
2289
|
+
confirmKind: "danger",
|
|
2290
|
+
},
|
|
2291
|
+
},
|
|
2203
2292
|
// ── Review moderation ──────────────────────────────────────────────────
|
|
2204
2293
|
"approve-review": {
|
|
2205
2294
|
iconKey: "approve",
|
|
@@ -227,7 +227,7 @@ export interface PayableOrderLike {
|
|
|
227
227
|
* money bug in all four payout paths (`autoPayoutEligibility`,
|
|
228
228
|
* `weeklyPayoutEligibility`, and both `seller-actions` entry points). That
|
|
229
229
|
* field is the GRAND TOTAL — `checkout/actions.ts` builds it as
|
|
230
|
-
* `goods − coupon + shipping + codHandling + giftWrap +
|
|
230
|
+
* `goods − coupon + shipping + codHandling + whatsappNotify + giftWrap +
|
|
231
231
|
* shipmentProtection + platformFee + platformFeeGst + emiSurcharge + gstAmount`
|
|
232
232
|
* — so using it as the payout base did three wrong things at once:
|
|
233
233
|
*
|
|
@@ -412,7 +412,7 @@ export function allocateAcrossMembers(total, weights) {
|
|
|
412
412
|
* money bug in all four payout paths (`autoPayoutEligibility`,
|
|
413
413
|
* `weeklyPayoutEligibility`, and both `seller-actions` entry points). That
|
|
414
414
|
* field is the GRAND TOTAL — `checkout/actions.ts` builds it as
|
|
415
|
-
* `goods − coupon + shipping + codHandling + giftWrap +
|
|
415
|
+
* `goods − coupon + shipping + codHandling + whatsappNotify + giftWrap +
|
|
416
416
|
* shipmentProtection + platformFee + platformFeeGst + emiSurcharge + gstAmount`
|
|
417
417
|
* — so using it as the payout base did three wrong things at once:
|
|
418
418
|
*
|
|
@@ -6,6 +6,8 @@ export declare const CHECKOUT_DEFAULT_COMMISSIONS: {
|
|
|
6
6
|
readonly codDepositPercent: 10;
|
|
7
7
|
readonly codHandlingFeeCap: 200;
|
|
8
8
|
readonly codHandlingFeePercent: 10;
|
|
9
|
+
readonly whatsappNotifyFeeEnabled: true;
|
|
10
|
+
readonly whatsappNotifyFee: 10;
|
|
9
11
|
readonly giftWrapFeeEnabled: true;
|
|
10
12
|
readonly giftWrapFee: 49;
|
|
11
13
|
readonly shipmentProtectionFeeEnabled: true;
|
|
@@ -13,6 +13,8 @@ export const CHECKOUT_DEFAULT_COMMISSIONS = {
|
|
|
13
13
|
// Mirrors the SiteSettingsDocument defaults, which enable every add-on.
|
|
14
14
|
// Consequence of the mirror: if the settings read fails, an add-on already
|
|
15
15
|
// ticked on the cart document is still billed rather than silently dropped.
|
|
16
|
+
whatsappNotifyFeeEnabled: true,
|
|
17
|
+
whatsappNotifyFee: 10,
|
|
16
18
|
giftWrapFeeEnabled: true,
|
|
17
19
|
giftWrapFee: 49,
|
|
18
20
|
shipmentProtectionFeeEnabled: true,
|
|
@@ -16,12 +16,7 @@
|
|
|
16
16
|
* and find the budget already covers them. A channel that is born capped
|
|
17
17
|
* cannot repeat what email did.
|
|
18
18
|
*/
|
|
19
|
-
|
|
20
|
-
* `whatsapp` was removed 2026-10-20 with the Cloud API integration. `sms`
|
|
21
|
-
* stays declared with no sender — the ceiling exists so one cannot ship
|
|
22
|
-
* uncapped — and `email` is the only channel with a live transport.
|
|
23
|
-
*/
|
|
24
|
-
export type MessageChannel = "email" | "sms";
|
|
19
|
+
export type MessageChannel = "email" | "whatsapp" | "sms";
|
|
25
20
|
/**
|
|
26
21
|
* Who the message is FOR — which is what the kill switch keys on.
|
|
27
22
|
*
|
|
@@ -81,12 +76,13 @@ export type SendDecision = {
|
|
|
81
76
|
/**
|
|
82
77
|
* Defaults, mirrored into `DEFAULT_SITE_SETTINGS_DATA.messaging`.
|
|
83
78
|
*
|
|
84
|
-
* 🛑 `emailEnabled`
|
|
79
|
+
* 🛑 `emailEnabled` and `whatsappEnabled` default to FALSE. An install that
|
|
85
80
|
* has not made a decision should be quiet, not loud — the opposite default is
|
|
86
81
|
* how a fresh deploy mails real people from a half-configured site.
|
|
87
82
|
*/
|
|
88
83
|
export declare const MESSAGE_BUDGET_DEFAULTS: {
|
|
89
84
|
readonly emailEnabled: false;
|
|
85
|
+
readonly whatsappEnabled: false;
|
|
90
86
|
readonly dailyCeiling: {
|
|
91
87
|
/**
|
|
92
88
|
* Resend's free tier is 100/day. 80 is not timidity — the counter is
|
|
@@ -96,6 +92,8 @@ export declare const MESSAGE_BUDGET_DEFAULTS: {
|
|
|
96
92
|
* headroom is what stops "at the limit" from becoming "over it".
|
|
97
93
|
*/
|
|
98
94
|
readonly email: 80;
|
|
95
|
+
/** Meta's pricing is per-conversation, not per-message; this is a sanity bound, not a tier limit. */
|
|
96
|
+
readonly whatsapp: 200;
|
|
99
97
|
/** No sender exists yet. Present so one cannot ship uncapped. */
|
|
100
98
|
readonly sms: 50;
|
|
101
99
|
};
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
/**
|
|
10
10
|
* Defaults, mirrored into `DEFAULT_SITE_SETTINGS_DATA.messaging`.
|
|
11
11
|
*
|
|
12
|
-
* 🛑 `emailEnabled`
|
|
12
|
+
* 🛑 `emailEnabled` and `whatsappEnabled` default to FALSE. An install that
|
|
13
13
|
* has not made a decision should be quiet, not loud — the opposite default is
|
|
14
14
|
* how a fresh deploy mails real people from a half-configured site.
|
|
15
15
|
*/
|
|
16
16
|
export const MESSAGE_BUDGET_DEFAULTS = {
|
|
17
17
|
emailEnabled: false,
|
|
18
|
+
whatsappEnabled: false,
|
|
18
19
|
dailyCeiling: {
|
|
19
20
|
/**
|
|
20
21
|
* Resend's free tier is 100/day. 80 is not timidity — the counter is
|
|
@@ -24,6 +25,8 @@ export const MESSAGE_BUDGET_DEFAULTS = {
|
|
|
24
25
|
* headroom is what stops "at the limit" from becoming "over it".
|
|
25
26
|
*/
|
|
26
27
|
email: 80,
|
|
28
|
+
/** Meta's pricing is per-conversation, not per-message; this is a sanity bound, not a tier limit. */
|
|
29
|
+
whatsapp: 200,
|
|
27
30
|
/** No sender exists yet. Present so one cannot ship uncapped. */
|
|
28
31
|
sms: 50,
|
|
29
32
|
},
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seller standing — the ONE definition of what earns the Trusted Seller tier.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is a single pure module
|
|
5
|
+
*
|
|
6
|
+
* Four surfaces must agree about the criteria: the monthly rollup job that
|
|
7
|
+
* grants and revokes, the seller's in-dashboard progress panel, the public
|
|
8
|
+
* `/seller-guide` criteria table, and the admin trust guide. Four hand-written
|
|
9
|
+
* copies of "25 delivered orders in 90 days" is Root Cause #73/#75's shape —
|
|
10
|
+
* the copy that gets fixed is never the copy the reader believes. So the
|
|
11
|
+
* thresholds, the predicate AND the display rows all come from here.
|
|
12
|
+
*
|
|
13
|
+
* 🛑 It had a fifth copy before this existed, and it was prose:
|
|
14
|
+
* `AdminStoresGuideView` told admins the verified badge was "awarded when the
|
|
15
|
+
* store has a consistent track record (>=20 delivered orders, >=4.0 rating, 3+
|
|
16
|
+
* months active)" — numbers no code has ever read, attached to a field nothing
|
|
17
|
+
* computes. That sentence is what the whole feature was remembered as being.
|
|
18
|
+
*
|
|
19
|
+
* ## Pure, and that is load-bearing
|
|
20
|
+
*
|
|
21
|
+
* No Firestore, no clock, no `Date.now()`. The job passes in metrics it has
|
|
22
|
+
* already measured and the UI passes in metrics it has already loaded, so the
|
|
23
|
+
* same input always produces the same verdict and the panel can never disagree
|
|
24
|
+
* with the grant.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* 🛑 A `Record<Criterion, true>`, not a string array.
|
|
28
|
+
*
|
|
29
|
+
* Adding a criterion then becomes a COMPILE error at every site that must
|
|
30
|
+
* display or evaluate it — the thresholds, the labels, the verdict's `met`
|
|
31
|
+
* map. An array lets a new criterion be evaluated while silently rendering
|
|
32
|
+
* nowhere, which is how ten hand-maintained enumerations of one union drifted
|
|
33
|
+
* in Root Cause #61.
|
|
34
|
+
*/
|
|
35
|
+
declare const SELLER_STANDING_CRITERION_MAP: {
|
|
36
|
+
readonly deliveredOrders: true;
|
|
37
|
+
readonly distinctBuyers: true;
|
|
38
|
+
readonly distinctAddresses: true;
|
|
39
|
+
readonly ratedOrders: true;
|
|
40
|
+
readonly averageRating: true;
|
|
41
|
+
};
|
|
42
|
+
export type SellerStandingCriterion = keyof typeof SELLER_STANDING_CRITERION_MAP;
|
|
43
|
+
export declare const SELLER_STANDING_CRITERIA: SellerStandingCriterion[];
|
|
44
|
+
/** The two tiers. Ordered: `trusted` is strictly above `seller`. */
|
|
45
|
+
export type SellerTier = "seller" | "trusted";
|
|
46
|
+
export declare const SELLER_TIER: {
|
|
47
|
+
readonly SELLER: "seller";
|
|
48
|
+
readonly TRUSTED: "trusted";
|
|
49
|
+
};
|
|
50
|
+
export interface SellerStandingThresholds {
|
|
51
|
+
/** The rolling window every metric is measured over. */
|
|
52
|
+
windowDays: number;
|
|
53
|
+
minDeliveredOrders: number;
|
|
54
|
+
minDistinctBuyers: number;
|
|
55
|
+
minDistinctAddresses: number;
|
|
56
|
+
minRatedOrders: number;
|
|
57
|
+
/** A mean, 1–5. Compared with `>=`. */
|
|
58
|
+
minAverageRating: number;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The shipped defaults.
|
|
62
|
+
*
|
|
63
|
+
* 🛑 Every count is measured INSIDE `windowDays`, not lifetime. That is the
|
|
64
|
+
* strict reading and it was chosen deliberately: the tier grants early payout,
|
|
65
|
+
* so it has to describe a seller who is shipping reliably *now*, not one who
|
|
66
|
+
* did two years ago. The consequence worth knowing up front is that on a
|
|
67
|
+
* freshly seeded catalogue **nobody qualifies**, and that is the correct
|
|
68
|
+
* result rather than a broken job.
|
|
69
|
+
*
|
|
70
|
+
* Admin-overridable via `siteSettings.sellerStanding` so the bar can be tuned
|
|
71
|
+
* once real volume exists, without a deploy.
|
|
72
|
+
*/
|
|
73
|
+
export declare const SELLER_STANDING_DEFAULTS: SellerStandingThresholds;
|
|
74
|
+
export interface SellerStandingMetrics {
|
|
75
|
+
/** Delivered orders inside the window, excluding self-bought and test rows. */
|
|
76
|
+
deliveredOrders: number;
|
|
77
|
+
/** Distinct buyer uids across those orders. */
|
|
78
|
+
distinctBuyers: number;
|
|
79
|
+
/** Distinct shipping-address fingerprints across those orders. */
|
|
80
|
+
distinctAddresses: number;
|
|
81
|
+
/** Approved reviews inside the window. */
|
|
82
|
+
ratedOrders: number;
|
|
83
|
+
/** Mean of those reviews' ratings, 1–5. `0` when there are none. */
|
|
84
|
+
averageRating: number;
|
|
85
|
+
}
|
|
86
|
+
export interface SellerStandingVerdict {
|
|
87
|
+
tier: SellerTier;
|
|
88
|
+
met: Record<SellerStandingCriterion, boolean>;
|
|
89
|
+
/** How many criteria are still short — drives "1 criterion to go". */
|
|
90
|
+
unmetCount: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Does this seller currently meet the bar?
|
|
94
|
+
*
|
|
95
|
+
* 🛑 ALL criteria must hold. There is no scoring and no partial credit: the
|
|
96
|
+
* reward is being paid before delivery, so "mostly reliable" is not a state
|
|
97
|
+
* this can grant on.
|
|
98
|
+
*/
|
|
99
|
+
export declare function evaluateSellerStanding(metrics: SellerStandingMetrics, thresholds?: SellerStandingThresholds): SellerStandingVerdict;
|
|
100
|
+
export declare const SELLER_STANDING_CRITERION_LABELS: Record<SellerStandingCriterion, string>;
|
|
101
|
+
/**
|
|
102
|
+
* Why each criterion exists, in the seller's words. Rendered on the guide
|
|
103
|
+
* pages — a bar with no stated reason reads as arbitrary, and two of these
|
|
104
|
+
* exist specifically to make the tier hard to manufacture.
|
|
105
|
+
*/
|
|
106
|
+
export declare const SELLER_STANDING_CRITERION_WHY: Record<SellerStandingCriterion, string>;
|
|
107
|
+
export type SellerStandingRowKind = "count" | "rating";
|
|
108
|
+
export interface SellerStandingRow {
|
|
109
|
+
criterion: SellerStandingCriterion;
|
|
110
|
+
label: string;
|
|
111
|
+
why: string;
|
|
112
|
+
kind: SellerStandingRowKind;
|
|
113
|
+
/** The seller's current figure. `null` when standing has never been computed. */
|
|
114
|
+
actual: number | null;
|
|
115
|
+
target: number;
|
|
116
|
+
met: boolean;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* One row per criterion, for rendering.
|
|
120
|
+
*
|
|
121
|
+
* `metrics` may be null — a store whose standing has never been computed shows
|
|
122
|
+
* every target with no figure beside it, which is honest. 🛑 It must NOT show
|
|
123
|
+
* zeros: a confident "0 of 25" on a store that simply has not been measured
|
|
124
|
+
* yet is the same lie as a counter that has never been written reading as 0
|
|
125
|
+
* (Root Cause #72).
|
|
126
|
+
*/
|
|
127
|
+
export declare function sellerStandingRows(metrics: SellerStandingMetrics | null | undefined, thresholds?: SellerStandingThresholds): SellerStandingRow[];
|
|
128
|
+
/** `4` → `"4.0"`, `25` → `"25"`. Keeps rating and count formatting in one place. */
|
|
129
|
+
export declare function formatSellerStandingValue(value: number, kind: SellerStandingRowKind): string;
|
|
130
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seller standing — the ONE definition of what earns the Trusted Seller tier.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this is a single pure module
|
|
5
|
+
*
|
|
6
|
+
* Four surfaces must agree about the criteria: the monthly rollup job that
|
|
7
|
+
* grants and revokes, the seller's in-dashboard progress panel, the public
|
|
8
|
+
* `/seller-guide` criteria table, and the admin trust guide. Four hand-written
|
|
9
|
+
* copies of "25 delivered orders in 90 days" is Root Cause #73/#75's shape —
|
|
10
|
+
* the copy that gets fixed is never the copy the reader believes. So the
|
|
11
|
+
* thresholds, the predicate AND the display rows all come from here.
|
|
12
|
+
*
|
|
13
|
+
* 🛑 It had a fifth copy before this existed, and it was prose:
|
|
14
|
+
* `AdminStoresGuideView` told admins the verified badge was "awarded when the
|
|
15
|
+
* store has a consistent track record (>=20 delivered orders, >=4.0 rating, 3+
|
|
16
|
+
* months active)" — numbers no code has ever read, attached to a field nothing
|
|
17
|
+
* computes. That sentence is what the whole feature was remembered as being.
|
|
18
|
+
*
|
|
19
|
+
* ## Pure, and that is load-bearing
|
|
20
|
+
*
|
|
21
|
+
* No Firestore, no clock, no `Date.now()`. The job passes in metrics it has
|
|
22
|
+
* already measured and the UI passes in metrics it has already loaded, so the
|
|
23
|
+
* same input always produces the same verdict and the panel can never disagree
|
|
24
|
+
* with the grant.
|
|
25
|
+
*/
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Criteria
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
/**
|
|
30
|
+
* 🛑 A `Record<Criterion, true>`, not a string array.
|
|
31
|
+
*
|
|
32
|
+
* Adding a criterion then becomes a COMPILE error at every site that must
|
|
33
|
+
* display or evaluate it — the thresholds, the labels, the verdict's `met`
|
|
34
|
+
* map. An array lets a new criterion be evaluated while silently rendering
|
|
35
|
+
* nowhere, which is how ten hand-maintained enumerations of one union drifted
|
|
36
|
+
* in Root Cause #61.
|
|
37
|
+
*/
|
|
38
|
+
const SELLER_STANDING_CRITERION_MAP = {
|
|
39
|
+
deliveredOrders: true,
|
|
40
|
+
distinctBuyers: true,
|
|
41
|
+
distinctAddresses: true,
|
|
42
|
+
ratedOrders: true,
|
|
43
|
+
averageRating: true,
|
|
44
|
+
};
|
|
45
|
+
export const SELLER_STANDING_CRITERIA = Object.keys(SELLER_STANDING_CRITERION_MAP);
|
|
46
|
+
export const SELLER_TIER = {
|
|
47
|
+
SELLER: "seller",
|
|
48
|
+
TRUSTED: "trusted",
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* The shipped defaults.
|
|
52
|
+
*
|
|
53
|
+
* 🛑 Every count is measured INSIDE `windowDays`, not lifetime. That is the
|
|
54
|
+
* strict reading and it was chosen deliberately: the tier grants early payout,
|
|
55
|
+
* so it has to describe a seller who is shipping reliably *now*, not one who
|
|
56
|
+
* did two years ago. The consequence worth knowing up front is that on a
|
|
57
|
+
* freshly seeded catalogue **nobody qualifies**, and that is the correct
|
|
58
|
+
* result rather than a broken job.
|
|
59
|
+
*
|
|
60
|
+
* Admin-overridable via `siteSettings.sellerStanding` so the bar can be tuned
|
|
61
|
+
* once real volume exists, without a deploy.
|
|
62
|
+
*/
|
|
63
|
+
export const SELLER_STANDING_DEFAULTS = {
|
|
64
|
+
windowDays: 90,
|
|
65
|
+
minDeliveredOrders: 25,
|
|
66
|
+
minDistinctBuyers: 25,
|
|
67
|
+
minDistinctAddresses: 25,
|
|
68
|
+
minRatedOrders: 25,
|
|
69
|
+
minAverageRating: 4,
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Does this seller currently meet the bar?
|
|
73
|
+
*
|
|
74
|
+
* 🛑 ALL criteria must hold. There is no scoring and no partial credit: the
|
|
75
|
+
* reward is being paid before delivery, so "mostly reliable" is not a state
|
|
76
|
+
* this can grant on.
|
|
77
|
+
*/
|
|
78
|
+
export function evaluateSellerStanding(metrics, thresholds = SELLER_STANDING_DEFAULTS) {
|
|
79
|
+
const met = {
|
|
80
|
+
deliveredOrders: metrics.deliveredOrders >= thresholds.minDeliveredOrders,
|
|
81
|
+
distinctBuyers: metrics.distinctBuyers >= thresholds.minDistinctBuyers,
|
|
82
|
+
distinctAddresses: metrics.distinctAddresses >= thresholds.minDistinctAddresses,
|
|
83
|
+
ratedOrders: metrics.ratedOrders >= thresholds.minRatedOrders,
|
|
84
|
+
averageRating: metrics.averageRating >= thresholds.minAverageRating,
|
|
85
|
+
};
|
|
86
|
+
const unmetCount = SELLER_STANDING_CRITERIA.filter((c) => !met[c]).length;
|
|
87
|
+
return {
|
|
88
|
+
tier: unmetCount === 0 ? SELLER_TIER.TRUSTED : SELLER_TIER.SELLER,
|
|
89
|
+
met,
|
|
90
|
+
unmetCount,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Display — so the panel, the public guide and the admin guide agree
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
export const SELLER_STANDING_CRITERION_LABELS = {
|
|
97
|
+
deliveredOrders: "Delivered orders",
|
|
98
|
+
distinctBuyers: "Different buyers",
|
|
99
|
+
distinctAddresses: "Different delivery addresses",
|
|
100
|
+
ratedOrders: "Rated orders",
|
|
101
|
+
averageRating: "Average rating",
|
|
102
|
+
};
|
|
103
|
+
/**
|
|
104
|
+
* Why each criterion exists, in the seller's words. Rendered on the guide
|
|
105
|
+
* pages — a bar with no stated reason reads as arbitrary, and two of these
|
|
106
|
+
* exist specifically to make the tier hard to manufacture.
|
|
107
|
+
*/
|
|
108
|
+
export const SELLER_STANDING_CRITERION_WHY = {
|
|
109
|
+
deliveredOrders: "Enough completed sales to show a pattern, not a one-off.",
|
|
110
|
+
distinctBuyers: "Sales to real, different customers.",
|
|
111
|
+
distinctAddresses: "Orders going to different places, not the same one repeatedly.",
|
|
112
|
+
ratedOrders: "Enough ratings for the average below to mean something.",
|
|
113
|
+
averageRating: "Buyers are consistently happy with what arrives.",
|
|
114
|
+
};
|
|
115
|
+
function targetFor(criterion, t) {
|
|
116
|
+
switch (criterion) {
|
|
117
|
+
case "deliveredOrders":
|
|
118
|
+
return t.minDeliveredOrders;
|
|
119
|
+
case "distinctBuyers":
|
|
120
|
+
return t.minDistinctBuyers;
|
|
121
|
+
case "distinctAddresses":
|
|
122
|
+
return t.minDistinctAddresses;
|
|
123
|
+
case "ratedOrders":
|
|
124
|
+
return t.minRatedOrders;
|
|
125
|
+
case "averageRating":
|
|
126
|
+
return t.minAverageRating;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* One row per criterion, for rendering.
|
|
131
|
+
*
|
|
132
|
+
* `metrics` may be null — a store whose standing has never been computed shows
|
|
133
|
+
* every target with no figure beside it, which is honest. 🛑 It must NOT show
|
|
134
|
+
* zeros: a confident "0 of 25" on a store that simply has not been measured
|
|
135
|
+
* yet is the same lie as a counter that has never been written reading as 0
|
|
136
|
+
* (Root Cause #72).
|
|
137
|
+
*/
|
|
138
|
+
export function sellerStandingRows(metrics, thresholds = SELLER_STANDING_DEFAULTS) {
|
|
139
|
+
const verdict = metrics ? evaluateSellerStanding(metrics, thresholds) : null;
|
|
140
|
+
return SELLER_STANDING_CRITERIA.map((criterion) => ({
|
|
141
|
+
criterion,
|
|
142
|
+
label: SELLER_STANDING_CRITERION_LABELS[criterion],
|
|
143
|
+
why: SELLER_STANDING_CRITERION_WHY[criterion],
|
|
144
|
+
kind: criterion === "averageRating" ? "rating" : "count",
|
|
145
|
+
actual: metrics ? metrics[criterion] : null,
|
|
146
|
+
target: targetFor(criterion, thresholds),
|
|
147
|
+
met: verdict ? verdict.met[criterion] : false,
|
|
148
|
+
}));
|
|
149
|
+
}
|
|
150
|
+
/** `4` → `"4.0"`, `25` → `"25"`. Keeps rating and count formatting in one place. */
|
|
151
|
+
export function formatSellerStandingValue(value, kind) {
|
|
152
|
+
return kind === "rating" ? value.toFixed(1) : String(value);
|
|
153
|
+
}
|
|
@@ -27,6 +27,8 @@ import type { EmiSettings } from "../emi/schedule";
|
|
|
27
27
|
* and each `compute*Fee` helper carries its own default.
|
|
28
28
|
*/
|
|
29
29
|
export interface BuyerFacingFees {
|
|
30
|
+
whatsappNotifyFeeEnabled?: boolean;
|
|
31
|
+
whatsappNotifyFee?: number;
|
|
30
32
|
giftWrapFeeEnabled?: boolean;
|
|
31
33
|
giftWrapFee?: number;
|
|
32
34
|
shipmentProtectionFeeEnabled?: boolean;
|
|
@@ -13,6 +13,8 @@ export function toBuyerFacingFees(commissions) {
|
|
|
13
13
|
if (!commissions)
|
|
14
14
|
return null;
|
|
15
15
|
return {
|
|
16
|
+
whatsappNotifyFeeEnabled: commissions.whatsappNotifyFeeEnabled,
|
|
17
|
+
whatsappNotifyFee: commissions.whatsappNotifyFee,
|
|
16
18
|
giftWrapFeeEnabled: commissions.giftWrapFeeEnabled,
|
|
17
19
|
giftWrapFee: commissions.giftWrapFee,
|
|
18
20
|
shipmentProtectionFeeEnabled: commissions.shipmentProtectionFeeEnabled,
|