@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
package/dist/.build-stamp.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"stampVersion": 1,
|
|
3
|
-
"version": "4.
|
|
4
|
-
"head": "
|
|
3
|
+
"version": "4.64.0",
|
|
4
|
+
"head": "51ae7f86674aba82540dc018d8baa92064719f20",
|
|
5
5
|
"branch": "main",
|
|
6
6
|
"dirty": false,
|
|
7
|
-
"builtAt": "2026-10-
|
|
7
|
+
"builtAt": "2026-10-24T02:32:37.960Z"
|
|
8
8
|
}
|
|
@@ -36,7 +36,7 @@ export declare const PUBLIC_CATEGORY_FIELDS: readonly ["id", "name", "slug", "de
|
|
|
36
36
|
* Fields withheld, each with the reason. Never remove one without replacing
|
|
37
37
|
* the reason — "it seemed unused" is how a field ends up public by accident.
|
|
38
38
|
*/
|
|
39
|
-
export declare const PRIVATE_CATEGORY_FIELDS: readonly ["position", "subtreeSize", "createdBy", "createdByType", "createdByStoreId", "createdByStoreName", "viewCount", "isBrand", "showOnHomepage", "isSearchable", "searchTxt", "aliases", "brandSlug", "bundleKind", "bundlePrice", "bundleStockStatus", "bundleProductIds", "bundleCategorySlugs", "bundleOriginalTotal", "bundleItemDetails", "contentBody", "descriptionTemplates", "productDefaults", "isTestData", "testDataExpiresAt", "bundleQueryRule", "bundleQueryResolvedAt"];
|
|
39
|
+
export declare const PRIVATE_CATEGORY_FIELDS: readonly ["seoCanonicalPath", "position", "subtreeSize", "createdBy", "createdByType", "createdByStoreId", "createdByStoreName", "viewCount", "isBrand", "showOnHomepage", "isSearchable", "searchTxt", "aliases", "brandSlug", "bundleKind", "bundlePrice", "bundleStockStatus", "bundleProductIds", "bundleCategorySlugs", "bundleOriginalTotal", "bundleItemDetails", "contentBody", "descriptionTemplates", "productDefaults", "isTestData", "testDataExpiresAt", "bundleQueryRule", "bundleQueryResolvedAt"];
|
|
40
40
|
/**
|
|
41
41
|
* Project a stored category into the public shape.
|
|
42
42
|
*
|
|
@@ -55,6 +55,19 @@ export const PUBLIC_CATEGORY_FIELDS = [
|
|
|
55
55
|
* the reason — "it seemed unused" is how a field ends up public by accident.
|
|
56
56
|
*/
|
|
57
57
|
export const PRIVATE_CATEGORY_FIELDS = [
|
|
58
|
+
/*
|
|
59
|
+
* The canonical path this category's page declares to crawlers.
|
|
60
|
+
*
|
|
61
|
+
* 🛑 Triaged PRIVATE as the default, because nothing emits it: it is
|
|
62
|
+
* consumed SERVER-side by the metadata builder, which reads the document
|
|
63
|
+
* rather than this projection, and `toCategoryListItem` does not carry it.
|
|
64
|
+
* A client reader would be a second place the canonical is decided, which
|
|
65
|
+
* is the drift `audit-seo-canonical-host` exists to prevent (Root Cause
|
|
66
|
+
* #81 — two owners of the canonical host cost the site its indexing).
|
|
67
|
+
*
|
|
68
|
+
* Move it to PUBLIC only alongside a named client reader.
|
|
69
|
+
*/
|
|
70
|
+
"seoCanonicalPath",
|
|
58
71
|
// Internal DFS coordinates. Useful only to `onCategoryWrite` and
|
|
59
72
|
// `positionsReconcile`; publishing them invites a client to depend on an
|
|
60
73
|
// ordering we renumber on every reseed.
|
|
@@ -226,6 +226,7 @@ export interface CheckoutPricingPreviewStore {
|
|
|
226
226
|
subtotal: number;
|
|
227
227
|
shippingFee: number;
|
|
228
228
|
codHandlingFee: number;
|
|
229
|
+
whatsappNotifyFee: number;
|
|
229
230
|
giftWrapFee: number;
|
|
230
231
|
shipmentProtectionFee: number;
|
|
231
232
|
saleProtectionFee: number;
|
|
@@ -294,6 +295,7 @@ export interface CheckoutPricingPreview {
|
|
|
294
295
|
subtotal: number;
|
|
295
296
|
shippingFee: number;
|
|
296
297
|
codHandlingFee: number;
|
|
298
|
+
whatsappNotifyFee: number;
|
|
297
299
|
giftWrapFee: number;
|
|
298
300
|
shipmentProtectionFee: number;
|
|
299
301
|
saleProtectionFee: number;
|
|
@@ -48,7 +48,7 @@ import { countDestinations, quoteSharedPurchase, } from "../../../shared/feature
|
|
|
48
48
|
import { resolveCouponCategorySlugs } from "../../../../features/promotions/actions/coupon-actions.js";
|
|
49
49
|
import { assertCheckoutLane, assertLockedLinesStillValid, finalizeLockedLines, } from "./locked-lines.js";
|
|
50
50
|
import { activeLane, defaultCheckoutItems, resolveCheckoutItems, laneOf, } from "../../../shared/checkout/lanes.js";
|
|
51
|
-
import { computeCodHandlingFee, computeGiftWrapFee, computeShipmentProtectionFee, computeSaleProtectionFee, computeCheckoutFees, allocateCheckoutFees, } from "../../../shared/fees/calculator.js";
|
|
51
|
+
import { computeCodHandlingFee, computeWhatsAppNotifyFee, computeGiftWrapFee, computeShipmentProtectionFee, computeSaleProtectionFee, computeCheckoutFees, allocateCheckoutFees, } from "../../../shared/fees/calculator.js";
|
|
52
52
|
import { getAdminDb, getAdminRealtimeDb, RTDB_PATHS, } from "../../../../providers/db-firebase/index.js";
|
|
53
53
|
import { snapshotDeliveryAddress, } from "../../../shared/features/orders/delivery-snapshot.js";
|
|
54
54
|
import { PRODUCT_COLLECTION, PRODUCT_CODES_SUBCOLLECTION } from "../../../../features/products/schemas/firestore.js";
|
|
@@ -192,7 +192,7 @@ function assertLiveJurisdiction(cartItemsArg, productByIdArg, buyerState) {
|
|
|
192
192
|
* either party. We emit the buyer + seller rows here at the create boundary.
|
|
193
193
|
*/
|
|
194
194
|
function emitOrderPlacedNotifications(args) {
|
|
195
|
-
const { orderId, buyerUid, buyerName, storeOwnerId, productLabel, paid } = args;
|
|
195
|
+
const { orderId, buyerUid, buyerName, storeOwnerId, productLabel, paid, storeId } = args;
|
|
196
196
|
const buyerNotif = sendNotification({
|
|
197
197
|
userId: buyerUid,
|
|
198
198
|
type: "order_placed",
|
|
@@ -202,6 +202,7 @@ function emitOrderPlacedNotifications(args) {
|
|
|
202
202
|
relatedId: orderId,
|
|
203
203
|
relatedType: "order",
|
|
204
204
|
actionUrl: `/user/orders/view/${orderId}`,
|
|
205
|
+
storeId,
|
|
205
206
|
}).catch((err) => serverLogger.warn("Failed to send buyer order_placed notification", {
|
|
206
207
|
err: err instanceof Error ? err.message : String(err),
|
|
207
208
|
orderId,
|
|
@@ -740,7 +741,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
740
741
|
const firstItem = group[0].item;
|
|
741
742
|
const firstProduct = group[0].product;
|
|
742
743
|
// Add-ons are this store's choice, not the cart's — see OrderGroupContext.
|
|
743
|
-
const { giftWrapAddon = false, giftWrapMessage, shipmentProtectionAddon = false, saleProtectionAddon = false, } = storeAddons?.[firstItem.storeId] ?? {};
|
|
744
|
+
const { whatsappNotifyAddon = false, giftWrapAddon = false, giftWrapMessage, shipmentProtectionAddon = false, saleProtectionAddon = false, } = storeAddons?.[firstItem.storeId] ?? {};
|
|
744
745
|
const { platformFee = 0, gstOnFee: platformFeeGst = 0 } = platformFeeByStore.get(firstItem.storeId) ?? {};
|
|
745
746
|
const groupTotal = group.reduce((sum, { item, product }) => sum + lineTotalFor(item, product), 0);
|
|
746
747
|
/*
|
|
@@ -873,6 +874,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
873
874
|
// Handling fee charged to the buyer for choosing COD, on top of the order
|
|
874
875
|
// total — not deducted from the deposit/remaining split above.
|
|
875
876
|
const codHandlingFee = paymentMethod === "cod" ? computeCodHandlingFee(groupTotal, commissions) : 0;
|
|
877
|
+
const whatsappNotifyFee = computeWhatsAppNotifyFee(whatsappNotifyAddon, commissions);
|
|
876
878
|
const giftWrapFee = computeGiftWrapFee(giftWrapAddon, commissions);
|
|
877
879
|
/*
|
|
878
880
|
* ── C5e: all three protection tiers, enforced SERVER-SIDE ────────────────
|
|
@@ -1052,7 +1054,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
1052
1054
|
* compounds nine half-paisa errors instead of one, which is how a total
|
|
1053
1055
|
* drifts from the sum of the lines shown beside it.
|
|
1054
1056
|
*/
|
|
1055
|
-
roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst + (emiSchedule?.surchargeAmount ?? 0) + sharedQuote.total);
|
|
1057
|
+
roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + codHandlingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst + (emiSchedule?.surchargeAmount ?? 0) + sharedQuote.total);
|
|
1056
1058
|
/*
|
|
1057
1059
|
* ── D43: the COD deposit/remaining split, on the ORDER TOTAL ──────────────
|
|
1058
1060
|
*
|
|
@@ -1263,6 +1265,8 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
1263
1265
|
* one so a later reader does not have to reconstruct it.
|
|
1264
1266
|
*/
|
|
1265
1267
|
platformFeeGst: !adminBypass && platformFeeGst > 0 ? platformFeeGst : undefined,
|
|
1268
|
+
whatsappNotifyAddon: !adminBypass && whatsappNotifyFee > 0 ? true : undefined,
|
|
1269
|
+
whatsappNotifyFee: !adminBypass && whatsappNotifyFee > 0 ? whatsappNotifyFee : undefined,
|
|
1266
1270
|
giftWrapAddon: !adminBypass && giftWrapFee > 0 ? true : undefined,
|
|
1267
1271
|
giftWrapFee: !adminBypass && giftWrapFee > 0 ? giftWrapFee : undefined,
|
|
1268
1272
|
giftWrapMessage: !adminBypass && giftWrapFee > 0 ? giftWrapMessage?.slice(0, 500) : undefined,
|
|
@@ -1405,6 +1409,7 @@ async function createOrderForGroup(group, orderType, ctx) {
|
|
|
1405
1409
|
storeOwnerId,
|
|
1406
1410
|
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
1407
1411
|
paid: adminBypass,
|
|
1412
|
+
storeId: order.storeId,
|
|
1408
1413
|
});
|
|
1409
1414
|
return groupTotal;
|
|
1410
1415
|
}
|
|
@@ -1885,6 +1890,7 @@ const EMPTY_PRICING_PREVIEW = {
|
|
|
1885
1890
|
subtotal: 0,
|
|
1886
1891
|
shippingFee: 0,
|
|
1887
1892
|
codHandlingFee: 0,
|
|
1893
|
+
whatsappNotifyFee: 0,
|
|
1888
1894
|
giftWrapFee: 0,
|
|
1889
1895
|
shipmentProtectionFee: 0,
|
|
1890
1896
|
saleProtectionFee: 0,
|
|
@@ -2029,6 +2035,7 @@ export async function previewCheckoutPricing(input) {
|
|
|
2029
2035
|
* rather than one number the buyer has to interpret.
|
|
2030
2036
|
*/
|
|
2031
2037
|
const groupEmiDeferred = groupEmiPreviewQuote?.remainingBalance ?? 0;
|
|
2038
|
+
const groupWhatsappFee = computeWhatsAppNotifyFee(addons.whatsappNotifyAddon ?? false, commissions);
|
|
2032
2039
|
const groupGiftWrapFee = computeGiftWrapFee(addons.giftWrapAddon ?? false, commissions);
|
|
2033
2040
|
/*
|
|
2034
2041
|
* C5e — the same three tiers `createOrderForGroup` enforces, so the figure
|
|
@@ -2129,6 +2136,7 @@ export async function previewCheckoutPricing(input) {
|
|
|
2129
2136
|
codHandlingFee: groupCodHandlingFee,
|
|
2130
2137
|
emiSurcharge: groupEmiSurcharge,
|
|
2131
2138
|
emiDeferred: groupEmiDeferred,
|
|
2139
|
+
whatsappNotifyFee: groupWhatsappFee,
|
|
2132
2140
|
giftWrapFee: groupGiftWrapFee,
|
|
2133
2141
|
shipmentProtectionFee: groupShipmentProtectionFee,
|
|
2134
2142
|
saleProtectionFee: groupSaleProtectionFee,
|
|
@@ -2147,6 +2155,7 @@ export async function previewCheckoutPricing(input) {
|
|
|
2147
2155
|
groupShippingFee +
|
|
2148
2156
|
groupCodHandlingFee +
|
|
2149
2157
|
groupEmiSurcharge +
|
|
2158
|
+
groupWhatsappFee +
|
|
2150
2159
|
groupGiftWrapFee +
|
|
2151
2160
|
groupShipmentProtectionFee +
|
|
2152
2161
|
groupSaleProtectionFee +
|
|
@@ -2169,6 +2178,7 @@ export async function previewCheckoutPricing(input) {
|
|
|
2169
2178
|
const codHandlingFee = sum((s) => s.codHandlingFee);
|
|
2170
2179
|
const emiSurcharge = sum((s) => s.emiSurcharge);
|
|
2171
2180
|
const emiDeferred = sum((s) => s.emiDeferred);
|
|
2181
|
+
const whatsappNotifyFeeTotal = sum((s) => s.whatsappNotifyFee);
|
|
2172
2182
|
const giftWrapFeeTotal = sum((s) => s.giftWrapFee);
|
|
2173
2183
|
const shipmentProtectionFeeTotal = sum((s) => s.shipmentProtectionFee);
|
|
2174
2184
|
const saleProtectionFeeTotal = sum((s) => s.saleProtectionFee);
|
|
@@ -2216,6 +2226,7 @@ export async function previewCheckoutPricing(input) {
|
|
|
2216
2226
|
shippingFee +
|
|
2217
2227
|
codHandlingFee +
|
|
2218
2228
|
emiSurcharge +
|
|
2229
|
+
whatsappNotifyFeeTotal +
|
|
2219
2230
|
giftWrapFeeTotal +
|
|
2220
2231
|
shipmentProtectionFeeTotal +
|
|
2221
2232
|
saleProtectionFeeTotal +
|
|
@@ -2271,6 +2282,7 @@ export async function previewCheckoutPricing(input) {
|
|
|
2271
2282
|
codHandlingFee,
|
|
2272
2283
|
emiSurcharge,
|
|
2273
2284
|
emiDeferred,
|
|
2285
|
+
whatsappNotifyFee: whatsappNotifyFeeTotal,
|
|
2274
2286
|
giftWrapFee: giftWrapFeeTotal,
|
|
2275
2287
|
shipmentProtectionFee: shipmentProtectionFeeTotal,
|
|
2276
2288
|
saleProtectionFee: saleProtectionFeeTotal,
|
|
@@ -2513,6 +2525,7 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2513
2525
|
// cart-wide fee was passed in and billed against every group.
|
|
2514
2526
|
const commissionRates = siteSettings?.commissions ?? CHECKOUT_DEFAULT_COMMISSIONS;
|
|
2515
2527
|
const addons = storeAddons?.[firstItem.storeId] ?? {};
|
|
2528
|
+
const whatsappNotifyFee = computeWhatsAppNotifyFee(addons.whatsappNotifyAddon ?? false, commissionRates);
|
|
2516
2529
|
const giftWrapFee = computeGiftWrapFee(addons.giftWrapAddon ?? false, commissionRates);
|
|
2517
2530
|
const giftWrapMessage = addons.giftWrapMessage;
|
|
2518
2531
|
/*
|
|
@@ -2617,7 +2630,7 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2617
2630
|
});
|
|
2618
2631
|
// D42: rounded once on the sum — see the COD/UPI path's 🛑 for why a raw
|
|
2619
2632
|
// float sum of seven 2dp terms reached Firestore as 1011.8000000000001.
|
|
2620
|
-
const orderTotal = roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst + (phonepeEmiSchedule?.surchargeAmount ?? 0) + phonepeSharedQuote.total);
|
|
2633
|
+
const orderTotal = roundRupees(Math.max(0, groupTotal - couponDiscount) + shippingFee + whatsappNotifyFee + giftWrapFee + shipmentProtectionFee + saleProtectionFee + platformFee + platformFeeGst + (phonepeEmiSchedule?.surchargeAmount ?? 0) + phonepeSharedQuote.total);
|
|
2621
2634
|
/*
|
|
2622
2635
|
* ── POINTS TENDER (D46/D47) — the same STAGE as the COD/manual path ───────
|
|
2623
2636
|
*
|
|
@@ -2767,6 +2780,8 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2767
2780
|
// leaving the refund path to re-derive it from live percentages (D32).
|
|
2768
2781
|
platformFeeGst: platformFeeGst > 0 ? platformFeeGst : undefined,
|
|
2769
2782
|
shippingFee: shippingFee > 0 ? shippingFee : undefined,
|
|
2783
|
+
whatsappNotifyAddon: whatsappNotifyFee > 0 ? true : undefined,
|
|
2784
|
+
whatsappNotifyFee: whatsappNotifyFee > 0 ? whatsappNotifyFee : undefined,
|
|
2770
2785
|
giftWrapAddon: giftWrapFee > 0 ? true : undefined,
|
|
2771
2786
|
giftWrapFee: giftWrapFee > 0 ? giftWrapFee : undefined,
|
|
2772
2787
|
giftWrapMessage: giftWrapFee > 0 ? giftWrapMessage?.slice(0, 500) : undefined,
|
|
@@ -2864,6 +2879,7 @@ async function createPhonePeGroupOrder(group, orderType, ctx) {
|
|
|
2864
2879
|
storeOwnerId,
|
|
2865
2880
|
productLabel: orderItems.length > 1 ? `${orderItems.length} items` : firstItem.productTitle,
|
|
2866
2881
|
paid: true,
|
|
2882
|
+
storeId: order.storeId,
|
|
2867
2883
|
});
|
|
2868
2884
|
return orderTotal;
|
|
2869
2885
|
}
|
|
@@ -3282,6 +3298,7 @@ export async function verifyAndPlaceGatewayOrderAction(input) {
|
|
|
3282
3298
|
const addons = cart.storeAddons?.[storeId] ?? {};
|
|
3283
3299
|
const groupSubtotal = g.items.reduce((gs, { item, product }) => gs + lineTotalFor(item, product), 0);
|
|
3284
3300
|
return (sum +
|
|
3301
|
+
computeWhatsAppNotifyFee(addons.whatsappNotifyAddon ?? false, commissionRates) +
|
|
3285
3302
|
computeGiftWrapFee(addons.giftWrapAddon ?? false, commissionRates) +
|
|
3286
3303
|
computeShipmentProtectionFee(groupSubtotal, addons.shipmentProtectionAddon ?? false, commissionRates));
|
|
3287
3304
|
}, 0);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event like actions — the write path, and the paged read for the modal.
|
|
3
|
+
*
|
|
4
|
+
* @tag domain:events
|
|
5
|
+
* @tag layer:server
|
|
6
|
+
* @tag access:server-only
|
|
7
|
+
*/
|
|
8
|
+
import { type ActionResult } from "../../../shared/types/action-result";
|
|
9
|
+
import { type EventLikersPage } from "./likes";
|
|
10
|
+
export declare function toggleEventLikeAction(idOrSlug: string, userId: string): Promise<ActionResult<{
|
|
11
|
+
liked: boolean;
|
|
12
|
+
likeCount: number;
|
|
13
|
+
}>>;
|
|
14
|
+
/**
|
|
15
|
+
* One page of likers, plus whether THIS viewer has liked it.
|
|
16
|
+
*
|
|
17
|
+
* 🛑 `viewerLiked` rides along here rather than having its own endpoint
|
|
18
|
+
* because the event page cannot resolve it server-side: that page is
|
|
19
|
+
* publicly CDN-cached, so a session read there would let one visitor's
|
|
20
|
+
* state be served to the next (audit-cdn-cache-headers R3). The client
|
|
21
|
+
* asks once after hydration, and this saves it a second round trip.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getEventLikersAction(idOrSlug: string, offset?: number, limit?: number, viewerId?: string): Promise<ActionResult<EventLikersPage & {
|
|
24
|
+
viewerLiked: boolean;
|
|
25
|
+
}>>;
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event like actions — the write path, and the paged read for the modal.
|
|
3
|
+
*
|
|
4
|
+
* @tag domain:events
|
|
5
|
+
* @tag layer:server
|
|
6
|
+
* @tag access:server-only
|
|
7
|
+
*/
|
|
8
|
+
import { wrapAction } from "../../../shared/types/action-result.js";
|
|
9
|
+
import { eventLikesRepository } from "../../../../features/events/repository/event-likes.repository.js";
|
|
10
|
+
import { resolveEventIdAction } from "../raffle/actions.js";
|
|
11
|
+
import { getEventLikers, viewerHasLiked } from "./likes.js";
|
|
12
|
+
import { NotFoundError } from "../../../../errors/index.js";
|
|
13
|
+
import { LIKERS_PAGE_SIZE } from "../../../../features/events/schemas/likes.js";
|
|
14
|
+
/**
|
|
15
|
+
* Resolve whatever the URL carried into the canonical document id.
|
|
16
|
+
*
|
|
17
|
+
* 🛑 THIS IS LOAD-BEARING, NOT DEFENSIVE. Public event URLs link by SLUG
|
|
18
|
+
* (`/events/festive-cashback-10`), so the route param is usually a slug while
|
|
19
|
+
* the like row's id is `{eventId}__{userId}` and the counter lives at
|
|
20
|
+
* `events/{docId}.stats.likeCount`.
|
|
21
|
+
*
|
|
22
|
+
* Without this, the same person liking from the slug URL and from an id URL
|
|
23
|
+
* would write TWO like rows and increment a counter on a document that may
|
|
24
|
+
* not exist — the same two-identities-for-one-event defect the spin route
|
|
25
|
+
* records as a silent `spinMaxPerUser` bypass. There it was a free reset;
|
|
26
|
+
* here it would be a double-counted like and an orphaned row the tester
|
|
27
|
+
* cascade cannot match.
|
|
28
|
+
*/
|
|
29
|
+
async function canonicalEventId(idOrSlug) {
|
|
30
|
+
const resolved = await resolveEventIdAction(idOrSlug);
|
|
31
|
+
const id = resolved.ok ? resolved.data : null;
|
|
32
|
+
if (!id)
|
|
33
|
+
throw new NotFoundError(`Event not found: ${idOrSlug}`);
|
|
34
|
+
return id;
|
|
35
|
+
}
|
|
36
|
+
export async function toggleEventLikeAction(idOrSlug, userId) {
|
|
37
|
+
return wrapAction(async () => {
|
|
38
|
+
const eventId = await canonicalEventId(idOrSlug);
|
|
39
|
+
return eventLikesRepository.toggle(eventId, userId);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* One page of likers, plus whether THIS viewer has liked it.
|
|
44
|
+
*
|
|
45
|
+
* 🛑 `viewerLiked` rides along here rather than having its own endpoint
|
|
46
|
+
* because the event page cannot resolve it server-side: that page is
|
|
47
|
+
* publicly CDN-cached, so a session read there would let one visitor's
|
|
48
|
+
* state be served to the next (audit-cdn-cache-headers R3). The client
|
|
49
|
+
* asks once after hydration, and this saves it a second round trip.
|
|
50
|
+
*/
|
|
51
|
+
export async function getEventLikersAction(idOrSlug, offset = 0, limit = LIKERS_PAGE_SIZE, viewerId) {
|
|
52
|
+
return wrapAction(async () => {
|
|
53
|
+
const eventId = await canonicalEventId(idOrSlug);
|
|
54
|
+
/*
|
|
55
|
+
* Clamped, because `limit` arrives from a query string. An unclamped
|
|
56
|
+
* caller-supplied page size is an unbounded read against Rule #6's
|
|
57
|
+
* budget — `?limit=100000` would be a full-collection scan plus one user
|
|
58
|
+
* lookup per row.
|
|
59
|
+
*/
|
|
60
|
+
const safeLimit = Math.min(Math.max(1, limit), LIKERS_PAGE_SIZE);
|
|
61
|
+
const [page, viewerLiked] = await Promise.all([
|
|
62
|
+
getEventLikers(eventId, safeLimit, Math.max(0, offset)),
|
|
63
|
+
viewerHasLiked(eventId, viewerId),
|
|
64
|
+
]);
|
|
65
|
+
return { ...page, viewerLiked };
|
|
66
|
+
});
|
|
67
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event likes — the server read path, and the only place a liker's identity
|
|
3
|
+
* is assembled.
|
|
4
|
+
*
|
|
5
|
+
* @tag domain:events
|
|
6
|
+
* @tag layer:server
|
|
7
|
+
* @tag access:server-only
|
|
8
|
+
*/
|
|
9
|
+
import { type PublicLiker } from "../../../../features/events/schemas/likes";
|
|
10
|
+
export interface EventLikersPage {
|
|
11
|
+
likers: PublicLiker[];
|
|
12
|
+
/** True when more rows exist past this page. */
|
|
13
|
+
hasMore: boolean;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The most recent likers, resolved to names and faces.
|
|
17
|
+
*
|
|
18
|
+
* Two hops, both bounded: one indexed query for the ids, then one batched
|
|
19
|
+
* lookup for the profiles. The join is why the like row stores no name — see
|
|
20
|
+
* `schemas/likes.ts`.
|
|
21
|
+
*
|
|
22
|
+
* A user that cannot be resolved (deleted account, say) is DROPPED rather
|
|
23
|
+
* than rendered as a placeholder: a face with no person behind it is worse
|
|
24
|
+
* than a shorter rail.
|
|
25
|
+
*/
|
|
26
|
+
export declare function getEventLikers(eventId: string, limit?: number, offset?: number): Promise<EventLikersPage>;
|
|
27
|
+
/**
|
|
28
|
+
* Cached for the page+metadata pair. `React.cache` memoises per argument
|
|
29
|
+
* list, so the default-arguments call site here and any explicit-limit call
|
|
30
|
+
* site would be two entries — which is why every caller on a page should use
|
|
31
|
+
* the same shape.
|
|
32
|
+
*/
|
|
33
|
+
export declare const getEventLikersCached: typeof getEventLikers;
|
|
34
|
+
/** Has the viewer liked it? `false` for a signed-out visitor, with no read. */
|
|
35
|
+
export declare function viewerHasLiked(eventId: string, userId?: string): Promise<boolean>;
|
|
36
|
+
export declare const viewerHasLikedCached: typeof viewerHasLiked;
|
|
37
|
+
/** Days plotted by the engagement chart. */
|
|
38
|
+
export declare const LIKE_TREND_DAYS = 14;
|
|
39
|
+
/**
|
|
40
|
+
* Below this many likes the chart renders NOTHING.
|
|
41
|
+
*
|
|
42
|
+
* 🛑 This threshold is the honest half of the feature. A brand-new event has
|
|
43
|
+
* two likes, and plotting them produces a flat line with a y-axis and a date
|
|
44
|
+
* range — which looks like a measurement and carries no information. An
|
|
45
|
+
* absent chart says "not enough data yet"; a flat one says "we measured, and
|
|
46
|
+
* this is the result", which is a different and false claim.
|
|
47
|
+
*/
|
|
48
|
+
export declare const LIKE_TREND_MIN_POINTS = 5;
|
|
49
|
+
export interface LikeTrend {
|
|
50
|
+
/** Day labels, oldest first, e.g. "6 Nov". */
|
|
51
|
+
categories: string[];
|
|
52
|
+
/** Likes received on each of those days. Same length as `categories`. */
|
|
53
|
+
values: number[];
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Likes per day over the last `LIKE_TREND_DAYS`, oldest first.
|
|
57
|
+
*
|
|
58
|
+
* Returns `null` — not an empty chart — when there is too little to plot.
|
|
59
|
+
*
|
|
60
|
+
* Bounded by `recentLikeTimes`' own 200-row cap: that is far more than 14
|
|
61
|
+
* days of a realistic event, and it means this cannot become an unbounded
|
|
62
|
+
* collection scan on a popular one (Rule #6).
|
|
63
|
+
*/
|
|
64
|
+
export declare function getLikeTrend(eventId: string): Promise<LikeTrend | null>;
|
|
65
|
+
export declare const getLikeTrendCached: typeof getLikeTrend;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Event likes — the server read path, and the only place a liker's identity
|
|
3
|
+
* is assembled.
|
|
4
|
+
*
|
|
5
|
+
* @tag domain:events
|
|
6
|
+
* @tag layer:server
|
|
7
|
+
* @tag access:server-only
|
|
8
|
+
*/
|
|
9
|
+
import { cache } from "react";
|
|
10
|
+
import { eventLikesRepository } from "../../../../features/events/repository/event-likes.repository.js";
|
|
11
|
+
import { userRepository } from "../../../../features/auth/repository/user.repository.js";
|
|
12
|
+
import { LIKERS_PREVIEW_LIMIT, } from "../../../../features/events/schemas/likes.js";
|
|
13
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
14
|
+
import { serverLogger } from "../../../../monitoring/index.js";
|
|
15
|
+
/**
|
|
16
|
+
* Project a user document down to the three fields a face needs.
|
|
17
|
+
*
|
|
18
|
+
* 🛑 AN ALLOW-LIST, NEVER A SPREAD. The user document carries email, phone,
|
|
19
|
+
* role, store ownership and encrypted PII; a spread-and-delete publishes
|
|
20
|
+
* every field nobody thought to remove, including fields the interface does
|
|
21
|
+
* not declare — which is precisely how `adSettings.providerCredentials` ended
|
|
22
|
+
* up on a public endpoint (Root Cause #70).
|
|
23
|
+
*
|
|
24
|
+
* 🛑 AND NOT `maskPii`. The obvious-looking alternative is to read the name
|
|
25
|
+
* off the event ENTRY and mask it, which is what the leaderboard and the spin
|
|
26
|
+
* results do. That renders "A*** K***" — correct for a leaderboard, useless
|
|
27
|
+
* as a face. `displayName` and `photoURL` are in `USER_PUBLIC_FIELDS`, are
|
|
28
|
+
* absent from `USER_PII_FIELDS`, and already render on public profile pages,
|
|
29
|
+
* so taking them from the user document publishes nothing new.
|
|
30
|
+
*/
|
|
31
|
+
function toPublicLiker(doc) {
|
|
32
|
+
const uid = doc?.uid ?? doc?.id;
|
|
33
|
+
if (!uid)
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
uid: String(uid),
|
|
37
|
+
// A real account with no display name renders as a person, not a blank.
|
|
38
|
+
displayName: doc?.displayName?.trim() || "A collector",
|
|
39
|
+
photoURL: doc?.photoURL || undefined,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The most recent likers, resolved to names and faces.
|
|
44
|
+
*
|
|
45
|
+
* Two hops, both bounded: one indexed query for the ids, then one batched
|
|
46
|
+
* lookup for the profiles. The join is why the like row stores no name — see
|
|
47
|
+
* `schemas/likes.ts`.
|
|
48
|
+
*
|
|
49
|
+
* A user that cannot be resolved (deleted account, say) is DROPPED rather
|
|
50
|
+
* than rendered as a placeholder: a face with no person behind it is worse
|
|
51
|
+
* than a shorter rail.
|
|
52
|
+
*/
|
|
53
|
+
export async function getEventLikers(eventId, limit = LIKERS_PREVIEW_LIMIT, offset = 0) {
|
|
54
|
+
try {
|
|
55
|
+
// One extra row is the cheapest possible "is there a next page" — it
|
|
56
|
+
// avoids a count query, which Firestore bills per matched document.
|
|
57
|
+
const ids = await eventLikesRepository.recentLikerIds(eventId, limit + 1, offset);
|
|
58
|
+
const hasMore = ids.length > limit;
|
|
59
|
+
const pageIds = hasMore ? ids.slice(0, limit) : ids;
|
|
60
|
+
const docs = await Promise.all(pageIds.map((uid) => userRepository
|
|
61
|
+
.findByUid(uid)
|
|
62
|
+
.catch(() => null)));
|
|
63
|
+
return {
|
|
64
|
+
likers: docs
|
|
65
|
+
.map((d) => toPublicLiker(d))
|
|
66
|
+
.filter((l) => l !== null),
|
|
67
|
+
hasMore,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
catch (err) {
|
|
71
|
+
const e = normalizeError(err);
|
|
72
|
+
serverLogger.warn("getEventLikers failed", { eventId, message: e.message });
|
|
73
|
+
// An empty rail is a degraded page; a thrown error is a dead one. The
|
|
74
|
+
// count beside it comes from the event document and still renders.
|
|
75
|
+
return { likers: [], hasMore: false };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Cached for the page+metadata pair. `React.cache` memoises per argument
|
|
80
|
+
* list, so the default-arguments call site here and any explicit-limit call
|
|
81
|
+
* site would be two entries — which is why every caller on a page should use
|
|
82
|
+
* the same shape.
|
|
83
|
+
*/
|
|
84
|
+
export const getEventLikersCached = cache(getEventLikers);
|
|
85
|
+
/** Has the viewer liked it? `false` for a signed-out visitor, with no read. */
|
|
86
|
+
export async function viewerHasLiked(eventId, userId) {
|
|
87
|
+
if (!userId)
|
|
88
|
+
return false;
|
|
89
|
+
return eventLikesRepository.hasLiked(eventId, userId);
|
|
90
|
+
}
|
|
91
|
+
export const viewerHasLikedCached = cache(viewerHasLiked);
|
|
92
|
+
/** Days plotted by the engagement chart. */
|
|
93
|
+
export const LIKE_TREND_DAYS = 14;
|
|
94
|
+
/**
|
|
95
|
+
* Below this many likes the chart renders NOTHING.
|
|
96
|
+
*
|
|
97
|
+
* 🛑 This threshold is the honest half of the feature. A brand-new event has
|
|
98
|
+
* two likes, and plotting them produces a flat line with a y-axis and a date
|
|
99
|
+
* range — which looks like a measurement and carries no information. An
|
|
100
|
+
* absent chart says "not enough data yet"; a flat one says "we measured, and
|
|
101
|
+
* this is the result", which is a different and false claim.
|
|
102
|
+
*/
|
|
103
|
+
export const LIKE_TREND_MIN_POINTS = 5;
|
|
104
|
+
/**
|
|
105
|
+
* Likes per day over the last `LIKE_TREND_DAYS`, oldest first.
|
|
106
|
+
*
|
|
107
|
+
* Returns `null` — not an empty chart — when there is too little to plot.
|
|
108
|
+
*
|
|
109
|
+
* Bounded by `recentLikeTimes`' own 200-row cap: that is far more than 14
|
|
110
|
+
* days of a realistic event, and it means this cannot become an unbounded
|
|
111
|
+
* collection scan on a popular one (Rule #6).
|
|
112
|
+
*/
|
|
113
|
+
export async function getLikeTrend(eventId) {
|
|
114
|
+
const times = await eventLikesRepository.recentLikeTimes(eventId);
|
|
115
|
+
if (times.length < LIKE_TREND_MIN_POINTS)
|
|
116
|
+
return null;
|
|
117
|
+
const now = new Date();
|
|
118
|
+
const buckets = [];
|
|
119
|
+
const index = new Map();
|
|
120
|
+
for (let i = LIKE_TREND_DAYS - 1; i >= 0; i -= 1) {
|
|
121
|
+
const d = new Date(now);
|
|
122
|
+
d.setDate(d.getDate() - i);
|
|
123
|
+
const key = d.toISOString().slice(0, 10);
|
|
124
|
+
index.set(key, buckets.length);
|
|
125
|
+
buckets.push({
|
|
126
|
+
label: d.toLocaleDateString("en-IN", { day: "numeric", month: "short" }),
|
|
127
|
+
count: 0,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
for (const t of times) {
|
|
131
|
+
const slot = index.get(t.toISOString().slice(0, 10));
|
|
132
|
+
// Older than the window — `recentLikeTimes` returns newest-first over a
|
|
133
|
+
// 200-row cap, so this is the normal case for a long-running event.
|
|
134
|
+
if (slot !== undefined)
|
|
135
|
+
buckets[slot].count += 1;
|
|
136
|
+
}
|
|
137
|
+
// Every like could be older than the window even though the count cleared
|
|
138
|
+
// the threshold, which would plot fourteen zeroes.
|
|
139
|
+
if (buckets.every((b) => b.count === 0))
|
|
140
|
+
return null;
|
|
141
|
+
return {
|
|
142
|
+
categories: buckets.map((b) => b.label),
|
|
143
|
+
values: buckets.map((b) => b.count),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export const getLikeTrendCached = cache(getLikeTrend);
|
|
@@ -185,7 +185,7 @@ export function orderDocumentToOrder(doc) {
|
|
|
185
185
|
couponDiscount: doc.couponDiscount,
|
|
186
186
|
appliedDiscounts: doc.appliedDiscounts,
|
|
187
187
|
// Add-ons are operational, not decorative: the status-change notifier reads
|
|
188
|
-
// the packer needs giftWrapMessage in hand. An
|
|
188
|
+
// whatsappNotifyAddon, and the packer needs giftWrapMessage in hand. An
|
|
189
189
|
// adapter that drops them makes every downstream surface render its
|
|
190
190
|
// "not applicable" fallback instead (Root Cause #57).
|
|
191
191
|
// EMI, so the buyer can see and pay their own schedule. Mapped with the
|
|
@@ -195,6 +195,8 @@ export function orderDocumentToOrder(doc) {
|
|
|
195
195
|
emiEnabled: doc.emiEnabled,
|
|
196
196
|
emiInstallments: doc.emiInstallments,
|
|
197
197
|
emiRemainingBalance: doc.emiRemainingBalance,
|
|
198
|
+
whatsappNotifyAddon: doc.whatsappNotifyAddon,
|
|
199
|
+
whatsappNotifyFee: doc.whatsappNotifyFee,
|
|
198
200
|
giftWrapAddon: doc.giftWrapAddon,
|
|
199
201
|
giftWrapFee: doc.giftWrapFee,
|
|
200
202
|
giftWrapMessage: doc.giftWrapMessage,
|
|
@@ -6,3 +6,6 @@ export { buildSitemap } from "./sitemap";
|
|
|
6
6
|
export type { SitemapOptions } from "./sitemap";
|
|
7
7
|
export { buildDefaultOgImage, DEFAULT_OG_SIZE, resolveOgImageUrl } from "./og";
|
|
8
8
|
export type { DefaultOgOptions } from "./og";
|
|
9
|
+
export { productDetailSlugs, brandDetailSlugs, franchiseDetailSlugs, blogPostSlugs, beyDetailSlugs, partDetailParams, } from "./static-params";
|
|
10
|
+
export { brandLinks, franchiseLinks, categoryRootLinks } from "./taxonomy-links";
|
|
11
|
+
export type { TaxonomyLink } from "./taxonomy-links";
|
|
@@ -2,3 +2,5 @@ export { buildRobots } from "./robots.js";
|
|
|
2
2
|
export { buildManifest } from "./manifest.js";
|
|
3
3
|
export { buildSitemap } from "./sitemap.js";
|
|
4
4
|
export { buildDefaultOgImage, DEFAULT_OG_SIZE, resolveOgImageUrl } from "./og.js";
|
|
5
|
+
export { productDetailSlugs, brandDetailSlugs, franchiseDetailSlugs, blogPostSlugs, beyDetailSlugs, partDetailParams, } from "./static-params.js";
|
|
6
|
+
export { brandLinks, franchiseLinks, categoryRootLinks } from "./taxonomy-links.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PRODUCT_DETAIL_LISTING_TYPES: readonly string[];
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which listing types actually RENDER at `/products/{slug}`.
|
|
3
|
+
*
|
|
4
|
+
* DERIVED from each plugin's own `detailRoute`, never hand-written. Two SEO
|
|
5
|
+
* surfaces need this set — the sitemap (what to submit) and
|
|
6
|
+
* `generateStaticParams` (what to prerender) — and a third, the page itself,
|
|
7
|
+
* enforces it by `permanentRedirect`ing a non-standard type away.
|
|
8
|
+
*
|
|
9
|
+
* 🛑 It used to be a literal `["standard", "art", "stickers"]` in `sitemap.ts`.
|
|
10
|
+
* When `spares` was added as a tenth listing type pointing at the same route,
|
|
11
|
+
* that literal was not updated, so the product became renderable at
|
|
12
|
+
* `/products/{slug}` and was never submitted — measured 2026-10-24, 14 product
|
|
13
|
+
* URLs in the sitemap against 15 that resolve. That is Root Cause #61's shape
|
|
14
|
+
* (N hand-written enumerations of one union, only the compile-checked ones
|
|
15
|
+
* staying correct), and deriving is the documented fix.
|
|
16
|
+
*
|
|
17
|
+
* Comparing rendered routes rather than naming types keeps it honest: a
|
|
18
|
+
* plugin that is repointed at its own page leaves this set automatically.
|
|
19
|
+
*/
|
|
20
|
+
import { ALL_LISTING_TYPES } from "../../../shared/listing-types/feature-flags.js";
|
|
21
|
+
import { pluginFor } from "../../../shared/listing-types/_registry.js";
|
|
22
|
+
import { ROUTES } from "../../../../next/routing/route-map.js";
|
|
23
|
+
/** An arbitrary probe slug — only the SHAPE of the resolved route matters. */
|
|
24
|
+
const PROBE = "__route_probe__";
|
|
25
|
+
const PRODUCT_DETAIL_ROUTE = String(ROUTES.PUBLIC.PRODUCT_DETAIL(PROBE));
|
|
26
|
+
export const PRODUCT_DETAIL_LISTING_TYPES = ALL_LISTING_TYPES.filter((type) => pluginFor(type).detailRoute(PROBE) === PRODUCT_DETAIL_ROUTE);
|
|
@@ -36,7 +36,26 @@ export function buildRobots({ siteUrl }) {
|
|
|
36
36
|
"/preview/",
|
|
37
37
|
"/track/",
|
|
38
38
|
"/unauthorized/",
|
|
39
|
-
|
|
39
|
+
/*
|
|
40
|
+
* 🛑 `/_next/` IS DELIBERATELY NOT DISALLOWED — do not re-add it.
|
|
41
|
+
*
|
|
42
|
+
* It was here until 2026-10-24 and it was blocking Googlebot from
|
|
43
|
+
* fetching the app's own JS and CSS chunks, i.e. from RENDERING the
|
|
44
|
+
* page at all. Google's guidance is explicit that blocking resources
|
|
45
|
+
* needed for rendering harms how a page is understood.
|
|
46
|
+
*
|
|
47
|
+
* It was actively load-bearing here rather than merely untidy: every
|
|
48
|
+
* dynamic route streams its metadata late, so the `<title>` lands
|
|
49
|
+
* inside `<body>` (measured at byte 563,190 of a 747 KB document,
|
|
50
|
+
* with the `<head>` ending at 8,180 carrying no title at all). The
|
|
51
|
+
* one mechanism that recovers it is React hoisting those tags into
|
|
52
|
+
* `<head>` during hydration — which cannot happen if the crawler is
|
|
53
|
+
* forbidden from downloading the script that performs the hoist.
|
|
54
|
+
*
|
|
55
|
+
* Blocking it buys nothing: these are immutable, content-hashed
|
|
56
|
+
* static assets that are not indexable documents, and Google does not
|
|
57
|
+
* list them as search results.
|
|
58
|
+
*/
|
|
40
59
|
"/profile/*/edit",
|
|
41
60
|
],
|
|
42
61
|
},
|