@mohasinac/appkit 4.61.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/client/features/lottery/LotteryAdminEditView.js +19 -3
- package/dist/_internal/client/features/maintenance/views/AnalysisRunnerView.js +1 -1
- package/dist/_internal/client/features/maintenance/views/CloudLogsListView.js +1 -1
- 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/paymentReconcile.js +87 -97
- 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 +177 -9
- 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/_internal/shared/media/limits.d.ts +10 -0
- package/dist/_internal/shared/media/limits.js +10 -0
- package/dist/_internal/shared/types/action-result.js +15 -1
- package/dist/client/api/surface-error.js +16 -2
- package/dist/client/api/useApiMutation.js +17 -1
- package/dist/client.d.ts +12 -1
- package/dist/client.js +31 -1
- package/dist/constants/api-endpoints.d.ts +45 -0
- package/dist/constants/api-endpoints.js +15 -0
- package/dist/constants/field-names.d.ts +18 -0
- package/dist/constants/field-names.js +28 -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/core/server-action.js +19 -3
- package/dist/errors/error-mapping.js +26 -1
- package/dist/features/about/components/AboutView.js +1 -1
- package/dist/features/account/components/BuyerAuctionsGuideView.js +4 -1
- package/dist/features/account/components/UserOffersPanel.js +5 -3
- package/dist/features/account/components/UserOrdersView.js +19 -33
- package/dist/features/admin/components/AdminActionIndexView.js +100 -5
- package/dist/features/admin/components/AdminAddressClustersView.js +21 -2
- package/dist/features/admin/components/AdminBlogEditorView.js +8 -8
- package/dist/features/admin/components/AdminCarouselEditorView.js +1 -1
- package/dist/features/admin/components/AdminCarouselView.js +96 -4
- package/dist/features/admin/components/AdminCouponEditorView.js +17 -0
- package/dist/features/admin/components/AdminFulfillmentView.js +3 -2
- package/dist/features/admin/components/AdminInvoicingView.d.ts +2 -0
- package/dist/features/admin/components/AdminInvoicingView.js +65 -0
- package/dist/features/admin/components/AdminMediaView.js +36 -3
- package/dist/features/admin/components/AdminNavigationView.js +4 -1
- package/dist/features/admin/components/AdminOffersView.js +2 -1
- 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/AdminPaymentClustersView.js +19 -2
- package/dist/features/admin/components/AdminProductEditorView.js +2 -2
- package/dist/features/admin/components/AdminSectionsView.d.ts +10 -0
- package/dist/features/admin/components/AdminSectionsView.js +486 -224
- package/dist/features/admin/components/OwnerQuickActions.js +24 -2
- package/dist/features/admin/components/ReferenceRecordEditor.js +1 -1
- 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/hooks/useOptimisticRowFlag.d.ts +33 -0
- package/dist/features/admin/hooks/useOptimisticRowFlag.js +87 -0
- package/dist/features/admin/repository/notification.repository.d.ts +15 -0
- package/dist/features/admin/repository/notification.repository.js +32 -0
- package/dist/features/admin/schemas/action-index-form.d.ts +34 -0
- package/dist/features/admin/schemas/action-index-form.js +115 -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/components/RegisterForm.js +2 -1
- 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/catalogue/components/CatalogueItemEditorView.js +3 -3
- 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/events/components/AdminEventEditorView.js +8 -6
- 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/layout/hooks/useFormBottomActions.js +20 -1
- 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/EmiInstallmentsPanel.js +25 -7
- 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 +22 -4
- 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 +60 -12
- 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/GroupMemberPicker.js +4 -1
- package/dist/features/products/components/PrizeDrawDetailPageView.js +2 -2
- package/dist/features/products/components/PrizeDrawEntryActions.js +2 -1
- package/dist/features/products/components/ProductDetailActions.js +2 -1
- 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/FulfillmentView.js +3 -2
- package/dist/features/seller/components/SellerAddressesView.js +32 -4
- package/dist/features/seller/components/SellerAnalyticsAlertsView.js +62 -3
- package/dist/features/seller/components/SellerBundlesView.js +53 -14
- package/dist/features/seller/components/SellerGroupedListingsView.js +23 -3
- package/dist/features/seller/components/SellerListingTypeView.js +56 -10
- package/dist/features/seller/components/SellerOrdersView.js +129 -27
- package/dist/features/seller/components/SellerPayoutMethodsView.js +33 -1
- package/dist/features/seller/components/SellerPayoutSettingsView.js +48 -9
- package/dist/features/seller/components/SellerProductsView.js +5 -1
- package/dist/features/seller/components/SellerShippingConfigsView.js +33 -1
- package/dist/features/seller/components/SellerShippingView.js +2 -1
- package/dist/features/seller/components/SellerStoreCategoriesView.js +33 -1
- package/dist/features/seller/components/SellerSupportView.js +69 -2
- package/dist/features/seller/hooks/useSellerListingData.js +25 -0
- package/dist/features/shell/SectionForm.js +20 -16
- package/dist/features/shell/build-sections.js +14 -1
- package/dist/features/shipments/components/AdminShipmentLotItemsView.js +54 -1
- package/dist/features/shipments/components/AdminShipmentProjectionsView.js +3 -2
- 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/sticker-trace/components/StickerCommitPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerPrintPanel.js +2 -1
- package/dist/features/sticker-trace/components/StickerRetryPanel.js +24 -4
- package/dist/features/sticker-trace/components/StickerTraceStudio.js +2 -1
- 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/tester/components/AdminTesterFeedbackIssuesView.js +3 -2
- package/dist/features/wishlist/hooks/useWishlistWithGuest.d.ts +1 -1
- package/dist/index.d.ts +11 -2
- package/dist/index.js +17 -2
- package/dist/next/routing/route-map.d.ts +24 -0
- package/dist/next/routing/route-map.js +19 -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 +51 -22
- package/dist/schemas/sieve.d.ts +5 -5
- package/dist/server-entry.d.ts +2 -2
- package/dist/server-entry.js +8 -1
- package/dist/server.d.ts +11 -2
- package/dist/server.js +40 -2
- 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/Checkbox.js +10 -2
- package/dist/ui/components/DateInput.js +8 -1
- package/dist/ui/components/Disclaimer.d.ts +19 -0
- package/dist/ui/components/Disclaimer.js +74 -0
- package/dist/ui/components/Input.js +22 -2
- package/dist/ui/components/PaginatedSelect.d.ts +8 -19
- package/dist/ui/components/PaginatedSelect.js +43 -9
- package/dist/ui/components/Radio.js +8 -1
- package/dist/ui/components/Select.js +10 -2
- package/dist/ui/components/TagInput.d.ts +8 -1
- package/dist/ui/components/TagInput.js +4 -3
- package/dist/ui/components/Textarea.js +10 -2
- package/dist/ui/components/tabs-overflow.js +64 -11
- package/dist/ui/forms/FieldCheckbox.js +1 -1
- package/dist/ui/forms/FormErrorList.d.ts +0 -16
- package/dist/ui/forms/FormErrorList.js +34 -1
- package/dist/ui/forms/apply-zod-issues.d.ts +19 -1
- package/dist/ui/forms/apply-zod-issues.js +23 -1
- 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
|
@@ -5,55 +5,88 @@
|
|
|
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
|
-
import { normalizeError } from "../../../../errors/normalize.js";
|
|
17
14
|
import { jobsRepository } from "../../../../repositories/index.js";
|
|
18
|
-
import {
|
|
19
|
-
import { RTDB_PATHS } from "../../../../providers/db-firebase/rtdb-paths.js";
|
|
20
|
-
import { JOB_RUNNERS } from "./jobRunners.js";
|
|
21
|
-
async function pingBulkEvent(jobId, payload, ctx) {
|
|
22
|
-
try {
|
|
23
|
-
await getAdminRealtimeDb()
|
|
24
|
-
.ref(`${RTDB_PATHS.BULK_EVENTS}/${jobId}`)
|
|
25
|
-
.set({ ...payload, updatedAt: Date.now() });
|
|
26
|
-
}
|
|
27
|
-
catch (err) {
|
|
28
|
-
void normalizeError(err);
|
|
29
|
-
ctx.logger.warn("onJobCreated: RTDB ping failed (non-fatal)", {
|
|
30
|
-
jobId,
|
|
31
|
-
error: err instanceof Error ? err.message : String(err),
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
15
|
+
import { runJobOnce } from "./runJobOnce.js";
|
|
35
16
|
export async function handleJobCreated(input, ctx) {
|
|
36
17
|
const { jobId, job } = input;
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
18
|
+
/*
|
|
19
|
+
* ── 🛑 A SCHEDULED RETRY MUST NOT RUN HERE ────────────────────────────────
|
|
20
|
+
*
|
|
21
|
+
* This guard is the whole reason Phase 8's backoff works, and its absence
|
|
22
|
+
* was a defect in this feature's own first draft.
|
|
23
|
+
*
|
|
24
|
+
* A retry is a NEW `jobs` document (it has to be — this trigger fires on
|
|
25
|
+
* CREATE, so flipping a job back to `pending` would never wake anything).
|
|
26
|
+
* But creating that document fires THIS trigger immediately — so without
|
|
27
|
+
* this check, every retry would run the instant it was scheduled, the
|
|
28
|
+
* 5-minute and 30-minute backoffs would be zero, and three attempts would
|
|
29
|
+
* burn in under a second against a fault that had not had time to clear.
|
|
30
|
+
* The backoff fields would still be written, so the database would read as
|
|
31
|
+
* though it had waited.
|
|
32
|
+
*
|
|
33
|
+
* `nextAttemptAt` in the future means "the sweep owns this one". Returning
|
|
34
|
+
* leaves the document `pending`, which is exactly what `getDueRetries`
|
|
35
|
+
* queries for.
|
|
36
|
+
*/
|
|
37
|
+
if (job.nextAttemptAt) {
|
|
38
|
+
const due = new Date(job.nextAttemptAt).getTime();
|
|
39
|
+
if (Number.isFinite(due) && due > Date.now()) {
|
|
40
|
+
ctx.logger.info("onJobCreated: retry scheduled, deferring to the sweep", {
|
|
41
|
+
jobId,
|
|
42
|
+
jobType: job.jobType,
|
|
43
|
+
attempt: job.attempt ?? 1,
|
|
44
|
+
nextAttemptAt: new Date(due).toISOString(),
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
51
48
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
49
|
+
await runJobOnce({ jobId, job }, ctx);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The retry sweep — runs every scheduled attempt whose time has come.
|
|
53
|
+
*
|
|
54
|
+
* ## 🛑 Folded into an EXISTING scheduled function, not a new one
|
|
55
|
+
*
|
|
56
|
+
* It is called from `paymentWindowTimeout` (every 5 minutes). Cloud Scheduler
|
|
57
|
+
* bills per registered job and this project already carries ~34 of them
|
|
58
|
+
* against 3 free; adding a 35th to run a usually-empty query is the cost this
|
|
59
|
+
* plan explicitly refuses. The same reasoning put the ledger reconciler inside
|
|
60
|
+
* `dailyDataCleanup` and R1 inside this same function.
|
|
61
|
+
*
|
|
62
|
+
* 🛑 And it is placed FIRST at its call site, before that function's early
|
|
63
|
+
* return — `paymentWindowTimeout` returns as soon as there are no expired
|
|
64
|
+
* payment windows, which is most ticks, so appending would run the sweep only
|
|
65
|
+
* on the minority of ticks with unrelated work (Root Cause #27).
|
|
66
|
+
*
|
|
67
|
+
* Sequential, not `Promise.all`. A job runner is heavy by definition — that is
|
|
68
|
+
* why it is a job — and twenty of them concurrently inside one 540s function
|
|
69
|
+
* is how a sweep exhausts its own budget. Twenty sequential attempts that run
|
|
70
|
+
* out of time simply leave the rest `pending` for the next tick, which is the
|
|
71
|
+
* safe failure.
|
|
72
|
+
*/
|
|
73
|
+
export async function runDueJobRetries(ctx) {
|
|
74
|
+
const due = await jobsRepository.getDueRetries();
|
|
75
|
+
if (due.length === 0)
|
|
76
|
+
return { ran: 0 };
|
|
77
|
+
ctx.logger.info("job retries: running due attempts", { count: due.length });
|
|
78
|
+
let ran = 0;
|
|
79
|
+
for (const { id, data } of due) {
|
|
80
|
+
/*
|
|
81
|
+
* `runJobOnce` never throws — it records every outcome on the job itself.
|
|
82
|
+
* The `.catch` is belt-and-braces so one pathological job cannot abort the
|
|
83
|
+
* rest of the sweep, which would leave the remainder un-retried with no
|
|
84
|
+
* record of why.
|
|
85
|
+
*/
|
|
86
|
+
await runJobOnce({ jobId: id, job: data }, ctx).catch((err) => {
|
|
87
|
+
ctx.logger.error("job retries: attempt threw", err, { jobId: id });
|
|
88
|
+
});
|
|
89
|
+
ran += 1;
|
|
58
90
|
}
|
|
91
|
+
return { ran };
|
|
59
92
|
}
|
|
@@ -6,9 +6,16 @@ export type OrderAfter = {
|
|
|
6
6
|
userEmail: string;
|
|
7
7
|
productTitle: string;
|
|
8
8
|
trackingNumber?: string;
|
|
9
|
+
/**
|
|
10
|
+
* Phase 7 — the invoice is numbered on the PAYMENT transition, which is a
|
|
11
|
+
* different axis from `status` and is why these two fields are here.
|
|
12
|
+
*/
|
|
13
|
+
paymentStatus?: string;
|
|
14
|
+
invoiceNumber?: string;
|
|
9
15
|
};
|
|
10
16
|
export type OrderBefore = {
|
|
11
17
|
status: OrderStatus;
|
|
18
|
+
paymentStatus?: string;
|
|
12
19
|
};
|
|
13
20
|
export interface HandleOrderStatusChangeInput {
|
|
14
21
|
orderId: string;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { decryptPii } from "../../../../security/index.js";
|
|
2
2
|
import { sendNotification } from "../../../../features/admin/actions/notification-actions.js";
|
|
3
|
+
import { orderRepository } from "../../../../features/orders/repository/orders.repository.js";
|
|
4
|
+
/* Phase 7 — numbering only; the PDF renderer lives in `functions/` because
|
|
5
|
+
* `pdfkit` must not enter appkit's module graph. */
|
|
6
|
+
import { allocateSeriesNumber } from "../../features/invoicing/allocate.js";
|
|
3
7
|
import { ORDER_MESSAGES } from "../handlers/messages.js";
|
|
4
8
|
const STATUS_CONFIG = {
|
|
5
9
|
confirmed: {
|
|
@@ -31,6 +35,64 @@ export async function handleOrderStatusChange(input, ctx) {
|
|
|
31
35
|
const { orderId, before, after } = input;
|
|
32
36
|
if (!before || !after)
|
|
33
37
|
return;
|
|
38
|
+
/*
|
|
39
|
+
* ── PHASE 7: number the invoice on the PAYMENT transition ─────────────────
|
|
40
|
+
*
|
|
41
|
+
* 🛑 PLACED FIRST, before the `status`-unchanged early return. That return
|
|
42
|
+
* is correct for the notification work below it, and it is also why this
|
|
43
|
+
* cannot go after it: `paymentStatus: pending → paid` frequently happens
|
|
44
|
+
* with NO change to `status` at all (a COD order verified by an admin, a
|
|
45
|
+
* manual proof approved), so appending this below would have numbered only
|
|
46
|
+
* the minority of paid orders whose status happened to move in the same
|
|
47
|
+
* write. That is Root Cause #27's sweep-coverage trap, and R1 in
|
|
48
|
+
* `paymentWindowTimeout` had to be placed the same way for the same reason.
|
|
49
|
+
*
|
|
50
|
+
* 🛑 Extending THIS trigger rather than adding a second one on `orders`.
|
|
51
|
+
* A second Firestore trigger on the same collection wakes for every write
|
|
52
|
+
* and returns — which is how `onShipmentDeleted` burned a million
|
|
53
|
+
* invocations a day sitting correctly-guarded beside a neighbour's loop
|
|
54
|
+
* (Root Cause #92). The sticker-trace definitions carry the same 🛑.
|
|
55
|
+
*
|
|
56
|
+
* 🛑 And it is numbering ONLY — no PDF. `pdfkit` is a Functions dependency
|
|
57
|
+
* and must not enter appkit's module graph; the bytes are rendered once,
|
|
58
|
+
* lazily, by the serve route and stored from there. Numbering is the half
|
|
59
|
+
* that must be immediate and transactional, because it is the half that
|
|
60
|
+
* cannot be corrected: a number cannot be withdrawn without leaving a gap.
|
|
61
|
+
*/
|
|
62
|
+
const becamePaid = before.paymentStatus !== "paid" && after.paymentStatus === "paid";
|
|
63
|
+
if (becamePaid && !after.invoiceNumber) {
|
|
64
|
+
try {
|
|
65
|
+
const allocation = await allocateSeriesNumber({
|
|
66
|
+
kind: "invoice",
|
|
67
|
+
subjectId: orderId,
|
|
68
|
+
orderId,
|
|
69
|
+
});
|
|
70
|
+
/*
|
|
71
|
+
* Only writes when THIS call allocated. A re-entry that found an
|
|
72
|
+
* existing claim skips the write entirely, which keeps a re-fired
|
|
73
|
+
* trigger from touching the order at all — the write that would
|
|
74
|
+
* otherwise re-fire this very trigger.
|
|
75
|
+
*/
|
|
76
|
+
if (allocation.allocated) {
|
|
77
|
+
await orderRepository.update(orderId, {
|
|
78
|
+
invoiceNumber: allocation.number,
|
|
79
|
+
invoiceIssuedAt: new Date(),
|
|
80
|
+
});
|
|
81
|
+
ctx.logger.info(`Invoice ${allocation.number} allocated`, { orderId });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
/*
|
|
86
|
+
* Logged and swallowed, so a numbering failure cannot suppress the
|
|
87
|
+
* buyer's order notification below. The order stays un-invoiced, which
|
|
88
|
+
* is a visible, retryable state — the next write that touches
|
|
89
|
+
* `paymentStatus` re-attempts, and the serve route can allocate on
|
|
90
|
+
* demand. Throwing here would also make the Firestore trigger retry the
|
|
91
|
+
* whole handler, re-sending the notification.
|
|
92
|
+
*/
|
|
93
|
+
ctx.logger.error("Invoice allocation failed", error, { orderId });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
34
96
|
if (before.status === after.status)
|
|
35
97
|
return;
|
|
36
98
|
const newStatus = after.status;
|
|
@@ -138,81 +138,7 @@ export async function reconcileStuckGatewayEvents(ctx) {
|
|
|
138
138
|
}
|
|
139
139
|
return { redispatched, escalated, reads: stuck.length };
|
|
140
140
|
}
|
|
141
|
-
|
|
142
|
-
* "The gateway captured a different amount from the one the order records."
|
|
143
|
-
*
|
|
144
|
-
* Extracted from the sweep below, where it sat inside
|
|
145
|
-
* fn → `for record` → `for order` → `if` and the whole case literal was a
|
|
146
|
-
* brace too deep. Pure construction — no branching, and `recorded` is passed
|
|
147
|
-
* in rather than re-derived, so the one money rule still lives at the call
|
|
148
|
-
* site (Root Cause #75).
|
|
149
|
-
*/
|
|
150
|
-
async function openAmountMismatchCase(record, orderId, recorded, now) {
|
|
151
|
-
await paymentCasesRepository().openOrObserve({
|
|
152
|
-
kind: "amount_mismatch",
|
|
153
|
-
subjectId: `${record.id}:${orderId}`,
|
|
154
|
-
severity: "error",
|
|
155
|
-
title: `Captured ${record.amount} but recorded ${recorded} — ${orderId}`,
|
|
156
|
-
detail: [
|
|
157
|
-
`The gateway captured a different amount from the one the order records.`,
|
|
158
|
-
`Either the buyer was over-charged or the order under-states what was paid.`,
|
|
159
|
-
"",
|
|
160
|
-
`Gateway: ${record.gateway}`,
|
|
161
|
-
`Payment: ${record.id}`,
|
|
162
|
-
`Order: ${orderId}`,
|
|
163
|
-
`Captured: ${record.amount} ${record.currency}`,
|
|
164
|
-
`Order total: ${recorded}`,
|
|
165
|
-
`Difference: ${(record.amount - recorded).toFixed(2)}`,
|
|
166
|
-
].join("\n"),
|
|
167
|
-
entityType: "order",
|
|
168
|
-
entityId: orderId,
|
|
169
|
-
amountAtRisk: Math.abs(record.amount - recorded),
|
|
170
|
-
orderIds: [orderId],
|
|
171
|
-
gatewayPaymentId: record.id,
|
|
172
|
-
now,
|
|
173
|
-
});
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* "Our refund total disagrees with the gateway's."
|
|
177
|
-
*
|
|
178
|
-
* Same extraction as `openAmountMismatchCase`. `ours` and `base` are both
|
|
179
|
-
* computed by the caller from the shared helpers, so this never re-derives a
|
|
180
|
-
* refund figure of its own.
|
|
181
|
-
*
|
|
182
|
-
* 🛑 The severity split is the whole point and must not be flattened: the
|
|
183
|
-
* gateway having refunded MORE than we recorded is money that left without a
|
|
184
|
-
* record (`error`); our side showing more is a refund reserved and never
|
|
185
|
-
* completed (`warning`).
|
|
186
|
-
*/
|
|
187
|
-
async function openRefundMismatchCase(record, orderId, ours, base, now) {
|
|
188
|
-
await paymentCasesRepository().openOrObserve({
|
|
189
|
-
kind: "refund_mismatch",
|
|
190
|
-
subjectId: `${record.id}:refunds`,
|
|
191
|
-
severity: record.amountRefunded > ours ? "error" : "warning",
|
|
192
|
-
title: `Refund total disagrees with the gateway — ${orderId}`,
|
|
193
|
-
detail: [
|
|
194
|
-
record.amountRefunded > ours
|
|
195
|
-
? `🛑 The gateway has refunded MORE than our records show. Money has left that we did not record.`
|
|
196
|
-
: `Our records show more refunded than the gateway did — a refund may have been reserved and never completed.`,
|
|
197
|
-
"",
|
|
198
|
-
`Gateway: ${record.gateway}`,
|
|
199
|
-
`Payment: ${record.id}`,
|
|
200
|
-
`Order: ${orderId}`,
|
|
201
|
-
`Gateway refunded: ${record.amountRefunded}`,
|
|
202
|
-
`Our committed refunds: ${ours}`,
|
|
203
|
-
`Refundable base: ${base}`,
|
|
204
|
-
record.refundStatus ? `Gateway refund status: ${record.refundStatus}` : null,
|
|
205
|
-
]
|
|
206
|
-
.filter((l) => l !== null)
|
|
207
|
-
.join("\n"),
|
|
208
|
-
entityType: "order",
|
|
209
|
-
entityId: orderId,
|
|
210
|
-
amountAtRisk: Math.abs(record.amountRefunded - ours),
|
|
211
|
-
orderIds: [orderId],
|
|
212
|
-
gatewayPaymentId: record.id,
|
|
213
|
-
now,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
141
|
+
/* ──────────────── R2: the gateway has it — do we? ───────────────────────── */
|
|
216
142
|
async function reconcileGatewayPayments(ctx) {
|
|
217
143
|
const payment = getProviders().payment;
|
|
218
144
|
let examined = 0;
|
|
@@ -298,28 +224,9 @@ async function reconcileGatewayPayments(ctx) {
|
|
|
298
224
|
continue;
|
|
299
225
|
}
|
|
300
226
|
for (const order of orders) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
* pricing pipeline here and disagree with it the day a rate changes,
|
|
305
|
-
* which is a sixth copy of the money rule (Root Cause #75).
|
|
306
|
-
*/
|
|
307
|
-
const recorded = order.totalPrice ?? 0;
|
|
308
|
-
if (Math.abs(recorded - record.amount) > MONEY_EPSILON) {
|
|
309
|
-
amountMismatch += 1;
|
|
310
|
-
await openAmountMismatchCase(record, order.id, recorded, ctx.now);
|
|
311
|
-
}
|
|
312
|
-
/*
|
|
313
|
-
* Refund check — the gateway's `amount_refunded` against our own
|
|
314
|
-
* non-failed refund events. `committedRefundTotal` is the same helper
|
|
315
|
-
* the refund cap uses, so "what we think we refunded" has ONE
|
|
316
|
-
* definition rather than a second one written here.
|
|
317
|
-
*/
|
|
318
|
-
const ours = committedRefundTotal(order);
|
|
319
|
-
if (Math.abs(ours - record.amountRefunded) > MONEY_EPSILON) {
|
|
320
|
-
refundMismatch += 1;
|
|
321
|
-
await openRefundMismatchCase(record, order.id, ours, refundableBase(order), ctx.now);
|
|
322
|
-
}
|
|
227
|
+
const outcome = await reconcilePaymentAgainstOrder(record, order, ctx);
|
|
228
|
+
amountMismatch += outcome.amountMismatch;
|
|
229
|
+
refundMismatch += outcome.refundMismatch;
|
|
323
230
|
}
|
|
324
231
|
}
|
|
325
232
|
catch (err) {
|
|
@@ -331,6 +238,89 @@ async function reconcileGatewayPayments(ctx) {
|
|
|
331
238
|
}
|
|
332
239
|
return { examined, missing, amountMismatch, refundMismatch, reads };
|
|
333
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* The two per-order checks R2 runs against one gateway record.
|
|
243
|
+
*
|
|
244
|
+
* Extracted so each `openOrObserve` call is a statement in a function body
|
|
245
|
+
* rather than five braces deep inside `for record -> try -> for order -> if`.
|
|
246
|
+
* Returns the counts rather than mutating the caller's accumulators, so the
|
|
247
|
+
* loop above reads as a sum rather than as two side effects.
|
|
248
|
+
*/
|
|
249
|
+
async function reconcilePaymentAgainstOrder(record, order, ctx) {
|
|
250
|
+
let amountMismatch = 0;
|
|
251
|
+
let refundMismatch = 0;
|
|
252
|
+
/*
|
|
253
|
+
* Amount check. Compared against the order's own recorded total rather
|
|
254
|
+
* than recomputed from its lines — recomputing would re-derive the
|
|
255
|
+
* pricing pipeline here and disagree with it the day a rate changes,
|
|
256
|
+
* which is a sixth copy of the money rule (Root Cause #75).
|
|
257
|
+
*/
|
|
258
|
+
const recorded = order.totalPrice ?? 0;
|
|
259
|
+
if (Math.abs(recorded - record.amount) > MONEY_EPSILON) {
|
|
260
|
+
amountMismatch += 1;
|
|
261
|
+
await paymentCasesRepository().openOrObserve({
|
|
262
|
+
kind: "amount_mismatch",
|
|
263
|
+
subjectId: `${record.id}:${order.id}`,
|
|
264
|
+
severity: "error",
|
|
265
|
+
title: `Captured ${record.amount} but recorded ${recorded} — ${order.id}`,
|
|
266
|
+
detail: [
|
|
267
|
+
`The gateway captured a different amount from the one the order records.`,
|
|
268
|
+
`Either the buyer was over-charged or the order under-states what was paid.`,
|
|
269
|
+
"",
|
|
270
|
+
`Gateway: ${record.gateway}`,
|
|
271
|
+
`Payment: ${record.id}`,
|
|
272
|
+
`Order: ${order.id}`,
|
|
273
|
+
`Captured: ${record.amount} ${record.currency}`,
|
|
274
|
+
`Order total: ${recorded}`,
|
|
275
|
+
`Difference: ${(record.amount - recorded).toFixed(2)}`,
|
|
276
|
+
].join("\n"),
|
|
277
|
+
entityType: "order",
|
|
278
|
+
entityId: order.id,
|
|
279
|
+
amountAtRisk: Math.abs(record.amount - recorded),
|
|
280
|
+
orderIds: [order.id],
|
|
281
|
+
gatewayPaymentId: record.id,
|
|
282
|
+
now: ctx.now,
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
/*
|
|
286
|
+
* Refund check — the gateway's `amount_refunded` against our own
|
|
287
|
+
* non-failed refund events. `committedRefundTotal` is the same helper
|
|
288
|
+
* the refund cap uses, so "what we think we refunded" has ONE
|
|
289
|
+
* definition rather than a second one written here.
|
|
290
|
+
*/
|
|
291
|
+
const ours = committedRefundTotal(order);
|
|
292
|
+
if (Math.abs(ours - record.amountRefunded) > MONEY_EPSILON) {
|
|
293
|
+
refundMismatch += 1;
|
|
294
|
+
await paymentCasesRepository().openOrObserve({
|
|
295
|
+
kind: "refund_mismatch",
|
|
296
|
+
subjectId: `${record.id}:refunds`,
|
|
297
|
+
severity: record.amountRefunded > ours ? "error" : "warning",
|
|
298
|
+
title: `Refund total disagrees with the gateway — ${order.id}`,
|
|
299
|
+
detail: [
|
|
300
|
+
record.amountRefunded > ours
|
|
301
|
+
? `🛑 The gateway has refunded MORE than our records show. Money has left that we did not record.`
|
|
302
|
+
: `Our records show more refunded than the gateway did — a refund may have been reserved and never completed.`,
|
|
303
|
+
"",
|
|
304
|
+
`Gateway: ${record.gateway}`,
|
|
305
|
+
`Payment: ${record.id}`,
|
|
306
|
+
`Order: ${order.id}`,
|
|
307
|
+
`Gateway refunded: ${record.amountRefunded}`,
|
|
308
|
+
`Our committed refunds: ${ours}`,
|
|
309
|
+
`Refundable base: ${refundableBase(order)}`,
|
|
310
|
+
record.refundStatus ? `Gateway refund status: ${record.refundStatus}` : null,
|
|
311
|
+
]
|
|
312
|
+
.filter((l) => l !== null)
|
|
313
|
+
.join("\n"),
|
|
314
|
+
entityType: "order",
|
|
315
|
+
entityId: order.id,
|
|
316
|
+
amountAtRisk: Math.abs(record.amountRefunded - ours),
|
|
317
|
+
orderIds: [order.id],
|
|
318
|
+
gatewayPaymentId: record.id,
|
|
319
|
+
now: ctx.now,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return { amountMismatch, refundMismatch };
|
|
323
|
+
}
|
|
334
324
|
/* ──────────────── R3: refund claimed, never settled ─────────────────────── */
|
|
335
325
|
/**
|
|
336
326
|
* A `refundClaims` row still `claimed` an hour later is a crash between
|
|
@@ -6,6 +6,9 @@ import { OrderStatusValues } from "../../../../features/orders/schemas/firestore
|
|
|
6
6
|
import { PAYMENT_WINDOW_EXPIRED_REASON } from "../../../../features/orders/constants/payment-window.js";
|
|
7
7
|
import { ORDER_MESSAGES } from "../handlers/messages.js";
|
|
8
8
|
import { reconcileStuckGatewayEvents } from "./paymentReconcile.js";
|
|
9
|
+
/* Phase 8 — the job retry sweep. Folded in here rather than given its own
|
|
10
|
+
* Scheduler job; see its own docstring and the 🛑 at the call site. */
|
|
11
|
+
import { runDueJobRetries } from "./onJobCreated.js";
|
|
9
12
|
/**
|
|
10
13
|
* 15-minute payment window sweep — cancels + restocks any `upi_manual`/
|
|
11
14
|
* `cash`/`emi` order still `pending`/`pending` with no proof uploaded past
|
|
@@ -53,6 +56,36 @@ export async function runPaymentWindowTimeout(ctx) {
|
|
|
53
56
|
err,
|
|
54
57
|
});
|
|
55
58
|
}
|
|
59
|
+
/*
|
|
60
|
+
* ── Phase 8 rides along too: job retries whose backoff has elapsed ────────
|
|
61
|
+
*
|
|
62
|
+
* Same two reasons as R1 above, and the same placement rule: **FIRST, before
|
|
63
|
+
* the early return**. A retry is a `pending` job document with a future
|
|
64
|
+
* `nextAttemptAt`, and nothing else in the system will ever run it — the
|
|
65
|
+
* create trigger deliberately defers to this sweep, because creating the
|
|
66
|
+
* retry document fires that trigger instantly and would collapse the backoff
|
|
67
|
+
* to zero.
|
|
68
|
+
*
|
|
69
|
+
* So if this call sat after the early return, a retry would run only on a
|
|
70
|
+
* tick that happened to have an expired payment window — which is the
|
|
71
|
+
* minority — and a job scheduled on a quiet afternoon could wait hours.
|
|
72
|
+
* Root Cause #27 again, now with a second mechanism depending on it.
|
|
73
|
+
*
|
|
74
|
+
* Never throws, for the same reason R1 does not: a stuck job must not stop
|
|
75
|
+
* the payment-window cancellations this function exists for.
|
|
76
|
+
*/
|
|
77
|
+
try {
|
|
78
|
+
const retries = await runDueJobRetries(ctx);
|
|
79
|
+
if (retries.ran > 0) {
|
|
80
|
+
ctx.logger.info("Phase 8: ran due job retries", { ...retries });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch (err) {
|
|
84
|
+
void normalizeError(err);
|
|
85
|
+
ctx.logger.error("job retry sweep failed inside paymentWindowTimeout — continuing", {
|
|
86
|
+
err,
|
|
87
|
+
});
|
|
88
|
+
}
|
|
56
89
|
ctx.logger.info("Scanning orders past their 15-minute payment deadline");
|
|
57
90
|
const expired = await orderRepository.getExpiredPaymentDeadlines();
|
|
58
91
|
if (expired.length === 0) {
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run ONE attempt of a job, and decide what happens next (Phase 8).
|
|
3
|
+
*
|
|
4
|
+
* ## 🛑 ONE dispatch path, shared by the trigger and the retry sweep
|
|
5
|
+
*
|
|
6
|
+
* `handleJobCreated` is the `jobs/{jobId}` create trigger; `runDueJobRetries`
|
|
7
|
+
* is the sweep that picks up scheduled retries. Both call THIS, so there is
|
|
8
|
+
* exactly one place that knows how to run a job, classify its failure and
|
|
9
|
+
* schedule the next attempt. Two dispatch paths would drift the first time one
|
|
10
|
+
* of them learned something — which is the shape of every defect in this
|
|
11
|
+
* plan's § "Recurrent Root Cause Patterns" that begins "a fix applied to the
|
|
12
|
+
* route but never back-ported".
|
|
13
|
+
*
|
|
14
|
+
* ## 🛑 A retry is a NEW DOCUMENT
|
|
15
|
+
*
|
|
16
|
+
* `onJobCreated` fires on CREATE. Re-running a job by flipping its status back
|
|
17
|
+
* to `pending` would never wake the trigger, so the job would sit `pending`
|
|
18
|
+
* forever looking scheduled — a stall that reads as "queued". So a retry is a
|
|
19
|
+
* fresh `jobs` document carrying `attempt + 1`, `parentJobId` and a
|
|
20
|
+
* `nextAttemptAt`, and the sweep is what runs it when due.
|
|
21
|
+
*
|
|
22
|
+
* ## What is retryable
|
|
23
|
+
*
|
|
24
|
+
* `mapToHttpError(err).status >= 500`. Reused rather than invented: that
|
|
25
|
+
* function already encodes "is this our fault or the caller's" for every error
|
|
26
|
+
* class in the codebase, and a second classification would answer differently
|
|
27
|
+
* the first time either changed.
|
|
28
|
+
*
|
|
29
|
+
* A 4xx is terminal IMMEDIATELY — no backoff, no attempts burned. A validation
|
|
30
|
+
* failure does not become valid by waiting, and retrying it three times over
|
|
31
|
+
* 35 minutes just delays the case by 35 minutes.
|
|
32
|
+
*/
|
|
33
|
+
import { type JobDocument } from "../../../../features/jobs/schemas/firestore";
|
|
34
|
+
import type { JobContext } from "../runtime/types";
|
|
35
|
+
export interface RunJobOnceInput {
|
|
36
|
+
jobId: string;
|
|
37
|
+
job: JobDocument;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Run one attempt. Marks the job done, schedules a retry, or declares it dead.
|
|
41
|
+
*
|
|
42
|
+
* Never throws: a job's failure is recorded on the job, and letting it escape
|
|
43
|
+
* would make the Firebase trigger retry the whole handler — a second,
|
|
44
|
+
* uncontrolled retry mechanism layered under this one, which is precisely the
|
|
45
|
+
* doubling that cost 55 sessions in Root Cause #91 (`run.mjs`'s internal retry
|
|
46
|
+
* multiplying the pool's).
|
|
47
|
+
*/
|
|
48
|
+
export declare function runJobOnce(input: RunJobOnceInput, ctx: JobContext): Promise<void>;
|