@mohasinac/appkit 4.62.0 → 4.62.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/server/features/checkout/actions.d.ts +80 -0
- package/dist/_internal/server/features/checkout/actions.js +542 -26
- package/dist/_internal/server/features/invoicing/allocate.d.ts +63 -0
- package/dist/_internal/server/features/invoicing/allocate.js +162 -0
- package/dist/_internal/server/features/invoicing/credit-note.d.ts +57 -0
- package/dist/_internal/server/features/invoicing/credit-note.js +126 -0
- package/dist/_internal/server/features/invoicing/gstr1.d.ts +74 -0
- package/dist/_internal/server/features/invoicing/gstr1.js +251 -0
- package/dist/_internal/server/features/invoicing/issue.d.ts +96 -0
- package/dist/_internal/server/features/invoicing/issue.js +202 -0
- package/dist/_internal/server/features/orders/actions.d.ts +8 -1
- package/dist/_internal/server/features/orders/actions.js +81 -8
- package/dist/_internal/server/features/orders/adapters.js +33 -0
- package/dist/_internal/server/features/orders/cancellation-refund.d.ts +15 -9
- package/dist/_internal/server/features/orders/cancellation-refund.js +15 -9
- package/dist/_internal/server/features/points/award.js +46 -4
- package/dist/_internal/server/features/refunds/actions.js +26 -0
- package/dist/_internal/server/features/shipping/courier-block.d.ts +69 -0
- package/dist/_internal/server/features/shipping/courier-block.js +239 -0
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +15 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +2 -2
- package/dist/_internal/server/features/stores/adapters.js +51 -0
- package/dist/_internal/server/functions/scheduled.js +38 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +9 -2
- package/dist/_internal/server/jobs/core/dailyStatusDigest.d.ts +4 -0
- package/dist/_internal/server/jobs/core/dailyStatusDigest.js +19 -2
- package/dist/_internal/server/jobs/core/index.d.ts +2 -1
- package/dist/_internal/server/jobs/core/index.js +2 -1
- package/dist/_internal/server/jobs/core/onJobCreated.d.ts +30 -7
- package/dist/_internal/server/jobs/core/onJobCreated.js +78 -45
- package/dist/_internal/server/jobs/core/onOrderStatusChange.d.ts +7 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +62 -0
- package/dist/_internal/server/jobs/core/paymentWindowTimeout.js +33 -0
- package/dist/_internal/server/jobs/core/runJobOnce.d.ts +48 -0
- package/dist/_internal/server/jobs/core/runJobOnce.js +195 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +10 -2
- package/dist/_internal/server/jobs/handlers/onOrderStatusChange.js +17 -0
- package/dist/_internal/server/notifications/channel-health.d.ts +26 -3
- package/dist/_internal/shared/actions/action-registry.js +28 -0
- package/dist/_internal/shared/checkout/coupon-discount.d.ts +43 -1
- package/dist/_internal/shared/checkout/coupon-discount.js +26 -1
- package/dist/_internal/shared/checkout/order-math.d.ts +145 -0
- package/dist/_internal/shared/checkout/order-math.js +173 -0
- package/dist/_internal/shared/checkout/refundable-base.d.ts +25 -0
- package/dist/_internal/shared/checkout/refundable-base.js +13 -1
- package/dist/_internal/shared/checkout/split.d.ts +87 -0
- package/dist/_internal/shared/checkout/split.js +100 -0
- package/dist/_internal/shared/content/disclaimers.d.ts +88 -0
- package/dist/_internal/shared/content/disclaimers.js +217 -0
- package/dist/_internal/shared/content/published-values.d.ts +88 -0
- package/dist/_internal/shared/content/published-values.js +198 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +109 -0
- package/dist/_internal/shared/features/admin/integrations.js +253 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.d.ts +113 -0
- package/dist/_internal/shared/features/orders/dispute-reasons.js +181 -0
- package/dist/_internal/shared/features/orders/refund-copy.d.ts +1 -1
- package/dist/_internal/shared/features/orders/refund-copy.js +18 -1
- package/dist/_internal/shared/features/points/accepts.d.ts +36 -0
- package/dist/_internal/shared/features/points/accepts.js +43 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.d.ts +91 -0
- package/dist/_internal/shared/features/shipping/shared-purchase.js +108 -0
- package/dist/_internal/shared/features/site-settings/types.d.ts +11 -0
- package/dist/_internal/shared/fees/calculator.d.ts +33 -1
- package/dist/_internal/shared/fees/calculator.js +32 -2
- package/dist/client.d.ts +11 -0
- package/dist/client.js +30 -0
- package/dist/constants/api-endpoints.d.ts +42 -0
- package/dist/constants/api-endpoints.js +14 -0
- package/dist/constants/field-names.d.ts +1 -0
- package/dist/constants/field-names.js +5 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/shipping.d.ts +45 -0
- package/dist/contracts/shipping.js +15 -0
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOrdersView.js +19 -33
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +18 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +11 -2
- package/dist/features/admin/components/AdminOrdersGuideView.js +23 -2
- package/dist/features/admin/components/AdminOrdersView.js +67 -5
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/constants/filter-tabs.d.ts +6 -0
- package/dist/features/admin/constants/filter-tabs.js +6 -0
- package/dist/features/admin/constants/site-settings-tabs.d.ts +1 -1
- package/dist/features/admin/hooks/useAdminListingData.js +16 -0
- package/dist/features/admin/schemas/admin-user-form.d.ts +2 -2
- package/dist/features/admin/schemas/firestore.d.ts +30 -0
- package/dist/features/admin/schemas/firestore.js +5 -0
- package/dist/features/audit-log/schemas/firestore.d.ts +30 -0
- package/dist/features/audit-log/schemas/firestore.js +15 -0
- package/dist/features/auth/permissions/constants.js +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +13 -0
- package/dist/features/cart/components/CartPriceBreakdown.js +6 -2
- package/dist/features/cart/components/PointsTenderPicker.d.ts +61 -0
- package/dist/features/cart/components/PointsTenderPicker.js +15 -0
- package/dist/features/cart/components/StoreAddonsPicker.js +2 -1
- package/dist/features/cart/components/index.d.ts +2 -0
- package/dist/features/cart/components/index.js +1 -0
- package/dist/features/cart/repository/cart.repository.d.ts +19 -0
- package/dist/features/cart/repository/cart.repository.js +27 -0
- package/dist/features/cart/schemas/firestore.d.ts +92 -0
- package/dist/features/contact/email.d.ts +27 -10
- package/dist/features/contact/email.js +20 -3
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +38 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +80 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +64 -0
- package/dist/features/dispute-reasons/schemas/firestore.js +40 -0
- package/dist/features/faq/components/FAQAccordion.d.ts +13 -1
- package/dist/features/faq/components/FAQAccordion.js +29 -4
- package/dist/features/invoicing/schemas/firestore.d.ts +112 -0
- package/dist/features/invoicing/schemas/firestore.js +45 -0
- package/dist/features/invoicing/utils/financial-year.d.ts +43 -0
- package/dist/features/invoicing/utils/financial-year.js +53 -0
- package/dist/features/jobs/actions/enqueue-job.js +18 -1
- package/dist/features/jobs/repository/jobs.repository.d.ts +62 -1
- package/dist/features/jobs/repository/jobs.repository.js +84 -1
- package/dist/features/jobs/schemas/firestore.d.ts +80 -0
- package/dist/features/jobs/schemas/firestore.js +37 -0
- package/dist/features/lottery/schemas/zod.d.ts +6 -6
- package/dist/features/media/MediaImage.js +22 -1
- package/dist/features/media/upload/ImageUpload.js +1 -1
- package/dist/features/obligations/constants/policies.js +24 -1
- package/dist/features/orders/components/OrderPaymentSummary.js +10 -1
- package/dist/features/orders/components/OrderProvenanceCard.d.ts +8 -0
- package/dist/features/orders/components/OrderProvenanceCard.js +185 -0
- package/dist/features/orders/components/RaiseDisputeForm.d.ts +14 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +111 -0
- package/dist/features/orders/components/RefundRequestView.js +19 -1
- package/dist/features/orders/components/index.d.ts +4 -0
- package/dist/features/orders/components/index.js +2 -0
- package/dist/features/orders/constants/order-lane-tabs.d.ts +69 -0
- package/dist/features/orders/constants/order-lane-tabs.js +88 -0
- package/dist/features/orders/repository/orders.repository.d.ts +57 -0
- package/dist/features/orders/repository/orders.repository.js +145 -0
- package/dist/features/orders/schemas/firestore.d.ts +196 -1
- package/dist/features/orders/schemas/index.d.ts +54 -6
- package/dist/features/orders/schemas/index.js +12 -0
- package/dist/features/orders/types/index.d.ts +26 -0
- package/dist/features/orders/utils/courier-block.d.ts +25 -0
- package/dist/features/orders/utils/courier-block.js +23 -0
- package/dist/features/payment-cases/schemas/firestore.d.ts +26 -1
- package/dist/features/payment-cases/schemas/firestore.js +1 -0
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/ProductDetailPageView.d.ts +17 -1
- package/dist/features/products/components/ProductDetailPageView.js +2 -2
- package/dist/features/products/schemas/product-features.validators.d.ts +10 -10
- package/dist/features/promotions/actions/coupon-stacking.d.ts +30 -3
- package/dist/features/promotions/actions/coupon-stacking.js +21 -2
- package/dist/features/promotions/components/CouponCard.js +13 -0
- package/dist/features/promotions/constants/coupon-help.d.ts +2 -2
- package/dist/features/promotions/constants/coupon-help.js +9 -1
- package/dist/features/promotions/schemas/firestore.d.ts +24 -0
- package/dist/features/promotions/schemas/firestore.js +4 -0
- package/dist/features/promotions/types/index.d.ts +19 -1
- package/dist/features/scams/components/ScamProfileView.js +2 -2
- package/dist/features/scams/components/ScamRegistryView.js +2 -2
- package/dist/features/search/action-index/tab-entries.js +1 -1
- package/dist/features/seller/actions/seller-actions.js +33 -3
- package/dist/features/seller/components/SellerOrdersView.js +129 -27
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/shipments/schemas/validation.d.ts +4 -4
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +43 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +90 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +82 -0
- package/dist/features/shipping-boxes/schemas/firestore.js +53 -0
- package/dist/features/store-extensions/schemas/report-create-form.d.ts +2 -2
- package/dist/features/store-extensions/schemas/shipping-config-form.d.ts +2 -2
- package/dist/features/stores/components/StoreOrdersGuideView.js +6 -1
- package/dist/features/stores/schemas/firestore.d.ts +71 -0
- package/dist/features/stores/types/index.d.ts +19 -0
- package/dist/features/support/schemas/index.d.ts +6 -6
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +14 -1
- package/dist/next/routing/route-map.d.ts +20 -0
- package/dist/next/routing/route-map.js +10 -0
- package/dist/providers/shipping-shiprocket/index.d.ts +27 -1
- package/dist/providers/shipping-shiprocket/index.js +84 -1
- package/dist/schemas/registry.d.ts +47 -18
- package/dist/schemas/sieve.d.ts +2 -2
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +10 -1
- package/dist/server.js +39 -1
- package/dist/styles.css +64 -80
- package/dist/tailwind-utilities.css +1 -1
- package/dist/tokens/themes/default-dark.js +1 -0
- package/dist/tokens/themes/default-light.js +1 -0
- package/dist/tokens/themes/required.d.ts +1 -1
- package/dist/tokens/themes/required.js +1 -0
- package/dist/tokens/tokens.css +15 -0
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +6 -0
- package/dist/ui/rich-text/RichText.style.css +48 -79
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +1831 -1822
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raising and clearing a courier block, and telling the ADMIN about it.
|
|
3
|
+
*
|
|
4
|
+
* ## The problem this exists for
|
|
5
|
+
*
|
|
6
|
+
* Shiprocket is PREPAID and has no recharge API, so topping up the courier
|
|
7
|
+
* wallet is irreducibly a human action. If the platform books couriers, an
|
|
8
|
+
* empty wallet silently stops dispatch on orders buyers have already paid for.
|
|
9
|
+
*
|
|
10
|
+
* ## The two rules
|
|
11
|
+
*
|
|
12
|
+
* 1. 🛑 **The order is never auto-cancelled over it.** `seller-dispatch`'s
|
|
13
|
+
* `precondition` carries `!hasOpenCourierBlock(o)`, so its 2-business-day
|
|
14
|
+
* `onDue: "act"` clock PAUSES while blocked instead of cancelling the order
|
|
15
|
+
* and refunding the buyer with the seller blamed for our funding state.
|
|
16
|
+
*
|
|
17
|
+
* 2. 🛑 **The admin alert is deduped on the WALLET, never on the order.** Fifty
|
|
18
|
+
* blocked orders are ONE problem with one fix. The daily email ceiling
|
|
19
|
+
* applies to staff mail too (`send-guard.ts:92` — the kill switch spares
|
|
20
|
+
* staff, the ceiling does not), so fifty per-order emails would spend the
|
|
21
|
+
* whole 80/day budget saying the same thing fifty times and leave the
|
|
22
|
+
* operator blind to everything else.
|
|
23
|
+
*/
|
|
24
|
+
import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
|
|
25
|
+
// `adminNotificationsRepository` lives behind the repositories barrel — the
|
|
26
|
+
// same import the two existing staff-signal call sites use.
|
|
27
|
+
import { adminNotificationsRepository } from "../../../../repositories/index.js";
|
|
28
|
+
import { siteSettingsRepository } from "../../../../features/admin/repository/site-settings.repository.js";
|
|
29
|
+
import { normalizeError } from "../../../../errors/normalize.js";
|
|
30
|
+
import { serverLogger } from "../../../../monitoring/server-logger.js";
|
|
31
|
+
import { sendEmail } from "../../../../features/contact/email.js";
|
|
32
|
+
/**
|
|
33
|
+
* The dedupe key: the PROVIDER, not the order.
|
|
34
|
+
*
|
|
35
|
+
* One string, so a hundred stuck orders collapse into one case whose
|
|
36
|
+
* `observedCount` is the number of orders waiting — which is also the only
|
|
37
|
+
* number an operator actually needs in order to decide how urgently to top up.
|
|
38
|
+
*/
|
|
39
|
+
function walletCaseKey(provider) {
|
|
40
|
+
return `courier-wallet:${provider}`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* How many shipments' worth of balance counts as "low".
|
|
44
|
+
*
|
|
45
|
+
* 🛑 Expressed in SHIPMENTS, not rupees. A ₹500 floor is meaningless without
|
|
46
|
+
* knowing what a booking costs, and the question an operator is really asking
|
|
47
|
+
* is "how many more parcels can I send before this stops". Four is roughly a
|
|
48
|
+
* day's slack at this volume, which is the point: the alert should arrive while
|
|
49
|
+
* topping up is still routine.
|
|
50
|
+
*
|
|
51
|
+
* `TYPICAL_BOOKING_COST` is an ESTIMATE and is labelled one — the real figure
|
|
52
|
+
* per parcel comes from `checkServiceability` and varies by weight and
|
|
53
|
+
* destination. It only has to be the right order of magnitude to make the
|
|
54
|
+
* warning land before the wallet is actually empty.
|
|
55
|
+
*/
|
|
56
|
+
const LOW_BALANCE_SHIPMENTS = 4;
|
|
57
|
+
const TYPICAL_BOOKING_COST = 80;
|
|
58
|
+
/**
|
|
59
|
+
* Read the courier wallet and, if it is running low, alert the admin ONCE.
|
|
60
|
+
*
|
|
61
|
+
* Called from `dailyStatusDigest` — 🛑 deliberately NOT a new scheduled
|
|
62
|
+
* function. Cloud Scheduler bills per registered job and this is one read a
|
|
63
|
+
* day; the same reasoning that put the ledger reconciler inside
|
|
64
|
+
* `dailyDataCleanup`.
|
|
65
|
+
*
|
|
66
|
+
* Never throws: a wallet read is diagnostics, and a digest that fails because
|
|
67
|
+
* one provider was unreachable stops reporting the fifteen things that were
|
|
68
|
+
* fine.
|
|
69
|
+
*/
|
|
70
|
+
export async function checkCourierWalletAndAlert(
|
|
71
|
+
/*
|
|
72
|
+
* 🛑 Accepts `undefined` rather than being handed a cast.
|
|
73
|
+
* `ProviderRegistry.shipping` is declared optional, and although
|
|
74
|
+
* `providers.config.ts` always populates it, that is a runtime guarantee the
|
|
75
|
+
* TYPE does not make — and `as IShippingProvider` would silence the one
|
|
76
|
+
* signal that the registry was never initialised.
|
|
77
|
+
*/
|
|
78
|
+
provider) {
|
|
79
|
+
if (!provider) {
|
|
80
|
+
return { balance: null, low: false, alerted: false, reason: "no shipping provider" };
|
|
81
|
+
}
|
|
82
|
+
let balance;
|
|
83
|
+
try {
|
|
84
|
+
balance = await provider.getWalletBalance();
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
const e = normalizeError(err);
|
|
88
|
+
return { balance: null, low: false, alerted: false, reason: e.message };
|
|
89
|
+
}
|
|
90
|
+
if (!balance.known) {
|
|
91
|
+
/*
|
|
92
|
+
* 🛑 No alert, and no "balance 0" either. An unreadable balance is not a
|
|
93
|
+
* low balance — alerting on it would page an operator every time the
|
|
94
|
+
* network hiccupped, and they would learn to ignore the one that mattered.
|
|
95
|
+
* The reason is returned so the digest can SAY it was unreadable.
|
|
96
|
+
*/
|
|
97
|
+
return {
|
|
98
|
+
balance: null,
|
|
99
|
+
low: false,
|
|
100
|
+
alerted: false,
|
|
101
|
+
reason: balance.supported ? (balance.reason ?? "unreadable") : "no wallet",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const low = balance.amount < LOW_BALANCE_SHIPMENTS * TYPICAL_BOOKING_COST;
|
|
105
|
+
if (!low)
|
|
106
|
+
return { balance: balance.amount, low: false, alerted: false };
|
|
107
|
+
const alerted = await alertAdminOnce({
|
|
108
|
+
provider: provider.name,
|
|
109
|
+
reason: "insufficient_funds",
|
|
110
|
+
detail: `balance ${balance.amount}`,
|
|
111
|
+
}).catch((err) => {
|
|
112
|
+
const e = normalizeError(err);
|
|
113
|
+
serverLogger.error("courier-wallet: admin alert failed", {
|
|
114
|
+
provider: provider.name,
|
|
115
|
+
error: e.message,
|
|
116
|
+
});
|
|
117
|
+
return false;
|
|
118
|
+
});
|
|
119
|
+
return { balance: balance.amount, low: true, alerted };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* One case, one in-app alert, one email — per WALLET, per unresolved episode.
|
|
123
|
+
*
|
|
124
|
+
* `openOrObserve` is what makes this idempotent: a second blocked order raises
|
|
125
|
+
* `observedCount` on the existing case rather than opening a new one, and the
|
|
126
|
+
* email is only sent when a case was actually OPENED.
|
|
127
|
+
*/
|
|
128
|
+
async function alertAdminOnce(input) {
|
|
129
|
+
const { provider, reason, detail } = input;
|
|
130
|
+
const key = walletCaseKey(provider);
|
|
131
|
+
const title = reason === "insufficient_funds"
|
|
132
|
+
? `Courier wallet needs a top-up (${provider})`
|
|
133
|
+
: `Courier booking failing (${provider})`;
|
|
134
|
+
const result = await paymentCasesRepository().openOrObserve({
|
|
135
|
+
kind: "courier_unavailable",
|
|
136
|
+
/*
|
|
137
|
+
* 🛑 `subjectId` identifies THE PROBLEM, not the observation — the
|
|
138
|
+
* repository's own words, and here the problem is the wallet. Passing the
|
|
139
|
+
* order id would open one case per stuck order, which is the exact noise
|
|
140
|
+
* `openOrObserve` exists to prevent ("an operator learns to ignore it, and
|
|
141
|
+
* the first real chargeback is lost in 8,000 rows").
|
|
142
|
+
*/
|
|
143
|
+
subjectId: key,
|
|
144
|
+
severity: "error",
|
|
145
|
+
title,
|
|
146
|
+
detail: reason === "insufficient_funds"
|
|
147
|
+
? `Bookings are failing for lack of funds. Paid orders are waiting and are NOT being auto-cancelled. Top up the ${provider} wallet, then retry from the order.`
|
|
148
|
+
: `Bookings are failing (${reason}). Paid orders are waiting and are NOT being auto-cancelled.${detail ? ` Upstream: ${detail.slice(0, 200)}` : ""}`,
|
|
149
|
+
/*
|
|
150
|
+
* The entity is the STORE-agnostic provider, so `entityType: "store"` would
|
|
151
|
+
* be a lie and `"order"` would re-key it to one order. `"job"` is the
|
|
152
|
+
* closest honest member of that union: this is a piece of our own
|
|
153
|
+
* machinery failing, not one record being wrong.
|
|
154
|
+
*/
|
|
155
|
+
entityType: "job",
|
|
156
|
+
entityId: key,
|
|
157
|
+
});
|
|
158
|
+
/*
|
|
159
|
+
* 🛑 Only on a NEWLY CREATED case. This is the whole dedupe: an already-open
|
|
160
|
+
* episode has already been emailed, and re-sending it daily — or once per
|
|
161
|
+
* blocked order, once booking is wired — is the budget bug this function
|
|
162
|
+
* exists to avoid. The daily ceiling applies to staff mail too.
|
|
163
|
+
*/
|
|
164
|
+
if (!result.created)
|
|
165
|
+
return false;
|
|
166
|
+
// One durable staff row the admin inbox reads and the digest counts —
|
|
167
|
+
// `audienceUserIds: []` is the "staff signal is a record, not a blast"
|
|
168
|
+
// pattern, rather than N personal notifications.
|
|
169
|
+
await adminNotificationsRepository
|
|
170
|
+
.create({
|
|
171
|
+
/*
|
|
172
|
+
* No cast. The fields are the real `AdminNotificationDocument` ones —
|
|
173
|
+
* `category`, not `type` — because a cast here would silence every other
|
|
174
|
+
* field's check too, which is how the `as never` on the older catalogue
|
|
175
|
+
* handler hid a wrong `createdAt` (CLAUDE.md § Notifications).
|
|
176
|
+
*/
|
|
177
|
+
category: "system",
|
|
178
|
+
severity: "error",
|
|
179
|
+
title,
|
|
180
|
+
body: `${title}. Paid orders are waiting; none will be auto-cancelled while this is open.`,
|
|
181
|
+
isRead: false,
|
|
182
|
+
entityType: "job",
|
|
183
|
+
entityId: key,
|
|
184
|
+
audienceUserIds: [],
|
|
185
|
+
})
|
|
186
|
+
.catch((err) => {
|
|
187
|
+
void normalizeError(err);
|
|
188
|
+
});
|
|
189
|
+
await emailStaff({ title, provider, reason });
|
|
190
|
+
return true;
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The staff email.
|
|
194
|
+
*
|
|
195
|
+
* 🛑 `feature` is a NAMED FLOW, not a `NotificationType`, and that is a
|
|
196
|
+
* deliberate correction to the plan this came from.
|
|
197
|
+
*
|
|
198
|
+
* `SendGuardContext.feature` is documented as "a notification type, or a named
|
|
199
|
+
* non-notification flow (`daily_digest`, `checkout_otp`, `payout_summary`)",
|
|
200
|
+
* and this is the second kind. A `NotificationType` is addressed to a USER and
|
|
201
|
+
* carries preferences, templates and an audience — none of which make sense
|
|
202
|
+
* here: nobody can opt out of "the courier wallet is empty", and there is no
|
|
203
|
+
* user it belongs to. Adding one would have meant four compile-checked map
|
|
204
|
+
* entries describing a notification nobody receives.
|
|
205
|
+
*
|
|
206
|
+
* `audience: "staff"` is load-bearing: the admin kill switch suppresses
|
|
207
|
+
* user-facing mail only, so switching customer email off must not stop the
|
|
208
|
+
* operator finding out that paid orders have stopped moving.
|
|
209
|
+
*/
|
|
210
|
+
async function emailStaff(input) {
|
|
211
|
+
const settings = await siteSettingsRepository.getSingleton().catch((err) => {
|
|
212
|
+
void normalizeError(err);
|
|
213
|
+
return null;
|
|
214
|
+
});
|
|
215
|
+
// Same recipient list the daily digest uses — one place to configure "who
|
|
216
|
+
// hears about operational problems".
|
|
217
|
+
const recipients = (settings?.emailSettings?.dailyDigest?.recipients ?? []).filter((r) => r.trim().length > 0);
|
|
218
|
+
if (recipients.length === 0) {
|
|
219
|
+
serverLogger.warn("courier-block: no staff recipients configured — case raised, no email", {
|
|
220
|
+
provider: input.provider,
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
const body = input.reason === "insufficient_funds"
|
|
225
|
+
? `Courier bookings are failing for lack of funds on the ${input.provider} wallet.\n\nPaid orders are waiting. They are NOT being auto-cancelled — the dispatch clock is paused while this is open.\n\nTop up the wallet, then retry from the order or the money-cases queue.`
|
|
226
|
+
: `Courier bookings are failing on ${input.provider} (${input.reason}).\n\nPaid orders are waiting. They are NOT being auto-cancelled — the dispatch clock is paused while this is open.`;
|
|
227
|
+
const { error } = await sendEmail({
|
|
228
|
+
to: recipients,
|
|
229
|
+
subject: input.title,
|
|
230
|
+
html: `<p>${body.replace(/\n\n/g, "</p><p>").replace(/\n/g, "<br/>")}</p>`,
|
|
231
|
+
text: body,
|
|
232
|
+
}, { channel: "email", feature: "courier_wallet_low", audience: "staff" });
|
|
233
|
+
if (error) {
|
|
234
|
+
serverLogger.error("courier-block: staff email failed", {
|
|
235
|
+
provider: input.provider,
|
|
236
|
+
error: String(error),
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
@@ -37,7 +37,7 @@ import type { PublicSiteSettings } from "../../../shared/features/site-settings/
|
|
|
37
37
|
* was traced to a call site; anything without one belongs in the private list,
|
|
38
38
|
* because an unused public field is pure attack surface.
|
|
39
39
|
*/
|
|
40
|
-
export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "listings", "notificationChannels", "announcementBar", "navConfig", "actionConfig", "background", "watermark", "disabledRoutes", "chat", "search"];
|
|
40
|
+
export declare const PUBLIC_SITE_SETTINGS_FIELDS: readonly ["contact", "payment", "listings", "notificationChannels", "announcementBar", "support", "navConfig", "actionConfig", "background", "watermark", "disabledRoutes", "chat", "search"];
|
|
41
41
|
/** Everything else, with the reason it stays server-side. */
|
|
42
42
|
export declare const PRIVATE_SITE_SETTINGS_FIELDS: readonly ["id", "createdAt", "updatedAt", "credentials", "emailSettings", "legalPages", "commissions", "emi", "gst", "laborRate", "adSettings", "integrations", "platformLimits", "auctionConfig", "siteName", "tagline", "motto", "logo", "favicon", "seo", "socialLinks", "features", "shipping", "returns", "faq", "aboutContent", "navbarConfig", "footerConfig", "theme", "featuredResults", "messaging"];
|
|
43
43
|
export interface ToPublicSiteSettingsOptions {
|
|
@@ -9,6 +9,18 @@ export const PUBLIC_SITE_SETTINGS_FIELDS = [
|
|
|
9
9
|
"listings", // useListingTypeFlags — narrowed to listings.listingTypes; categoryTypes has no client reader
|
|
10
10
|
"notificationChannels", // NotificationPreferencesPanel reads the per-channel toggles
|
|
11
11
|
"announcementBar", // the homepage banner (AdminSiteSettingsView edits it)
|
|
12
|
+
/*
|
|
13
|
+
* `support.enabled` — a REAL client reader, which is the bar this list sets:
|
|
14
|
+
* `getUserNavGroups` filters the buyer's support nav entry on it, and
|
|
15
|
+
* `UserLayoutClient` is a client component.
|
|
16
|
+
*
|
|
17
|
+
* 🛑 Publishing it is also harmless by inspection, and that is worth stating
|
|
18
|
+
* rather than assuming: the group holds exactly one boolean, and "is support
|
|
19
|
+
* reachable" is a fact a buyer discovers by looking at the page anyway. If a
|
|
20
|
+
* second field is ever added here, triage it separately — the group being
|
|
21
|
+
* public does not make its future members public.
|
|
22
|
+
*/
|
|
23
|
+
"support",
|
|
12
24
|
"navConfig", // NavbarWithSettings, DashboardLayoutClient
|
|
13
25
|
"actionConfig", // useAuthGate
|
|
14
26
|
"background", // DashboardLayoutClient (background.light / .dark)
|
|
@@ -114,6 +126,9 @@ export function toPublicSiteSettings(doc, opts) {
|
|
|
114
126
|
},
|
|
115
127
|
notificationChannels: doc.notificationChannels,
|
|
116
128
|
announcementBar: doc.announcementBar,
|
|
129
|
+
// Narrowed to the one boolean a client reads, not the group by reference —
|
|
130
|
+
// so a field added to `support` later is not published by accident.
|
|
131
|
+
support: doc.support ? { enabled: doc.support.enabled !== false } : undefined,
|
|
117
132
|
navConfig: doc.navConfig,
|
|
118
133
|
actionConfig: doc.actionConfig,
|
|
119
134
|
background: doc.background,
|
|
@@ -25,9 +25,9 @@ import type { StoreDetail, StoreListItem } from "../../../../features/stores/typ
|
|
|
25
25
|
* Document fields that may reach an unauthenticated client. Keep in sync with
|
|
26
26
|
* the object literals below — the audit asserts both directions.
|
|
27
27
|
*/
|
|
28
|
-
export declare const PUBLIC_STORE_FIELDS: readonly ["id", "storeSlug", "ownerId", "storeName", "storeDescription", "storeCategory", "storeLogoURL", "storeBannerURL", "status", "isPublic", "stats", "bio", "location", "website", "socialLinks", "returnPolicy", "shippingPolicy", "isVacationMode", "vacationMessage", "supportsShippingProtection", "supportsSaleProtection", "createdAt"];
|
|
28
|
+
export declare const PUBLIC_STORE_FIELDS: readonly ["id", "storeSlug", "ownerId", "storeName", "storeDescription", "storeCategory", "storeLogoURL", "storeBannerURL", "status", "isPublic", "stats", "bio", "location", "website", "socialLinks", "returnPolicy", "shippingPolicy", "isVacationMode", "vacationMessage", "supportsShippingProtection", "supportsSaleProtection", "allowSharedPurchase", "sharedPurchaseHandlingFee", "sharedPurchaseExtraShippingPerDestination", "createdAt"];
|
|
29
29
|
/** Everything else, with the reason it stays server-side. */
|
|
30
|
-
export declare const PRIVATE_STORE_FIELDS: readonly ["searchTxt", "updatedAt", "adminNotes", "statusHistory", "statusHistoryTruncated", "isVerified", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "pricesIncludeGst", "shippingPricing", "returnsDirectToSeller", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt", "gstin", "legalName", "registeredAddress"];
|
|
30
|
+
export declare const PRIVATE_STORE_FIELDS: readonly ["acceptsPoints", "searchTxt", "updatedAt", "adminNotes", "statusHistory", "statusHistoryTruncated", "isVerified", "suspensionReason", "capabilities", "customCommissionRate", "googleReviews", "shippingConfig", "emiEnabled", "pricesIncludeGst", "shippingPricing", "returnsDirectToSeller", "isFeatured", "vacationReturnDate", "isTestData", "testDataExpiresAt", "gstin", "legalName", "registeredAddress"];
|
|
31
31
|
/**
|
|
32
32
|
* Accepted input: a `StoreDocument` (the common case) or a loosely-typed
|
|
33
33
|
* Firestore row. The legacy top-level counters below predate `stats` and are
|
|
@@ -33,10 +33,48 @@ export const PUBLIC_STORE_FIELDS = [
|
|
|
33
33
|
// which is the one outcome the tri-state rule forbids.
|
|
34
34
|
"supportsShippingProtection",
|
|
35
35
|
"supportsSaleProtection",
|
|
36
|
+
/*
|
|
37
|
+
* ── D46/D47 + S5a — three more public store flags, same argument ──────────
|
|
38
|
+
*
|
|
39
|
+
* PUBLIC for the tri-state reason the two protection flags above are: the
|
|
40
|
+
* buyer has to know whether this seller will split an order across
|
|
41
|
+
* addresses BEFORE they commit, and withholding it makes "has not opted in"
|
|
42
|
+
* indistinguishable from "we did not tell you".
|
|
43
|
+
*
|
|
44
|
+
* The two shared-purchase FEES are public for a stronger reason still: a
|
|
45
|
+
* coordinator commits to a quoted total, and a handling fee plus N−1
|
|
46
|
+
* shipping legs discovered after they committed is exactly the surprise the
|
|
47
|
+
* arrangement makes expensive.
|
|
48
|
+
*/
|
|
49
|
+
"allowSharedPurchase",
|
|
50
|
+
"sharedPurchaseHandlingFee",
|
|
51
|
+
"sharedPurchaseExtraShippingPerDestination",
|
|
36
52
|
"createdAt",
|
|
37
53
|
];
|
|
38
54
|
/** Everything else, with the reason it stays server-side. */
|
|
39
55
|
export const PRIVATE_STORE_FIELDS = [
|
|
56
|
+
/*
|
|
57
|
+
* 🛑 PRIVATE, and it was briefly classified PUBLIC on a mistaken argument
|
|
58
|
+
* worth recording so nobody re-makes it.
|
|
59
|
+
*
|
|
60
|
+
* The argument was "the cart has to show a buyer whether their points can be
|
|
61
|
+
* used HERE before they commit" — which is true, and is NOT a reason for
|
|
62
|
+
* this projection to carry the field. Measured: the cart reads
|
|
63
|
+
* `pricingPreview.stores[].acceptsPoints`, produced by the checkout pricing
|
|
64
|
+
* preview (checkout/actions.ts:2980), and the only client readers are
|
|
65
|
+
* `CartPriceBreakdown.tsx:317` and `CartRouteClient.tsx:1282`, both off that
|
|
66
|
+
* preview. Neither touches `toStoreListItem` / `toStoreDetail`.
|
|
67
|
+
*
|
|
68
|
+
* So the store projection has no reader, and the rule for this file is
|
|
69
|
+
* explicit: a field moves to PUBLIC only once you can NAME the client that
|
|
70
|
+
* reads it from this payload. "A different payload needs it" is not that.
|
|
71
|
+
*
|
|
72
|
+
* A store page could legitimately advertise "accepts points" one day. When a
|
|
73
|
+
* reader exists, move it and add it to both builders — the omission is what
|
|
74
|
+
* `audit-public-projection-parity` caught here (declared PUBLIC, emitted by
|
|
75
|
+
* nothing), which is the half-migrated state that reads as done.
|
|
76
|
+
*/
|
|
77
|
+
"acceptsPoints",
|
|
40
78
|
// Internal search index, not content. It is a normalized re-encoding of
|
|
41
79
|
// storeName/description/category — publishing it would leak nothing new, but
|
|
42
80
|
// it has no client reader and would bloat every store payload with a few
|
|
@@ -147,6 +185,19 @@ export function toStoreListItem(doc, _opts) {
|
|
|
147
185
|
*/
|
|
148
186
|
supportsShippingProtection: doc.supportsShippingProtection ?? false,
|
|
149
187
|
supportsSaleProtection: doc.supportsSaleProtection ?? false,
|
|
188
|
+
/*
|
|
189
|
+
* Same `?? false` rule for the same reason — a definite "not offered"
|
|
190
|
+
* rather than an `undefined` a truthiness test reads as neither.
|
|
191
|
+
*
|
|
192
|
+
* 🛑 The two FEES are passed through as `undefined` when unset, and that
|
|
193
|
+
* asymmetry is deliberate: a flag's absence means "no", but a fee's
|
|
194
|
+
* absence means "not configured", which is NOT the same as ₹0. Defaulting
|
|
195
|
+
* them to zero would quote a coordinator a free arrangement from a store
|
|
196
|
+
* that simply has not set a price.
|
|
197
|
+
*/
|
|
198
|
+
allowSharedPurchase: doc.allowSharedPurchase ?? false,
|
|
199
|
+
sharedPurchaseHandlingFee: doc.sharedPurchaseHandlingFee,
|
|
200
|
+
sharedPurchaseExtraShippingPerDestination: doc.sharedPurchaseExtraShippingPerDestination,
|
|
150
201
|
createdAt: toIsoString(doc.createdAt),
|
|
151
202
|
};
|
|
152
203
|
}
|
|
@@ -18,9 +18,46 @@ export const pendingOrderTimeout = defineFunction({
|
|
|
18
18
|
handler: pendingOrderTimeoutHandler,
|
|
19
19
|
options: { region: REGION, timeoutSeconds: 120, memory: "256MiB", maxInstances: 1 },
|
|
20
20
|
});
|
|
21
|
+
/*
|
|
22
|
+
* ── 🛑 THE "RETIRE THREE SWEEPS" PLAN STEP IS SELF-CONTRADICTORY ────────────
|
|
23
|
+
*
|
|
24
|
+
* Phase 8's plan text reads: *"a retry is a new document … picked up by a
|
|
25
|
+
* sweep folded into `paymentWindowTimeout` … **Then** retire the three
|
|
26
|
+
* superseded sweeps"* — and names those three as `pendingOrderTimeout`,
|
|
27
|
+
* **`paymentWindowTimeout`** and `paymentReviewAutoApprove`.
|
|
28
|
+
*
|
|
29
|
+
* It cannot be both. `paymentWindowTimeout` is now the HOST of the job-retry
|
|
30
|
+
* sweep (see `runDueJobRetries`, called first in its handler, before the early
|
|
31
|
+
* return), so retiring it would delete the only mechanism that runs a
|
|
32
|
+
* scheduled retry — and the create trigger deliberately defers to that sweep,
|
|
33
|
+
* because creating the retry document fires the trigger instantly and would
|
|
34
|
+
* collapse the backoff to zero. Deleting the host leaves every retry
|
|
35
|
+
* permanently `pending`: a stall that reads as a queue, which is the exact
|
|
36
|
+
* failure Phase 8 exists to remove.
|
|
37
|
+
*
|
|
38
|
+
* So the retry/DLQ half is built and the retirement is NOT done. That is a
|
|
39
|
+
* deliberate stop, not an omission:
|
|
40
|
+
*
|
|
41
|
+
* - These three cancel unpaid orders and restock real inventory. Deleting
|
|
42
|
+
* them on the strength of a plan line, in a session that cannot run a
|
|
43
|
+
* single check, is the autonomous destructive decision Rule #1 forbids.
|
|
44
|
+
* - The claim that obligations supersede them needs reading all three
|
|
45
|
+
* against the obligation policies AND confirming `obligationSweep`'s `act`
|
|
46
|
+
* tier covers each — and the sweep's own comment records that its acting
|
|
47
|
+
* branch was narrowed precisely because monetary outcomes were not safe to
|
|
48
|
+
* automate. That narrowing was widened for six kinds; whether it covers
|
|
49
|
+
* these three is a measurement, not an inference.
|
|
50
|
+
* - The Scheduler arithmetic in the plan (34 → 31) is therefore unchanged by
|
|
51
|
+
* this commit. Nothing was added either, so the count stands wherever it
|
|
52
|
+
* actually is — and CLAUDE.md says to recount rather than quote it, this
|
|
53
|
+
* number having drifted six times.
|
|
54
|
+
*
|
|
55
|
+
* Whoever takes the retirement: pick a different host for the retry sweep
|
|
56
|
+
* first, or keep `paymentWindowTimeout` and retire only the other two.
|
|
57
|
+
*/
|
|
21
58
|
export const paymentWindowTimeout = defineFunction({
|
|
22
59
|
name: "paymentWindowTimeout",
|
|
23
|
-
description: "Cancel + restock orders whose 15-minute manual-payment window expired without proof.",
|
|
60
|
+
description: "Cancel + restock orders whose 15-minute manual-payment window expired without proof. Also hosts R1 (stuck gateway events) and the Phase 8 job-retry sweep, both before its early return.",
|
|
24
61
|
trigger: { kind: "schedule", cron: EVERY_5_MIN },
|
|
25
62
|
handler: paymentWindowTimeoutHandler,
|
|
26
63
|
options: { region: REGION, timeoutSeconds: 120, memory: "256MiB", maxInstances: 1 },
|
|
@@ -3,7 +3,7 @@ import { getDefaultCurrency } from "../../../../core/index.js";
|
|
|
3
3
|
import { computePayoutDeduction } from "../../../shared/fees/calculator.js";
|
|
4
4
|
import { payoutCutoff } from "../../../shared/fees/payout-hold.js";
|
|
5
5
|
import { PROTECTION_CLAIM_WINDOW_HOURS } from "../../../shared/features/shipping/protections.js";
|
|
6
|
-
import { ordersTaxableValue, ordersShippingReimbursement, } from "../../../shared/checkout/order-math.js";
|
|
6
|
+
import { ordersTaxableValue, ordersShippingReimbursement, ordersPointsBorneByStore, } from "../../../shared/checkout/order-math.js";
|
|
7
7
|
import { BATCH_LIMIT } from "../handlers/messages.js";
|
|
8
8
|
/*
|
|
9
9
|
* 🛑 `AUTO_PAYOUT_WINDOW_DAYS = 7` and `getBusinessDayCutoff` were REMOVED on
|
|
@@ -120,7 +120,14 @@ export async function runAutoPayoutEligibility(ctx) {
|
|
|
120
120
|
* `{ id, data }` wrappers, and passing the wrapper would read every field
|
|
121
121
|
* as undefined and silently reimburse zero.
|
|
122
122
|
*/
|
|
123
|
-
ordersShippingReimbursement(orders.map((o) => o.data))
|
|
123
|
+
ordersShippingReimbursement(orders.map((o) => o.data)),
|
|
124
|
+
/*
|
|
125
|
+
* D46/D47 — the store-funded points, both ends (decisions 3 and 17).
|
|
126
|
+
* Same `.data` unwrap for the same reason, and the same position outside
|
|
127
|
+
* the fee bases: a commission on a discount the seller funded is not a
|
|
128
|
+
* commission on a sale.
|
|
129
|
+
*/
|
|
130
|
+
ordersPointsBorneByStore(orders.map((o) => o.data)));
|
|
124
131
|
const payoutInput = {
|
|
125
132
|
storeId,
|
|
126
133
|
// `displayName` is plaintext by D1; the email fallback must be revealed.
|
|
@@ -58,6 +58,10 @@ export interface DailyStatusDigestSummary {
|
|
|
58
58
|
nearestCaseDueAt: Date | null;
|
|
59
59
|
/** Gateway events that never reached a terminal status — the inbox backlog. */
|
|
60
60
|
unprocessedGatewayEventCount: number;
|
|
61
|
+
courierWalletBalance: number | null;
|
|
62
|
+
courierWalletLow: boolean;
|
|
63
|
+
/** Why the balance is null, when it is. Printed instead of a figure. */
|
|
64
|
+
courierWalletNote?: string;
|
|
61
65
|
}
|
|
62
66
|
/** Gathers the last-24h numbers. Exported so the admin manual-trigger route can reuse it. */
|
|
63
67
|
export declare function collectDailyStatusSummary(): Promise<DailyStatusDigestSummary>;
|
|
@@ -16,6 +16,8 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
16
16
|
import { Fragment } from "react";
|
|
17
17
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
18
18
|
import { getAdminDb } from "../../../../providers/db-firebase/index.js";
|
|
19
|
+
import { getProviders } from "../../../../contracts/registry.js";
|
|
20
|
+
import { checkCourierWalletAndAlert } from "../../features/shipping/courier-block.js";
|
|
19
21
|
import { sendEmail } from "../../../../features/contact/email.js";
|
|
20
22
|
import { siteSettingsRepository } from "../../../../features/admin/repository/site-settings.repository.js";
|
|
21
23
|
import { ORDER_COLLECTION, OrderStatusValues } from "../../../../features/orders/schemas/firestore.js";
|
|
@@ -163,6 +165,14 @@ export async function collectDailyStatusSummary() {
|
|
|
163
165
|
* nearest-deadline query below swallows and returns null rather than
|
|
164
166
|
* throwing.
|
|
165
167
|
*/
|
|
168
|
+
/*
|
|
169
|
+
* The courier wallet. Same spirit as `safeCount` above — it can never fail
|
|
170
|
+
* the digest, and it ALERTS at most once per unresolved episode rather than
|
|
171
|
+
* once per day (the case's `openOrObserve` is the dedupe). `getProviders()`
|
|
172
|
+
* always populates `shipping`, and the base class's `getWalletBalance`
|
|
173
|
+
* default answers "no wallet" for manual, so this needs no capability check.
|
|
174
|
+
*/
|
|
175
|
+
const courierWallet = await checkCourierWalletAndAlert(getProviders().shipping);
|
|
166
176
|
const [paymentCasesError, paymentCasesWarning, paymentCasesInfo, unprocessedGatewayEventCount] = await Promise.all([
|
|
167
177
|
safeCount(db
|
|
168
178
|
.collection(PAYMENT_CASES_COLLECTION)
|
|
@@ -245,6 +255,9 @@ export async function collectDailyStatusSummary() {
|
|
|
245
255
|
paymentCasesInfo,
|
|
246
256
|
nearestCaseDueAt,
|
|
247
257
|
unprocessedGatewayEventCount,
|
|
258
|
+
courierWalletBalance: courierWallet.balance,
|
|
259
|
+
courierWalletLow: courierWallet.low,
|
|
260
|
+
...(courierWallet.reason ? { courierWalletNote: courierWallet.reason } : {}),
|
|
248
261
|
};
|
|
249
262
|
}
|
|
250
263
|
function renderDigestHtml(summary, siteName, version) {
|
|
@@ -254,7 +267,9 @@ function renderDigestHtml(summary, siteName, version) {
|
|
|
254
267
|
? `Deployment ${version} is live. Activity for the last 24 hours.`
|
|
255
268
|
: "Activity for the last 24 hours.", _jsx("br", {}), _jsx("br", {}), _jsx(StatLine, { label: "Orders placed", value: summary.orderCount }), _jsx(StatLine, { label: "Revenue (excl. cancelled/refunded)", value: currency(summary.revenue) }), _jsx(StatLine, { label: "Active listings", value: summary.activeListingCount }), _jsx(StatLine, { label: "Pending > 24h (needs attention)", value: summary.stuckPendingCount })] }), _jsx(EmailDivider, {}), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Orders by status" }), _jsx("br", {}), _jsx("br", {}), statusRows.length === 0 ? ("No orders in this window.") : (statusRows.map(([status, count]) => (_jsx(StatLine, { label: status, value: count }, status))))] }), _jsx(EmailDivider, {}), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Inbox" }), _jsx("br", {}), _jsx("br", {}), _jsx(StatLine, { label: "New contact messages", value: summary.contactTotal }), _jsx(StatLine, { label: "New support tickets", value: summary.newTicketCount }), _jsx(StatLine, { label: "Tickets open / in progress / waiting on user", value: `${summary.ticketsOpen} / ${summary.ticketsInProgress} / ${summary.ticketsWaitingOnUser}` }), _jsx(StatLine, { label: "Unread admin notifications", value: summary.unreadAdminNotificationCount })] }), summary.contactMessages.length > 0 ? (_jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Contact messages" }), _jsx("br", {}), _jsx("br", {}), summary.contactMessages.map((m) => (_jsxs(Fragment, { children: [_jsx(EmailBold, { children: m.subject }), _jsx("br", {}), m.name, " \u2014", " ", _jsxs(EmailLink, { href: `mailto:${m.email}?subject=${encodeURIComponent(`Re: ${m.subject}`)}`, children: ["Reply to ", m.email] }), _jsx("br", {}), m.excerpt, _jsx("br", {}), _jsx("br", {})] }, m.id))), summary.contactTotal > summary.contactMessages.length
|
|
256
269
|
? `…and ${summary.contactTotal - summary.contactMessages.length} more in /admin/contact.`
|
|
257
|
-
: null] })) : null, _jsx(EmailDivider, {}), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Needs attention" }), _jsx("br", {}), _jsx("br", {}), _jsx(StatLine, { label: "Manual payments pending", value: summary.manualPaymentPendingCount }), _jsx(StatLine, { label: "New scam reports", value: summary.newScamReportCount }), _jsx(StatLine, { label: "New newsletter subscribers", value: summary.newSubscriberCount }), _jsx(StatLine, { label: "Admin actions logged", value: summary.adminActionCount })] }), _jsx(EmailDivider, {}), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Money at risk" }), _jsx("br", {}), _jsx("br", {}), _jsx(StatLine, { label: "Open cases \u2014 urgent", value: summary.paymentCasesError }), _jsx(StatLine, { label: "Open cases \u2014 warning", value: summary.paymentCasesWarning }), _jsx(StatLine, { label: "Open cases \u2014 info", value: summary.paymentCasesInfo }), _jsx(StatLine, { label: "Nearest external deadline", value: summary.nearestCaseDueAt ? summary.nearestCaseDueAt.toISOString() : "none" }), _jsx(StatLine, { label: "Unfinished gateway events", value: summary.unprocessedGatewayEventCount })
|
|
270
|
+
: null] })) : null, _jsx(EmailDivider, {}), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Needs attention" }), _jsx("br", {}), _jsx("br", {}), _jsx(StatLine, { label: "Manual payments pending", value: summary.manualPaymentPendingCount }), _jsx(StatLine, { label: "New scam reports", value: summary.newScamReportCount }), _jsx(StatLine, { label: "New newsletter subscribers", value: summary.newSubscriberCount }), _jsx(StatLine, { label: "Admin actions logged", value: summary.adminActionCount })] }), _jsx(EmailDivider, {}), _jsxs(EmailRow, { children: [_jsx(EmailBold, { children: "Money at risk" }), _jsx("br", {}), _jsx("br", {}), _jsx(StatLine, { label: "Open cases \u2014 urgent", value: summary.paymentCasesError }), _jsx(StatLine, { label: "Open cases \u2014 warning", value: summary.paymentCasesWarning }), _jsx(StatLine, { label: "Open cases \u2014 info", value: summary.paymentCasesInfo }), _jsx(StatLine, { label: "Nearest external deadline", value: summary.nearestCaseDueAt ? summary.nearestCaseDueAt.toISOString() : "none" }), _jsx(StatLine, { label: "Unfinished gateway events", value: summary.unprocessedGatewayEventCount }), _jsx(StatLine, { label: "Courier wallet", value: summary.courierWalletBalance === null
|
|
271
|
+
? `unknown (${summary.courierWalletNote ?? "not read"})`
|
|
272
|
+
: `${currency(summary.courierWalletBalance)}${summary.courierWalletLow ? " — LOW, top up" : ""}` })] }), _jsx(EmailFooter, { copyright: `© ${new Date().getFullYear()}${siteName ? ` ${siteName}` : ""}. All rights reserved.` })] }) }))}`;
|
|
258
273
|
}
|
|
259
274
|
/**
|
|
260
275
|
* The plain-text twin of `renderDigestHtml`.
|
|
@@ -288,7 +303,9 @@ function renderDigestText(summary, version) {
|
|
|
288
303
|
* the same unconditional rendering, because a mail client showing the text
|
|
289
304
|
* part must not get a digest missing the one block with a deadline in it.
|
|
290
305
|
*/
|
|
291
|
-
lines.push("", "MONEY AT RISK", `Open cases — urgent: ${summary.paymentCasesError}`, `Open cases — warning: ${summary.paymentCasesWarning}`, `Open cases — info: ${summary.paymentCasesInfo}`, `Nearest external deadline: ${summary.nearestCaseDueAt ? summary.nearestCaseDueAt.toISOString() : "none"}`, `Unfinished gateway events: ${summary.unprocessedGatewayEventCount}`
|
|
306
|
+
lines.push("", "MONEY AT RISK", `Open cases — urgent: ${summary.paymentCasesError}`, `Open cases — warning: ${summary.paymentCasesWarning}`, `Open cases — info: ${summary.paymentCasesInfo}`, `Nearest external deadline: ${summary.nearestCaseDueAt ? summary.nearestCaseDueAt.toISOString() : "none"}`, `Unfinished gateway events: ${summary.unprocessedGatewayEventCount}`, `Courier wallet: ${summary.courierWalletBalance === null
|
|
307
|
+
? `unknown (${summary.courierWalletNote ?? "not read"})`
|
|
308
|
+
: `${currency(summary.courierWalletBalance)}${summary.courierWalletLow ? " — LOW, top up" : ""}`}`);
|
|
292
309
|
return lines.join("\n");
|
|
293
310
|
}
|
|
294
311
|
const DEPLOY_MARKER_COLLECTION = "system";
|
|
@@ -46,4 +46,5 @@ export { handleProductStockChange, type HandleProductStockChangeInput, syncBundl
|
|
|
46
46
|
export { handleReviewWrite, type HandleReviewWriteInput } from "./onReviewWrite";
|
|
47
47
|
export { handleCategoryWrite, type HandleCategoryWriteInput, type CategoryDoc, } from "./onCategoryWrite";
|
|
48
48
|
export { handleStoreWrite, type HandleStoreWriteInput, type StoreDoc, } from "./onStoreWrite";
|
|
49
|
-
export { handleJobCreated, type HandleJobCreatedInput } from "./onJobCreated";
|
|
49
|
+
export { handleJobCreated, runDueJobRetries, type HandleJobCreatedInput, } from "./onJobCreated";
|
|
50
|
+
export { runJobOnce, type RunJobOnceInput } from "./runJobOnce";
|
|
@@ -49,4 +49,5 @@ export { handleProductStockChange, syncBundlesForProduct, syncGroupedListingsFor
|
|
|
49
49
|
export { handleReviewWrite } from "./onReviewWrite.js";
|
|
50
50
|
export { handleCategoryWrite, } from "./onCategoryWrite.js";
|
|
51
51
|
export { handleStoreWrite, } from "./onStoreWrite.js";
|
|
52
|
-
export { handleJobCreated } from "./onJobCreated.js";
|
|
52
|
+
export { handleJobCreated, runDueJobRetries, } from "./onJobCreated.js";
|
|
53
|
+
export { runJobOnce } from "./runJobOnce.js";
|
|
@@ -5,13 +5,11 @@
|
|
|
5
5
|
* (CLAUDE.md Rule #6 — Vercel routes never do heavy work; they write a
|
|
6
6
|
* `jobs` doc and return, this Function does the actual processing).
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* `useBulkEvent` — every RTDB write is its own try/catch so a ping
|
|
14
|
-
* failure never masks the real Firestore result.
|
|
8
|
+
* ## 🛑 It no longer runs the job itself — `runJobOnce` does (Phase 8)
|
|
9
|
+
*
|
|
10
|
+
* The attempt, the failure classification and the retry scheduling all live in
|
|
11
|
+
* `runJobOnce`, which the retry sweep also calls. One dispatch path for both
|
|
12
|
+
* entry points, or the two drift the first time either learns something.
|
|
15
13
|
*/
|
|
16
14
|
import type { JobDocument } from "../../../../features/jobs/schemas/firestore";
|
|
17
15
|
import type { JobContext } from "../runtime/types";
|
|
@@ -20,3 +18,28 @@ export interface HandleJobCreatedInput {
|
|
|
20
18
|
job: JobDocument;
|
|
21
19
|
}
|
|
22
20
|
export declare function handleJobCreated(input: HandleJobCreatedInput, ctx: JobContext): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* The retry sweep — runs every scheduled attempt whose time has come.
|
|
23
|
+
*
|
|
24
|
+
* ## 🛑 Folded into an EXISTING scheduled function, not a new one
|
|
25
|
+
*
|
|
26
|
+
* It is called from `paymentWindowTimeout` (every 5 minutes). Cloud Scheduler
|
|
27
|
+
* bills per registered job and this project already carries ~34 of them
|
|
28
|
+
* against 3 free; adding a 35th to run a usually-empty query is the cost this
|
|
29
|
+
* plan explicitly refuses. The same reasoning put the ledger reconciler inside
|
|
30
|
+
* `dailyDataCleanup` and R1 inside this same function.
|
|
31
|
+
*
|
|
32
|
+
* 🛑 And it is placed FIRST at its call site, before that function's early
|
|
33
|
+
* return — `paymentWindowTimeout` returns as soon as there are no expired
|
|
34
|
+
* payment windows, which is most ticks, so appending would run the sweep only
|
|
35
|
+
* on the minority of ticks with unrelated work (Root Cause #27).
|
|
36
|
+
*
|
|
37
|
+
* Sequential, not `Promise.all`. A job runner is heavy by definition — that is
|
|
38
|
+
* why it is a job — and twenty of them concurrently inside one 540s function
|
|
39
|
+
* is how a sweep exhausts its own budget. Twenty sequential attempts that run
|
|
40
|
+
* out of time simply leave the rest `pending` for the next tick, which is the
|
|
41
|
+
* safe failure.
|
|
42
|
+
*/
|
|
43
|
+
export declare function runDueJobRetries(ctx: JobContext): Promise<{
|
|
44
|
+
ran: number;
|
|
45
|
+
}>;
|