@mohasinac/appkit 4.63.5 → 4.64.1

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.
Files changed (119) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/server/features/categories/adapters.d.ts +1 -1
  3. package/dist/_internal/server/features/categories/adapters.js +13 -0
  4. package/dist/_internal/server/features/events/like-actions.d.ts +25 -0
  5. package/dist/_internal/server/features/events/like-actions.js +67 -0
  6. package/dist/_internal/server/features/events/likes.d.ts +65 -0
  7. package/dist/_internal/server/features/events/likes.js +146 -0
  8. package/dist/_internal/server/features/seo/index.d.ts +3 -0
  9. package/dist/_internal/server/features/seo/index.js +2 -0
  10. package/dist/_internal/server/features/seo/product-detail-types.d.ts +1 -0
  11. package/dist/_internal/server/features/seo/product-detail-types.js +26 -0
  12. package/dist/_internal/server/features/seo/robots.js +20 -1
  13. package/dist/_internal/server/features/seo/sitemap.js +125 -27
  14. package/dist/_internal/server/features/seo/static-params.d.ts +107 -0
  15. package/dist/_internal/server/features/seo/static-params.js +228 -0
  16. package/dist/_internal/server/features/seo/taxonomy-links.d.ts +44 -0
  17. package/dist/_internal/server/features/seo/taxonomy-links.js +85 -0
  18. package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
  19. package/dist/_internal/server/features/stores/adapters.js +87 -1
  20. package/dist/_internal/server/functions/scheduled.d.ts +16 -1
  21. package/dist/_internal/server/functions/scheduled.js +23 -1
  22. package/dist/_internal/server/jobs/core/sellerStandingRollup.d.ts +63 -0
  23. package/dist/_internal/server/jobs/core/sellerStandingRollup.js +412 -0
  24. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  25. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  26. package/dist/_internal/server/jobs/handlers/sellerStandingRollup.d.ts +12 -0
  27. package/dist/_internal/server/jobs/handlers/sellerStandingRollup.js +14 -0
  28. package/dist/_internal/shared/actions/action-registry.js +61 -0
  29. package/dist/_internal/shared/features/seller-standing/config.d.ts +130 -0
  30. package/dist/_internal/shared/features/seller-standing/config.js +153 -0
  31. package/dist/constants/api-endpoints.d.ts +24 -0
  32. package/dist/constants/api-endpoints.js +8 -0
  33. package/dist/features/about/components/HelpPageView.js +9 -1
  34. package/dist/features/account/components/BecomeSellerView.d.ts +15 -3
  35. package/dist/features/account/components/BecomeSellerView.js +2 -2
  36. package/dist/features/admin/components/AdminStoreEditorView.d.ts +12 -1
  37. package/dist/features/admin/components/AdminStoreEditorView.js +30 -2
  38. package/dist/features/admin/components/AdminStoresGuideView.js +1 -1
  39. package/dist/features/admin/components/AdminStoresView.js +59 -2
  40. package/dist/features/admin/components/AdminTrustGuideView.js +10 -0
  41. package/dist/features/admin/schemas/admin-editor-forms.d.ts +10 -0
  42. package/dist/features/admin/schemas/admin-editor-forms.js +34 -0
  43. package/dist/features/blog/components/BlogIndexPageView.js +2 -2
  44. package/dist/features/blog/components/BlogListView.js +2 -1
  45. package/dist/features/blog/components/BlogPostView.js +2 -1
  46. package/dist/features/blog/constants/category-badge.d.ts +33 -0
  47. package/dist/features/blog/constants/category-badge.js +40 -0
  48. package/dist/features/categories/components/TaxonomyLinkIndex.d.ts +39 -0
  49. package/dist/features/categories/components/TaxonomyLinkIndex.js +11 -0
  50. package/dist/features/categories/schemas/firestore.d.ts +25 -0
  51. package/dist/features/categories/utils/load-category-options.d.ts +33 -0
  52. package/dist/features/categories/utils/load-category-options.js +50 -0
  53. package/dist/features/events/components/EventCard.js +4 -12
  54. package/dist/features/events/components/EventInsights.d.ts +27 -0
  55. package/dist/features/events/components/EventInsights.js +10 -0
  56. package/dist/features/events/components/EventLikeButton.d.ts +21 -0
  57. package/dist/features/events/components/EventLikeButton.js +59 -0
  58. package/dist/features/events/components/EventLikersRail.d.ts +23 -0
  59. package/dist/features/events/components/EventLikersRail.js +54 -0
  60. package/dist/features/events/components/EventTypeBadge.d.ts +26 -0
  61. package/dist/features/events/components/EventTypeBadge.js +7 -0
  62. package/dist/features/events/components/EventsListPageView.js +2 -2
  63. package/dist/features/events/components/SpinWheel.d.ts +41 -0
  64. package/dist/features/events/components/SpinWheel.js +128 -0
  65. package/dist/features/events/components/SpinWheelView.js +36 -15
  66. package/dist/features/events/constants/event-type-meta.d.ts +54 -0
  67. package/dist/features/events/constants/event-type-meta.js +109 -0
  68. package/dist/features/events/repository/event-likes.repository.d.ts +57 -0
  69. package/dist/features/events/repository/event-likes.repository.js +151 -0
  70. package/dist/features/events/schemas/firestore.d.ts +111 -0
  71. package/dist/features/events/schemas/likes.d.ts +81 -0
  72. package/dist/features/events/schemas/likes.js +59 -0
  73. package/dist/features/events/types/index.d.ts +7 -0
  74. package/dist/features/events/utils/carousel-slide.d.ts +42 -0
  75. package/dist/features/events/utils/carousel-slide.js +174 -0
  76. package/dist/features/events/utils/event-charts.d.ts +65 -0
  77. package/dist/features/events/utils/event-charts.js +172 -0
  78. package/dist/features/faq/constants/help-guides.js +9 -0
  79. package/dist/features/faq/schemas/firestore.js +1 -0
  80. package/dist/features/faq/types/index.d.ts +19 -0
  81. package/dist/features/homepage/components/BrandsSection.js +25 -1
  82. package/dist/features/homepage/components/HeroCarousel.js +47 -10
  83. package/dist/features/homepage/types/index.d.ts +7 -0
  84. package/dist/features/stores/components/InteractiveStoreCard.js +2 -1
  85. package/dist/features/stores/components/SellerStandingPanel.d.ts +31 -0
  86. package/dist/features/stores/components/SellerStandingPanel.js +25 -0
  87. package/dist/features/stores/components/SellerTierBadge.d.ts +57 -0
  88. package/dist/features/stores/components/SellerTierBadge.js +19 -0
  89. package/dist/features/stores/components/StoreCapabilitiesGuideView.js +32 -2
  90. package/dist/features/stores/components/StoreCreateForm.d.ts +46 -0
  91. package/dist/features/stores/components/StoreCreateForm.js +107 -0
  92. package/dist/features/stores/components/StoreGuideHubView.js +17 -4
  93. package/dist/features/stores/components/StoreHeader.js +2 -1
  94. package/dist/features/stores/repository/store.repository.js +23 -0
  95. package/dist/features/stores/schemas/firestore.d.ts +60 -1
  96. package/dist/features/stores/schemas/firestore.js +18 -0
  97. package/dist/features/stores/schemas/index.d.ts +2 -2
  98. package/dist/features/stores/schemas/store-create.d.ts +47 -0
  99. package/dist/features/stores/schemas/store-create.js +86 -0
  100. package/dist/features/stores/types/index.d.ts +42 -0
  101. package/dist/index.d.ts +2 -0
  102. package/dist/index.js +21 -0
  103. package/dist/next/routing/route-map.d.ts +3 -1
  104. package/dist/next/routing/route-map.js +13 -0
  105. package/dist/schemas/registry.d.ts +10 -0
  106. package/dist/server.d.ts +5 -0
  107. package/dist/server.js +10 -0
  108. package/dist/styles.css +36 -1
  109. package/dist/tailwind-utilities.css +1 -1
  110. package/dist/ui/components/HorizontalScroller.js +15 -2
  111. package/dist/ui/components/PageHeroHeading.d.ts +40 -0
  112. package/dist/ui/components/PageHeroHeading.js +15 -0
  113. package/dist/ui/components/Typography.d.ts +3 -3
  114. package/dist/ui/components/Typography.js +59 -31
  115. package/dist/ui/components/Typography.style.css +35 -0
  116. package/dist/ui/index.d.ts +2 -0
  117. package/dist/ui/index.js +1 -0
  118. package/package.json +1 -1
  119. 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", "isVerified", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "pricesIncludeGst", "shippingPricing", "returnsDirectToSeller", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt", "gstin", "legalName", "registeredAddress"];
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
- "isVerified", // admin moderation state; surfaced via a separate badge feed if ever needed
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>;