@mohasinac/appkit 4.63.5 → 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/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/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/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +87 -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/sellerStandingRollup.d.ts +63 -0
- package/dist/_internal/server/jobs/core/sellerStandingRollup.js +412 -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/shared/actions/action-registry.js +61 -0
- 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/constants/api-endpoints.d.ts +24 -0
- package/dist/constants/api-endpoints.js +8 -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/admin/components/AdminStoreEditorView.d.ts +12 -1
- package/dist/features/admin/components/AdminStoreEditorView.js +30 -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/schemas/admin-editor-forms.d.ts +10 -0
- package/dist/features/admin/schemas/admin-editor-forms.js +34 -0
- 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/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/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/types/index.d.ts +7 -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 +32 -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 +17 -4
- package/dist/features/stores/components/StoreHeader.js +2 -1
- package/dist/features/stores/repository/store.repository.js +23 -0
- package/dist/features/stores/schemas/firestore.d.ts +60 -1
- package/dist/features/stores/schemas/firestore.js +18 -0
- package/dist/features/stores/schemas/index.d.ts +2 -2
- 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/next/routing/route-map.d.ts +3 -1
- package/dist/next/routing/route-map.js +13 -0
- package/dist/schemas/registry.d.ts +10 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +10 -0
- 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/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
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-side link lists for the taxonomy hub pages.
|
|
3
|
+
*
|
|
4
|
+
* Feeds `<TaxonomyLinkIndex>`, which renders them as real `<a href>`s in the
|
|
5
|
+
* server HTML. See that component for why: measured 2026-10-24, `/categories`,
|
|
6
|
+
* `/brands` and `/parts` each emitted ZERO outbound entity links because all
|
|
7
|
+
* three filter client-side, so a crawler landing on a hub had nothing to
|
|
8
|
+
* follow and the franchise detail pages were reachable by no link at all.
|
|
9
|
+
*
|
|
10
|
+
* 🛑 Every function FAILS OPEN with `[]`. These pages are statically rendered;
|
|
11
|
+
* a Firestore hiccup at build time must degrade to "no index section" rather
|
|
12
|
+
* than fail the build or 500 the page. An empty list makes
|
|
13
|
+
* `<TaxonomyLinkIndex>` render nothing at all, which is the correct fallback —
|
|
14
|
+
* the interactive grid above it is unaffected either way.
|
|
15
|
+
*
|
|
16
|
+
* 🛑 A read here does NOT make the hub dynamic. Only `searchParams`,
|
|
17
|
+
* `cookies()` and `headers()` do (Root Cause #94); a Server Component awaiting
|
|
18
|
+
* Firestore stays static and simply bakes the result in at build/ISR time.
|
|
19
|
+
*/
|
|
20
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
21
|
+
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
22
|
+
import { ROUTES } from "../../../../next/routing/route-map.js";
|
|
23
|
+
import { categoriesRepository } from "../../../../features/categories/repository/categories.repository.js";
|
|
24
|
+
/** Alphabetical, so the list reads as an index rather than an arbitrary dump. */
|
|
25
|
+
function toLinks(rows, href) {
|
|
26
|
+
return rows
|
|
27
|
+
.filter((row) => Boolean(row.slug) && Boolean(row.name))
|
|
28
|
+
.map((row) => ({ href: href(row.slug), label: row.name }))
|
|
29
|
+
.sort((a, b) => a.label.localeCompare(b.label));
|
|
30
|
+
}
|
|
31
|
+
async function safeLinks(label, load) {
|
|
32
|
+
try {
|
|
33
|
+
return await load();
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
void normalizeError(err);
|
|
37
|
+
serverLogger.error(`taxonomy link index "${label}" failed and rendered 0 links`, {
|
|
38
|
+
section: label,
|
|
39
|
+
error: err,
|
|
40
|
+
});
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/** Every active brand — 28 of them, and none was linked from `/brands`. */
|
|
45
|
+
export function brandLinks() {
|
|
46
|
+
return safeLinks("brand", async () => toLinks(await categoriesRepository.findActiveBrands(), (slug) => String(ROUTES.PUBLIC.BRAND_DETAIL(slug))));
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Active franchises only.
|
|
50
|
+
*
|
|
51
|
+
* 7 of the 8 are deliberately `isActive: false` because an unstocked facet
|
|
52
|
+
* "renders as a card in the grid that opens an empty page" (`brand-rows.ts`
|
|
53
|
+
* trap 3), so this list is short on purpose. Linking the inactive ones would
|
|
54
|
+
* re-create exactly the empty-page problem the rest of this work removes.
|
|
55
|
+
*/
|
|
56
|
+
export function franchiseLinks() {
|
|
57
|
+
return safeLinks("franchise", async () => toLinks(await categoriesRepository.findActiveFranchises(), (slug) => String(ROUTES.PUBLIC.FRANCHISE_DETAIL(slug))));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The tier-0 category roots — the six real entry points a visitor drills down
|
|
61
|
+
* from, not the 4,698-row forest.
|
|
62
|
+
*
|
|
63
|
+
* `CategoryDetailPageView` already renders a root as a tier-grouped index of
|
|
64
|
+
* its descendants, so a root is where a crawl of the taxonomy should start.
|
|
65
|
+
* Listing every leaf here would put the 4,658 empty pages back in front of
|
|
66
|
+
* Google by another route, which is the thing being undone.
|
|
67
|
+
*/
|
|
68
|
+
export function categoryRootLinks() {
|
|
69
|
+
return safeLinks("category-root", async () => {
|
|
70
|
+
const roots = await categoriesRepository.getRootCategories();
|
|
71
|
+
/*
|
|
72
|
+
* 🛑 `getRootCategories()` filters only on the DEPRECATED `isBrand` flag,
|
|
73
|
+
* and brands, franchises and bundles are all tier-0 by design — they sit
|
|
74
|
+
* outside the forest's numbering and are pointed AT rather than lived
|
|
75
|
+
* under. A franchise row carries no `isBrand`, so it survives that filter
|
|
76
|
+
* and would appear in a list captioned "categories".
|
|
77
|
+
*
|
|
78
|
+
* Rejecting any row that carries a `categoryType` is the same predicate
|
|
79
|
+
* the /categories tab uses ("categoryType empty AND tier === 0"), and it
|
|
80
|
+
* is the one that stays correct when a fifth discriminator is added.
|
|
81
|
+
*/
|
|
82
|
+
const plain = roots.filter((row) => !row.categoryType);
|
|
83
|
+
return toLinks(plain, (slug) => String(ROUTES.PUBLIC.CATEGORY_DETAIL(slug)));
|
|
84
|
+
});
|
|
85
|
+
}
|
|
@@ -25,9 +25,9 @@ import type { StoreDetail, StoreListItem } from "../../../../features/stores/typ
|
|
|
25
25
|
* Document fields that may reach an unauthenticated client. Keep in sync with
|
|
26
26
|
* the object literals below — the audit asserts both directions.
|
|
27
27
|
*/
|
|
28
|
-
export declare const PUBLIC_STORE_FIELDS: readonly ["id", "storeSlug", "ownerId", "storeName", "storeDescription", "storeCategory", "storeLogoURL", "storeBannerURL", "status", "isPublic", "stats", "bio", "location", "website", "socialLinks", "returnPolicy", "shippingPolicy", "isVacationMode", "vacationMessage", "supportsShippingProtection", "supportsSaleProtection", "allowSharedPurchase", "sharedPurchaseHandlingFee", "sharedPurchaseExtraShippingPerDestination", "createdAt"];
|
|
28
|
+
export declare const PUBLIC_STORE_FIELDS: readonly ["id", "storeSlug", "ownerId", "storeName", "storeDescription", "storeCategory", "storeLogoURL", "storeBannerURL", "status", "isPublic", "stats", "bio", "location", "website", "socialLinks", "returnPolicy", "shippingPolicy", "isVacationMode", "vacationMessage", "supportsShippingProtection", "supportsSaleProtection", "allowSharedPurchase", "sharedPurchaseHandlingFee", "sharedPurchaseExtraShippingPerDestination", "isVerified", "sellerTier", "standing", "createdAt"];
|
|
29
29
|
/** Everything else, with the reason it stays server-side. */
|
|
30
|
-
export declare const PRIVATE_STORE_FIELDS: readonly ["trustedSeller", "acceptsPoints", "searchTxt", "updatedAt", "whatsappConfig", "adminNotes", "statusHistory", "statusHistoryTruncated", "
|
|
30
|
+
export declare const PRIVATE_STORE_FIELDS: readonly ["whatsappNotifySubscribed", "trustedSeller", "acceptsPoints", "searchTxt", "whatsappNotifySubscribed", "updatedAt", "whatsappConfig", "adminNotes", "statusHistory", "statusHistoryTruncated", "tierLockedByAdmin", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "pricesIncludeGst", "shippingPricing", "returnsDirectToSeller", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt", "gstin", "legalName", "registeredAddress"];
|
|
31
31
|
/**
|
|
32
32
|
* Accepted input: a `StoreDocument` (the common case) or a loosely-typed
|
|
33
33
|
* Firestore row. The legacy top-level counters below predate `stats` and are
|
|
@@ -49,10 +49,61 @@ export const PUBLIC_STORE_FIELDS = [
|
|
|
49
49
|
"allowSharedPurchase",
|
|
50
50
|
"sharedPurchaseHandlingFee",
|
|
51
51
|
"sharedPurchaseExtraShippingPerDestination",
|
|
52
|
+
/*
|
|
53
|
+
* ── Seller trust, 2026-10-24 ──────────────────────────────────────────────
|
|
54
|
+
*
|
|
55
|
+
* 🛑 `isVerified` MOVED here from PRIVATE, deliberately, and this is a
|
|
56
|
+
* reclassification rather than a drive-by.
|
|
57
|
+
*
|
|
58
|
+
* It was filed as "admin moderation state; surfaced via a separate badge
|
|
59
|
+
* feed if ever needed". That badge feed never existed, and meanwhile three
|
|
60
|
+
* things were already treating the field as public: `/sellers` filters on
|
|
61
|
+
* `isVerified == true` to offer a "verified sellers" view,
|
|
62
|
+
* `StoreCapabilitiesGuideView` told sellers in as many words that "the
|
|
63
|
+
* Verified badge appears on your store profile page and on every listing
|
|
64
|
+
* card", and the seller onboarding checklist offered "Request verified
|
|
65
|
+
* badge" as a step. All three described a public badge; the projection
|
|
66
|
+
* stripped the field, so no badge could render anywhere and the guide copy
|
|
67
|
+
* was simply false.
|
|
68
|
+
*
|
|
69
|
+
* It is a badge, not moderation state — `status` / `isPublic` /
|
|
70
|
+
* `suspensionReason` are the moderation fields and all three stay private
|
|
71
|
+
* or already are public for their own reasons. The named reader is
|
|
72
|
+
* `<SellerTierBadge>`.
|
|
73
|
+
*
|
|
74
|
+
* `sellerTier` and `standing` are public for the same reason: the tier is
|
|
75
|
+
* the whole point of the feature being visible to a buyer, and `standing`
|
|
76
|
+
* carries the METRICS behind it (counts and a mean — no buyer uids, no
|
|
77
|
+
* addresses) so a seller's own dashboard panel can render progress from the
|
|
78
|
+
* store document it already holds without a second read.
|
|
79
|
+
*
|
|
80
|
+
* 🛑 `trustedSeller` stays PRIVATE and must never be added here. It is the
|
|
81
|
+
* money-moving half of the same decision — see its entry below.
|
|
82
|
+
*/
|
|
83
|
+
"isVerified",
|
|
84
|
+
"sellerTier",
|
|
85
|
+
"standing",
|
|
52
86
|
"createdAt",
|
|
53
87
|
];
|
|
54
88
|
/** Everything else, with the reason it stays server-side. */
|
|
55
89
|
export const PRIVATE_STORE_FIELDS = [
|
|
90
|
+
/*
|
|
91
|
+
* 🛑 `whatsappNotifySubscribed` — a BILLING-BEARING operational flag.
|
|
92
|
+
*
|
|
93
|
+
* It records whether this seller opted in to the platform WhatsApp number,
|
|
94
|
+
* and the gate fails CLOSED on it: absent means not subscribed, because
|
|
95
|
+
* defaulting a missing flag to true would message customers on behalf of
|
|
96
|
+
* every seller who never signed up and bill the platform per template.
|
|
97
|
+
*
|
|
98
|
+
* A buyer has no use for it — it says nothing about the goods, the price
|
|
99
|
+
* or the dispatch record. Published, it would also advertise which sellers
|
|
100
|
+
* can be reached on WhatsApp, which is a channel the platform owns rather
|
|
101
|
+
* than the seller, and an invitation to contact them off-platform.
|
|
102
|
+
*
|
|
103
|
+
* Default-private per this file's own rule: a field is private until
|
|
104
|
+
* someone can name the client that reads it.
|
|
105
|
+
*/
|
|
106
|
+
"whatsappNotifySubscribed",
|
|
56
107
|
/*
|
|
57
108
|
* 🛑 `trustedSeller` — OUR RISK POSTURE, not a badge.
|
|
58
109
|
*
|
|
@@ -94,6 +145,16 @@ export const PRIVATE_STORE_FIELDS = [
|
|
|
94
145
|
// it has no client reader and would bloat every store payload with a few
|
|
95
146
|
// hundred tokens.
|
|
96
147
|
"searchTxt",
|
|
148
|
+
/*
|
|
149
|
+
* A BILLING RELATIONSHIP between the seller and the platform: whether this
|
|
150
|
+
* store pays the monthly add-on that sends its buyers WhatsApp order
|
|
151
|
+
* updates. The buyer learns about it where it is actually decided — the
|
|
152
|
+
* per-store add-on picker at checkout, which reads the pricing preview, not
|
|
153
|
+
* this projection. Publishing it here would expose which sellers pay us for
|
|
154
|
+
* what, which is commercial terms (same reason `customCommissionRate` is
|
|
155
|
+
* below), and it has no reader on any public store surface.
|
|
156
|
+
*/
|
|
157
|
+
"whatsappNotifySubscribed",
|
|
97
158
|
"updatedAt", // bookkeeping; no public reader
|
|
98
159
|
"whatsappConfig", // Meta access token + WABA/catalog ids — secret
|
|
99
160
|
"adminNotes", // schema comment: never shown to the owner or the public
|
|
@@ -102,7 +163,14 @@ export const PRIVATE_STORE_FIELDS = [
|
|
|
102
163
|
// no business reading who suspended whom and why.
|
|
103
164
|
"statusHistory",
|
|
104
165
|
"statusHistoryTruncated",
|
|
105
|
-
|
|
166
|
+
// `isVerified` moved to PUBLIC_STORE_FIELDS on 2026-10-24 — it is a badge,
|
|
167
|
+
// and three surfaces already described it as one. See the note there.
|
|
168
|
+
//
|
|
169
|
+
// `tierLockedByAdmin` is the opposite call: `sellerTier` is public because a
|
|
170
|
+
// buyer should see the badge, but HOW we administer that record — whether an
|
|
171
|
+
// admin pinned it against the monthly recompute — says nothing about the
|
|
172
|
+
// goods or the seller and is nobody's business outside the back office.
|
|
173
|
+
"tierLockedByAdmin",
|
|
106
174
|
"suspensionReason", // admin moderation note
|
|
107
175
|
"capabilities", // internal feature gating; server-enforced, never client-read
|
|
108
176
|
"customCommissionRate", // commercial terms for this seller
|
|
@@ -213,6 +281,24 @@ export function toStoreListItem(doc, _opts) {
|
|
|
213
281
|
allowSharedPurchase: doc.allowSharedPurchase ?? false,
|
|
214
282
|
sharedPurchaseHandlingFee: doc.sharedPurchaseHandlingFee,
|
|
215
283
|
sharedPurchaseExtraShippingPerDestination: doc.sharedPurchaseExtraShippingPerDestination,
|
|
284
|
+
/*
|
|
285
|
+
* 🛑 `?? false` and `?? "seller"` — the same tri-state rule as the
|
|
286
|
+
* protection flags, for a sharper reason. A badge component testing
|
|
287
|
+
* `undefined` loosely renders nothing, which is correct here, but a
|
|
288
|
+
* component testing `!== false` or `!== "seller"` would render a VERIFIED
|
|
289
|
+
* or TRUSTED badge on a store that has earned neither. Defaulting on the
|
|
290
|
+
* server means no client can make that mistake.
|
|
291
|
+
*/
|
|
292
|
+
isVerified: doc.isVerified ?? false,
|
|
293
|
+
sellerTier: doc.sellerTier ?? "seller",
|
|
294
|
+
/*
|
|
295
|
+
* `standing` is passed through as `undefined` when absent, NOT defaulted
|
|
296
|
+
* to a zeroed object. "Never computed" and "computed, and you have zero
|
|
297
|
+
* delivered orders" are different facts, and the panel renders them
|
|
298
|
+
* differently — a confident "0 of 25" on a store that has simply never
|
|
299
|
+
* been measured is the regression Root Cause #72 names.
|
|
300
|
+
*/
|
|
301
|
+
standing: doc.standing,
|
|
216
302
|
createdAt: toIsoString(doc.createdAt),
|
|
217
303
|
};
|
|
218
304
|
}
|
|
@@ -7,6 +7,21 @@ export declare const couponExpiry: import("./types").ScheduledFunctionDefinition
|
|
|
7
7
|
export declare const offerExpiry: import("./types").ScheduledFunctionDefinition;
|
|
8
8
|
export declare const reservationExpiry: import("./types").ScheduledFunctionDefinition;
|
|
9
9
|
export declare const productStatsSync: import("./types").ScheduledFunctionDefinition;
|
|
10
|
+
/**
|
|
11
|
+
* Seller standing — MONTHLY, on the last day of the month.
|
|
12
|
+
*
|
|
13
|
+
* 🛑 `28-31`, never `31`. Cloud Scheduler is unix-cron with no `L` token, so
|
|
14
|
+
* `0 2 31 * *` **never fires in February, April, June, September or
|
|
15
|
+
* November** — it would silently skip 5 of 12 months and present as a tier
|
|
16
|
+
* that updates "sometimes". This fires on each candidate day and
|
|
17
|
+
* `isLastDayOfMonth` in the core keeps exactly the real one, so at most 3 of
|
|
18
|
+
* the 4 invocations are no-ops that return before reading anything.
|
|
19
|
+
*
|
|
20
|
+
* 540s because it scans a 90-day window of orders and reviews, then writes
|
|
21
|
+
* one document per store. `maxInstances: 1` because two concurrent runs would
|
|
22
|
+
* both compute a tier and race on the same store documents.
|
|
23
|
+
*/
|
|
24
|
+
export declare const sellerStandingRollup: import("./types").ScheduledFunctionDefinition;
|
|
10
25
|
export declare const revenueRollup: import("./types").ScheduledFunctionDefinition;
|
|
11
26
|
export declare const searchIndexBuild: import("./types").ScheduledFunctionDefinition;
|
|
12
27
|
export declare const priceIndexRollup: import("./types").ScheduledFunctionDefinition;
|
|
@@ -100,4 +115,4 @@ export declare const prizeDrawExpiryReveal: import("./types").ScheduledFunctionD
|
|
|
100
115
|
export declare const bundleStockSync: import("./types").ScheduledFunctionDefinition;
|
|
101
116
|
export declare const emiInstallmentReminder: import("./types").ScheduledFunctionDefinition;
|
|
102
117
|
export declare const catalogueImageStalenessReminder: import("./types").ScheduledFunctionDefinition;
|
|
103
|
-
export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
|
|
118
|
+
export declare const SCHEDULED_FUNCTIONS: readonly [import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition, import("./types").ScheduledFunctionDefinition];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Scheduled function definitions (Cloud Scheduler triggers).
|
|
2
2
|
// Cron syntax accepts both Firebase shorthand and standard cron expressions.
|
|
3
|
-
import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, boxRollupReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, reservationExpiryHandler, obligationSweepHandler, paymentReconcileHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, priceIndexRollupHandler, marketPriceRollupHandler, searchIndexBuildHandler, analyticsAlertEvaluateHandler, pageViewRollupHandler, pageViewPruneHandler, dailyStatusDigestHandler, weeklyPayoutEligibilityHandler, testDataCleanupHandler, } from "../jobs/handlers/index.js";
|
|
3
|
+
import { auctionSettlementHandler, autoPayoutEligibilityHandler, bundleStockSyncHandler, cartPruneHandler, cleanupRtdbEventsHandler, countersReconcileHandler, boxRollupReconcileHandler, couponExpiryHandler, dailyDataCleanupHandler, draftPruneHandler, emiInstallmentReminderHandler, catalogueImageStalenessReminderHandler, mediaTmpCleanupHandler, notificationPruneHandler, offerExpiryHandler, reservationExpiryHandler, obligationSweepHandler, paymentReconcileHandler, paymentWindowTimeoutHandler, hardBanReinstatementHandler, paymentReviewAutoApproveHandler, pendingOrderTimeoutHandler, positionsReconcileHandler, prizeDrawExpiryRevealHandler, productStatsSyncHandler, revenueRollupHandler, sellerStandingRollupHandler, priceIndexRollupHandler, marketPriceRollupHandler, searchIndexBuildHandler, analyticsAlertEvaluateHandler, pageViewRollupHandler, pageViewPruneHandler, dailyStatusDigestHandler, weeklyPayoutEligibilityHandler, testDataCleanupHandler, } from "../jobs/handlers/index.js";
|
|
4
4
|
import { defineFunction } from "./define.js";
|
|
5
5
|
const REGION = "asia-south1";
|
|
6
6
|
const EVERY_5_MIN = "every 5 minutes";
|
|
@@ -152,6 +152,27 @@ export const productStatsSync = defineFunction({
|
|
|
152
152
|
handler: productStatsSyncHandler,
|
|
153
153
|
options: { region: REGION, timeoutSeconds: 540, memory: "256MiB", maxInstances: 1 },
|
|
154
154
|
});
|
|
155
|
+
/**
|
|
156
|
+
* Seller standing — MONTHLY, on the last day of the month.
|
|
157
|
+
*
|
|
158
|
+
* 🛑 `28-31`, never `31`. Cloud Scheduler is unix-cron with no `L` token, so
|
|
159
|
+
* `0 2 31 * *` **never fires in February, April, June, September or
|
|
160
|
+
* November** — it would silently skip 5 of 12 months and present as a tier
|
|
161
|
+
* that updates "sometimes". This fires on each candidate day and
|
|
162
|
+
* `isLastDayOfMonth` in the core keeps exactly the real one, so at most 3 of
|
|
163
|
+
* the 4 invocations are no-ops that return before reading anything.
|
|
164
|
+
*
|
|
165
|
+
* 540s because it scans a 90-day window of orders and reviews, then writes
|
|
166
|
+
* one document per store. `maxInstances: 1` because two concurrent runs would
|
|
167
|
+
* both compute a tier and race on the same store documents.
|
|
168
|
+
*/
|
|
169
|
+
export const sellerStandingRollup = defineFunction({
|
|
170
|
+
name: "sellerStandingRollup",
|
|
171
|
+
description: "Recompute seller standing on the last day of each month and grant/revoke the Trusted Seller tier, which also sets trustedSeller (paid at shipping). Fires on days 28-31; the handler keeps only the real month-end.",
|
|
172
|
+
trigger: { kind: "schedule", cron: "0 2 28-31 * *", timeZone: "UTC" },
|
|
173
|
+
handler: sellerStandingRollupHandler,
|
|
174
|
+
options: { region: REGION, timeoutSeconds: 540, memory: "256MiB", maxInstances: 1 },
|
|
175
|
+
});
|
|
155
176
|
export const revenueRollup = defineFunction({
|
|
156
177
|
name: "revenueRollup",
|
|
157
178
|
description: "Pre-compute total delivered-order revenue into analytics/dashboardRollup, replacing an unbounded per-request scan (daily 01:30 UTC).",
|
|
@@ -502,6 +523,7 @@ export const SCHEDULED_FUNCTIONS = [
|
|
|
502
523
|
reservationExpiry,
|
|
503
524
|
productStatsSync,
|
|
504
525
|
revenueRollup,
|
|
526
|
+
sellerStandingRollup,
|
|
505
527
|
priceIndexRollup,
|
|
506
528
|
marketPriceRollup,
|
|
507
529
|
searchIndexBuild,
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core: MONTHLY seller-standing rollup — grants and revokes the Trusted
|
|
3
|
+
* Seller tier, and with it early payout.
|
|
4
|
+
*
|
|
5
|
+
* ## What it writes
|
|
6
|
+
*
|
|
7
|
+
* `stores/{id}.standing` — the cached metrics (this IS the cache; the seller's
|
|
8
|
+
* progress panel, the public badge and the admin view all read it off the
|
|
9
|
+
* store document they already load, and none of them recomputes anything).
|
|
10
|
+
* On a transition it also writes `sellerTier` and `trustedSeller` TOGETHER.
|
|
11
|
+
*
|
|
12
|
+
* 🛑 `trustedSeller` moves MONEY. It is read by both payout sweeps
|
|
13
|
+
* (`weeklyPayoutEligibility`, `autoPayoutEligibility`) via
|
|
14
|
+
* `storeRepository.findTrustedStoreIds()`, and it means the store is paid at
|
|
15
|
+
* SHIPPING instead of at delivery — so the platform carries a lost parcel with
|
|
16
|
+
* the money already gone. That is why this job is the only automatic writer,
|
|
17
|
+
* why every transition gets an audit row, and why an admin can pin a store
|
|
18
|
+
* with `standing.tierLockedByAdmin`.
|
|
19
|
+
*
|
|
20
|
+
* 🛑 The flag's author wrote, at `store.repository.ts`: *"trust is granted by
|
|
21
|
+
* hand, one store at a time, on risk grounds. If this ever saturates, the
|
|
22
|
+
* grant has stopped being deliberate."* Automatic granting is exactly that
|
|
23
|
+
* case, accepted deliberately by the owner. The mitigations are real and must
|
|
24
|
+
* not be quietly dropped: both a distinct-BUYER and a distinct-ADDRESS floor,
|
|
25
|
+
* self-bought orders excluded, test data excluded, and the 500-store bound on
|
|
26
|
+
* `findTrustedStoreIds` left in place so saturation is visible.
|
|
27
|
+
*/
|
|
28
|
+
import type { JobContext } from "../runtime/types";
|
|
29
|
+
export interface SellerStandingRollupResult {
|
|
30
|
+
/** Stores examined. */
|
|
31
|
+
examined: number;
|
|
32
|
+
/** Stores whose `standing` cache was written. */
|
|
33
|
+
written: number;
|
|
34
|
+
granted: string[];
|
|
35
|
+
revoked: string[];
|
|
36
|
+
/** Stores skipped because an admin pinned their tier. */
|
|
37
|
+
locked: number;
|
|
38
|
+
/** True when the month-end guard declined to run. */
|
|
39
|
+
skippedNotMonthEnd: boolean;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 🛑 THIS IS WHY THE CRON IS `0 2 28-31 * *` AND NOT `0 0 31 * *`.
|
|
43
|
+
*
|
|
44
|
+
* Cloud Scheduler uses unix-cron, which has no `L` (last-day) token. A cron on
|
|
45
|
+
* day 31 **never fires in February, April, June, September or November** — it
|
|
46
|
+
* would silently skip 5 of 12 months, and the symptom is not an error but a
|
|
47
|
+
* tier that updates "sometimes". So the schedule fires on each of the last four
|
|
48
|
+
* candidate days and this guard keeps exactly the real one.
|
|
49
|
+
*
|
|
50
|
+
* Uses UTC parts throughout, matching the scheduler's declared `timeZone: UTC`.
|
|
51
|
+
* A local-time implementation would disagree with the trigger near midnight.
|
|
52
|
+
*/
|
|
53
|
+
export declare function isLastDayOfMonth(d: Date): boolean;
|
|
54
|
+
/** The next month-end at or after `d`, for the seller-facing `nextComputeAt`. */
|
|
55
|
+
export declare function nextLastDayOfMonth(d: Date): Date;
|
|
56
|
+
export interface SellerStandingRollupOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Run regardless of the date. For the admin "recompute now" path and for
|
|
59
|
+
* verification — NOT for the schedule.
|
|
60
|
+
*/
|
|
61
|
+
force?: boolean;
|
|
62
|
+
}
|
|
63
|
+
export declare function runSellerStandingRollup(ctx: JobContext, opts?: SellerStandingRollupOptions): Promise<SellerStandingRollupResult>;
|