@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
|
@@ -0,0 +1,195 @@
|
|
|
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 { normalizeError } from "../../../../errors/normalize.js";
|
|
34
|
+
import { mapToHttpError } from "../../../../errors/error-mapping.js";
|
|
35
|
+
import { jobsRepository } from "../../../../repositories/index.js";
|
|
36
|
+
import { paymentCasesRepository } from "../../../../features/payment-cases/repository/payment-cases.repository.js";
|
|
37
|
+
import { getAdminRealtimeDb } from "../../../../providers/db-firebase/index.js";
|
|
38
|
+
import { RTDB_PATHS } from "../../../../providers/db-firebase/rtdb-paths.js";
|
|
39
|
+
import { JOB_BACKOFF_MS, JOB_MAX_ATTEMPTS, } from "../../../../features/jobs/schemas/firestore.js";
|
|
40
|
+
import { JOB_RUNNERS } from "./jobRunners.js";
|
|
41
|
+
async function pingBulkEvent(jobId, payload, ctx) {
|
|
42
|
+
try {
|
|
43
|
+
await getAdminRealtimeDb()
|
|
44
|
+
.ref(`${RTDB_PATHS.BULK_EVENTS}/${jobId}`)
|
|
45
|
+
.set({ ...payload, updatedAt: Date.now() });
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
void normalizeError(err);
|
|
49
|
+
ctx.logger.warn("runJobOnce: RTDB ping failed (non-fatal)", {
|
|
50
|
+
jobId,
|
|
51
|
+
error: err instanceof Error ? err.message : String(err),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The delay before the next attempt.
|
|
57
|
+
*
|
|
58
|
+
* 🛑 Clamped to the LAST element rather than indexing past the end. An
|
|
59
|
+
* out-of-range index yields `undefined`, `new Date(Date.now() + undefined)` is
|
|
60
|
+
* `Invalid Date`, and the sweep's `nextAttemptAt <= now` comparison is false
|
|
61
|
+
* for an invalid date forever — so the retry would be scheduled and never run.
|
|
62
|
+
* A stall that looks like a queue is the exact failure this phase removes.
|
|
63
|
+
*/
|
|
64
|
+
function backoffFor(attempt) {
|
|
65
|
+
const idx = Math.max(0, Math.min(attempt - 1, JOB_BACKOFF_MS.length - 1));
|
|
66
|
+
return JOB_BACKOFF_MS[idx];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Run one attempt. Marks the job done, schedules a retry, or declares it dead.
|
|
70
|
+
*
|
|
71
|
+
* Never throws: a job's failure is recorded on the job, and letting it escape
|
|
72
|
+
* would make the Firebase trigger retry the whole handler — a second,
|
|
73
|
+
* uncontrolled retry mechanism layered under this one, which is precisely the
|
|
74
|
+
* doubling that cost 55 sessions in Root Cause #91 (`run.mjs`'s internal retry
|
|
75
|
+
* multiplying the pool's).
|
|
76
|
+
*/
|
|
77
|
+
export async function runJobOnce(input, ctx) {
|
|
78
|
+
const { jobId, job } = input;
|
|
79
|
+
const attempt = job.attempt ?? 1;
|
|
80
|
+
const maxAttempts = job.maxAttempts ?? JOB_MAX_ATTEMPTS;
|
|
81
|
+
await jobsRepository.markProcessing(jobId);
|
|
82
|
+
const runner = JOB_RUNNERS[job.jobType];
|
|
83
|
+
if (!runner) {
|
|
84
|
+
/*
|
|
85
|
+
* 🛑 Terminal immediately, and DEAD rather than failed. An unregistered
|
|
86
|
+
* jobType cannot become registered by waiting, and the thing that would
|
|
87
|
+
* fix it is a deploy — so this is the clearest possible case for a human,
|
|
88
|
+
* not a retry. It is also the failure mode of the seed-loader registry
|
|
89
|
+
* (Root Cause #90): a missing map entry whose symptom is silence.
|
|
90
|
+
*/
|
|
91
|
+
const error = `No runner registered for jobType: ${job.jobType}`;
|
|
92
|
+
await jobsRepository.markDead(jobId, error);
|
|
93
|
+
await pingBulkEvent(jobId, { status: "failed", action: job.jobType, error }, ctx);
|
|
94
|
+
await openDeadJobCase(jobId, job, error, ctx);
|
|
95
|
+
ctx.logger.error("runJobOnce: unregistered jobType", null, { jobId, jobType: job.jobType });
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
const result = await runner(job.payload, ctx);
|
|
100
|
+
await jobsRepository.markDone(jobId, result);
|
|
101
|
+
await pingBulkEvent(jobId, { status: "success", action: job.jobType, ...result }, ctx);
|
|
102
|
+
ctx.logger.info("runJobOnce: job complete", {
|
|
103
|
+
jobId,
|
|
104
|
+
jobType: job.jobType,
|
|
105
|
+
attempt,
|
|
106
|
+
summary: result.summary,
|
|
107
|
+
});
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
void normalizeError(err);
|
|
112
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
113
|
+
/*
|
|
114
|
+
* 🛑 `mapToHttpError` rather than a bespoke test. It already decides "ours
|
|
115
|
+
* or theirs" for every error class here, so a 5xx is a transient fault
|
|
116
|
+
* worth retrying and a 4xx is a permanent one that will fail identically
|
|
117
|
+
* in five minutes.
|
|
118
|
+
*/
|
|
119
|
+
const status = mapToHttpError(err).status;
|
|
120
|
+
const retryable = status >= 500;
|
|
121
|
+
const attemptsLeft = attempt < maxAttempts;
|
|
122
|
+
if (retryable && attemptsLeft) {
|
|
123
|
+
const nextAttemptAt = new Date(Date.now() + backoffFor(attempt));
|
|
124
|
+
/*
|
|
125
|
+
* 🛑 The NEW document is created BEFORE this one is marked failed. The
|
|
126
|
+
* reverse ordering leaves a window where the job reads as failed with no
|
|
127
|
+
* retry scheduled — and if the process dies in that window, nothing
|
|
128
|
+
* retries and nothing says so. This way the worst case is a scheduled
|
|
129
|
+
* retry whose parent still reads `processing`, which the sweep handles
|
|
130
|
+
* and a human can see.
|
|
131
|
+
*/
|
|
132
|
+
const retryId = await jobsRepository.createRetry(jobId, job, {
|
|
133
|
+
attempt: attempt + 1,
|
|
134
|
+
maxAttempts,
|
|
135
|
+
nextAttemptAt,
|
|
136
|
+
});
|
|
137
|
+
await jobsRepository.markFailed(jobId, message);
|
|
138
|
+
/*
|
|
139
|
+
* The client is told "retrying", not "failed". `useBulkEvent` watches
|
|
140
|
+
* for a terminal state, and reporting failure here would stop a progress
|
|
141
|
+
* UI on a job that is still going.
|
|
142
|
+
*/
|
|
143
|
+
await pingBulkEvent(jobId, { status: "retrying", action: job.jobType, error: message, retryJobId: retryId }, ctx);
|
|
144
|
+
ctx.logger.warn("runJobOnce: attempt failed, retry scheduled", {
|
|
145
|
+
jobId,
|
|
146
|
+
retryId,
|
|
147
|
+
jobType: job.jobType,
|
|
148
|
+
attempt,
|
|
149
|
+
status,
|
|
150
|
+
nextAttemptAt: nextAttemptAt.toISOString(),
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
await jobsRepository.markDead(jobId, message);
|
|
155
|
+
await pingBulkEvent(jobId, { status: "failed", action: job.jobType, error: message }, ctx);
|
|
156
|
+
await openDeadJobCase(jobId, job, message, ctx);
|
|
157
|
+
ctx.logger.error("runJobOnce: job DEAD", err, {
|
|
158
|
+
jobId,
|
|
159
|
+
jobType: job.jobType,
|
|
160
|
+
attempt,
|
|
161
|
+
status,
|
|
162
|
+
reason: retryable ? "attempts exhausted" : "permanent failure",
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* A dead job is a case, not a log line.
|
|
168
|
+
*
|
|
169
|
+
* 🛑 Keyed on the ORIGINAL job id (`parentJobId ?? jobId`), so three exhausted
|
|
170
|
+
* attempts of one job produce ONE case rather than three. `openOrObserve`
|
|
171
|
+
* increments its own counter — the property that makes it safe to open a case
|
|
172
|
+
* from a hot path at all.
|
|
173
|
+
*
|
|
174
|
+
* Best-effort: a job is already dead by this point, and failing to file the
|
|
175
|
+
* case must not turn that into a thrown error from a trigger.
|
|
176
|
+
*/
|
|
177
|
+
async function openDeadJobCase(jobId, job, error, ctx) {
|
|
178
|
+
const subjectId = job.parentJobId ?? jobId;
|
|
179
|
+
try {
|
|
180
|
+
await paymentCasesRepository().openOrObserve({
|
|
181
|
+
kind: "dead_job",
|
|
182
|
+
subjectId,
|
|
183
|
+
severity: "error",
|
|
184
|
+
entityType: "job",
|
|
185
|
+
entityId: subjectId,
|
|
186
|
+
title: `Job ${job.jobType} gave up after ${job.attempt ?? 1} attempt(s)`,
|
|
187
|
+
detail: `The last error was: ${error}. ` +
|
|
188
|
+
`Requested by ${job.requestedBy}. The job document holds the payload.`,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
void normalizeError(err);
|
|
193
|
+
ctx.logger.error("runJobOnce: could not open dead_job case", err, { jobId });
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -8,7 +8,7 @@ import { getDefaultCurrency } from "../../../../core/index.js";
|
|
|
8
8
|
import { computePayoutDeduction } from "../../../shared/fees/calculator.js";
|
|
9
9
|
import { payoutCutoff } from "../../../shared/fees/payout-hold.js";
|
|
10
10
|
import { PROTECTION_CLAIM_WINDOW_HOURS } from "../../../shared/features/shipping/protections.js";
|
|
11
|
-
import { ordersTaxableValue, ordersShippingReimbursement, } from "../../../shared/checkout/order-math.js";
|
|
11
|
+
import { ordersTaxableValue, ordersShippingReimbursement, ordersPointsBorneByStore, } from "../../../shared/checkout/order-math.js";
|
|
12
12
|
export async function runWeeklyPayoutEligibility(ctx) {
|
|
13
13
|
// `FEATURE_PAYOUTS` gate deleted 2026-10-19 — see `autoPayoutEligibility.ts`.
|
|
14
14
|
const siteSettings = await siteSettingsRepository.getSingleton();
|
|
@@ -75,7 +75,15 @@ export async function runWeeklyPayoutEligibility(ctx) {
|
|
|
75
75
|
* `{ id, data }` wrappers, and passing the wrapper would read every field
|
|
76
76
|
* as undefined and silently reimburse zero.
|
|
77
77
|
*/
|
|
78
|
-
ordersShippingReimbursement(orders.map((o) => o.data))
|
|
78
|
+
ordersShippingReimbursement(orders.map((o) => o.data)),
|
|
79
|
+
/*
|
|
80
|
+
* D46/D47 — the store-funded points, both ends. Must match
|
|
81
|
+
* `autoPayoutEligibility` exactly, for the reason stated above
|
|
82
|
+
* `grossAmount`: the weekly run and the automatic one pay the same orders,
|
|
83
|
+
* and a term present in one and absent in the other is a seller paid two
|
|
84
|
+
* different amounts depending on which job got there first.
|
|
85
|
+
*/
|
|
86
|
+
ordersPointsBorneByStore(orders.map((o) => o.data)));
|
|
79
87
|
/*
|
|
80
88
|
* 🛑 REVEALED before building a payout — see the identical note in
|
|
81
89
|
* `autoPayoutEligibility.ts`. `findById` is ciphertext-by-default and
|
|
@@ -1,4 +1,21 @@
|
|
|
1
1
|
import { handleOrderStatusChange, } from "../core/onOrderStatusChange.js";
|
|
2
|
+
/*
|
|
3
|
+
* 🛑 Bound as **`documentUpdated`**, which is what Phase 7's numbering needs
|
|
4
|
+
* and is already what this function uses — verified rather than assumed.
|
|
5
|
+
*
|
|
6
|
+
* `paymentStatus: pending → paid` is frequently the ONLY field that moves on a
|
|
7
|
+
* COD or manual order, and `documentUpdated` fires on any update, so the
|
|
8
|
+
* handler sees it. `documentCreated` would miss every one of them, and
|
|
9
|
+
* `documentWritten` would add nothing except a wake-up on create, where an
|
|
10
|
+
* order is `pending` and there is nothing to number.
|
|
11
|
+
*
|
|
12
|
+
* 🛑 What makes a write-back from this handler safe is the allocation claim:
|
|
13
|
+
* it updates `orders` only when IT allocated, so the re-fire caused by its own
|
|
14
|
+
* write finds the claim taken and writes nothing. That is the guard Root Cause
|
|
15
|
+
* #92's 12M-invocation loop lacked — there the no-op check compared Firestore
|
|
16
|
+
* maps with `JSON.stringify`, which is key-order sensitive and could never
|
|
17
|
+
* match, so every invocation wrote and every write re-triggered.
|
|
18
|
+
*/
|
|
2
19
|
export const onOrderStatusChangeHandler = async (event, ctx) => {
|
|
3
20
|
await handleOrderStatusChange({ orderId: event.params.orderId, before: event.before, after: event.after }, ctx);
|
|
4
21
|
};
|
|
@@ -10,10 +10,33 @@
|
|
|
10
10
|
* its own outcome decides whether the circuit re-closes or re-opens.
|
|
11
11
|
*/
|
|
12
12
|
/**
|
|
13
|
-
* Email is the only outbound channel with a transport.
|
|
14
|
-
* other arm until 2026-10-20, when the Cloud API
|
|
13
|
+
* Email is the only outbound *notification* channel with a transport.
|
|
14
|
+
* "whatsapp" was the other arm until 2026-10-20, when the Cloud API
|
|
15
|
+
* integration was removed.
|
|
16
|
+
*
|
|
17
|
+
* ## 🛑 Phase 8 widened this to the RECONCILERS' reads — and only those
|
|
18
|
+
*
|
|
19
|
+
* `razorpay_read` and `shiprocket_read` name the one placement where "stop
|
|
20
|
+
* hammering a dead API" is the right behaviour. Two placements are **actively
|
|
21
|
+
* harmful** and must never be added:
|
|
22
|
+
*
|
|
23
|
+
* 1. **NOT the webhook ingest path.** An open breaker converts a transient
|
|
24
|
+
* blip into a DROPPED money event — and dropping is precisely what a
|
|
25
|
+
* breaker does. The ingest path's whole job is to persist first and ack
|
|
26
|
+
* honestly so the gateway retries; a breaker there would ack a 5xx we never
|
|
27
|
+
* recorded.
|
|
28
|
+
*
|
|
29
|
+
* 2. **NOT around `payment.refund()`.** A refund POST that timed out may have
|
|
30
|
+
* SUCCEEDED, so retrying it issues two refunds — and the idempotency claim
|
|
31
|
+
* does not help, because it is the same call with the same key making a
|
|
32
|
+
* second network attempt. The two-phase reserve/settle exists so a human
|
|
33
|
+
* can resolve that by reading the gateway, not so a breaker can guess.
|
|
34
|
+
*
|
|
35
|
+
* The names carry `_read` for exactly that reason: a reader is idempotent, so
|
|
36
|
+
* backing off costs only freshness. Anything that moves money is not a reader
|
|
37
|
+
* and does not belong in this union.
|
|
15
38
|
*/
|
|
16
|
-
export type NotificationChannel = "email";
|
|
39
|
+
export type NotificationChannel = "email" | "razorpay_read" | "shiprocket_read";
|
|
17
40
|
/**
|
|
18
41
|
* Returns false only when the circuit is open and still within its cooldown
|
|
19
42
|
* window. Any other state (closed, or open-but-cooldown-elapsed) is treated
|
|
@@ -1427,6 +1427,40 @@ export const ACTIONS = {
|
|
|
1427
1427
|
confirmKind: "danger",
|
|
1428
1428
|
},
|
|
1429
1429
|
},
|
|
1430
|
+
/*
|
|
1431
|
+
* 🛑 The confirmation names the consequence rather than the mechanism.
|
|
1432
|
+
* "Revoke session" means nothing to a buyer; "that device is signed out"
|
|
1433
|
+
* is the fact they need to weigh, and the warning about their own device
|
|
1434
|
+
* is the one mistake this control makes easy to make.
|
|
1435
|
+
*/
|
|
1436
|
+
"revoke-session": {
|
|
1437
|
+
iconKey: "lock",
|
|
1438
|
+
id: "user.revoke-session",
|
|
1439
|
+
label: "Sign out device",
|
|
1440
|
+
ariaLabel: "Sign out this device",
|
|
1441
|
+
description: "End an active session so that device has to sign in again.",
|
|
1442
|
+
kind: "danger",
|
|
1443
|
+
confirmation: {
|
|
1444
|
+
title: "Sign out this device?",
|
|
1445
|
+
body: "Whoever is using it will be signed out and will need your password to get back in. If this is the device you are on right now, you will be signed out too.",
|
|
1446
|
+
confirmLabel: "Sign out device",
|
|
1447
|
+
confirmKind: "danger",
|
|
1448
|
+
},
|
|
1449
|
+
},
|
|
1450
|
+
"delete-payment-method": {
|
|
1451
|
+
iconKey: "delete",
|
|
1452
|
+
id: "user.delete-payment-method",
|
|
1453
|
+
label: "Remove card",
|
|
1454
|
+
ariaLabel: "Remove this saved payment method",
|
|
1455
|
+
description: "Remove a saved payment method from the account.",
|
|
1456
|
+
kind: "danger",
|
|
1457
|
+
confirmation: {
|
|
1458
|
+
title: "Remove this payment method?",
|
|
1459
|
+
body: "It will no longer appear at checkout. Any order already placed with it is unaffected.",
|
|
1460
|
+
confirmLabel: "Remove",
|
|
1461
|
+
confirmKind: "danger",
|
|
1462
|
+
},
|
|
1463
|
+
},
|
|
1430
1464
|
"set-default-address": {
|
|
1431
1465
|
iconKey: "confirm",
|
|
1432
1466
|
id: "user.set-default-address",
|
|
@@ -2397,17 +2431,44 @@ export const ACTIONS = {
|
|
|
2397
2431
|
kind: "ghost",
|
|
2398
2432
|
permissions: ["admin"],
|
|
2399
2433
|
},
|
|
2434
|
+
"activate-carousel": {
|
|
2435
|
+
iconKey: "resume",
|
|
2436
|
+
id: "admin.activate-carousel",
|
|
2437
|
+
label: "Activate",
|
|
2438
|
+
ariaLabel: "Show this slide in the carousel",
|
|
2439
|
+
description: "Set a carousel slide to active so the hero carousel renders it.",
|
|
2440
|
+
kind: "primary",
|
|
2441
|
+
permissions: ["admin"],
|
|
2442
|
+
},
|
|
2443
|
+
"deactivate-carousel": {
|
|
2444
|
+
iconKey: "hide",
|
|
2445
|
+
id: "admin.deactivate-carousel",
|
|
2446
|
+
label: "Deactivate",
|
|
2447
|
+
ariaLabel: "Hide this slide from the carousel",
|
|
2448
|
+
description: "Set a carousel slide to inactive without deleting it.",
|
|
2449
|
+
kind: "secondary",
|
|
2450
|
+
permissions: ["admin"],
|
|
2451
|
+
},
|
|
2400
2452
|
/*
|
|
2401
|
-
* Homepage sections have no bucket of their own, so their
|
|
2402
|
-
*
|
|
2403
|
-
*
|
|
2453
|
+
* Homepage sections have no bucket of their own, so their leaves live here
|
|
2454
|
+
* beside the carousel ones for the same reason — both are homepage chrome
|
|
2455
|
+
* an admin edits from a list rather than from a detail page.
|
|
2456
|
+
*
|
|
2457
|
+
* 🛑 `delete-section` exists but is NOT reachable from the public homepage.
|
|
2458
|
+
* Section `order` is POSITIONAL, so removing one leaves a gap in its
|
|
2459
|
+
* neighbours' numbering — not something to offer from a public page, where
|
|
2460
|
+
* the only confirmation a person gets is a dialog.
|
|
2404
2461
|
*
|
|
2405
|
-
*
|
|
2406
|
-
*
|
|
2407
|
-
*
|
|
2408
|
-
* a
|
|
2409
|
-
*
|
|
2410
|
-
*
|
|
2462
|
+
* That prohibition is enforced INDEPENDENTLY of this registry, which is
|
|
2463
|
+
* why the leaf can exist: `RESOURCE_LEAVES.SECTION` in `OwnerQuickActions`
|
|
2464
|
+
* omits `remove`, and `showDelete` gates on `leaves.remove` rather than on
|
|
2465
|
+
* what a call site passes — so the pencil strip renders no bin regardless.
|
|
2466
|
+
* This comment read "there is deliberately NO delete-section counterpart"
|
|
2467
|
+
* until 2026-10-22, when the admin sections listing gained a real one.
|
|
2468
|
+
*
|
|
2469
|
+
* The listing is the right home for it: it owns these records, it shows
|
|
2470
|
+
* the `order` column the gap appears in, and its Reorder panel has a
|
|
2471
|
+
* Reindex 1..N button beside it to close one.
|
|
2411
2472
|
*/
|
|
2412
2473
|
"edit-section": {
|
|
2413
2474
|
iconKey: "edit",
|
|
@@ -2418,6 +2479,62 @@ export const ACTIONS = {
|
|
|
2418
2479
|
kind: "ghost",
|
|
2419
2480
|
permissions: ["admin"],
|
|
2420
2481
|
},
|
|
2482
|
+
"enable-section": {
|
|
2483
|
+
iconKey: "resume",
|
|
2484
|
+
id: "admin.enable-section",
|
|
2485
|
+
label: "Enable",
|
|
2486
|
+
ariaLabel: "Enable this homepage section",
|
|
2487
|
+
description: "Render this section on its surface again.",
|
|
2488
|
+
kind: "primary",
|
|
2489
|
+
permissions: ["admin"],
|
|
2490
|
+
},
|
|
2491
|
+
"disable-section": {
|
|
2492
|
+
iconKey: "hide",
|
|
2493
|
+
id: "admin.disable-section",
|
|
2494
|
+
label: "Disable",
|
|
2495
|
+
ariaLabel: "Disable this homepage section",
|
|
2496
|
+
description: "Stop rendering this section without deleting it or its saved config.",
|
|
2497
|
+
kind: "secondary",
|
|
2498
|
+
permissions: ["admin"],
|
|
2499
|
+
},
|
|
2500
|
+
"delete-section": {
|
|
2501
|
+
iconKey: "delete",
|
|
2502
|
+
id: "admin.delete-section",
|
|
2503
|
+
label: "Delete Section",
|
|
2504
|
+
ariaLabel: "Delete this homepage section",
|
|
2505
|
+
description: "Permanently remove this homepage section and its saved config.",
|
|
2506
|
+
kind: "danger",
|
|
2507
|
+
permissions: ["admin"],
|
|
2508
|
+
confirmation: {
|
|
2509
|
+
title: "Delete this section?",
|
|
2510
|
+
/*
|
|
2511
|
+
* The last sentence is load-bearing: it names the reversible
|
|
2512
|
+
* alternative at the moment of the irreversible choice, and that
|
|
2513
|
+
* alternative is one click away in the same row.
|
|
2514
|
+
*/
|
|
2515
|
+
body: "The section and its saved config are removed permanently. Display order is positional, so the sections after it keep their current numbers and a gap is left — Reorder sections → Reindex 1..N closes it. To hide the section from the homepage while keeping its config, disable it instead.",
|
|
2516
|
+
confirmLabel: "Delete section",
|
|
2517
|
+
confirmKind: "danger",
|
|
2518
|
+
},
|
|
2519
|
+
},
|
|
2520
|
+
"show-nav-item": {
|
|
2521
|
+
iconKey: "view",
|
|
2522
|
+
id: "admin.show-nav-item",
|
|
2523
|
+
label: "Show",
|
|
2524
|
+
ariaLabel: "Show this navigation item",
|
|
2525
|
+
description: "Make a navigation item visible in the sidebar and menus.",
|
|
2526
|
+
kind: "primary",
|
|
2527
|
+
permissions: ["admin"],
|
|
2528
|
+
},
|
|
2529
|
+
"hide-nav-item": {
|
|
2530
|
+
iconKey: "hide",
|
|
2531
|
+
id: "admin.hide-nav-item",
|
|
2532
|
+
label: "Hide",
|
|
2533
|
+
ariaLabel: "Hide this navigation item",
|
|
2534
|
+
description: "Hide a navigation item without deleting it.",
|
|
2535
|
+
kind: "secondary",
|
|
2536
|
+
permissions: ["admin"],
|
|
2537
|
+
},
|
|
2421
2538
|
"delete-carousel": {
|
|
2422
2539
|
iconKey: "delete",
|
|
2423
2540
|
id: "admin.delete-carousel",
|
|
@@ -3085,6 +3202,29 @@ export const ACTIONS = {
|
|
|
3085
3202
|
confirmKind: "danger",
|
|
3086
3203
|
},
|
|
3087
3204
|
},
|
|
3205
|
+
/*
|
|
3206
|
+
* 🛑 Distinct from `clear-previews` and `discard-staged`, which are
|
|
3207
|
+
* SESSION/STAGING operations over files no record points at yet. This one
|
|
3208
|
+
* removes a FINALISED asset, so the confirmation has to say what nothing
|
|
3209
|
+
* else in this bucket has to: the bytes are referenced by whatever already
|
|
3210
|
+
* embedded them, and the proxy resolves `/media/<shortId>` through the
|
|
3211
|
+
* `mediaAssets` row — so deleting is how a live page gets a broken image.
|
|
3212
|
+
*/
|
|
3213
|
+
"delete-media": {
|
|
3214
|
+
iconKey: "delete",
|
|
3215
|
+
id: "media.delete-media",
|
|
3216
|
+
label: "Delete file",
|
|
3217
|
+
ariaLabel: "Delete this media file",
|
|
3218
|
+
description: "Permanently delete a media file and the asset row that resolves it.",
|
|
3219
|
+
kind: "danger",
|
|
3220
|
+
permissions: ["admin"],
|
|
3221
|
+
confirmation: {
|
|
3222
|
+
title: "Delete this file?",
|
|
3223
|
+
body: "The file is removed from storage and stops resolving. Any product, blog post or section still embedding it will show a broken image — this cannot be undone.",
|
|
3224
|
+
confirmLabel: "Delete file",
|
|
3225
|
+
confirmKind: "danger",
|
|
3226
|
+
},
|
|
3227
|
+
},
|
|
3088
3228
|
},
|
|
3089
3229
|
SUPPORT: {
|
|
3090
3230
|
"create-ticket": {
|
|
@@ -3116,6 +3256,34 @@ export const ACTIONS = {
|
|
|
3116
3256
|
confirmLabel: "Close ticket",
|
|
3117
3257
|
},
|
|
3118
3258
|
},
|
|
3259
|
+
/*
|
|
3260
|
+
* Raising a dispute on an order.
|
|
3261
|
+
*
|
|
3262
|
+
* Filed under SUPPORT rather than a new ORDER bucket: there is no such
|
|
3263
|
+
* bucket, and a dispute's destination is a support investigation. A first
|
|
3264
|
+
* draft of the form wrote `ACTIONS.ORDERS?.["raise-dispute"]`, where the
|
|
3265
|
+
* optional chain would have silently resolved to `undefined` and rendered
|
|
3266
|
+
* a button with no label, no aria-label and no confirmation — which is
|
|
3267
|
+
* precisely what Rule #7 exists to stop.
|
|
3268
|
+
*
|
|
3269
|
+
* NO `confirmation`, deliberately. It is not destructive: nothing is
|
|
3270
|
+
* cancelled, no money moves, and the action's own docstring says it "does
|
|
3271
|
+
* not itself reverse payment/order status". A confirm dialog in front of
|
|
3272
|
+
* "tell us something went wrong" adds friction to the one path a buyer
|
|
3273
|
+
* with a problem needs to be able to take.
|
|
3274
|
+
*/
|
|
3275
|
+
"raise-dispute": {
|
|
3276
|
+
// `report`, which IS the Flag glyph (icon-registry.ts:133). `"flag"` is
|
|
3277
|
+
// not a registered key — and because `iconKey` is typed `IconKey` rather
|
|
3278
|
+
// than `string`, that would have been a compile error rather than the
|
|
3279
|
+
// silent no-op it used to be.
|
|
3280
|
+
iconKey: "report",
|
|
3281
|
+
id: "support.raise-dispute",
|
|
3282
|
+
label: "Report this",
|
|
3283
|
+
ariaLabel: "Report a problem with this order",
|
|
3284
|
+
description: "Raise a dispute on an order for support to investigate.",
|
|
3285
|
+
kind: "primary",
|
|
3286
|
+
},
|
|
3119
3287
|
},
|
|
3120
3288
|
TESTER: {
|
|
3121
3289
|
"save-note": {
|
|
@@ -32,12 +32,54 @@ export interface GroupCouponDiscountResult {
|
|
|
32
32
|
appliedDiscounts: {
|
|
33
33
|
code: string;
|
|
34
34
|
couponId?: string;
|
|
35
|
+
/**
|
|
36
|
+
* 🛑 This is NOT `CouponType`, and the two are easy to confuse now that
|
|
37
|
+
* both live in this file. This one records WHERE the discount came from —
|
|
38
|
+
* a coupon the buyer typed, a deal, or an automatic rule — and it is what
|
|
39
|
+
* the order document stores in `appliedDiscounts[]`.
|
|
40
|
+
*
|
|
41
|
+
* `CartAppliedCoupon.type` is the coupon's own kind (`percentage`,
|
|
42
|
+
* `points_earn`, …) and is what the loop below skips on. Reading one where
|
|
43
|
+
* the other is meant would compile cleanly and be wrong: every value of
|
|
44
|
+
* this union is absent from that one, so the `points_earn` skip would
|
|
45
|
+
* never fire and a points coupon would discount the order.
|
|
46
|
+
*/
|
|
35
47
|
type: "coupon" | "deal" | "auto";
|
|
36
48
|
discountAmount: number;
|
|
37
49
|
scope?: "admin" | "seller";
|
|
38
50
|
storeId?: string;
|
|
39
51
|
}[];
|
|
40
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* What the CART-level call returns: the same totals, plus the per-group
|
|
55
|
+
* breakdown.
|
|
56
|
+
*
|
|
57
|
+
* 🛑 A separate interface rather than one optional field on the result above.
|
|
58
|
+
* `computeGroupCouponDiscount` scores ONE group, so it has no per-group list
|
|
59
|
+
* to report and never will — giving it an empty `perGroup` would be a value
|
|
60
|
+
* that type-checks and means nothing, and the capture route's points tender
|
|
61
|
+
* reads this array positionally. A caller that gets `[]` where it expected one
|
|
62
|
+
* entry per group apportions every point against group zero.
|
|
63
|
+
*/
|
|
64
|
+
export interface CartCouponDiscountResult extends GroupCouponDiscountResult {
|
|
65
|
+
/**
|
|
66
|
+
* The per-group discount, in the order the groups were passed.
|
|
67
|
+
*
|
|
68
|
+
* 🛑 Surfaced rather than aggregated-and-forgotten. It was already being
|
|
69
|
+
* computed inside the loop below and then summed away — the same shape as
|
|
70
|
+
* `CheckoutPricingPreviewStore`'s per-store fees, which existed only as an
|
|
71
|
+
* aggregate until a seller card needed them.
|
|
72
|
+
*
|
|
73
|
+
* The caller that needs it is the capture route's points tender: the
|
|
74
|
+
* per-group base includes that group's own discount, and re-deriving it
|
|
75
|
+
* there would be a second copy of the apportionment rule (Root Cause #75) on
|
|
76
|
+
* the one path D37 proves gets left behind.
|
|
77
|
+
*/
|
|
78
|
+
perGroup: {
|
|
79
|
+
storeId: string | undefined;
|
|
80
|
+
couponDiscount: number;
|
|
81
|
+
}[];
|
|
82
|
+
}
|
|
41
83
|
/**
|
|
42
84
|
* Prorates every buyer-applied coupon against one seller-group's total.
|
|
43
85
|
*
|
|
@@ -70,4 +112,4 @@ export declare function computeCartCouponDiscount(appliedCoupons: CartAppliedCou
|
|
|
70
112
|
itemId: string;
|
|
71
113
|
lineTotal: number;
|
|
72
114
|
}[];
|
|
73
|
-
}[]):
|
|
115
|
+
}[]): CartCouponDiscountResult;
|
|
@@ -42,6 +42,20 @@ export function computeGroupCouponDiscount(appliedCoupons, groupLines, groupTota
|
|
|
42
42
|
let couponDiscount = 0;
|
|
43
43
|
const appliedDiscounts = [];
|
|
44
44
|
for (const coupon of appliedCoupons) {
|
|
45
|
+
/*
|
|
46
|
+
* 🛑 D51 — a `points_earn` coupon DISCOUNTS NOTHING. It awards points on
|
|
47
|
+
* the post-discount amount paid, and it carries `pointsPercent` rather than
|
|
48
|
+
* a discount value.
|
|
49
|
+
*
|
|
50
|
+
* Skipped here rather than relied on to carry `discountAmount: 0`. The
|
|
51
|
+
* apply route writes whatever `validateCouponForCart` returned, and a
|
|
52
|
+
* points coupon that ever came back with a non-zero `discountAmount` —
|
|
53
|
+
* through a mis-seeded `discount.value`, or an admin editing a coupon's
|
|
54
|
+
* type after creation — would silently discount the order AND award points.
|
|
55
|
+
* A type check cannot be bypassed by bad data; a zero cannot be relied on.
|
|
56
|
+
*/
|
|
57
|
+
if (coupon.type === "points_earn")
|
|
58
|
+
continue;
|
|
45
59
|
let couponGroupDiscount = 0;
|
|
46
60
|
const isSellerScoped = coupon.scope === "seller" && coupon.storeId;
|
|
47
61
|
if (isSellerScoped) {
|
|
@@ -93,11 +107,22 @@ export function computeCartCouponDiscount(appliedCoupons, groups) {
|
|
|
93
107
|
const cartSubtotal = groups.reduce((sum, g) => sum + g.lines.reduce((s, l) => s + l.lineTotal, 0), 0);
|
|
94
108
|
let couponDiscount = 0;
|
|
95
109
|
const appliedDiscounts = [];
|
|
110
|
+
const perGroup = [];
|
|
96
111
|
for (const group of groups) {
|
|
97
112
|
const groupTotal = group.lines.reduce((s, l) => s + l.lineTotal, 0);
|
|
98
113
|
const res = computeGroupCouponDiscount(appliedCoupons, group.lines, groupTotal, cartSubtotal, group.storeId);
|
|
99
114
|
couponDiscount += res.couponDiscount;
|
|
100
115
|
appliedDiscounts.push(...res.appliedDiscounts);
|
|
116
|
+
/*
|
|
117
|
+
* 🛑 NOT rounded here, and the aggregate below IS. Rounding each group
|
|
118
|
+
* then summing can differ from summing then rounding by a paisa per group,
|
|
119
|
+
* and the aggregate is what the gateway captures — so the group figures
|
|
120
|
+
* stay raw and the caller rounds its own composed base once, exactly as
|
|
121
|
+
* `createOrderForGroup` does with a single `roundRupees` on `orderTotal`
|
|
122
|
+
* (D42). A rounded-then-summed discount is how a total ends up a paisa
|
|
123
|
+
* short of its own lines.
|
|
124
|
+
*/
|
|
125
|
+
perGroup.push({ storeId: group.storeId, couponDiscount: res.couponDiscount });
|
|
101
126
|
}
|
|
102
|
-
return { couponDiscount: roundRupees(couponDiscount), appliedDiscounts };
|
|
127
|
+
return { couponDiscount: roundRupees(couponDiscount), appliedDiscounts, perGroup };
|
|
103
128
|
}
|