@mohasinac/appkit 4.42.18 → 4.44.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/_internal/client/features/lottery/LotteryDetailView.js +22 -1
- package/dist/_internal/server/features/categories/adapters.d.ts +47 -0
- package/dist/_internal/server/features/categories/adapters.js +235 -0
- package/dist/_internal/server/features/checkout/actions.js +161 -22
- package/dist/_internal/server/features/checkout/expand-order-items.js +34 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/products/list-public.d.ts +24 -0
- package/dist/_internal/server/features/products/list-public.js +10 -2
- package/dist/_internal/server/features/refunds/actions.js +1 -1
- package/dist/_internal/server/features/seo/sitemap.js +54 -15
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +6 -0
- package/dist/_internal/server/functions/scheduled.d.ts +4 -2
- package/dist/_internal/server/functions/scheduled.js +64 -6
- package/dist/_internal/server/jobs/core/adminAnalytics.d.ts +0 -1
- package/dist/_internal/server/jobs/core/adminAnalytics.js +9 -1
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.d.ts +47 -0
- package/dist/_internal/server/jobs/core/analyticsAlertEvaluate.js +468 -0
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +14 -1
- package/dist/_internal/server/jobs/core/countersReconcile.js +24 -8
- package/dist/_internal/server/jobs/core/onCategoryWrite.js +59 -0
- package/dist/_internal/server/jobs/core/onProductWrite.js +31 -0
- package/dist/_internal/server/jobs/core/onReviewWrite.js +8 -0
- package/dist/_internal/server/jobs/core/onStoreWrite.d.ts +1 -1
- package/dist/_internal/server/jobs/core/onStoreWrite.js +8 -2
- package/dist/_internal/server/jobs/core/priceIndexRollup.d.ts +47 -0
- package/dist/_internal/server/jobs/core/priceIndexRollup.js +234 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.d.ts +28 -0
- package/dist/_internal/server/jobs/core/revalidateNotify.js +125 -0
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.d.ts → testDataCleanup.d.ts} +3 -3
- package/dist/_internal/server/jobs/core/{testerSandboxCleanup.js → testDataCleanup.js} +3 -3
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +5 -1
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/analyticsAlertEvaluate.js +2 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +3 -1
- package/dist/_internal/server/jobs/handlers/index.js +3 -1
- package/dist/_internal/server/jobs/handlers/messages.d.ts +5 -2
- package/dist/_internal/server/jobs/handlers/messages.js +5 -2
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/priceIndexRollup.js +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/testDataCleanup.js +2 -0
- package/dist/_internal/shared/actions/action-registry.js +34 -0
- package/dist/_internal/shared/cache/revalidate-targets.d.ts +49 -0
- package/dist/_internal/shared/cache/revalidate-targets.js +157 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +113 -3
- package/dist/_internal/shared/checkout/order-math.js +194 -7
- package/dist/_internal/shared/features/categories/price-guidance.d.ts +110 -0
- package/dist/_internal/shared/features/categories/price-guidance.js +136 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.d.ts +90 -0
- package/dist/_internal/shared/features/site-settings/gst-registration.js +126 -0
- package/dist/_internal/shared/fees/calculator.d.ts +105 -0
- package/dist/_internal/shared/fees/calculator.js +143 -0
- package/dist/_internal/shared/listing-types/art/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/classified/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/digital-code/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/live/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/standard/config.d.ts +0 -6
- package/dist/_internal/shared/listing-types/stickers/config.d.ts +0 -6
- package/dist/_internal/shared/templating/placeholders.d.ts +70 -0
- package/dist/_internal/shared/templating/placeholders.js +73 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/features/admin/actions/admin-actions.d.ts +1 -1
- package/dist/features/admin/actions/admin-actions.js +1 -1
- package/dist/features/admin/components/AdminCategoryEditorView.js +269 -3
- package/dist/features/admin/components/AdminProductEditorView.js +8 -1
- package/dist/features/admin/components/AdminProductsView.js +78 -7
- package/dist/features/admin/components/AdminSiteSettingsView.js +20 -1
- package/dist/features/admin/components/AdminTaxCodeEditorView.d.ts +10 -0
- package/dist/features/admin/components/AdminTaxCodeEditorView.js +160 -0
- package/dist/features/admin/components/AdminTaxCodesView.d.ts +13 -0
- package/dist/features/admin/components/AdminTaxCodesView.js +86 -0
- package/dist/features/admin/components/AdminTesterChecklistView.js +1 -1
- package/dist/features/admin/components/analytics/AdminTopProductsTable.js +1 -1
- package/dist/features/auctions/components/AuctionBottomActions.js +10 -1
- package/dist/features/auctions/components/AuctionDetailPageView.js +2 -1
- package/dist/features/auctions/components/AuctionLiveStatusBadge.d.ts +20 -0
- package/dist/features/auctions/components/AuctionLiveStatusBadge.js +21 -0
- package/dist/features/auctions/components/PlaceBidFormClient.d.ts +1 -1
- package/dist/features/auctions/components/PlaceBidFormClient.js +20 -2
- package/dist/features/auctions/hooks/useIsAuctionEnded.d.ts +41 -0
- package/dist/features/auctions/hooks/useIsAuctionEnded.js +76 -0
- package/dist/features/auth/schemas/firestore.d.ts +0 -1
- package/dist/features/blog/api/route.js +16 -1
- package/dist/features/blog/schemas/firestore.d.ts +2 -2
- package/dist/features/catalogue/components/CatalogueItemEditorView.js +14 -3
- package/dist/features/categories/actions/category-actions.d.ts +2 -2
- package/dist/features/categories/actions/category-actions.js +22 -10
- package/dist/features/categories/api/[id]/description-template/route.d.ts +45 -0
- package/dist/features/categories/api/[id]/description-template/route.js +252 -0
- package/dist/features/categories/api/route.js +156 -11
- package/dist/features/categories/components/CategoriesIndexListing.d.ts +1 -3
- package/dist/features/categories/components/CategoriesIndexListing.js +1 -1
- package/dist/features/categories/components/CategoriesIndexPageView.d.ts +27 -1
- package/dist/features/categories/components/CategoriesIndexPageView.js +24 -33
- package/dist/features/categories/components/CategoryDetailPageView.js +43 -10
- package/dist/features/categories/components/CategoryTemplatePicker.d.ts +65 -0
- package/dist/features/categories/components/CategoryTemplatePicker.js +170 -0
- package/dist/features/categories/constants/listing-categories.d.ts +17 -0
- package/dist/features/categories/constants/listing-categories.js +53 -0
- package/dist/features/categories/hooks/useCategoryTree.js +28 -2
- package/dist/features/categories/repository/categories.repository.d.ts +73 -12
- package/dist/features/categories/repository/categories.repository.js +203 -42
- package/dist/features/categories/repository/price-index.repository.d.ts +115 -0
- package/dist/features/categories/repository/price-index.repository.js +85 -0
- package/dist/features/categories/schemas/category-content.d.ts +206 -0
- package/dist/features/categories/schemas/category-content.js +28 -0
- package/dist/features/categories/schemas/firestore.d.ts +74 -5
- package/dist/features/categories/schemas/firestore.js +74 -3
- package/dist/features/categories/schemas/index.d.ts +1047 -0
- package/dist/features/categories/schemas/index.js +96 -0
- package/dist/features/categories/server.d.ts +1 -0
- package/dist/features/categories/server.js +7 -0
- package/dist/features/categories/types/index.d.ts +9 -0
- package/dist/features/events/schemas/firestore.d.ts +2 -2
- package/dist/features/faq/schemas/firestore.d.ts +2 -1
- package/dist/features/faq/schemas/firestore.js +20 -8
- package/dist/features/faq/utils/getHomepageFaqsCached.d.ts +20 -0
- package/dist/features/faq/utils/getHomepageFaqsCached.js +22 -0
- package/dist/features/homepage/components/MarketplaceHomepageView.js +13 -3
- package/dist/features/homepage/lib/live-stats.js +18 -8
- package/dist/features/media/MediaImage.js +19 -2
- package/dist/features/orders/schemas/firestore.d.ts +70 -0
- package/dist/features/orders/types/index.d.ts +55 -0
- package/dist/features/products/actions/product-actions.d.ts +0 -9
- package/dist/features/products/actions/product-actions.js +25 -0
- package/dist/features/products/columns/index.d.ts +0 -1
- package/dist/features/products/columns/index.js +5 -1
- package/dist/features/products/components/ArtStickersListView.js +7 -1
- package/dist/features/products/components/PrizeDrawsSection.js +5 -0
- package/dist/features/products/components/ProductFilters.d.ts +0 -9
- package/dist/features/products/components/ProductForm.d.ts +27 -1
- package/dist/features/products/components/ProductForm.js +26 -12
- package/dist/features/products/components/ProductsIndexPageView.js +1 -1
- package/dist/features/products/constants/sieve.d.ts +0 -6
- package/dist/features/products/constants/sieve.js +35 -2
- package/dist/features/products/repository/products.repository.d.ts +115 -1
- package/dist/features/products/repository/products.repository.js +491 -39
- package/dist/features/products/schemas/firestore.d.ts +65 -5
- package/dist/features/products/schemas/firestore.js +9 -0
- package/dist/features/products/schemas/product-features.d.ts +140 -2
- package/dist/features/products/schemas/product-features.js +62 -1
- package/dist/features/promotions/repository/coupons.repository.js +6 -1
- package/dist/features/promotions/schemas/firestore.d.ts +44 -1
- package/dist/features/promotions/schemas/firestore.js +76 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +22 -0
- package/dist/features/reviews/repository/reviews.repository.js +35 -0
- package/dist/features/seller/actions/seller-actions.js +17 -2
- package/dist/features/seller/components/PrintCenterView.d.ts +3 -2
- package/dist/features/seller/components/PrintCenterView.js +3 -2
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +21 -11
- package/dist/features/seller/components/SellerOrdersView.js +6 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +19 -3
- package/dist/features/seller/components/SellerProductShell.d.ts +50 -2
- package/dist/features/seller/components/SellerProductShell.js +101 -9
- package/dist/features/seller/components/SellerProductsFilterDrawer.d.ts +13 -1
- package/dist/features/seller/components/SellerProductsFilterDrawer.js +2 -2
- package/dist/features/seller/components/SellerProductsView.js +108 -3
- package/dist/features/seller/types/index.d.ts +17 -0
- package/dist/features/seller/utils/product-draft-mapping.d.ts +13 -0
- package/dist/features/seller/utils/product-draft-mapping.js +17 -0
- package/dist/features/store-extensions/constants/alert-metrics.d.ts +86 -0
- package/dist/features/store-extensions/constants/alert-metrics.js +142 -0
- package/dist/features/store-extensions/schemas/firestore.d.ts +19 -1
- package/dist/features/stores/schemas/firestore.d.ts +31 -2
- package/dist/features/tax-codes/repository/tax-codes.repository.d.ts +55 -0
- package/dist/features/tax-codes/repository/tax-codes.repository.js +131 -0
- package/dist/features/tax-codes/schemas/firestore.d.ts +125 -0
- package/dist/features/tax-codes/schemas/firestore.js +82 -0
- package/dist/features/tax-codes/schemas/tax-code-form.d.ts +63 -0
- package/dist/features/tax-codes/schemas/tax-code-form.js +152 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.d.ts +107 -0
- package/dist/features/tax-codes/schemas/tax-codes.validators.js +85 -0
- package/dist/features/tester/actions/index.d.ts +0 -1
- package/dist/features/tester/actions/index.js +0 -1
- package/dist/features/tester/components/AdminTesterFeedbackIssuesView.js +1 -1
- package/dist/features/tester/components/AdminTesterFeedbackListView.js +1 -1
- package/dist/features/tester/components/index.d.ts +0 -4
- package/dist/features/tester/components/index.js +0 -2
- package/dist/features/tester/repository/tester-checklist-item.repository.d.ts +1 -20
- package/dist/features/tester/repository/tester-checklist-item.repository.js +0 -52
- package/dist/features/tester/schemas/firestore.d.ts +0 -6
- package/dist/features/tester/seed-data/authored/admin__admin-detail-round-trips.js +3 -3
- package/dist/features/tester/seed-data/authored/admin__site-system.js +4 -5
- package/dist/features/tester/seed-data/authored/content-discovery__category-counts-and-rollup.js +2 -2
- package/dist/features/tester/seed-data/authored/index.js +0 -2
- package/dist/features/tester/seed-data/authored/page-wiring__reachability.js +0 -19
- package/dist/features/tester/seed-data/tester-checklist-seed-data.js +10 -29
- package/dist/index.d.ts +21 -7
- package/dist/index.js +53 -7
- package/dist/next/routing/route-map.d.ts +2 -4
- package/dist/next/routing/route-map.js +1 -2
- package/dist/repositories/index.d.ts +1 -0
- package/dist/repositories/index.js +1 -0
- package/dist/schemas/registry.d.ts +506 -0
- package/dist/seed/_helpers/brand-rows.d.ts +35 -0
- package/dist/seed/_helpers/brand-rows.js +576 -0
- package/dist/seed/_helpers/category-content.d.ts +39 -0
- package/dist/seed/_helpers/category-content.js +634 -0
- package/dist/seed/_helpers/category-forest.d.ts +2 -0
- package/dist/seed/_helpers/category-forest.js +585 -243
- package/dist/seed/_helpers/category-models.d.ts +5 -0
- package/dist/seed/_helpers/category-models.js +1364 -0
- package/dist/seed/_helpers/category-tree.js +17 -1
- package/dist/seed/_helpers/character-features.d.ts +10 -0
- package/dist/seed/_helpers/character-features.js +118 -0
- package/dist/seed/_helpers/media.d.ts +1 -1
- package/dist/seed/_helpers/media.js +44 -8
- package/dist/seed/_helpers/search-txt-wrappers.d.ts +10 -0
- package/dist/seed/_helpers/search-txt-wrappers.js +12 -1
- package/dist/seed/categories-seed-data.js +147 -169
- package/dist/seed/index.d.ts +1 -0
- package/dist/seed/index.js +1 -0
- package/dist/seed/manifest.d.ts +1 -1
- package/dist/seed/manifest.js +5 -0
- package/dist/seed/orders-seed-data.js +5 -3
- package/dist/seed/product-features-seed-data.d.ts +1 -8
- package/dist/seed/product-features-seed-data.js +358 -154
- package/dist/seed/products-art-seed-data.js +16 -4
- package/dist/seed/products-auctions-seed-data.js +6 -6
- package/dist/seed/products-classifieds-seed-data.js +17 -7
- package/dist/seed/products-digital-codes-seed-data.js +12 -12
- package/dist/seed/products-preorders-seed-data.js +8 -8
- package/dist/seed/products-prize-draws-seed-data.js +25 -8
- package/dist/seed/products-standard-seed-data.js +10 -60
- package/dist/seed/products-stickers-seed-data.js +6 -6
- package/dist/seed/site-settings-seed-data.js +24 -0
- package/dist/seed/store-extensions-seed-data.js +10 -4
- package/dist/seed/tax-codes-seed-data.d.ts +34 -0
- package/dist/seed/tax-codes-seed-data.js +100 -0
- package/dist/seed/test-data-ttl.d.ts +29 -0
- package/dist/seed/test-data-ttl.js +31 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +6 -1
- package/dist/server.d.ts +4 -1
- package/dist/server.js +11 -2
- package/dist/styles.css +40 -0
- package/dist/ui/components/PaginatedSelect.d.ts +14 -0
- package/dist/ui/components/PaginatedSelect.js +0 -0
- package/dist/ui/components/PaginatedSelect.style.css +40 -0
- package/dist/utils/media-url.d.ts +26 -0
- package/dist/utils/media-url.js +144 -0
- package/dist/utils/physical-location.d.ts +23 -0
- package/dist/utils/physical-location.js +52 -0
- package/dist/utils/search-txt-builders.d.ts +20 -0
- package/dist/utils/search-txt-builders.js +36 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +4 -1
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.d.ts +0 -2
- package/dist/_internal/server/jobs/handlers/testerSandboxCleanup.js +0 -2
- package/dist/features/tester/actions/leaderboard-actions.d.ts +0 -3
- package/dist/features/tester/actions/leaderboard-actions.js +0 -5
- package/dist/features/tester/components/BugHunterLeaderboardView.d.ts +0 -23
- package/dist/features/tester/components/BugHunterLeaderboardView.js +0 -18
- package/dist/features/tester/components/TesterHubView.d.ts +0 -5
- package/dist/features/tester/components/TesterHubView.js +0 -110
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.d.ts +0 -2
- package/dist/features/tester/seed-data/authored/public-pages__bug-hunters.js +0 -78
- package/dist/features/tester/seed-data/tester-ttl.d.ts +0 -4
- package/dist/features/tester/seed-data/tester-ttl.js +0 -6
|
@@ -1,8 +1,16 @@
|
|
|
1
|
+
import { notifyRevalidate } from "./revalidateNotify";
|
|
1
2
|
import { reviewRepository, storeRepository } from "../../../../repositories";
|
|
2
3
|
import { ReviewStatusValues } from "../../../../features/reviews/schemas/firestore";
|
|
3
4
|
import { PRODUCT_COLLECTION } from "../../../../features/products/schemas/firestore";
|
|
4
5
|
export async function handleReviewWrite(input, ctx) {
|
|
5
6
|
const { reviewId, before: beforeData, after: afterData } = input;
|
|
7
|
+
/*
|
|
8
|
+
* A review changes the product's rating badge AND the homepage's
|
|
9
|
+
* `platform_rating` / `total_reviews` stats, so the targets are the listing
|
|
10
|
+
* pages rather than a per-review page (there isn't one). Uses the review's own
|
|
11
|
+
* id purely for log correlation. No Firestore write here (Root Cause #92).
|
|
12
|
+
*/
|
|
13
|
+
await notifyRevalidate({ collection: "reviews", id: reviewId }, ctx);
|
|
6
14
|
const beforeStatus = beforeData?.status ?? null;
|
|
7
15
|
const afterStatus = afterData?.status ?? null;
|
|
8
16
|
const wasApproved = beforeStatus === ReviewStatusValues.APPROVED;
|
|
@@ -10,4 +10,4 @@ export interface HandleStoreWriteInput {
|
|
|
10
10
|
before: StoreDoc | null;
|
|
11
11
|
after: StoreDoc | null;
|
|
12
12
|
}
|
|
13
|
-
export declare function handleStoreWrite(
|
|
13
|
+
export declare function handleStoreWrite(input: HandleStoreWriteInput, ctx: JobContext): Promise<void>;
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* Core: stores/{id} onWrite — currently a no-op. Previous implementation
|
|
3
3
|
* synced an external search index that has since been removed.
|
|
4
4
|
*/
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { notifyRevalidate } from "./revalidateNotify";
|
|
6
|
+
export async function handleStoreWrite(input, ctx) {
|
|
7
|
+
// The search-index sync this used to perform is gone (Firestore queries handle
|
|
8
|
+
// search now), so the only remaining side-effect is cache invalidation: drop
|
|
9
|
+
// the ISR entries for this store's page and the stores listing. Reuses the
|
|
10
|
+
// existing trigger, so this costs no additional invocations, and performs no
|
|
11
|
+
// Firestore write (Root Cause #92).
|
|
12
|
+
await notifyRevalidate({ collection: "stores", id: input.storeId }, ctx);
|
|
7
13
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core: the realised-price index — what things actually SOLD for here.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Every price signal we show a seller today is an ASK: the seeded `market`
|
|
7
|
+
* band is a competitor's listed price, and a competitor's sold-out row proves
|
|
8
|
+
* the item is GONE, not that it sold at that figure. With 76–93% of listings
|
|
9
|
+
* across the crawled catalogues never selling, the gap between an ask and a
|
|
10
|
+
* realised price is the single most useful thing we can tell a seller — and we
|
|
11
|
+
* are the only party who can measure it on our own sales.
|
|
12
|
+
*
|
|
13
|
+
* A seeded blog post already tells sellers to *"check what similar items sold
|
|
14
|
+
* for on LetItRip in the last 30 days"*, which has never been possible.
|
|
15
|
+
*
|
|
16
|
+
* ## 🛑 `market` and `sold` are NEVER blended
|
|
17
|
+
*
|
|
18
|
+
* They answer different questions and the gap IS the information. Averaging
|
|
19
|
+
* them would launder an inference into a measurement. This job writes only the
|
|
20
|
+
* sold half, into its own collection, and never touches the seeded band.
|
|
21
|
+
*
|
|
22
|
+
* ## The authoritative field
|
|
23
|
+
*
|
|
24
|
+
* **`OrderDocument.items[].unitPrice`** — and specifically not:
|
|
25
|
+
*
|
|
26
|
+
* `OrderDocument.unitPrice` the FIRST line's price (`unitPriceFor(items[0])`),
|
|
27
|
+
* wrong for every other line
|
|
28
|
+
* `OrderDocument.totalPrice` folds in GST, platform fee, shipping and add-ons
|
|
29
|
+
* `BidDocument.status==won` its own comment: `orderId` is absent on a
|
|
30
|
+
* won-but-unpaid bid
|
|
31
|
+
* `OfferDocument==accepted` an agreement, not a sale. `paid` is the sale
|
|
32
|
+
* `product.currentBid` a display mirror a pending buyout never writes
|
|
33
|
+
*
|
|
34
|
+
* Won auctions and accepted offers arrive through the same field via
|
|
35
|
+
* `lockedPrice`, so one field covers all six acquisition paths and
|
|
36
|
+
* `sourceContext` labels each for free.
|
|
37
|
+
*
|
|
38
|
+
* ## Why daily and never live
|
|
39
|
+
*
|
|
40
|
+
* A live index would scan orders per request — the unbounded-per-request
|
|
41
|
+
* pattern that paused this project on Hobby caps. One pre-computed read per
|
|
42
|
+
* row, with a visible "prices as of {date}". Deliberately one day stale.
|
|
43
|
+
*/
|
|
44
|
+
import type { JobContext } from "../runtime/types";
|
|
45
|
+
/** How far back a band looks. */
|
|
46
|
+
export declare const PRICE_INDEX_WINDOW_DAYS = 90;
|
|
47
|
+
export declare function runPriceIndexRollup(ctx: JobContext): Promise<void>;
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core: the realised-price index — what things actually SOLD for here.
|
|
3
|
+
*
|
|
4
|
+
* ## Why this exists
|
|
5
|
+
*
|
|
6
|
+
* Every price signal we show a seller today is an ASK: the seeded `market`
|
|
7
|
+
* band is a competitor's listed price, and a competitor's sold-out row proves
|
|
8
|
+
* the item is GONE, not that it sold at that figure. With 76–93% of listings
|
|
9
|
+
* across the crawled catalogues never selling, the gap between an ask and a
|
|
10
|
+
* realised price is the single most useful thing we can tell a seller — and we
|
|
11
|
+
* are the only party who can measure it on our own sales.
|
|
12
|
+
*
|
|
13
|
+
* A seeded blog post already tells sellers to *"check what similar items sold
|
|
14
|
+
* for on LetItRip in the last 30 days"*, which has never been possible.
|
|
15
|
+
*
|
|
16
|
+
* ## 🛑 `market` and `sold` are NEVER blended
|
|
17
|
+
*
|
|
18
|
+
* They answer different questions and the gap IS the information. Averaging
|
|
19
|
+
* them would launder an inference into a measurement. This job writes only the
|
|
20
|
+
* sold half, into its own collection, and never touches the seeded band.
|
|
21
|
+
*
|
|
22
|
+
* ## The authoritative field
|
|
23
|
+
*
|
|
24
|
+
* **`OrderDocument.items[].unitPrice`** — and specifically not:
|
|
25
|
+
*
|
|
26
|
+
* `OrderDocument.unitPrice` the FIRST line's price (`unitPriceFor(items[0])`),
|
|
27
|
+
* wrong for every other line
|
|
28
|
+
* `OrderDocument.totalPrice` folds in GST, platform fee, shipping and add-ons
|
|
29
|
+
* `BidDocument.status==won` its own comment: `orderId` is absent on a
|
|
30
|
+
* won-but-unpaid bid
|
|
31
|
+
* `OfferDocument==accepted` an agreement, not a sale. `paid` is the sale
|
|
32
|
+
* `product.currentBid` a display mirror a pending buyout never writes
|
|
33
|
+
*
|
|
34
|
+
* Won auctions and accepted offers arrive through the same field via
|
|
35
|
+
* `lockedPrice`, so one field covers all six acquisition paths and
|
|
36
|
+
* `sourceContext` labels each for free.
|
|
37
|
+
*
|
|
38
|
+
* ## Why daily and never live
|
|
39
|
+
*
|
|
40
|
+
* A live index would scan orders per request — the unbounded-per-request
|
|
41
|
+
* pattern that paused this project on Hobby caps. One pre-computed read per
|
|
42
|
+
* row, with a visible "prices as of {date}". Deliberately one day stale.
|
|
43
|
+
*/
|
|
44
|
+
import { orderRepository, productRepository } from "../../../../repositories";
|
|
45
|
+
import { priceIndexRepository, } from "../../../../features/categories/repository/price-index.repository";
|
|
46
|
+
/** How far back a band looks. */
|
|
47
|
+
export const PRICE_INDEX_WINDOW_DAYS = 90;
|
|
48
|
+
/**
|
|
49
|
+
* Hard ceiling on the products scan.
|
|
50
|
+
*
|
|
51
|
+
* The scan exists only to build `productId → categorySlugs[0]`, and that join
|
|
52
|
+
* disappears for future orders once `categorySlugs[0]` is snapshotted onto the
|
|
53
|
+
* order item (done in the same change as this job). So the cap is a guard
|
|
54
|
+
* against a catalogue that grows faster than the snapshot backfills, not a
|
|
55
|
+
* permanent design limit — and it is logged when it bites rather than
|
|
56
|
+
* truncating silently.
|
|
57
|
+
*/
|
|
58
|
+
const PRODUCT_SCAN_LIMIT = 5000;
|
|
59
|
+
function toDate(v) {
|
|
60
|
+
if (!v)
|
|
61
|
+
return null;
|
|
62
|
+
const d = v instanceof Date ? v : new Date(v);
|
|
63
|
+
return Number.isNaN(d.getTime()) ? null : d;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Which acquisition path produced this sale.
|
|
67
|
+
*
|
|
68
|
+
* `sourceContext.path` is the richer signal — it distinguishes a settled
|
|
69
|
+
* auction win from a buy-now, which `orderType` cannot (both are `"auction"`).
|
|
70
|
+
* Falls back to `orderType`, then to `standard`.
|
|
71
|
+
*/
|
|
72
|
+
function pathOf(order) {
|
|
73
|
+
const p = order.sourceContext?.path ?? order.orderType;
|
|
74
|
+
if (p === "auction-won" || p === "auction-buy-now" || p === "auction")
|
|
75
|
+
return "auction";
|
|
76
|
+
if (p === "offer-accepted" || p === "offer")
|
|
77
|
+
return "offer";
|
|
78
|
+
return "standard";
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 🛑 Completed means delivered AND paid AND not refunded.
|
|
82
|
+
*
|
|
83
|
+
* The repo's convention elsewhere is `delivered` alone, and **nothing
|
|
84
|
+
* currently excludes a delivered-then-refunded order** — whose realised price
|
|
85
|
+
* is zero. Including one would publish a sale that was undone as evidence of
|
|
86
|
+
* what things sell for.
|
|
87
|
+
*/
|
|
88
|
+
function isCompleted(order) {
|
|
89
|
+
if (order.status !== "delivered")
|
|
90
|
+
return false;
|
|
91
|
+
if (order.paymentStatus !== "paid")
|
|
92
|
+
return false;
|
|
93
|
+
return !(order.refunds && order.refunds.length > 0);
|
|
94
|
+
}
|
|
95
|
+
function median(sorted) {
|
|
96
|
+
const n = sorted.length;
|
|
97
|
+
if (n === 0)
|
|
98
|
+
return 0;
|
|
99
|
+
const mid = Math.floor(n / 2);
|
|
100
|
+
const raw = n % 2 === 1 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
|
|
101
|
+
return Math.round(raw * 100) / 100;
|
|
102
|
+
}
|
|
103
|
+
function toBand(samples) {
|
|
104
|
+
const amounts = samples.map((s) => s.amount).sort((a, b) => a - b);
|
|
105
|
+
const byPath = {};
|
|
106
|
+
for (const s of samples)
|
|
107
|
+
byPath[s.via] = (byPath[s.via] ?? 0) + 1;
|
|
108
|
+
const latest = samples.reduce((best, s) => (!best || s.at > best.at ? s : best), null);
|
|
109
|
+
return {
|
|
110
|
+
min: amounts[0],
|
|
111
|
+
median: median(amounts),
|
|
112
|
+
max: amounts[amounts.length - 1],
|
|
113
|
+
sampleSize: samples.length,
|
|
114
|
+
byPath,
|
|
115
|
+
...(latest ? { lastSold: { amount: latest.amount, at: latest.at, via: latest.via } } : {}),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** `YYYYMMDD` in UTC, so the id is stable regardless of where the job runs. */
|
|
119
|
+
function utcDateKey(d) {
|
|
120
|
+
return `${d.getUTCFullYear()}${String(d.getUTCMonth() + 1).padStart(2, "0")}${String(d.getUTCDate()).padStart(2, "0")}`;
|
|
121
|
+
}
|
|
122
|
+
export async function runPriceIndexRollup(ctx) {
|
|
123
|
+
const now = new Date();
|
|
124
|
+
const cutoff = new Date(now.getTime() - PRICE_INDEX_WINDOW_DAYS * 24 * 60 * 60 * 1000);
|
|
125
|
+
/*
|
|
126
|
+
* One products scan into a MAP, never N `findById` calls.
|
|
127
|
+
*
|
|
128
|
+
* `storeAnalytics.ts:94` does the per-item lookup and it is the pattern not
|
|
129
|
+
* to copy — an order with 4 items costs 4 reads there, so a 90-day window
|
|
130
|
+
* over a growing order list is an unbounded read multiplier.
|
|
131
|
+
*/
|
|
132
|
+
const products = await productRepository.list({
|
|
133
|
+
page: 1,
|
|
134
|
+
pageSize: PRODUCT_SCAN_LIMIT,
|
|
135
|
+
});
|
|
136
|
+
const rows = products.items ?? [];
|
|
137
|
+
if (rows.length >= PRODUCT_SCAN_LIMIT) {
|
|
138
|
+
ctx.logger.warn("priceIndexRollup: product scan saturated — the leaf join may be incomplete", {
|
|
139
|
+
limit: PRODUCT_SCAN_LIMIT,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const leafOf = new Map();
|
|
143
|
+
const typeOf = new Map();
|
|
144
|
+
for (const p of rows) {
|
|
145
|
+
if (!p.id)
|
|
146
|
+
continue;
|
|
147
|
+
const leaf = p.categorySlugs?.[0];
|
|
148
|
+
if (leaf)
|
|
149
|
+
leafOf.set(p.id, leaf);
|
|
150
|
+
if (p.listingType)
|
|
151
|
+
typeOf.set(p.id, p.listingType);
|
|
152
|
+
}
|
|
153
|
+
const delivered = (await orderRepository.findByStatus("delivered"));
|
|
154
|
+
/** leafId → listingType → samples */
|
|
155
|
+
const byLeaf = new Map();
|
|
156
|
+
let considered = 0;
|
|
157
|
+
let skippedNoLeaf = 0;
|
|
158
|
+
for (const order of delivered) {
|
|
159
|
+
if (!isCompleted(order))
|
|
160
|
+
continue;
|
|
161
|
+
const at = toDate(order.orderDate) ?? toDate(order.createdAt);
|
|
162
|
+
if (!at || at < cutoff)
|
|
163
|
+
continue;
|
|
164
|
+
const via = pathOf(order);
|
|
165
|
+
for (const item of order.items ?? []) {
|
|
166
|
+
const amount = Number(item.unitPrice ?? 0);
|
|
167
|
+
if (!Number.isFinite(amount) || amount <= 0)
|
|
168
|
+
continue;
|
|
169
|
+
/*
|
|
170
|
+
* A fully-cancelled line is not a sale. A partially-cancelled one is —
|
|
171
|
+
* the units that shipped sold at that unit price, and the unit price is
|
|
172
|
+
* what a band is made of, so the quantity does not weight it.
|
|
173
|
+
*/
|
|
174
|
+
const live = (item.quantity ?? 1) - (item.cancelledQuantity ?? 0);
|
|
175
|
+
if (live <= 0)
|
|
176
|
+
continue;
|
|
177
|
+
/*
|
|
178
|
+
* The snapshot first, the join second. New orders carry
|
|
179
|
+
* `categorySlug` and need no products read at all; older ones fall
|
|
180
|
+
* back to the map. An item whose product has since been deleted
|
|
181
|
+
* resolves to neither and is COUNTED AS SKIPPED rather than dropped
|
|
182
|
+
* silently — a band missing sales is indistinguishable from a quiet
|
|
183
|
+
* category unless somebody says so.
|
|
184
|
+
*/
|
|
185
|
+
const leaf = item.categorySlug ?? (item.productId ? leafOf.get(item.productId) : undefined);
|
|
186
|
+
if (!leaf) {
|
|
187
|
+
skippedNoLeaf += 1;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const listingType = item.listingType ?? (item.productId ? typeOf.get(item.productId) : undefined) ?? "standard";
|
|
191
|
+
const types = byLeaf.get(leaf) ?? new Map();
|
|
192
|
+
const samples = types.get(listingType) ?? [];
|
|
193
|
+
samples.push({ amount, at, via });
|
|
194
|
+
types.set(listingType, samples);
|
|
195
|
+
byLeaf.set(leaf, types);
|
|
196
|
+
considered += 1;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
const dateKey = utcDateKey(now);
|
|
200
|
+
let written = 0;
|
|
201
|
+
for (const [leafId, types] of byLeaf) {
|
|
202
|
+
const byListingType = {};
|
|
203
|
+
const point = {};
|
|
204
|
+
for (const [listingType, samples] of types) {
|
|
205
|
+
const band = toBand(samples);
|
|
206
|
+
byListingType[listingType] = band;
|
|
207
|
+
point[listingType] = { median: band.median, sampleSize: band.sampleSize };
|
|
208
|
+
}
|
|
209
|
+
/*
|
|
210
|
+
* 🛑 Only leaves WITH sales are written, and nothing is ever zeroed.
|
|
211
|
+
*
|
|
212
|
+
* An absent band means "we have no sales here", which the UI renders as
|
|
213
|
+
* silence. A zeroed one would render as "things sell here for ₹0", which
|
|
214
|
+
* is worse than silence and is exactly what a defaulting write would
|
|
215
|
+
* manufacture on the first quiet night. A refund removing the last sale in
|
|
216
|
+
* a leaf therefore leaves a stale band for a day and then it ages out of
|
|
217
|
+
* the window — which is the correct behaviour for a figure labelled "as
|
|
218
|
+
* of" a date.
|
|
219
|
+
*/
|
|
220
|
+
await priceIndexRepository.setForLeaf(leafId, {
|
|
221
|
+
byListingType,
|
|
222
|
+
windowDays: PRICE_INDEX_WINDOW_DAYS,
|
|
223
|
+
});
|
|
224
|
+
await priceIndexRepository.setDailyPoint(leafId, dateKey, point);
|
|
225
|
+
written += 1;
|
|
226
|
+
}
|
|
227
|
+
ctx.logger.info("priceIndexRollup: complete", {
|
|
228
|
+
deliveredScanned: delivered.length,
|
|
229
|
+
salesCounted: considered,
|
|
230
|
+
leavesWritten: written,
|
|
231
|
+
skippedNoLeaf,
|
|
232
|
+
windowDays: PRICE_INDEX_WINDOW_DAYS,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { JobContext } from "../runtime/types";
|
|
2
|
+
import type { JsonValue } from "@mohasinac/appkit";
|
|
3
|
+
/** Same shape the sibling job cores use (`ProductDoc`, `CategoryDoc`, …). */
|
|
4
|
+
type Doc = Record<string, JsonValue> | null | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Did anything a page renders actually change?
|
|
7
|
+
*
|
|
8
|
+
* 🛑 Compared with `JSON.stringify` per FIELD, never over the whole document.
|
|
9
|
+
* Root Cause #92's bug was a whole-object `JSON.stringify` comparison that could
|
|
10
|
+
* never report "equal" because Firestore returns map keys alphabetically while
|
|
11
|
+
* the code built them in construction order. Per-field, the values are scalars
|
|
12
|
+
* or small arrays whose order IS meaningful, so stringify is sound here.
|
|
13
|
+
*
|
|
14
|
+
* A create or a delete always counts — there is no previous/next state to diff.
|
|
15
|
+
*/
|
|
16
|
+
export declare function productCacheFieldsChanged(before: Doc, after: Doc): boolean;
|
|
17
|
+
export interface RevalidateNotifyInput {
|
|
18
|
+
collection: string;
|
|
19
|
+
id: string;
|
|
20
|
+
hints?: {
|
|
21
|
+
listingType?: string;
|
|
22
|
+
categorySlugs?: string[];
|
|
23
|
+
brandSlug?: string;
|
|
24
|
+
storeId?: string;
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export declare function notifyRevalidate(input: RevalidateNotifyInput, ctx: JobContext): Promise<void>;
|
|
28
|
+
export {};
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { normalizeError } from "../../../../errors/normalize";
|
|
2
|
+
/**
|
|
3
|
+
* Tell the Next.js app that a document changed, so it can drop the stale ISR
|
|
4
|
+
* entries for the pages that render it.
|
|
5
|
+
*
|
|
6
|
+
* ## Why an HTTP call and not `revalidatePath()`
|
|
7
|
+
*
|
|
8
|
+
* `revalidatePath` only exists inside the Next.js runtime. The writers that
|
|
9
|
+
* matter most do NOT run there: a checkout stock decrement, auction settlement,
|
|
10
|
+
* the nightly reconcilers and every `appkit-seed` write all execute in Cloud
|
|
11
|
+
* Functions or a CLI. A Firestore trigger is the one place that observes all of
|
|
12
|
+
* them, and the only way it can reach Next's cache is over the network.
|
|
13
|
+
*
|
|
14
|
+
* ## 🛑 This does not write to Firestore — by construction
|
|
15
|
+
*
|
|
16
|
+
* Root Cause #92: a trigger that writes back to the collection it watches is how
|
|
17
|
+
* this project produced 1.02M invocations/day and knocked out production. This
|
|
18
|
+
* performs exactly one `fetch` and touches no document, so it cannot re-trigger
|
|
19
|
+
* itself. Keep it that way — if this ever needs to persist anything, it needs a
|
|
20
|
+
* different trigger path, not a write.
|
|
21
|
+
*
|
|
22
|
+
* ## Best-effort, and silent about it
|
|
23
|
+
*
|
|
24
|
+
* A failed invalidation must never fail the write side-effects it rides along
|
|
25
|
+
* with — the product is already saved; the worst case is a page that stays stale
|
|
26
|
+
* until its TTL rolls. So this swallows every error after classifying it, and
|
|
27
|
+
* logs at `warn` once per failure rather than per document.
|
|
28
|
+
*/
|
|
29
|
+
/** Fields whose change can alter what a public page renders. */
|
|
30
|
+
const CACHE_RELEVANT_PRODUCT_FIELDS = [
|
|
31
|
+
"status",
|
|
32
|
+
"stockQuantity",
|
|
33
|
+
"isSold",
|
|
34
|
+
"price",
|
|
35
|
+
"salePrice",
|
|
36
|
+
"title",
|
|
37
|
+
"images",
|
|
38
|
+
"categorySlugs",
|
|
39
|
+
"brandSlug",
|
|
40
|
+
"brand",
|
|
41
|
+
"storeId",
|
|
42
|
+
"listingType",
|
|
43
|
+
"auctionEndDate",
|
|
44
|
+
"currentBid",
|
|
45
|
+
"buyNowPrice",
|
|
46
|
+
"isFeatured",
|
|
47
|
+
"isPromoted",
|
|
48
|
+
"featured",
|
|
49
|
+
];
|
|
50
|
+
/**
|
|
51
|
+
* Did anything a page renders actually change?
|
|
52
|
+
*
|
|
53
|
+
* 🛑 Compared with `JSON.stringify` per FIELD, never over the whole document.
|
|
54
|
+
* Root Cause #92's bug was a whole-object `JSON.stringify` comparison that could
|
|
55
|
+
* never report "equal" because Firestore returns map keys alphabetically while
|
|
56
|
+
* the code built them in construction order. Per-field, the values are scalars
|
|
57
|
+
* or small arrays whose order IS meaningful, so stringify is sound here.
|
|
58
|
+
*
|
|
59
|
+
* A create or a delete always counts — there is no previous/next state to diff.
|
|
60
|
+
*/
|
|
61
|
+
export function productCacheFieldsChanged(before, after) {
|
|
62
|
+
if (!before || !after)
|
|
63
|
+
return true;
|
|
64
|
+
for (const field of CACHE_RELEVANT_PRODUCT_FIELDS) {
|
|
65
|
+
if (JSON.stringify(before[field] ?? null) !== JSON.stringify(after[field] ?? null)) {
|
|
66
|
+
return true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The app origin to call.
|
|
73
|
+
*
|
|
74
|
+
* No existing job needed one, so this introduces `APP_ORIGIN`. Returning null
|
|
75
|
+
* (rather than guessing a host) makes an unconfigured deployment a no-op with a
|
|
76
|
+
* one-line warning instead of a stream of failed requests to the wrong site —
|
|
77
|
+
* and the stale page it leaves behind is bounded by the TTL.
|
|
78
|
+
*/
|
|
79
|
+
function appOrigin(ctx) {
|
|
80
|
+
const raw = ctx.env("APP_ORIGIN");
|
|
81
|
+
if (!raw)
|
|
82
|
+
return null;
|
|
83
|
+
return raw.replace(/\/+$/, "");
|
|
84
|
+
}
|
|
85
|
+
export async function notifyRevalidate(input, ctx) {
|
|
86
|
+
const origin = appOrigin(ctx);
|
|
87
|
+
const secret = ctx.env("CACHE_REVALIDATION_SECRET");
|
|
88
|
+
if (!origin || !secret) {
|
|
89
|
+
ctx.logger.warn("revalidate-notify: not configured — skipping", {
|
|
90
|
+
hasOrigin: !!origin,
|
|
91
|
+
hasSecret: !!secret,
|
|
92
|
+
collection: input.collection,
|
|
93
|
+
});
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const res = await fetch(`${origin}/api/cache/revalidate`, {
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: { "content-type": "application/json", "x-api-key": secret },
|
|
100
|
+
body: JSON.stringify({
|
|
101
|
+
collections: [input.collection],
|
|
102
|
+
id: input.id,
|
|
103
|
+
hints: input.hints ?? {},
|
|
104
|
+
}),
|
|
105
|
+
// The caller is a background trigger, not a user request — but an
|
|
106
|
+
// unbounded fetch would pin the function open until the platform kills it.
|
|
107
|
+
signal: AbortSignal.timeout(5000),
|
|
108
|
+
});
|
|
109
|
+
if (!res.ok) {
|
|
110
|
+
ctx.logger.warn("revalidate-notify: endpoint returned non-ok", {
|
|
111
|
+
status: res.status,
|
|
112
|
+
collection: input.collection,
|
|
113
|
+
id: input.id,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
void normalizeError(err);
|
|
119
|
+
ctx.logger.warn("revalidate-notify: request failed", {
|
|
120
|
+
collection: input.collection,
|
|
121
|
+
id: input.id,
|
|
122
|
+
error: err instanceof Error ? err.message : String(err),
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -14,9 +14,9 @@ import type { JobContext } from "../runtime/types";
|
|
|
14
14
|
* (This used to be shared with testerSandboxRefresh.ts, which was removed when
|
|
15
15
|
* the tester runner took over fixture state — see scheduled.ts.)
|
|
16
16
|
*/
|
|
17
|
-
export declare const
|
|
18
|
-
export interface
|
|
17
|
+
export declare const TEST_DATA_COLLECTIONS: readonly ["categories", "stores", "products", "blogPosts", "events", "offers"];
|
|
18
|
+
export interface TestDataCleanupOptions {
|
|
19
19
|
/** true = wipe every isTestData:true doc regardless of testDataExpiresAt (manual force-purge). */
|
|
20
20
|
force?: boolean;
|
|
21
21
|
}
|
|
22
|
-
export declare function
|
|
22
|
+
export declare function runTestDataCleanup(ctx: JobContext, opts?: TestDataCleanupOptions): Promise<void>;
|
|
@@ -15,16 +15,16 @@ import { BID_FIELDS } from "../../../../constants/field-names";
|
|
|
15
15
|
* (This used to be shared with testerSandboxRefresh.ts, which was removed when
|
|
16
16
|
* the tester runner took over fixture state — see scheduled.ts.)
|
|
17
17
|
*/
|
|
18
|
-
export const
|
|
18
|
+
export const TEST_DATA_COLLECTIONS = ["categories", "stores", "products", "blogPosts", "events", "offers"];
|
|
19
19
|
const BID_CHUNK_SIZE = 30; // Firestore `in` query cap
|
|
20
|
-
export async function
|
|
20
|
+
export async function runTestDataCleanup(ctx, opts = {}) {
|
|
21
21
|
const cutoff = opts.force ? null : new Date();
|
|
22
22
|
ctx.logger.info(opts.force
|
|
23
23
|
? "Force-purging all tester sandbox test data"
|
|
24
24
|
: "Sweeping expired tester sandbox test data");
|
|
25
25
|
const allRefs = [];
|
|
26
26
|
let deletedProductIds = [];
|
|
27
|
-
for (const collection of
|
|
27
|
+
for (const collection of TEST_DATA_COLLECTIONS) {
|
|
28
28
|
const refs = await getTestDataRefs(ctx.db, collection, cutoff);
|
|
29
29
|
allRefs.push(...refs);
|
|
30
30
|
if (collection === "products") {
|
|
@@ -6,6 +6,7 @@ import { storeRepository, userRepository, orderRepository, payoutRepository, sit
|
|
|
6
6
|
import { BATCH_LIMIT } from "../handlers/messages";
|
|
7
7
|
import { getDefaultCurrency } from "../../../../core";
|
|
8
8
|
import { computePayoutDeduction } from "../../../shared/fees/calculator";
|
|
9
|
+
import { ordersTaxableValue } from "../../../shared/checkout/order-math";
|
|
9
10
|
export async function runWeeklyPayoutEligibility(ctx) {
|
|
10
11
|
if (ctx.env("FEATURE_PAYOUTS") !== "true") {
|
|
11
12
|
ctx.logger.info("FEATURE_PAYOUTS disabled — skipping weekly payout eligibility");
|
|
@@ -42,7 +43,10 @@ export async function runWeeklyPayoutEligibility(ctx) {
|
|
|
42
43
|
ctx.logger.warn(`Store/seller ${storeId} not found — skipping ${orders.length} order(s)`);
|
|
43
44
|
continue;
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
/* The TAXABLE VALUE — see `orderTaxableValue`. This job and
|
|
47
|
+
* `autoPayoutEligibility` must agree, or the weekly run and the automatic
|
|
48
|
+
* one pay different amounts for the same orders. */
|
|
49
|
+
const grossAmount = ordersTaxableValue(orders.map((o) => o.data));
|
|
46
50
|
const { platformFee, gatewayFee, gstOnFee, netAmount } = computePayoutDeduction(grossAmount, commissions);
|
|
47
51
|
const payoutInput = {
|
|
48
52
|
storeId,
|
|
@@ -28,6 +28,8 @@ export { hardBanReinstatementHandler } from "./hardBanReinstatement";
|
|
|
28
28
|
export { paymentReviewAutoApproveHandler } from "./paymentReviewAutoApprove";
|
|
29
29
|
export { productStatsSyncHandler } from "./productStatsSync";
|
|
30
30
|
export { revenueRollupHandler } from "./revenueRollup";
|
|
31
|
+
export { priceIndexRollupHandler } from "./priceIndexRollup";
|
|
32
|
+
export { analyticsAlertEvaluateHandler } from "./analyticsAlertEvaluate";
|
|
31
33
|
export { pageViewRollupHandler } from "./pageViewRollup";
|
|
32
34
|
export { pageViewPruneHandler } from "./pageViewPrune";
|
|
33
35
|
export { dailyStatusDigestHandler } from "./dailyStatusDigest";
|
|
@@ -52,7 +54,7 @@ export { onUserBanChangeHandler } from "./onUserBanChange";
|
|
|
52
54
|
export { onScamReportCreateHandler } from "./onScamReportCreate";
|
|
53
55
|
export { onScamReportUpdateHandler } from "./onScamReportUpdate";
|
|
54
56
|
export { draftPruneHandler } from "./draftPrune";
|
|
55
|
-
export {
|
|
57
|
+
export { testDataCleanupHandler } from "./testDataCleanup";
|
|
56
58
|
export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
|
|
57
59
|
export { onShipmentItemWriteHandler } from "./onShipmentItemWrite";
|
|
58
60
|
export { onShipmentLotWriteHandler, onShipmentHeaderWriteHandler } from "./onShipmentAllocationSync";
|
|
@@ -28,6 +28,8 @@ export { hardBanReinstatementHandler } from "./hardBanReinstatement";
|
|
|
28
28
|
export { paymentReviewAutoApproveHandler } from "./paymentReviewAutoApprove";
|
|
29
29
|
export { productStatsSyncHandler } from "./productStatsSync";
|
|
30
30
|
export { revenueRollupHandler } from "./revenueRollup";
|
|
31
|
+
export { priceIndexRollupHandler } from "./priceIndexRollup";
|
|
32
|
+
export { analyticsAlertEvaluateHandler } from "./analyticsAlertEvaluate";
|
|
31
33
|
export { pageViewRollupHandler } from "./pageViewRollup";
|
|
32
34
|
export { pageViewPruneHandler } from "./pageViewPrune";
|
|
33
35
|
export { dailyStatusDigestHandler } from "./dailyStatusDigest";
|
|
@@ -64,7 +66,7 @@ export { onScamReportUpdateHandler } from "./onScamReportUpdate";
|
|
|
64
66
|
// SB-UNI-Y-1 — 30-day draft prune
|
|
65
67
|
export { draftPruneHandler } from "./draftPrune";
|
|
66
68
|
// Tester program — 7-day auto-expiry sweep for the shared admin-seeded test sandbox
|
|
67
|
-
export {
|
|
69
|
+
export { testDataCleanupHandler } from "./testDataCleanup";
|
|
68
70
|
// Tester program — every-4h revert-live-edits + prune-extras refresh (faster than the 7-day sweep above)
|
|
69
71
|
// EMI — installment reminder + overdue-flagging sweep
|
|
70
72
|
export { emiInstallmentReminderHandler } from "./emiInstallmentReminder";
|
|
@@ -55,8 +55,11 @@ export declare const JOB_ERROR_MESSAGES: {
|
|
|
55
55
|
export declare const CART_TTL_DAYS = 60;
|
|
56
56
|
/** Default Notification TTL in days (matches functions/config/constants). */
|
|
57
57
|
export declare const NOTIFICATION_TTL_DAYS = 30;
|
|
58
|
-
/**
|
|
59
|
-
|
|
58
|
+
/**
|
|
59
|
+
* Test-data TTL in days. RE-EXPORTED, not re-declared — this was a second
|
|
60
|
+
* literal `7` whose only tie to the real one was a comment saying it matched.
|
|
61
|
+
*/
|
|
62
|
+
export { TEST_DATA_TTL_DAYS } from "../../../../seed/test-data-ttl";
|
|
60
63
|
/** Firestore batch write hard ceiling. */
|
|
61
64
|
export declare const BATCH_LIMIT = 500;
|
|
62
65
|
/** Firestore single-query soft ceiling for reconcile sweeps. */
|
|
@@ -58,8 +58,11 @@ export const JOB_ERROR_MESSAGES = {
|
|
|
58
58
|
export const CART_TTL_DAYS = 60;
|
|
59
59
|
/** Default Notification TTL in days (matches functions/config/constants). */
|
|
60
60
|
export const NOTIFICATION_TTL_DAYS = 30;
|
|
61
|
-
/**
|
|
62
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Test-data TTL in days. RE-EXPORTED, not re-declared — this was a second
|
|
63
|
+
* literal `7` whose only tie to the real one was a comment saying it matched.
|
|
64
|
+
*/
|
|
65
|
+
export { TEST_DATA_TTL_DAYS } from "../../../../seed/test-data-ttl";
|
|
63
66
|
/** Firestore batch write hard ceiling. */
|
|
64
67
|
export const BATCH_LIMIT = 500;
|
|
65
68
|
/** Firestore single-query soft ceiling for reconcile sweeps. */
|