@mohasinac/appkit 4.62.1 → 4.63.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.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
- package/dist/_internal/server/features/checkout/actions.js +335 -80
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.js +26 -1
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +14 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +57 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -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/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +268 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
- package/dist/_internal/shared/checkout/order-math.js +22 -36
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +0 -12
- package/dist/_internal/shared/checkout/split.js +22 -2
- package/dist/_internal/shared/content/disclaimers.js +7 -7
- package/dist/_internal/shared/content/published-values.d.ts +6 -1
- package/dist/_internal/shared/content/published-values.js +8 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
- package/dist/_internal/shared/features/admin/integrations.js +19 -9
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
- package/dist/_internal/shared/features/points/accepts.js +12 -6
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +11 -1
- package/dist/client.js +13 -0
- package/dist/constants/api-endpoints.d.ts +72 -0
- package/dist/constants/api-endpoints.js +41 -0
- package/dist/constants/field-names.d.ts +41 -0
- package/dist/constants/field-names.js +47 -0
- package/dist/features/account/components/UserOrdersView.js +28 -2
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
- package/dist/features/admin/components/AdminOrdersView.js +20 -1
- package/dist/features/admin/schemas/firestore.d.ts +32 -0
- package/dist/features/ads/schemas/booking-write.d.ts +157 -0
- package/dist/features/ads/schemas/booking-write.js +88 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
- package/dist/features/audit-log/schemas/firestore.js +74 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
- package/dist/features/cart/repository/cart.repository.d.ts +14 -0
- package/dist/features/cart/repository/cart.repository.js +20 -0
- package/dist/features/cart/schemas/firestore.d.ts +24 -0
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
- package/dist/features/orders/repository/orders.repository.d.ts +28 -1
- package/dist/features/orders/repository/orders.repository.js +60 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +202 -0
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +21 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +4 -0
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +94 -2
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +41 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -0
- package/dist/next/routing/route-map.d.ts +92 -0
- package/dist/next/routing/route-map.js +53 -0
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +22 -16
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +10 -1
- package/dist/server.d.ts +29 -1
- package/dist/server.js +158 -0
- package/dist/styles.css +349 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.js +10 -1
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +35 -0
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +3 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D56 — a SELF-SERVE ad booking: one advertiser, one placement, one period.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Flat fee per slot per period, and explicitly NOT CPM
|
|
5
|
+
*
|
|
6
|
+
* Impression counting is a write per page view, which is the exact cost
|
|
7
|
+
* pattern that suspended this project (Root Cause #104: 3.1M ISR writes
|
|
8
|
+
* against a 200K allowance, 52.52 GB of Fast Origin Transfer against 10 GB,
|
|
9
|
+
* for eleven monthly active users). A flat fee is one write per BOOKING.
|
|
10
|
+
* Nothing here counts anything per render, and nothing should be added that
|
|
11
|
+
* does.
|
|
12
|
+
*
|
|
13
|
+
* ## Why a PERIOD rather than an arbitrary date range
|
|
14
|
+
*
|
|
15
|
+
* The plan's requirement is slot-availability locking — two advertisers must
|
|
16
|
+
* not book the same slot for overlapping dates. Firestore has no range
|
|
17
|
+
* constraint, so an arbitrary `startAt`/`endAt` pair can only be checked by
|
|
18
|
+
* QUERYING for overlaps and then writing, and two concurrent bookings both
|
|
19
|
+
* read "no overlap" and both write. That race is invisible in testing and
|
|
20
|
+
* sells the same slot twice.
|
|
21
|
+
*
|
|
22
|
+
* Discretising to a period makes the conflict a DOCUMENT COLLISION instead:
|
|
23
|
+
* the booking id is derived from `(placementId, period)`, so the second
|
|
24
|
+
* writer fails on an existing document inside its own transaction and
|
|
25
|
+
* Firestore does the locking. There is no query to race against.
|
|
26
|
+
*
|
|
27
|
+
* It also matches what is being sold. "The homepage banner for November" is
|
|
28
|
+
* the product; "the homepage banner from the 9th to the 23rd" is a pricing
|
|
29
|
+
* model nobody asked for and an availability calendar nobody can render.
|
|
30
|
+
*/
|
|
31
|
+
import type { BaseDocument } from "../../../_internal/shared/types/base-document";
|
|
32
|
+
export declare const AD_BOOKING_COLLECTION = "adBookings";
|
|
33
|
+
/**
|
|
34
|
+
* A calendar month, `YYYY-MM`.
|
|
35
|
+
*
|
|
36
|
+
* 🛑 A STRING, not a Date. It is an identity — half of the booking's primary
|
|
37
|
+
* key — and a Date would make "the same month" a comparison rather than an
|
|
38
|
+
* equality, which is precisely what the document-collision lock depends on
|
|
39
|
+
* not being.
|
|
40
|
+
*/
|
|
41
|
+
export type AdPeriod = string;
|
|
42
|
+
export declare function isAdPeriod(value: string): value is AdPeriod;
|
|
43
|
+
/** The period a date falls in, in UTC. */
|
|
44
|
+
export declare function adPeriodOf(date: Date): AdPeriod;
|
|
45
|
+
/** First instant of a period, UTC. */
|
|
46
|
+
export declare function adPeriodStart(period: AdPeriod): Date;
|
|
47
|
+
/** First instant of the NEXT period — the exclusive end. */
|
|
48
|
+
export declare function adPeriodEnd(period: AdPeriod): Date;
|
|
49
|
+
/**
|
|
50
|
+
* 🛑 The booking id IS the lock. Derived, never generated.
|
|
51
|
+
*
|
|
52
|
+
* Two advertisers booking `banner-home` for `2026-11` compute the same id and
|
|
53
|
+
* the second `tx.create` throws `ALREADY_EXISTS` — which is the whole
|
|
54
|
+
* availability guarantee, enforced by Firestore rather than by a check
|
|
55
|
+
* somebody could forget to run.
|
|
56
|
+
*/
|
|
57
|
+
export declare function adBookingId(placementId: string, period: AdPeriod): string;
|
|
58
|
+
/**
|
|
59
|
+
* Payment state. Deliberately coarse: this records whether the slot is PAID
|
|
60
|
+
* FOR, not how. The invoice and the ledger posting are Part 4's job and carry
|
|
61
|
+
* the detail; duplicating any of it here would be two records of one fact.
|
|
62
|
+
*/
|
|
63
|
+
export declare const AD_BOOKING_PAYMENT_STATES: readonly ["unpaid", "paid", "refunded"];
|
|
64
|
+
export type AdBookingPaymentState = (typeof AD_BOOKING_PAYMENT_STATES)[number];
|
|
65
|
+
/**
|
|
66
|
+
* Creative moderation. **Nothing renders on the site until `approved`.**
|
|
67
|
+
*
|
|
68
|
+
* 🛑 `pending` is the initial value and there is no way to write `approved`
|
|
69
|
+
* through the advertiser's own schema — see `adBookingWriteSchema`. A
|
|
70
|
+
* self-serve flow where the submitter can set their own moderation state is
|
|
71
|
+
* not moderated; it is a form with an extra field (Root Cause #76 — a field
|
|
72
|
+
* the form can send is a field the form can destroy, here inverted into a
|
|
73
|
+
* field the form can grant itself).
|
|
74
|
+
*/
|
|
75
|
+
export declare const AD_MODERATION_STATES: readonly ["pending", "approved", "rejected"];
|
|
76
|
+
export type AdModerationState = (typeof AD_MODERATION_STATES)[number];
|
|
77
|
+
export interface AdBookingCreative {
|
|
78
|
+
title: string;
|
|
79
|
+
body?: string;
|
|
80
|
+
imageUrl?: string;
|
|
81
|
+
ctaLabel?: string;
|
|
82
|
+
ctaHref?: string;
|
|
83
|
+
}
|
|
84
|
+
export interface AdBookingDocument extends BaseDocument {
|
|
85
|
+
/** Matches a `SiteSettingsAdPlacement.id`. */
|
|
86
|
+
placementId: string;
|
|
87
|
+
period: AdPeriod;
|
|
88
|
+
/** Auth uid of the advertiser. */
|
|
89
|
+
advertiserUid: string;
|
|
90
|
+
/** Denormalised for the admin queue — a moderator should not fan out to `users`. */
|
|
91
|
+
advertiserName?: string;
|
|
92
|
+
/**
|
|
93
|
+
* Rupees, SNAPSHOTTED at booking.
|
|
94
|
+
*
|
|
95
|
+
* 🛑 Never recomputed from the live rate card. The rate is a price the
|
|
96
|
+
* advertiser agreed to; re-deriving it at render or invoice time would
|
|
97
|
+
* silently reprice a sold slot the first time an admin edits the card —
|
|
98
|
+
* the same rule `sourceContext` follows for order provenance.
|
|
99
|
+
*/
|
|
100
|
+
priceRs: number;
|
|
101
|
+
paymentState: AdBookingPaymentState;
|
|
102
|
+
/** Set by the Part 4 invoicing path once the booking is billed. */
|
|
103
|
+
invoiceId?: string;
|
|
104
|
+
creative: AdBookingCreative;
|
|
105
|
+
moderation: {
|
|
106
|
+
state: AdModerationState;
|
|
107
|
+
/** Required on a rejection — see `moderateAdCreative`. */
|
|
108
|
+
note?: string;
|
|
109
|
+
reviewedAt?: Date;
|
|
110
|
+
/**
|
|
111
|
+
* Staff uid. 🛑 Never returned to the advertiser: who rejected you is
|
|
112
|
+
* not information a self-serve submitter needs, and naming an individual
|
|
113
|
+
* moderator invites them being contacted directly.
|
|
114
|
+
*/
|
|
115
|
+
reviewedBy?: string;
|
|
116
|
+
};
|
|
117
|
+
/** Set when an admin or the advertiser cancels. A cancelled booking frees nothing — see below. */
|
|
118
|
+
cancelledAt?: Date;
|
|
119
|
+
cancelledReason?: string;
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Does this booking put a creative on the page right now?
|
|
123
|
+
*
|
|
124
|
+
* 🛑 All four conditions, and the order is not an accident — moderation is
|
|
125
|
+
* checked before the calendar so an approved-but-future booking and an
|
|
126
|
+
* unreviewed current one are distinguishable in a debugger.
|
|
127
|
+
*/
|
|
128
|
+
export declare function isAdBookingLive(booking: Pick<AdBookingDocument, "moderation" | "paymentState" | "period" | "cancelledAt">, now?: Date): boolean;
|
|
129
|
+
/**
|
|
130
|
+
* 🛑 A cancelled booking KEEPS its document, and therefore keeps the slot
|
|
131
|
+
* locked for its own period.
|
|
132
|
+
*
|
|
133
|
+
* Deleting it would free the slot — which sounds like the point and is the
|
|
134
|
+
* wrong behaviour twice over: the advertiser has paid for a period that has
|
|
135
|
+
* possibly already started, and re-selling a month mid-flight means two
|
|
136
|
+
* creatives were live in one slot at different times with one invoice
|
|
137
|
+
* between them. Freeing a period is an ADMIN act (`releaseAdSlot`), done
|
|
138
|
+
* knowingly, not a side effect of a cancel button.
|
|
139
|
+
*/
|
|
140
|
+
export declare const AD_BOOKING_CANCELLED_KEEPS_SLOT = true;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* D56 — a SELF-SERVE ad booking: one advertiser, one placement, one period.
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 Flat fee per slot per period, and explicitly NOT CPM
|
|
5
|
+
*
|
|
6
|
+
* Impression counting is a write per page view, which is the exact cost
|
|
7
|
+
* pattern that suspended this project (Root Cause #104: 3.1M ISR writes
|
|
8
|
+
* against a 200K allowance, 52.52 GB of Fast Origin Transfer against 10 GB,
|
|
9
|
+
* for eleven monthly active users). A flat fee is one write per BOOKING.
|
|
10
|
+
* Nothing here counts anything per render, and nothing should be added that
|
|
11
|
+
* does.
|
|
12
|
+
*
|
|
13
|
+
* ## Why a PERIOD rather than an arbitrary date range
|
|
14
|
+
*
|
|
15
|
+
* The plan's requirement is slot-availability locking — two advertisers must
|
|
16
|
+
* not book the same slot for overlapping dates. Firestore has no range
|
|
17
|
+
* constraint, so an arbitrary `startAt`/`endAt` pair can only be checked by
|
|
18
|
+
* QUERYING for overlaps and then writing, and two concurrent bookings both
|
|
19
|
+
* read "no overlap" and both write. That race is invisible in testing and
|
|
20
|
+
* sells the same slot twice.
|
|
21
|
+
*
|
|
22
|
+
* Discretising to a period makes the conflict a DOCUMENT COLLISION instead:
|
|
23
|
+
* the booking id is derived from `(placementId, period)`, so the second
|
|
24
|
+
* writer fails on an existing document inside its own transaction and
|
|
25
|
+
* Firestore does the locking. There is no query to race against.
|
|
26
|
+
*
|
|
27
|
+
* It also matches what is being sold. "The homepage banner for November" is
|
|
28
|
+
* the product; "the homepage banner from the 9th to the 23rd" is a pricing
|
|
29
|
+
* model nobody asked for and an availability calendar nobody can render.
|
|
30
|
+
*/
|
|
31
|
+
export const AD_BOOKING_COLLECTION = "adBookings";
|
|
32
|
+
const PERIOD_PATTERN = /^\d{4}-(0[1-9]|1[0-2])$/;
|
|
33
|
+
export function isAdPeriod(value) {
|
|
34
|
+
return PERIOD_PATTERN.test(value);
|
|
35
|
+
}
|
|
36
|
+
/** The period a date falls in, in UTC. */
|
|
37
|
+
export function adPeriodOf(date) {
|
|
38
|
+
const y = date.getUTCFullYear();
|
|
39
|
+
const m = String(date.getUTCMonth() + 1).padStart(2, "0");
|
|
40
|
+
return `${y}-${m}`;
|
|
41
|
+
}
|
|
42
|
+
/** First instant of a period, UTC. */
|
|
43
|
+
export function adPeriodStart(period) {
|
|
44
|
+
const [y, m] = period.split("-").map(Number);
|
|
45
|
+
return new Date(Date.UTC(y, m - 1, 1, 0, 0, 0, 0));
|
|
46
|
+
}
|
|
47
|
+
/** First instant of the NEXT period — the exclusive end. */
|
|
48
|
+
export function adPeriodEnd(period) {
|
|
49
|
+
const [y, m] = period.split("-").map(Number);
|
|
50
|
+
return new Date(Date.UTC(y, m, 1, 0, 0, 0, 0));
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* 🛑 The booking id IS the lock. Derived, never generated.
|
|
54
|
+
*
|
|
55
|
+
* Two advertisers booking `banner-home` for `2026-11` compute the same id and
|
|
56
|
+
* the second `tx.create` throws `ALREADY_EXISTS` — which is the whole
|
|
57
|
+
* availability guarantee, enforced by Firestore rather than by a check
|
|
58
|
+
* somebody could forget to run.
|
|
59
|
+
*/
|
|
60
|
+
export function adBookingId(placementId, period) {
|
|
61
|
+
return `${placementId}__${period}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Payment state. Deliberately coarse: this records whether the slot is PAID
|
|
65
|
+
* FOR, not how. The invoice and the ledger posting are Part 4's job and carry
|
|
66
|
+
* the detail; duplicating any of it here would be two records of one fact.
|
|
67
|
+
*/
|
|
68
|
+
export const AD_BOOKING_PAYMENT_STATES = ["unpaid", "paid", "refunded"];
|
|
69
|
+
/**
|
|
70
|
+
* Creative moderation. **Nothing renders on the site until `approved`.**
|
|
71
|
+
*
|
|
72
|
+
* 🛑 `pending` is the initial value and there is no way to write `approved`
|
|
73
|
+
* through the advertiser's own schema — see `adBookingWriteSchema`. A
|
|
74
|
+
* self-serve flow where the submitter can set their own moderation state is
|
|
75
|
+
* not moderated; it is a form with an extra field (Root Cause #76 — a field
|
|
76
|
+
* the form can send is a field the form can destroy, here inverted into a
|
|
77
|
+
* field the form can grant itself).
|
|
78
|
+
*/
|
|
79
|
+
export const AD_MODERATION_STATES = ["pending", "approved", "rejected"];
|
|
80
|
+
/**
|
|
81
|
+
* Does this booking put a creative on the page right now?
|
|
82
|
+
*
|
|
83
|
+
* 🛑 All four conditions, and the order is not an accident — moderation is
|
|
84
|
+
* checked before the calendar so an approved-but-future booking and an
|
|
85
|
+
* unreviewed current one are distinguishable in a debugger.
|
|
86
|
+
*/
|
|
87
|
+
export function isAdBookingLive(booking, now = new Date()) {
|
|
88
|
+
if (booking.cancelledAt)
|
|
89
|
+
return false;
|
|
90
|
+
if (booking.moderation.state !== "approved")
|
|
91
|
+
return false;
|
|
92
|
+
if (booking.paymentState !== "paid")
|
|
93
|
+
return false;
|
|
94
|
+
const start = adPeriodStart(booking.period);
|
|
95
|
+
const end = adPeriodEnd(booking.period);
|
|
96
|
+
return now >= start && now < end;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* 🛑 A cancelled booking KEEPS its document, and therefore keeps the slot
|
|
100
|
+
* locked for its own period.
|
|
101
|
+
*
|
|
102
|
+
* Deleting it would free the slot — which sounds like the point and is the
|
|
103
|
+
* wrong behaviour twice over: the advertiser has paid for a period that has
|
|
104
|
+
* possibly already started, and re-selling a month mid-flight means two
|
|
105
|
+
* creatives were live in one slot at different times with one invoice
|
|
106
|
+
* between them. Freeing a period is an ADMIN act (`releaseAdSlot`), done
|
|
107
|
+
* knowingly, not a side effect of a cancel button.
|
|
108
|
+
*/
|
|
109
|
+
export const AD_BOOKING_CANCELLED_KEEPS_SLOT = true;
|
|
@@ -276,7 +276,9 @@ export class BidRepository extends BaseRepository {
|
|
|
276
276
|
reason: ctx?.reason,
|
|
277
277
|
note: ctx?.note,
|
|
278
278
|
// `userName`/`userEmail` are denormalised onto every bid, and
|
|
279
|
-
// `encryptPiiFields`
|
|
279
|
+
// `encryptPiiFields` covers a field only where it is NAMED. It CAN reach
|
|
280
|
+
// into an array via `array[].key`, but a bare key does not — so an
|
|
281
|
+
// undeclared nested value is stored in plaintext.
|
|
280
282
|
piiFields: BID_HISTORY_PII_FIELDS,
|
|
281
283
|
});
|
|
282
284
|
return withEntry ?? patch;
|
|
@@ -59,6 +59,61 @@ export declare const AdminAuditActionValues: {
|
|
|
59
59
|
* returns the existing row, which is not an act worth logging.
|
|
60
60
|
*/
|
|
61
61
|
readonly DISPUTE_REASON_CREATE: "dispute_reason_create";
|
|
62
|
+
readonly DISPUTE_REASON_RETIRE: "dispute_reason_retire";
|
|
63
|
+
/**
|
|
64
|
+
* An admin changed a platform rate through `/admin/pricing`.
|
|
65
|
+
*
|
|
66
|
+
* 🛑 Recorded here as well as in `siteSettings.rateChanges`, and the two are
|
|
67
|
+
* not redundant. The rate log answers "when did THIS rate last move" and is
|
|
68
|
+
* capped, so it can lose its oldest entries; this answers "what have admins
|
|
69
|
+
* done lately" across every privileged act, and is the one that has to
|
|
70
|
+
* survive. Same split as `points_adjust` against its ledger.
|
|
71
|
+
*
|
|
72
|
+
* Every rate here is money the platform charges or withholds, so this is the
|
|
73
|
+
* same class as `coupon_update` and `payout_mark_paid`.
|
|
74
|
+
*/
|
|
75
|
+
readonly PRICING_UPDATE: "pricing_update";
|
|
76
|
+
/**
|
|
77
|
+
* An admin granted or revoked `trustedSeller` on a store.
|
|
78
|
+
*
|
|
79
|
+
* 🛑 Its OWN action rather than folded into `store_status_change`, because
|
|
80
|
+
* "what changed about this store" and "who accepted this risk" are
|
|
81
|
+
* different questions. Trusting a seller means paying before delivery, so
|
|
82
|
+
* the platform carries a lost parcel and a non-delivery dispute — and when
|
|
83
|
+
* one of those lands, the question is who decided and when.
|
|
84
|
+
*
|
|
85
|
+
* Recorded on a real change only. Re-saving an unrelated field on an
|
|
86
|
+
* already-trusted store writes nothing; a log of non-events stops being
|
|
87
|
+
* read.
|
|
88
|
+
*/
|
|
89
|
+
readonly STORE_TRUST_CHANGE: "store_trust_change";
|
|
90
|
+
/** A role's permission set changed — the definition of who can do what. */
|
|
91
|
+
readonly ROLE_CHANGE: "role_change";
|
|
92
|
+
/**
|
|
93
|
+
* A tax code's rate or HSN mapping changed.
|
|
94
|
+
*
|
|
95
|
+
* 🛑 The one STATUTORY thing an admin can edit, which is why
|
|
96
|
+
* `/admin/pricing` deliberately does not mirror it. A wrong rate here is a
|
|
97
|
+
* wrong return, so "who set this and when" is the question an assessment
|
|
98
|
+
* asks first.
|
|
99
|
+
*/
|
|
100
|
+
readonly TAX_CODE_CHANGE: "tax_code_change";
|
|
101
|
+
/** Sessions forcibly revoked — the user is signed out everywhere, now. */
|
|
102
|
+
readonly SESSION_REVOKE: "session_revoke";
|
|
103
|
+
/** A bulk operation over many users at once. */
|
|
104
|
+
readonly USER_BULK_ACTION: "user_bulk_action";
|
|
105
|
+
/** A moderation report resolved, dismissed or actioned. */
|
|
106
|
+
readonly MODERATION_DECISION: "moderation_decision";
|
|
107
|
+
/** A payment case claimed or resolved — money a buyer says did not arrive. */
|
|
108
|
+
readonly PAYMENT_CASE_DECISION: "payment_case_decision";
|
|
109
|
+
/**
|
|
110
|
+
* A scammer profile edited.
|
|
111
|
+
*
|
|
112
|
+
* 🛑 Public-facing accusation. The profile names a person or a payment
|
|
113
|
+
* handle, so the wording and the status are reputational — and the one
|
|
114
|
+
* question that follows a complaint is who wrote it.
|
|
115
|
+
*/
|
|
116
|
+
readonly SCAMMER_PROFILE_UPDATE: "scammer_profile_update";
|
|
62
117
|
};
|
|
63
118
|
export type AdminAuditAction = (typeof AdminAuditActionValues)[keyof typeof AdminAuditActionValues];
|
|
64
119
|
export interface AdminAuditLogDocument extends BaseDocument {
|
|
@@ -127,5 +182,60 @@ export declare const ADMIN_AUDIT_LOG_FIELDS: {
|
|
|
127
182
|
* returns the existing row, which is not an act worth logging.
|
|
128
183
|
*/
|
|
129
184
|
readonly DISPUTE_REASON_CREATE: "dispute_reason_create";
|
|
185
|
+
readonly DISPUTE_REASON_RETIRE: "dispute_reason_retire";
|
|
186
|
+
/**
|
|
187
|
+
* An admin changed a platform rate through `/admin/pricing`.
|
|
188
|
+
*
|
|
189
|
+
* 🛑 Recorded here as well as in `siteSettings.rateChanges`, and the two are
|
|
190
|
+
* not redundant. The rate log answers "when did THIS rate last move" and is
|
|
191
|
+
* capped, so it can lose its oldest entries; this answers "what have admins
|
|
192
|
+
* done lately" across every privileged act, and is the one that has to
|
|
193
|
+
* survive. Same split as `points_adjust` against its ledger.
|
|
194
|
+
*
|
|
195
|
+
* Every rate here is money the platform charges or withholds, so this is the
|
|
196
|
+
* same class as `coupon_update` and `payout_mark_paid`.
|
|
197
|
+
*/
|
|
198
|
+
readonly PRICING_UPDATE: "pricing_update";
|
|
199
|
+
/**
|
|
200
|
+
* An admin granted or revoked `trustedSeller` on a store.
|
|
201
|
+
*
|
|
202
|
+
* 🛑 Its OWN action rather than folded into `store_status_change`, because
|
|
203
|
+
* "what changed about this store" and "who accepted this risk" are
|
|
204
|
+
* different questions. Trusting a seller means paying before delivery, so
|
|
205
|
+
* the platform carries a lost parcel and a non-delivery dispute — and when
|
|
206
|
+
* one of those lands, the question is who decided and when.
|
|
207
|
+
*
|
|
208
|
+
* Recorded on a real change only. Re-saving an unrelated field on an
|
|
209
|
+
* already-trusted store writes nothing; a log of non-events stops being
|
|
210
|
+
* read.
|
|
211
|
+
*/
|
|
212
|
+
readonly STORE_TRUST_CHANGE: "store_trust_change";
|
|
213
|
+
/** A role's permission set changed — the definition of who can do what. */
|
|
214
|
+
readonly ROLE_CHANGE: "role_change";
|
|
215
|
+
/**
|
|
216
|
+
* A tax code's rate or HSN mapping changed.
|
|
217
|
+
*
|
|
218
|
+
* 🛑 The one STATUTORY thing an admin can edit, which is why
|
|
219
|
+
* `/admin/pricing` deliberately does not mirror it. A wrong rate here is a
|
|
220
|
+
* wrong return, so "who set this and when" is the question an assessment
|
|
221
|
+
* asks first.
|
|
222
|
+
*/
|
|
223
|
+
readonly TAX_CODE_CHANGE: "tax_code_change";
|
|
224
|
+
/** Sessions forcibly revoked — the user is signed out everywhere, now. */
|
|
225
|
+
readonly SESSION_REVOKE: "session_revoke";
|
|
226
|
+
/** A bulk operation over many users at once. */
|
|
227
|
+
readonly USER_BULK_ACTION: "user_bulk_action";
|
|
228
|
+
/** A moderation report resolved, dismissed or actioned. */
|
|
229
|
+
readonly MODERATION_DECISION: "moderation_decision";
|
|
230
|
+
/** A payment case claimed or resolved — money a buyer says did not arrive. */
|
|
231
|
+
readonly PAYMENT_CASE_DECISION: "payment_case_decision";
|
|
232
|
+
/**
|
|
233
|
+
* A scammer profile edited.
|
|
234
|
+
*
|
|
235
|
+
* 🛑 Public-facing accusation. The profile names a person or a payment
|
|
236
|
+
* handle, so the wording and the status are reputational — and the one
|
|
237
|
+
* question that follows a complaint is who wrote it.
|
|
238
|
+
*/
|
|
239
|
+
readonly SCAMMER_PROFILE_UPDATE: "scammer_profile_update";
|
|
130
240
|
};
|
|
131
241
|
};
|
|
@@ -57,6 +57,80 @@ export const AdminAuditActionValues = {
|
|
|
57
57
|
* returns the existing row, which is not an act worth logging.
|
|
58
58
|
*/
|
|
59
59
|
DISPUTE_REASON_CREATE: "dispute_reason_create",
|
|
60
|
+
/*
|
|
61
|
+
* Retiring is recorded and reactivating is not, which is deliberate rather
|
|
62
|
+
* than an omission. Retiring REMOVES an option buyers could pick, so "who
|
|
63
|
+
* narrowed the list, and when" is the question an audit of a dispute trend
|
|
64
|
+
* needs answered. Putting a row back restores the documented default, and a
|
|
65
|
+
* log of both would read as churn.
|
|
66
|
+
*/
|
|
67
|
+
DISPUTE_REASON_RETIRE: "dispute_reason_retire",
|
|
68
|
+
/**
|
|
69
|
+
* An admin changed a platform rate through `/admin/pricing`.
|
|
70
|
+
*
|
|
71
|
+
* 🛑 Recorded here as well as in `siteSettings.rateChanges`, and the two are
|
|
72
|
+
* not redundant. The rate log answers "when did THIS rate last move" and is
|
|
73
|
+
* capped, so it can lose its oldest entries; this answers "what have admins
|
|
74
|
+
* done lately" across every privileged act, and is the one that has to
|
|
75
|
+
* survive. Same split as `points_adjust` against its ledger.
|
|
76
|
+
*
|
|
77
|
+
* Every rate here is money the platform charges or withholds, so this is the
|
|
78
|
+
* same class as `coupon_update` and `payout_mark_paid`.
|
|
79
|
+
*/
|
|
80
|
+
PRICING_UPDATE: "pricing_update",
|
|
81
|
+
/**
|
|
82
|
+
* An admin granted or revoked `trustedSeller` on a store.
|
|
83
|
+
*
|
|
84
|
+
* 🛑 Its OWN action rather than folded into `store_status_change`, because
|
|
85
|
+
* "what changed about this store" and "who accepted this risk" are
|
|
86
|
+
* different questions. Trusting a seller means paying before delivery, so
|
|
87
|
+
* the platform carries a lost parcel and a non-delivery dispute — and when
|
|
88
|
+
* one of those lands, the question is who decided and when.
|
|
89
|
+
*
|
|
90
|
+
* Recorded on a real change only. Re-saving an unrelated field on an
|
|
91
|
+
* already-trusted store writes nothing; a log of non-events stops being
|
|
92
|
+
* read.
|
|
93
|
+
*/
|
|
94
|
+
STORE_TRUST_CHANGE: "store_trust_change",
|
|
95
|
+
/*
|
|
96
|
+
* ── Part 14 / A5 ─────────────────────────────────────────────────────────
|
|
97
|
+
*
|
|
98
|
+
* `audit-privileged-write-unlogged` found 14 admin write paths that record
|
|
99
|
+
* nothing. These are the action values the ones being closed now need; the
|
|
100
|
+
* rest stay on the ratchet until each is given one deliberately.
|
|
101
|
+
*
|
|
102
|
+
* 🛑 Each is a SEPARATE value rather than a shared `admin_write`. The log's
|
|
103
|
+
* question is "what have admins done lately", and an undifferentiated value
|
|
104
|
+
* answers it with a row that says only "something". Filtering is the whole
|
|
105
|
+
* point of the collection.
|
|
106
|
+
*/
|
|
107
|
+
/** A role's permission set changed — the definition of who can do what. */
|
|
108
|
+
ROLE_CHANGE: "role_change",
|
|
109
|
+
/**
|
|
110
|
+
* A tax code's rate or HSN mapping changed.
|
|
111
|
+
*
|
|
112
|
+
* 🛑 The one STATUTORY thing an admin can edit, which is why
|
|
113
|
+
* `/admin/pricing` deliberately does not mirror it. A wrong rate here is a
|
|
114
|
+
* wrong return, so "who set this and when" is the question an assessment
|
|
115
|
+
* asks first.
|
|
116
|
+
*/
|
|
117
|
+
TAX_CODE_CHANGE: "tax_code_change",
|
|
118
|
+
/** Sessions forcibly revoked — the user is signed out everywhere, now. */
|
|
119
|
+
SESSION_REVOKE: "session_revoke",
|
|
120
|
+
/** A bulk operation over many users at once. */
|
|
121
|
+
USER_BULK_ACTION: "user_bulk_action",
|
|
122
|
+
/** A moderation report resolved, dismissed or actioned. */
|
|
123
|
+
MODERATION_DECISION: "moderation_decision",
|
|
124
|
+
/** A payment case claimed or resolved — money a buyer says did not arrive. */
|
|
125
|
+
PAYMENT_CASE_DECISION: "payment_case_decision",
|
|
126
|
+
/**
|
|
127
|
+
* A scammer profile edited.
|
|
128
|
+
*
|
|
129
|
+
* 🛑 Public-facing accusation. The profile names a person or a payment
|
|
130
|
+
* handle, so the wording and the status are reputational — and the one
|
|
131
|
+
* question that follows a complaint is who wrote it.
|
|
132
|
+
*/
|
|
133
|
+
SCAMMER_PROFILE_UPDATE: "scammer_profile_update",
|
|
60
134
|
};
|
|
61
135
|
export const ADMIN_AUDIT_LOG_FIELDS = {
|
|
62
136
|
ACTOR_UID: "actorUid",
|
|
@@ -23,6 +23,64 @@ export type UpdateProfileInput = {
|
|
|
23
23
|
export declare function updateUserProfile(userId: string, input: UpdateProfileInput): Promise<UserDocument>;
|
|
24
24
|
export declare function getUserProfile(userId: string): Promise<UserDocument | null>;
|
|
25
25
|
export declare function getUserSessions(userId: string): Promise<import("..").SessionDocument[]>;
|
|
26
|
+
/**
|
|
27
|
+
* What an account has actually DONE, counted from the collections that record
|
|
28
|
+
* it rather than read off `users/{uid}.stats`.
|
|
29
|
+
*
|
|
30
|
+
* 🛑 `users/{uid}.stats.*` HAS NO WRITER. Measured 2026-10-23: every occurrence
|
|
31
|
+
* of `stats.totalOrders` / `auctionsWon` / `itemsSold` / `reviewsCount` across
|
|
32
|
+
* both repos is a READ, and the only `stats.itemsSold` write in the codebase is
|
|
33
|
+
* `StoreRepository`'s, on the STORE document. So the stored numbers are
|
|
34
|
+
* whatever `users-seed-data.ts` hardcoded — the admin persona ships
|
|
35
|
+
* `{ totalOrders: 20, auctionsWon: 2, itemsSold: 200 }` — and a real signup gets
|
|
36
|
+
* `DEFAULT_USER_DATA.stats`, all zeros, which stay zero after twenty orders.
|
|
37
|
+
*
|
|
38
|
+
* A number nobody maintains is worse than no number: it is indistinguishable
|
|
39
|
+
* from a maintained one at every call site. These are counted live instead.
|
|
40
|
+
*
|
|
41
|
+
* ## Cost
|
|
42
|
+
*
|
|
43
|
+
* Three Firestore AGGREGATIONS, in parallel — billed at one read per 1000 index
|
|
44
|
+
* entries matched, so ~3 reads for any real account and flat as history grows.
|
|
45
|
+
* That is what makes this affordable on `GET /api/user/profile`, which the
|
|
46
|
+
* session hits on every page load. Never replace one with `findByUser(...).length`:
|
|
47
|
+
* that downloads the whole history to learn its size.
|
|
48
|
+
*
|
|
49
|
+
* ## Failure is `undefined`, never 0
|
|
50
|
+
*
|
|
51
|
+
* A failed count means "unknown", and 0 means "none" — Root Cause #72's rule.
|
|
52
|
+
* Callers render nothing for `undefined`, so a transient Firestore error hides
|
|
53
|
+
* a tile rather than telling a buyer with 30 orders that they have none.
|
|
54
|
+
*/
|
|
55
|
+
export interface BuyerActivityStats {
|
|
56
|
+
/** Orders this account placed, all lanes. */
|
|
57
|
+
totalOrders?: number;
|
|
58
|
+
/** Orders that came from winning (or buying out) an auction. */
|
|
59
|
+
auctionsWon?: number;
|
|
60
|
+
/** Reviews this account WROTE — the set `/user/reviews` lists. */
|
|
61
|
+
reviewsCount?: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* 🛑 React.cache-wrapped: `/api/user/profile` and the public profile page can
|
|
65
|
+
* both ask for the same uid inside one request, and three aggregations is a
|
|
66
|
+
* cost worth paying once.
|
|
67
|
+
*/
|
|
68
|
+
export declare const getBuyerActivityStats: (uid: string) => Promise<BuyerActivityStats>;
|
|
69
|
+
/**
|
|
70
|
+
* Buyer activity plus, for an account that owns a store, the one SELLER number
|
|
71
|
+
* that is genuinely maintained.
|
|
72
|
+
*
|
|
73
|
+
* `stores/{storeId}.stats.itemsSold` has real writers — `incrementItemsSold`
|
|
74
|
+
* from the fulfilment trigger and `setStats` from the nightly reconciliation —
|
|
75
|
+
* unlike its namesake on the user document. It is read here, from the store, so
|
|
76
|
+
* a seller's PUBLIC profile can keep showing it honestly.
|
|
77
|
+
*
|
|
78
|
+
* It deliberately does NOT reach the sidebar's own profile block, which is the
|
|
79
|
+
* buyer's view of their own account: see `AppLayoutShell`.
|
|
80
|
+
*/
|
|
81
|
+
export declare function getProfileStats(user: Pick<UserDocument, "uid" | "id" | "storeId">): Promise<BuyerActivityStats & {
|
|
82
|
+
itemsSold?: number;
|
|
83
|
+
}>;
|
|
26
84
|
/**
|
|
27
85
|
* Resolve a profile by slug, then document id, then Auth uid.
|
|
28
86
|
*
|
|
@@ -51,8 +109,18 @@ export declare function resolveProfileUser(identifier: string): Promise<UserDocu
|
|
|
51
109
|
* page + generateMetadata pair is memoised per argument list, so the second call
|
|
52
110
|
* is free. Note it caches the NARROW public projection, which is what both
|
|
53
111
|
* callers want; anything needing the whole document still goes to the repository.
|
|
112
|
+
*
|
|
113
|
+
* 🛑 `stats` is DERIVED by `getProfileStats`, not `user.stats`. The stored field
|
|
114
|
+
* has no writer and is seed fiction — see `getBuyerActivityStats`. It is derived
|
|
115
|
+
* here as well as on the session endpoint deliberately: fixing only the sidebar
|
|
116
|
+
* would have left a signed-in user reading real counts in the nav and seeded
|
|
117
|
+
* ones on their own public profile, which is a worse state than both being wrong.
|
|
54
118
|
*/
|
|
55
|
-
export declare const getPublicUserProfile: (userId: string) => Promise<Pick<UserDocument, "id" | "slug" | "displayName" | "photoURL" | "role" | "createdAt" | "storeId" | "storeSlug" | "publicProfile"
|
|
119
|
+
export declare const getPublicUserProfile: (userId: string) => Promise<(Pick<UserDocument, "id" | "slug" | "displayName" | "photoURL" | "role" | "createdAt" | "storeId" | "storeSlug" | "publicProfile"> & {
|
|
120
|
+
stats: BuyerActivityStats & {
|
|
121
|
+
itemsSold?: number;
|
|
122
|
+
};
|
|
123
|
+
}) | null>;
|
|
56
124
|
/** Fetch approved reviews for a store. storeId === storeSlug in this project. */
|
|
57
125
|
export declare function getSellerReviews(storeId: string): Promise<Review[]>;
|
|
58
126
|
export declare function getProfileStoreProducts(storeId: string): Promise<import("../../products").ProductDocument[]>;
|
|
@@ -12,6 +12,8 @@ import { sessionRepository } from "../repository/session.repository.js";
|
|
|
12
12
|
import { productRepository } from "../../products/repository/products.repository.js";
|
|
13
13
|
import { ProductStatusValues } from "../../products/schemas/index.js";
|
|
14
14
|
import { reviewRepository } from "../../reviews/repository/reviews.repository.js";
|
|
15
|
+
import { orderRepository } from "../../orders/repository/orders.repository.js";
|
|
16
|
+
import { storeRepository } from "../../stores/repository/store.repository.js";
|
|
15
17
|
export async function updateUserProfile(userId, input) {
|
|
16
18
|
const finalPhotoURL = await finalizeStagedMediaField(input.photoURL);
|
|
17
19
|
const data = finalPhotoURL !== undefined ? { ...input, photoURL: finalPhotoURL } : input;
|
|
@@ -23,6 +25,59 @@ export async function getUserProfile(userId) {
|
|
|
23
25
|
export async function getUserSessions(userId) {
|
|
24
26
|
return sessionRepository.findAllByUser(userId);
|
|
25
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* 🛑 React.cache-wrapped: `/api/user/profile` and the public profile page can
|
|
30
|
+
* both ask for the same uid inside one request, and three aggregations is a
|
|
31
|
+
* cost worth paying once.
|
|
32
|
+
*/
|
|
33
|
+
export const getBuyerActivityStats = cache(async function getBuyerActivityStats(uid) {
|
|
34
|
+
const route = "profile:buyer-activity-stats";
|
|
35
|
+
const [totalOrders, auctionsWon, reviewsCount] = await Promise.all([
|
|
36
|
+
safeRead(() => orderRepository.countForUser(uid), {
|
|
37
|
+
route,
|
|
38
|
+
key: "orders:total",
|
|
39
|
+
fallback: undefined,
|
|
40
|
+
}),
|
|
41
|
+
safeRead(() => orderRepository.countForUser(uid, "auction"), {
|
|
42
|
+
route,
|
|
43
|
+
key: "orders:auction",
|
|
44
|
+
fallback: undefined,
|
|
45
|
+
}),
|
|
46
|
+
safeRead(() => reviewRepository.countByUser(uid), {
|
|
47
|
+
route,
|
|
48
|
+
key: "reviews:written",
|
|
49
|
+
fallback: undefined,
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
return { totalOrders, auctionsWon, reviewsCount };
|
|
53
|
+
});
|
|
54
|
+
/**
|
|
55
|
+
* Buyer activity plus, for an account that owns a store, the one SELLER number
|
|
56
|
+
* that is genuinely maintained.
|
|
57
|
+
*
|
|
58
|
+
* `stores/{storeId}.stats.itemsSold` has real writers — `incrementItemsSold`
|
|
59
|
+
* from the fulfilment trigger and `setStats` from the nightly reconciliation —
|
|
60
|
+
* unlike its namesake on the user document. It is read here, from the store, so
|
|
61
|
+
* a seller's PUBLIC profile can keep showing it honestly.
|
|
62
|
+
*
|
|
63
|
+
* It deliberately does NOT reach the sidebar's own profile block, which is the
|
|
64
|
+
* buyer's view of their own account: see `AppLayoutShell`.
|
|
65
|
+
*/
|
|
66
|
+
export async function getProfileStats(user) {
|
|
67
|
+
const uid = user.uid ?? user.id;
|
|
68
|
+
const [activity, itemsSold] = await Promise.all([
|
|
69
|
+
getBuyerActivityStats(uid),
|
|
70
|
+
user.storeId
|
|
71
|
+
? safeRead(async () => (await storeRepository.findById(user.storeId))?.stats
|
|
72
|
+
?.itemsSold, {
|
|
73
|
+
route: "profile:store-items-sold",
|
|
74
|
+
key: "store:itemsSold",
|
|
75
|
+
fallback: undefined,
|
|
76
|
+
})
|
|
77
|
+
: Promise.resolve(undefined),
|
|
78
|
+
]);
|
|
79
|
+
return { ...activity, itemsSold };
|
|
80
|
+
}
|
|
26
81
|
/**
|
|
27
82
|
* Resolve a profile by slug, then document id, then Auth uid.
|
|
28
83
|
*
|
|
@@ -59,6 +114,12 @@ export async function resolveProfileUser(identifier) {
|
|
|
59
114
|
* page + generateMetadata pair is memoised per argument list, so the second call
|
|
60
115
|
* is free. Note it caches the NARROW public projection, which is what both
|
|
61
116
|
* callers want; anything needing the whole document still goes to the repository.
|
|
117
|
+
*
|
|
118
|
+
* 🛑 `stats` is DERIVED by `getProfileStats`, not `user.stats`. The stored field
|
|
119
|
+
* has no writer and is seed fiction — see `getBuyerActivityStats`. It is derived
|
|
120
|
+
* here as well as on the session endpoint deliberately: fixing only the sidebar
|
|
121
|
+
* would have left a signed-in user reading real counts in the nav and seeded
|
|
122
|
+
* ones on their own public profile, which is a worse state than both being wrong.
|
|
62
123
|
*/
|
|
63
124
|
export const getPublicUserProfile = cache(async function getPublicUserProfile(userId) {
|
|
64
125
|
const user = await resolveProfileUser(userId);
|
|
@@ -74,7 +135,7 @@ export const getPublicUserProfile = cache(async function getPublicUserProfile(us
|
|
|
74
135
|
storeId: user.storeId,
|
|
75
136
|
storeSlug: user.storeSlug,
|
|
76
137
|
publicProfile: user.publicProfile,
|
|
77
|
-
stats: user
|
|
138
|
+
stats: await getProfileStats(user),
|
|
78
139
|
};
|
|
79
140
|
});
|
|
80
141
|
/** Fetch approved reviews for a store. storeId === storeSlug in this project. */
|