@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,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart specs for event pages — pure builders, no React, no recharts.
|
|
3
|
+
*
|
|
4
|
+
* Kept free of any import that reaches the DOM so a Server Component can call
|
|
5
|
+
* these and hand the result to `<Chart>`, whose own `"use client"` boundary is
|
|
6
|
+
* internal (`chart-spec.ts` is deliberately the same shape of module).
|
|
7
|
+
*
|
|
8
|
+
* ## 🛑 EVERY NUMBER HERE IS MEASURED, NOT ILLUSTRATIVE
|
|
9
|
+
*
|
|
10
|
+
* A chart asserts "we measured this". So each builder returns `null` rather
|
|
11
|
+
* than a plausible-looking shape when it has nothing real to plot — an absent
|
|
12
|
+
* chart says "not enough data yet", a flat or invented one says "we measured,
|
|
13
|
+
* and this is the result", which is a different and false claim.
|
|
14
|
+
*
|
|
15
|
+
* @tag domain:events
|
|
16
|
+
* @tag layer:utils
|
|
17
|
+
*/
|
|
18
|
+
import { calculateDiscount } from "../../promotions/schemas/firestore.js";
|
|
19
|
+
/** A poll needs at least one real vote before a bar chart means anything. */
|
|
20
|
+
const MIN_VOTES = 1;
|
|
21
|
+
/**
|
|
22
|
+
* Poll results as bars.
|
|
23
|
+
*
|
|
24
|
+
* The data already existed and was rendered as the plain string
|
|
25
|
+
* "12 votes (34%)" — `getEventPollResults` computes both server-side and
|
|
26
|
+
* `EventLeaderboard` printed them in a flex row. This is the same numbers,
|
|
27
|
+
* shaped so the differences between options are visible at a glance.
|
|
28
|
+
*/
|
|
29
|
+
export function buildPollChart(
|
|
30
|
+
// Structural, not the full `PollConfig`: this reads `options` and nothing
|
|
31
|
+
// else, and the page carries a narrower local declaration of its own.
|
|
32
|
+
pollConfig, results) {
|
|
33
|
+
if (!pollConfig?.options?.length || !results?.length)
|
|
34
|
+
return null;
|
|
35
|
+
const byId = new Map(results.map((r) => [r.optionId, r.count]));
|
|
36
|
+
const total = results.reduce((sum, r) => sum + r.count, 0);
|
|
37
|
+
if (total < MIN_VOTES)
|
|
38
|
+
return null;
|
|
39
|
+
return {
|
|
40
|
+
kind: "bar",
|
|
41
|
+
title: "Votes so far",
|
|
42
|
+
categories: pollConfig.options.map((o) => o.label),
|
|
43
|
+
series: [{ label: "Votes", values: pollConfig.options.map((o) => byId.get(o.id) ?? 0) }],
|
|
44
|
+
unit: "votes",
|
|
45
|
+
caption: `${total} vote${total === 1 ? "" : "s"} counted.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The basket sizes the savings chart compares across.
|
|
50
|
+
*
|
|
51
|
+
* 🛑 THE RANGE IS CHOSEN TO SHOW THE CROSSOVER, which is the only thing this
|
|
52
|
+
* chart can usefully tell someone. At ₹2,000 the two 10% codes TIE at ₹200 and
|
|
53
|
+
* their caps are irrelevant; an uncapped 5% only overtakes a ₹500-capped 10%
|
|
54
|
+
* once the basket passes ₹10,000. A single-basket bar chart would show three
|
|
55
|
+
* bars, two of them identical, and teach nothing.
|
|
56
|
+
*/
|
|
57
|
+
const BASKET_SIZES = [1000, 2000, 5000, 12000];
|
|
58
|
+
const INR = new Intl.NumberFormat("en-IN", { maximumFractionDigits: 0 });
|
|
59
|
+
/**
|
|
60
|
+
* What each cashback coupon actually saves, across basket sizes.
|
|
61
|
+
*
|
|
62
|
+
* 🛑 COMPUTED WITH THE REAL `calculateDiscount`, never a re-derived formula.
|
|
63
|
+
* That function is where `maxDiscount`, `minPurchase` and the tier table are
|
|
64
|
+
* honoured; computing the percentage inline here would silently ignore all three and
|
|
65
|
+
* overstate every capped code. Root Cause #75 is a money rule hand-copied into
|
|
66
|
+
* six places, where the copies never learned about a branch added later.
|
|
67
|
+
*
|
|
68
|
+
* 🛑 `free_shipping` AND `buy_x_get_y` ARE EXCLUDED, not plotted at zero.
|
|
69
|
+
* `calculateDiscount` returns 0 for free shipping BY DESIGN — the waiver is a
|
|
70
|
+
* shipping line, not a subtotal discount — and a BOGO's value depends on the
|
|
71
|
+
* basket's composition, not its total (₹500 on 4x₹500, ₹0 on 2x₹1,000). A bar
|
|
72
|
+
* at zero for either would read as "this code saves you nothing", which is a
|
|
73
|
+
* lie by omission. They belong in the caption, and that is where they go.
|
|
74
|
+
*/
|
|
75
|
+
/**
|
|
76
|
+
* 🛑 TAKES THE PUBLIC PROJECTION, NOT THE DOCUMENT — and still calls the
|
|
77
|
+
* real `calculateDiscount`.
|
|
78
|
+
*
|
|
79
|
+
* The event page only ever holds `AttachedCoupon`, the allow-list
|
|
80
|
+
* projection (Root Cause #70 — the full document carries fields no public
|
|
81
|
+
* surface should see). That projection carries exactly the four facts the
|
|
82
|
+
* calculator needs, under different names: `type`, `discountValue`,
|
|
83
|
+
* `maxDiscount`, `minPurchase`.
|
|
84
|
+
*
|
|
85
|
+
* So this MAPS NAMES and delegates. The tempting shortcut — computing
|
|
86
|
+
* the percentage inline right here, because the projection makes it
|
|
87
|
+
* easy — is precisely Root Cause #75: a money rule re-derived at a call
|
|
88
|
+
* site, which then never learns about the cap, the minimum or the tier
|
|
89
|
+
* table the real function honours.
|
|
90
|
+
*/
|
|
91
|
+
function toCalculable(c) {
|
|
92
|
+
return {
|
|
93
|
+
type: c.type,
|
|
94
|
+
discount: {
|
|
95
|
+
value: c.discountValue,
|
|
96
|
+
maxDiscount: c.maxDiscount,
|
|
97
|
+
minPurchase: c.minPurchase,
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export function buildCouponSavingsChart(coupons) {
|
|
102
|
+
const plottable = coupons.filter((c) => c.type === "percentage" || c.type === "fixed");
|
|
103
|
+
if (plottable.length === 0)
|
|
104
|
+
return null;
|
|
105
|
+
const excluded = coupons
|
|
106
|
+
.filter((c) => !plottable.includes(c))
|
|
107
|
+
.map((c) => c.code);
|
|
108
|
+
const series = plottable.map((coupon) => ({
|
|
109
|
+
label: coupon.code,
|
|
110
|
+
values: BASKET_SIZES.map((basket) => Math.round(calculateDiscount(toCalculable(coupon), basket))),
|
|
111
|
+
}));
|
|
112
|
+
// Every code saving nothing at every size means the minimums are all above
|
|
113
|
+
// our largest basket — a chart of four empty groups.
|
|
114
|
+
if (series.every((s) => s.values.every((v) => v === 0)))
|
|
115
|
+
return null;
|
|
116
|
+
const captionParts = ["One code per order — this is a choose-one comparison."];
|
|
117
|
+
if (excluded.length) {
|
|
118
|
+
captionParts.push(`${excluded.join(" and ")} ${excluded.length === 1 ? "is" : "are"} not shown: ` +
|
|
119
|
+
`their value depends on delivery or on what is in the basket, not on the total.`);
|
|
120
|
+
}
|
|
121
|
+
return {
|
|
122
|
+
kind: "groupedBar",
|
|
123
|
+
title: "What each code saves",
|
|
124
|
+
categories: BASKET_SIZES.map((b) => `₹${INR.format(b)}`),
|
|
125
|
+
series,
|
|
126
|
+
unit: "₹",
|
|
127
|
+
caption: captionParts.join(" "),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Likes over time. Data comes from `getLikeTrend`, which returns `null` below
|
|
132
|
+
* `LIKE_TREND_MIN_POINTS` so a two-like event plots nothing rather than a flat
|
|
133
|
+
* line with axes.
|
|
134
|
+
*/
|
|
135
|
+
export function buildLikeTrendChart(trend) {
|
|
136
|
+
if (!trend)
|
|
137
|
+
return null;
|
|
138
|
+
return {
|
|
139
|
+
kind: "area",
|
|
140
|
+
title: "Interest over the last fortnight",
|
|
141
|
+
categories: trend.categories,
|
|
142
|
+
series: [{ label: "Likes", values: trend.values }],
|
|
143
|
+
unit: "likes",
|
|
144
|
+
caption: "Likes received per day.",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Time elapsed in the sale window, and redemptions against a real cap.
|
|
149
|
+
*
|
|
150
|
+
* 🛑 `claimedPercent` IS null UNLESS A REAL CAP EXISTS. `usage.totalLimit` is
|
|
151
|
+
* optional, and an uncapped coupon has no denominator — rendering a progress
|
|
152
|
+
* bar against an invented 100 would show a bar filling toward a limit nobody
|
|
153
|
+
* set. Absent is the honest state and the caller renders only the countdown.
|
|
154
|
+
*/
|
|
155
|
+
export function buildSaleProgress(startsAt, endsAt, coupon, now = new Date()) {
|
|
156
|
+
if (!startsAt || !endsAt)
|
|
157
|
+
return null;
|
|
158
|
+
const start = startsAt.getTime();
|
|
159
|
+
const end = endsAt.getTime();
|
|
160
|
+
if (!(end > start))
|
|
161
|
+
return null;
|
|
162
|
+
const elapsed = ((now.getTime() - start) / (end - start)) * 100;
|
|
163
|
+
const limit = coupon?.usage?.totalLimit;
|
|
164
|
+
const used = coupon?.usage?.currentUsage ?? 0;
|
|
165
|
+
const hasCap = typeof limit === "number" && limit > 0;
|
|
166
|
+
return {
|
|
167
|
+
elapsedPercent: Math.min(100, Math.max(0, Math.round(elapsed))),
|
|
168
|
+
endsAt: now.getTime() < end ? endsAt : null,
|
|
169
|
+
claimedPercent: hasCap ? Math.min(100, Math.round((used / limit) * 100)) : null,
|
|
170
|
+
claimedLabel: hasCap ? `${INR.format(used)} of ${INR.format(limit)} claimed` : null,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -52,6 +52,15 @@ export const FAQ_CATEGORY_HELP_GUIDE = {
|
|
|
52
52
|
// live.
|
|
53
53
|
scam_awareness: { path: "/help/account", label: "Account & Safety guide" },
|
|
54
54
|
fees_and_taxes: { path: "/help/shopping", label: "Shopping & Checkout guide" },
|
|
55
|
+
/*
|
|
56
|
+
* The BUYER-facing badge page, not `/seller-guide`.
|
|
57
|
+
*
|
|
58
|
+
* This map sits on the public FAQ page, so its audience is a visitor rather
|
|
59
|
+
* than a seller — and "what does Trusted Seller mean" is the question they
|
|
60
|
+
* are most likely to have arrived with. A seller reading these FAQs still
|
|
61
|
+
* gets there: the badge page links on to the seller guide.
|
|
62
|
+
*/
|
|
63
|
+
selling: { path: "/help/seller-badges", label: "What seller badges mean" },
|
|
55
64
|
technical_support: null,
|
|
56
65
|
general: null,
|
|
57
66
|
};
|
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
export type FAQAnswerFormat = "plain" | "markdown" | "html";
|
|
2
2
|
export type FAQCategory = "orders_payment" | "shipping_delivery" | "returns_refunds" | "product_information" | "account_security" | "technical_support" | "general" | "scam_awareness"
|
|
3
|
+
/**
|
|
4
|
+
* Questions asked by SELLERS, and the buyer-facing questions about them —
|
|
5
|
+
* how to open a store, what the badges mean, how standing is earned and
|
|
6
|
+
* lost.
|
|
7
|
+
*
|
|
8
|
+
* Kept out of `general` for the reason `fees_and_taxes` was: these have a
|
|
9
|
+
* distinct audience. A buyer browsing FAQs is not looking for "how do I
|
|
10
|
+
* list an item", and a seller looking for it should not have to read past
|
|
11
|
+
* twenty buyer questions to find it.
|
|
12
|
+
*
|
|
13
|
+
* 🛑 Adding a member here is FIVE coordinated edits, three of which are
|
|
14
|
+
* compile-checked and two of which are not: this union,
|
|
15
|
+
* `FAQ_CATEGORY_LABELS`, the `Record<FAQCategory, …>` in
|
|
16
|
+
* `constants/help-guides.ts` — and then the HARDCODED category arrays in
|
|
17
|
+
* `src/app/[locale]/faqs/page.tsx` and `faqs/[category]/page.tsx`, which
|
|
18
|
+
* nothing type-checks. A comment in those files records `fees_and_taxes`
|
|
19
|
+
* needing exactly this.
|
|
20
|
+
*/
|
|
21
|
+
| "selling"
|
|
3
22
|
/**
|
|
4
23
|
* D6b. Money questions, kept out of `orders_payment` on purpose: that
|
|
5
24
|
* category is about an ORDER (where is it, how do I pay, can I cancel),
|
|
@@ -29,7 +29,31 @@ function BrandLogo({ brand }) {
|
|
|
29
29
|
const iconSrc = brand.display?.icon;
|
|
30
30
|
const coverImage = brand.display?.coverImage;
|
|
31
31
|
const initial = brand.name[0]?.toUpperCase() ?? "?";
|
|
32
|
-
return (_jsxs(Link
|
|
32
|
+
return (_jsxs(Link
|
|
33
|
+
/*
|
|
34
|
+
* 🛑 BRAND_DETAIL, not CATEGORY_DETAIL.
|
|
35
|
+
*
|
|
36
|
+
* A brand IS a `categoryType: "brand"` row, so `/categories/brand-hasbro`
|
|
37
|
+
* resolves just as `/brands/brand-hasbro` does — and both are indexable.
|
|
38
|
+
* This component was the one surface linking brands by the category path
|
|
39
|
+
* while every other surface used `/brands/…`, so the homepage shipped two
|
|
40
|
+
* different URLs for the same entity and split its signals between them.
|
|
41
|
+
* `/brands/{slug}` is the one the sitemap advertises and the one the
|
|
42
|
+
* brand page declares canonical.
|
|
43
|
+
*/
|
|
44
|
+
, {
|
|
45
|
+
/*
|
|
46
|
+
* 🛑 BRAND_DETAIL, not CATEGORY_DETAIL.
|
|
47
|
+
*
|
|
48
|
+
* A brand IS a `categoryType: "brand"` row, so `/categories/brand-hasbro`
|
|
49
|
+
* resolves just as `/brands/brand-hasbro` does — and both are indexable.
|
|
50
|
+
* This component was the one surface linking brands by the category path
|
|
51
|
+
* while every other surface used `/brands/…`, so the homepage shipped two
|
|
52
|
+
* different URLs for the same entity and split its signals between them.
|
|
53
|
+
* `/brands/{slug}` is the one the sitemap advertises and the one the
|
|
54
|
+
* brand page declares canonical.
|
|
55
|
+
*/
|
|
56
|
+
href: ROUTES.PUBLIC.BRAND_DETAIL(brand.slug), className: "flex h-32 w-36 flex-col items-center justify-center gap-[var(--appkit-space-2)] rounded-xl border p-[var(--appkit-space-3)] shadow-sm transition-all hover:border-primary-300 hover:shadow-md border-[var(--appkit-color-border)] bg-[var(--appkit-color-surface)] md:h-40", children: [iconSrc || coverImage ? (_jsx(Image, { src: resolveMediaUrl(iconSrc ?? coverImage), alt: brand.name, width: 64, height: 64, className: "h-16 w-16 rounded object-contain" })) : (_jsx(Row, { textWeight: "bold", textSize: "sm", className: "h-16 w-16 bg-primary-100 text-primary-700 dark:bg-primary-900 dark:text-primary-300", align: "center", justify: "center", rounded: "lg", children: initial })), _jsx(Text, { className: "w-full truncate", color: "muted", size: "xs", weight: "medium", align: "start", children: brand.name })] }));
|
|
33
57
|
}
|
|
34
58
|
export function BrandsSection({ title = "Top Brands", subtitle, limit = 12, viewMoreHref, viewMoreLabel = "All brands →", className = "", initialItems, cta, filters, autoScroll = true, scrollInterval = 4000, loop = true, }) {
|
|
35
59
|
const themed = { textPrimary: THEMED_TEXT_PRIMARY };
|
|
@@ -72,7 +72,16 @@ function getSlideHeightClass(height) {
|
|
|
72
72
|
/** Renders the slide background — image, video, solid colour, or gradient. */
|
|
73
73
|
function SlideBackground({ bg, legacy, mobileLegacy, isMobile, priority, }) {
|
|
74
74
|
const effectiveBg = bg ?? (legacy ? { type: legacy.type, url: legacy.url } : undefined);
|
|
75
|
-
|
|
75
|
+
/*
|
|
76
|
+
* 🛑 Both halves are gated on `isMobile`. `effectiveBg.mobileUrl` was read
|
|
77
|
+
* UNCONDITIONALLY, so any slide carrying a mobile crop served that
|
|
78
|
+
* phone-sized image to desktop too — a narrow picture stretched across a
|
|
79
|
+
* 1920px hero, and the desktop asset never loaded at all. The legacy half
|
|
80
|
+
* was already gated correctly, which is what made the asymmetry easy to miss.
|
|
81
|
+
*/
|
|
82
|
+
const mobileUrl = isMobile
|
|
83
|
+
? (effectiveBg?.mobileUrl ?? mobileLegacy?.url)
|
|
84
|
+
: undefined;
|
|
76
85
|
const src = mobileUrl ?? effectiveBg?.url ?? "";
|
|
77
86
|
if (!effectiveBg) {
|
|
78
87
|
return _jsx(Div, { className: `${POSITION_FILL} bg-neutral-900` });
|
|
@@ -111,17 +120,25 @@ function CardBackground({ bg }) {
|
|
|
111
120
|
return null;
|
|
112
121
|
}
|
|
113
122
|
function CarouselCardRenderer({ card, isMobile, push, }) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
123
|
+
/*
|
|
124
|
+
* 🛑 On mobile the card takes NO explicit grid placement at all — the parent
|
|
125
|
+
* is a one-column auto-flow grid, so source order is the layout.
|
|
126
|
+
*
|
|
127
|
+
* The old branch mapped `mobileZone` to a `gridColumn`, which against a
|
|
128
|
+
* single declared column created an implicit column 2 and stacked every
|
|
129
|
+
* card into one cell. `mobileZone` was also typed `2 | 5` only, so it could
|
|
130
|
+
* not express a six-card slide even in principle. Zones are a desktop
|
|
131
|
+
* concept; a phone has one column and nothing to place into.
|
|
132
|
+
*/
|
|
133
|
+
const gridPos = isMobile ? null : resolveZoneToGrid(card.zone);
|
|
118
134
|
const hoverClass = getHoverClass(card.hover?.effect);
|
|
119
135
|
const textAlign = card.content?.textAlign ?? "left";
|
|
120
136
|
// audit-content-alignment-ok: admin-configurable per-card textAlign field, defaults to "left" — not a hardcoded center
|
|
121
137
|
const textAlignClass = textAlign === "center" ? "text-center" : textAlign === "right" ? "text-right" : "text-left";
|
|
122
138
|
return (_jsxs(Div, { className: `relative overflow-hidden transition-all duration-300 ${hoverClass}`, rounded: "lg", shadow: "lg", style: {
|
|
123
|
-
|
|
124
|
-
|
|
139
|
+
...(gridPos
|
|
140
|
+
? { gridRow: String(gridPos.gridRow), gridColumn: String(gridPos.gridColumn) }
|
|
141
|
+
: {}),
|
|
125
142
|
minHeight: isMobile ? 100 : 140,
|
|
126
143
|
}, children: [_jsx(CardBackground, { bg: card.background }), !card.isButtonOnly && (_jsxs(_Fragment, { children: [_jsx(Scrim, { direction: "bottom-up", intensity: "strong", className: POSITION_FILL }), _jsxs(Stack, { justify: "end", className: `${POSITION_FILL} md:p-[1.5rem] ${textAlignClass}`, padding: "xs", children: [card.content?.eyebrow && (_jsx(Text, { color: "inverse", shadow: "sm", className: "text-[10px] opacity-70 mb-0.5 tracking-wider", mdSize: "xs", transform: "uppercase", children: card.content.eyebrow })), card.content?.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "hidden md:block opacity-90 mb-0.5 md:mb-2", mdSize: "sm", size: "xs", children: card.content.subtitle })), card.content?.title && (
|
|
127
144
|
// level={3}: a card sits INSIDE a slide, so it is subordinate to
|
|
@@ -202,15 +219,35 @@ export function HeroCarousel({ initialSlides, push, height, defaultAutoplayDelay
|
|
|
202
219
|
onMouseEnter: pauseOnHover ? () => setIsPaused(true) : undefined, onMouseLeave: pauseOnHover ? () => setIsPaused(false) : undefined, onFocus: () => setIsPaused(true), onBlur: () => setIsPaused(false), tabIndex: 0, children: [_jsx(Div, { "aria-live": "polite", "aria-atomic": "true", className: "sr-only", children: `Slide ${currentSlide + 1} of ${slides.length}` }), _jsx(Div, { className: POSITION_FILL, children: _jsx(Div, { layout: "flex", ref: slidesRef, onScroll: handleSlidesScroll, className: "h-full overflow-x-auto scroll-smooth snap-x snap-mandatory [scrollbar-width:none] [&::-webkit-scrollbar]:hidden", style: { gap: 0 }, children: slides.map((slide, slideIndex) => {
|
|
203
220
|
const slideHeightClass = getSlideHeightClass(slide.settings?.height);
|
|
204
221
|
const hasCards = slide.cards && slide.cards.length > 0;
|
|
205
|
-
return (_jsxs(Div, { className: `snap-start flex-none w-full relative self-stretch bg-neutral-900 ${slideHeightClass}`, children: [_jsx(Div, { className: "absolute bottom-2 left-2 z-10", children: _jsx(OwnerQuickActions, { resource: "CAROUSEL", placement: "card",
|
|
222
|
+
return (_jsxs(Div, { className: `snap-start flex-none w-full relative self-stretch bg-neutral-900 ${slideHeightClass}`, children: [_jsx(Div, { className: "absolute bottom-2 left-2 z-10", children: _jsx(OwnerQuickActions, { resource: "CAROUSEL", placement: "card",
|
|
223
|
+
/* Sits on the slide photograph — without this it renders
|
|
224
|
+
* the light `surface-elevated` chip, a pale rounded box
|
|
225
|
+
* floating on the artwork. `onMedia` selects the dark
|
|
226
|
+
* translucent backdrop that variant exists for. */
|
|
227
|
+
onMedia: true, recordLabel: slide.title ?? `Slide ${slideIndex + 1}`, editHref: String(ROUTES.ADMIN.CAROUSEL_EDIT(slide.id)) }) }), _jsx(Div, { className: POSITION_FILL, children: _jsx(SlideBackground, { bg: slide.background, legacy: slide.media, mobileLegacy: slide.mobileMedia, isMobile: isMobile, priority: slideIndex === 0 }) }), slide.overlay && slide.overlay.enabled !== false && (_jsxs(Div, { layout: "flex-col", align: "start", justify: "center", className: `${POSITION_FILL} text-left md:px-[4rem] lg:px-[8rem]`, padding: "x-lg", children: [slide.overlay.subtitle && (_jsx(Text, { color: "inverse", shadow: "sm", className: "stagger-1 opacity-80 mb-1 md:mb-2 tracking-widest", mdSize: "sm", size: "xs", transform: "uppercase", children: slide.overlay.subtitle })), slide.overlay.title && (
|
|
206
228
|
// level={2}, NOT 1. The welcome section above this
|
|
207
229
|
// carousel owns the page's single <h1>; a carousel that
|
|
208
230
|
// emits one <h1> per slide gives the document as many
|
|
209
231
|
// h1s as it has slides. `size` is independent of `level`,
|
|
210
232
|
// so this changes the outline and not one pixel.
|
|
211
|
-
_jsx(Heading, { color: "inverse", level: 2,
|
|
212
|
-
|
|
233
|
+
_jsx(Heading, { color: "inverse", level: 2,
|
|
234
|
+
/*
|
|
235
|
+
* 🛑 `max-w-[16ch]` is load-bearing, not taste. The box
|
|
236
|
+
* is `absolute inset-0`, so with no cap the title runs
|
|
237
|
+
* the full slide width minus padding and an 8xl
|
|
238
|
+
* headline wraps into a wall. A character-based cap
|
|
239
|
+
* tracks the font size instead of guessing at pixels.
|
|
240
|
+
*
|
|
241
|
+
* `text-balance` evens the lines so a two-line title
|
|
242
|
+
* does not leave one word stranded on line two.
|
|
243
|
+
*
|
|
244
|
+
* (A stray bare `!` token sat in this className and
|
|
245
|
+
* generated nothing — removed.)
|
|
246
|
+
*/
|
|
247
|
+
className: "stagger-2 font-display mb-2 md:mb-4 break-words text-balance max-w-[16ch] md:max-w-[18ch] lg:max-w-[20ch]", size: "4xl", shadow: "2xl", mdSize: "6xl", lgSize: "8xl", children: slide.overlay.title })), slide.overlay.description && (_jsx(Text, { color: "inverse", shadow: "sm", className: "stagger-3 opacity-90 mb-4 md:mb-8 max-w-2xl", mdSize: "lg", lgSize: "xl", size: "sm", children: slide.overlay.description })), slide.overlay.button && (_jsx(Div, { className: "stagger-4", children: _jsx(Button, { variant: slide.overlay.button.variant, size: "sm", onClick: makeButtonClickHandler(slide.overlay.button.link, slide.overlay.button.openInNewTab, push), children: slide.overlay.button.text }) }))] })), hasCards && (_jsx(Div, { className: `${POSITION_FILL} grid gap-[var(--appkit-space-2)] md:gap-[var(--appkit-space-4)] md:p-[var(--appkit-space-8)] overflow-y-auto`, padding: "md", style: {
|
|
248
|
+
gridTemplateRows: isMobile ? "auto" : "repeat(2, 1fr)",
|
|
213
249
|
gridTemplateColumns: isMobile ? "1fr" : "repeat(3, 1fr)",
|
|
250
|
+
gridAutoFlow: "row",
|
|
214
251
|
alignContent: "center",
|
|
215
252
|
alignItems: "stretch",
|
|
216
253
|
}, children: slide.cards.slice(0, 6).map((card) => (_jsx(CarouselCardRenderer, { card: card, isMobile: isMobile, push: push }, card.id))) }))] }, slide.id));
|
|
@@ -259,7 +259,7 @@ const SECTION_REGISTRY = {
|
|
|
259
259
|
return (_jsx(WhatsAppCommunitySection, { title: cleanTitle(cfg?.title) || SECTION_TITLE.whatsappCommunity, descriptionHtml: cfg?.description || SECTION_COPY.whatsappDescription,
|
|
260
260
|
// No `|| 5000` fallback: inventing a member count the site cannot
|
|
261
261
|
// verify is a fabricated social-proof number.
|
|
262
|
-
memberCount: cfg?.memberCount, groupLink: cfg?.groupLink ||
|
|
262
|
+
memberCount: cfg?.memberCount, groupLink: cfg?.groupLink || "https://chat.whatsapp.com/", benefits: cfg?.benefits ?? [], testimonial: cfg?.testimonial, buttonText: cfg?.buttonText }));
|
|
263
263
|
},
|
|
264
264
|
},
|
|
265
265
|
faq: {
|
|
@@ -125,6 +125,13 @@ export interface CarouselSlide {
|
|
|
125
125
|
};
|
|
126
126
|
cards: CarouselSlideCard[];
|
|
127
127
|
overlay?: {
|
|
128
|
+
/**
|
|
129
|
+
* Absent means SHOWN — most stored overlays predate this flag. Only an
|
|
130
|
+
* explicit `false` hides it. The seed and `push-editorial-content.mjs`
|
|
131
|
+
* have always written `enabled: true`; the renderer simply never read it,
|
|
132
|
+
* so an admin switching it off changed nothing.
|
|
133
|
+
*/
|
|
134
|
+
enabled?: boolean;
|
|
128
135
|
title?: string;
|
|
129
136
|
subtitle?: string;
|
|
130
137
|
description?: string;
|
|
@@ -79,6 +79,7 @@ export async function issuePartialRefund(adminUid, orderId, deductFees, refundNo
|
|
|
79
79
|
: `Your full refund of ₹${grossRefund} is being processed.`,
|
|
80
80
|
relatedId: orderId,
|
|
81
81
|
relatedType: "order",
|
|
82
|
+
storeId: order.storeId,
|
|
82
83
|
});
|
|
83
84
|
serverLogger.info("Admin partial refund initiated", {
|
|
84
85
|
adminUid,
|
|
@@ -2,7 +2,7 @@ import React from "react";
|
|
|
2
2
|
/**
|
|
3
3
|
* Icon badges for an order's paid add-ons and applied coupon.
|
|
4
4
|
*
|
|
5
|
-
* These are fulfilment instructions, not decoration:
|
|
5
|
+
* These are fulfilment instructions, not decoration: WhatsApp tells the
|
|
6
6
|
* status-change notifier to message this buyer, gift wrap tells the packer to
|
|
7
7
|
* wrap the parcel and enclose the message, and either protection changes how a
|
|
8
8
|
* claim is handled. They are recorded on the order (and filterable in the
|
|
@@ -14,6 +14,8 @@ import React from "react";
|
|
|
14
14
|
* at once instead of being wired per screen.
|
|
15
15
|
*/
|
|
16
16
|
export interface OrderAddonBadgesOrder {
|
|
17
|
+
whatsappNotifyAddon?: boolean;
|
|
18
|
+
whatsappNotifyFee?: number;
|
|
17
19
|
giftWrapAddon?: boolean;
|
|
18
20
|
giftWrapFee?: number;
|
|
19
21
|
giftWrapMessage?: string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Gift, ShieldAlert, ShieldCheck, Tag } from "lucide-react";
|
|
3
|
+
import { Gift, MessageCircle, ShieldAlert, ShieldCheck, Tag } from "lucide-react";
|
|
4
4
|
import { Row, Span, Stack, Text } from "../../../ui/index.js";
|
|
5
5
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
6
6
|
const CHIP_BASE = "inline-flex items-center gap-[var(--appkit-space-1)] rounded-full px-[var(--appkit-space-2)] py-[var(--appkit-space-0-5)] text-[length:var(--appkit-text-xs)] font-medium";
|
|
@@ -15,14 +15,15 @@ function Chip({ icon, label, tone, title, }) {
|
|
|
15
15
|
export function OrderAddonBadges({ order, variant = "compact", showCoupon = true, currency = "INR", className = "", }) {
|
|
16
16
|
const money = (v) => (typeof v === "number" && v > 0 ? formatCurrency(v, currency) : undefined);
|
|
17
17
|
const hasCoupon = showCoupon && !!order.couponCode;
|
|
18
|
-
const hasAny = order.
|
|
18
|
+
const hasAny = order.whatsappNotifyAddon ||
|
|
19
|
+
order.giftWrapAddon ||
|
|
19
20
|
order.shipmentProtectionAddon ||
|
|
20
21
|
order.saleProtectionAddon ||
|
|
21
22
|
hasCoupon;
|
|
22
23
|
if (!hasAny)
|
|
23
24
|
return null;
|
|
24
25
|
const isDetail = variant === "detail";
|
|
25
|
-
return (_jsxs(Stack, { gap: "xs", className: `min-w-0 ${className}`, children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, className: "min-w-0", children: [order.giftWrapAddon && (_jsx(Chip, { icon: _jsx(Gift, { size: 12 }), label: isDetail && money(order.giftWrapFee) ? `Gift wrap · ${money(order.giftWrapFee)}` : "Gift wrap", tone: "brand", title: "This parcel must be gift-wrapped before dispatch." })), order.shipmentProtectionAddon && (_jsx(Chip, { icon: _jsx(ShieldCheck, { size: 12 }), label: isDetail && money(order.shipmentProtectionFee) ? `Protected · ${money(order.shipmentProtectionFee)}` : "Protected", tone: "info", title: "Shipment protection purchased \u2014 loss or damage in transit is covered, claimed against the held funds." })), order.saleProtectionAddon && (_jsx(Chip, { icon: _jsx(ShieldAlert, { size: 12 }), label: isDetail && money(order.saleProtectionFee) ? `Sale protected · ${money(order.saleProtectionFee)}` : "Sale protected", tone: "info", title: "Sale protection purchased \u2014 if the item is not as described the buyer is refunded, no questions asked." })), hasCoupon && (_jsx(Chip, { icon: _jsx(Tag, { size: 12 }), label: isDetail && money(order.couponDiscount)
|
|
26
|
+
return (_jsxs(Stack, { gap: "xs", className: `min-w-0 ${className}`, children: [_jsxs(Row, { gap: "xs", align: "center", wrap: true, className: "min-w-0", children: [order.whatsappNotifyAddon && (_jsx(Chip, { icon: _jsx(MessageCircle, { size: 12 }), label: isDetail && money(order.whatsappNotifyFee) ? `WhatsApp updates · ${money(order.whatsappNotifyFee)}` : "WhatsApp updates", tone: "success", title: "Buyer opted into WhatsApp order-status updates \u2014 status changes on this order should notify them on WhatsApp." })), order.giftWrapAddon && (_jsx(Chip, { icon: _jsx(Gift, { size: 12 }), label: isDetail && money(order.giftWrapFee) ? `Gift wrap · ${money(order.giftWrapFee)}` : "Gift wrap", tone: "brand", title: "This parcel must be gift-wrapped before dispatch." })), order.shipmentProtectionAddon && (_jsx(Chip, { icon: _jsx(ShieldCheck, { size: 12 }), label: isDetail && money(order.shipmentProtectionFee) ? `Protected · ${money(order.shipmentProtectionFee)}` : "Protected", tone: "info", title: "Shipment protection purchased \u2014 loss or damage in transit is covered, claimed against the held funds." })), order.saleProtectionAddon && (_jsx(Chip, { icon: _jsx(ShieldAlert, { size: 12 }), label: isDetail && money(order.saleProtectionFee) ? `Sale protected · ${money(order.saleProtectionFee)}` : "Sale protected", tone: "info", title: "Sale protection purchased \u2014 if the item is not as described the buyer is refunded, no questions asked." })), hasCoupon && (_jsx(Chip, { icon: _jsx(Tag, { size: 12 }), label: isDetail && money(order.couponDiscount)
|
|
26
27
|
? `${order.couponCode} · −${money(order.couponDiscount)}`
|
|
27
28
|
: order.couponCode, tone: "success", title: `Coupon ${order.couponCode} applied to this order.` }))] }), isDetail && order.giftWrapAddon && order.giftWrapMessage && (_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsx(Text, { size: "xs", color: "muted", weight: "semibold", transform: "uppercase", className: "tracking-wide", children: "Gift message" }), _jsx(Text, { size: "sm", color: "primary", className: "min-w-0 whitespace-pre-wrap break-words", children: order.giftWrapMessage })] }))] }));
|
|
28
29
|
}
|
|
@@ -215,6 +215,10 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
215
215
|
canFilter: boolean;
|
|
216
216
|
canSort: boolean;
|
|
217
217
|
};
|
|
218
|
+
whatsappNotifyAddon: {
|
|
219
|
+
canFilter: boolean;
|
|
220
|
+
canSort: boolean;
|
|
221
|
+
};
|
|
218
222
|
giftWrapAddon: {
|
|
219
223
|
canFilter: boolean;
|
|
220
224
|
canSort: boolean;
|
|
@@ -335,6 +339,10 @@ declare class OrderRepository extends BaseRepository<OrderDocument> {
|
|
|
335
339
|
canFilter: boolean;
|
|
336
340
|
canSort: boolean;
|
|
337
341
|
};
|
|
342
|
+
whatsappNotifyAddon: {
|
|
343
|
+
canFilter: boolean;
|
|
344
|
+
canSort: boolean;
|
|
345
|
+
};
|
|
338
346
|
giftWrapAddon: {
|
|
339
347
|
canFilter: boolean;
|
|
340
348
|
canSort: boolean;
|
|
@@ -1364,6 +1364,7 @@ OrderRepository.SELLER_SIEVE_FIELDS = {
|
|
|
1364
1364
|
// Fulfilment flags. Filterable so "which of my orders need gift wrap?" is a
|
|
1365
1365
|
// query against the orders themselves — the whole point of recording the
|
|
1366
1366
|
// add-on on the order rather than keeping a side list of who asked for what.
|
|
1367
|
+
whatsappNotifyAddon: { canFilter: true, canSort: false },
|
|
1367
1368
|
giftWrapAddon: { canFilter: true, canSort: false },
|
|
1368
1369
|
shipmentProtectionAddon: { canFilter: true, canSort: false },
|
|
1369
1370
|
saleProtectionAddon: { canFilter: true, canSort: false },
|
|
@@ -1396,6 +1397,7 @@ OrderRepository.ADMIN_SIEVE_FIELDS = {
|
|
|
1396
1397
|
// Same fulfilment flags as SELLER_SIEVE_FIELDS — admin needs to answer
|
|
1397
1398
|
// "who opted into WhatsApp updates?" when a status change goes out, without
|
|
1398
1399
|
// maintaining a parallel tracker.
|
|
1400
|
+
whatsappNotifyAddon: { canFilter: true, canSort: false },
|
|
1399
1401
|
giftWrapAddon: { canFilter: true, canSort: false },
|
|
1400
1402
|
shipmentProtectionAddon: { canFilter: true, canSort: false },
|
|
1401
1403
|
saleProtectionAddon: { canFilter: true, canSort: false },
|
|
@@ -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;
|