@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
|
@@ -13,8 +13,69 @@ export declare class JobsRepository extends BaseRepository<JobDocument> {
|
|
|
13
13
|
markDone(id: string, result: JobResult): Promise<JobDocument>;
|
|
14
14
|
markFailed(id: string, error: string): Promise<JobDocument>;
|
|
15
15
|
/**
|
|
16
|
-
*
|
|
16
|
+
* Attempts exhausted, or a permanent failure. The dead-letter state
|
|
17
|
+
* (Phase 8).
|
|
18
|
+
*
|
|
19
|
+
* 🛑 Distinct from `markFailed`, which now means "this attempt failed and a
|
|
20
|
+
* retry is scheduled". Before Phase 8 there was only `failed`, and it was
|
|
21
|
+
* terminal with no signal — so a job that gave up looked exactly like a job
|
|
22
|
+
* that had just failed once, and nothing anywhere distinguished them.
|
|
23
|
+
*/
|
|
24
|
+
markDead(id: string, error: string): Promise<JobDocument>;
|
|
25
|
+
/**
|
|
26
|
+
* Schedule the next attempt as a NEW document (Phase 8).
|
|
27
|
+
*
|
|
28
|
+
* ## 🛑 Why a new document and not a reset of this one
|
|
29
|
+
*
|
|
30
|
+
* `onJobCreated` fires on CREATE. Flipping a job back to `pending` would
|
|
31
|
+
* never wake the trigger, so it would sit `pending` forever looking
|
|
32
|
+
* scheduled — a stall that reads as a queue. A fresh document is what makes
|
|
33
|
+
* the retry visible to the only mechanism that can run it.
|
|
34
|
+
*
|
|
35
|
+
* It also keeps the history: three attempts are three documents, each with
|
|
36
|
+
* its own error, so a repeating failure is legible instead of being
|
|
37
|
+
* overwritten by the last one.
|
|
38
|
+
*
|
|
39
|
+
* `parentJobId` points at the ORIGINAL — `parentJobId ?? id`, so attempt 3
|
|
40
|
+
* is linked to attempt 1 rather than to attempt 2. A flat fan means "every
|
|
41
|
+
* attempt of this job" is one equality query, where a linked list needs N
|
|
42
|
+
* reads to walk.
|
|
43
|
+
*/
|
|
44
|
+
createRetry(id: string, job: JobDocument, next: {
|
|
45
|
+
attempt: number;
|
|
46
|
+
maxAttempts: number;
|
|
47
|
+
nextAttemptAt: Date;
|
|
48
|
+
}): Promise<string>;
|
|
49
|
+
/**
|
|
50
|
+
* Retries whose `nextAttemptAt` has passed — the sweep's only query.
|
|
51
|
+
*
|
|
52
|
+
* 🛑 `status == pending` AND `nextAttemptAt <= now`, so it can never pick up
|
|
53
|
+
* a FIRST attempt: an original job carries no `nextAttemptAt`, and a
|
|
54
|
+
* Firestore inequality excludes every document that lacks the field. That
|
|
55
|
+
* exclusion is usually the trap (see `.where("isTestData","!=",true)` in
|
|
56
|
+
* CLAUDE.md, and D26's `orderBy` on an optional field); here it is exactly
|
|
57
|
+
* the filter wanted, and it is written down so nobody "fixes" it by adding
|
|
58
|
+
* a default.
|
|
59
|
+
*
|
|
60
|
+
* Bounded at 20 per tick. The sweep rides `paymentWindowTimeout`, which runs
|
|
61
|
+
* every 5 minutes, so that is 240 retries an hour — far above any real
|
|
62
|
+
* backlog, and low enough that a pathological queue cannot exhaust the
|
|
63
|
+
* function's own budget in one pass.
|
|
64
|
+
*/
|
|
65
|
+
getDueRetries(limit?: number): Promise<Array<{
|
|
66
|
+
id: string;
|
|
67
|
+
data: JobDocument;
|
|
68
|
+
}>>;
|
|
69
|
+
/**
|
|
70
|
+
* Refs for finished jobs older than `ttlDays` — used by the
|
|
17
71
|
* `cleanupRtdbEvents` sweep to prune the `jobs` collection.
|
|
72
|
+
*
|
|
73
|
+
* 🛑 `DEAD` is deliberately NOT in this list. A dead job has an open
|
|
74
|
+
* `dead_job` case pointing at it, and the job document holds the payload
|
|
75
|
+
* that would let someone re-run the work by hand — pruning it would leave a
|
|
76
|
+
* case referencing nothing, which is worse than a stale row. They are
|
|
77
|
+
* retained until a human resolves the case and can be pruned by a separate,
|
|
78
|
+
* case-aware sweep if they ever accumulate.
|
|
18
79
|
*/
|
|
19
80
|
getStaleFinishedRefs(ttlDays?: number): Promise<DocumentReference[]>;
|
|
20
81
|
}
|
|
@@ -31,8 +31,91 @@ export class JobsRepository extends BaseRepository {
|
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
/**
|
|
34
|
-
*
|
|
34
|
+
* Attempts exhausted, or a permanent failure. The dead-letter state
|
|
35
|
+
* (Phase 8).
|
|
36
|
+
*
|
|
37
|
+
* 🛑 Distinct from `markFailed`, which now means "this attempt failed and a
|
|
38
|
+
* retry is scheduled". Before Phase 8 there was only `failed`, and it was
|
|
39
|
+
* terminal with no signal — so a job that gave up looked exactly like a job
|
|
40
|
+
* that had just failed once, and nothing anywhere distinguished them.
|
|
41
|
+
*/
|
|
42
|
+
markDead(id, error) {
|
|
43
|
+
return this.update(id, {
|
|
44
|
+
status: JobStatusValues.DEAD,
|
|
45
|
+
error,
|
|
46
|
+
finishedAt: new Date(),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Schedule the next attempt as a NEW document (Phase 8).
|
|
51
|
+
*
|
|
52
|
+
* ## 🛑 Why a new document and not a reset of this one
|
|
53
|
+
*
|
|
54
|
+
* `onJobCreated` fires on CREATE. Flipping a job back to `pending` would
|
|
55
|
+
* never wake the trigger, so it would sit `pending` forever looking
|
|
56
|
+
* scheduled — a stall that reads as a queue. A fresh document is what makes
|
|
57
|
+
* the retry visible to the only mechanism that can run it.
|
|
58
|
+
*
|
|
59
|
+
* It also keeps the history: three attempts are three documents, each with
|
|
60
|
+
* its own error, so a repeating failure is legible instead of being
|
|
61
|
+
* overwritten by the last one.
|
|
62
|
+
*
|
|
63
|
+
* `parentJobId` points at the ORIGINAL — `parentJobId ?? id`, so attempt 3
|
|
64
|
+
* is linked to attempt 1 rather than to attempt 2. A flat fan means "every
|
|
65
|
+
* attempt of this job" is one equality query, where a linked list needs N
|
|
66
|
+
* reads to walk.
|
|
67
|
+
*/
|
|
68
|
+
async createRetry(id, job, next) {
|
|
69
|
+
const created = await this.create({
|
|
70
|
+
jobType: job.jobType,
|
|
71
|
+
status: JobStatusValues.PENDING,
|
|
72
|
+
payload: job.payload,
|
|
73
|
+
requestedBy: job.requestedBy,
|
|
74
|
+
attempt: next.attempt,
|
|
75
|
+
maxAttempts: next.maxAttempts,
|
|
76
|
+
nextAttemptAt: next.nextAttemptAt,
|
|
77
|
+
parentJobId: job.parentJobId ?? id,
|
|
78
|
+
lastError: job.error,
|
|
79
|
+
});
|
|
80
|
+
return created.id;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Retries whose `nextAttemptAt` has passed — the sweep's only query.
|
|
84
|
+
*
|
|
85
|
+
* 🛑 `status == pending` AND `nextAttemptAt <= now`, so it can never pick up
|
|
86
|
+
* a FIRST attempt: an original job carries no `nextAttemptAt`, and a
|
|
87
|
+
* Firestore inequality excludes every document that lacks the field. That
|
|
88
|
+
* exclusion is usually the trap (see `.where("isTestData","!=",true)` in
|
|
89
|
+
* CLAUDE.md, and D26's `orderBy` on an optional field); here it is exactly
|
|
90
|
+
* the filter wanted, and it is written down so nobody "fixes" it by adding
|
|
91
|
+
* a default.
|
|
92
|
+
*
|
|
93
|
+
* Bounded at 20 per tick. The sweep rides `paymentWindowTimeout`, which runs
|
|
94
|
+
* every 5 minutes, so that is 240 retries an hour — far above any real
|
|
95
|
+
* backlog, and low enough that a pathological queue cannot exhaust the
|
|
96
|
+
* function's own budget in one pass.
|
|
97
|
+
*/
|
|
98
|
+
async getDueRetries(limit = 20) {
|
|
99
|
+
const snap = await this.getCollection()
|
|
100
|
+
.where(JOB_FIELDS.STATUS, "==", JobStatusValues.PENDING)
|
|
101
|
+
.where(JOB_FIELDS.NEXT_ATTEMPT_AT, "<=", new Date())
|
|
102
|
+
.limit(limit)
|
|
103
|
+
.get();
|
|
104
|
+
return snap.docs.map((d) => ({
|
|
105
|
+
id: d.id,
|
|
106
|
+
data: { id: d.id, ...d.data() },
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Refs for finished jobs older than `ttlDays` — used by the
|
|
35
111
|
* `cleanupRtdbEvents` sweep to prune the `jobs` collection.
|
|
112
|
+
*
|
|
113
|
+
* 🛑 `DEAD` is deliberately NOT in this list. A dead job has an open
|
|
114
|
+
* `dead_job` case pointing at it, and the job document holds the payload
|
|
115
|
+
* that would let someone re-run the work by hand — pruning it would leave a
|
|
116
|
+
* case referencing nothing, which is worse than a stale row. They are
|
|
117
|
+
* retained until a human resolves the case and can be pruned by a separate,
|
|
118
|
+
* case-aware sweep if they ever accumulate.
|
|
36
119
|
*/
|
|
37
120
|
async getStaleFinishedRefs(ttlDays = 30) {
|
|
38
121
|
const cutoff = new Date();
|
|
@@ -23,6 +23,20 @@ export declare const JobStatusValues: {
|
|
|
23
23
|
readonly PROCESSING: "processing";
|
|
24
24
|
readonly DONE: "done";
|
|
25
25
|
readonly FAILED: "failed";
|
|
26
|
+
/**
|
|
27
|
+
* Phase 8 — attempts exhausted. The dead-letter state.
|
|
28
|
+
*
|
|
29
|
+
* 🛑 `failed` and `dead` are DIFFERENT facts and conflating them is what
|
|
30
|
+
* made a failed job terminal with no signal. `failed` now means "this
|
|
31
|
+
* attempt failed, and a retry is scheduled or the failure was permanent";
|
|
32
|
+
* `dead` means "we have stopped trying" and is the only one that opens a
|
|
33
|
+
* case.
|
|
34
|
+
*
|
|
35
|
+
* A reader that treats every non-`done` as hopeless loses that distinction —
|
|
36
|
+
* which is why the retention sweep excludes `dead` while an open case
|
|
37
|
+
* references it: the row is the evidence.
|
|
38
|
+
*/
|
|
39
|
+
readonly DEAD: "dead";
|
|
26
40
|
};
|
|
27
41
|
export type JobStatus = (typeof JobStatusValues)[keyof typeof JobStatusValues];
|
|
28
42
|
export interface JobResult {
|
|
@@ -52,7 +66,55 @@ export interface JobDocument extends BaseDocument {
|
|
|
52
66
|
error?: string;
|
|
53
67
|
startedAt?: Date;
|
|
54
68
|
finishedAt?: Date;
|
|
69
|
+
/**
|
|
70
|
+
* 1-based. Absent means 1 — every job written before this field is a first
|
|
71
|
+
* attempt, which is both true and what every reader should assume.
|
|
72
|
+
*/
|
|
73
|
+
attempt?: number;
|
|
74
|
+
/** Defaults to `JOB_MAX_ATTEMPTS`; a jobType may lower it. */
|
|
75
|
+
maxAttempts?: number;
|
|
76
|
+
/**
|
|
77
|
+
* When a retry becomes eligible.
|
|
78
|
+
*
|
|
79
|
+
* 🛑 A retry is a **NEW DOCUMENT**, not a mutation of this one, because
|
|
80
|
+
* `onJobCreated` fires on CREATE — re-running a job by flipping its status
|
|
81
|
+
* back to `pending` would never wake the trigger, and the job would sit
|
|
82
|
+
* `pending` forever looking scheduled. So this field lives on the NEW
|
|
83
|
+
* document and a sweep is what moves it from scheduled to running.
|
|
84
|
+
*/
|
|
85
|
+
nextAttemptAt?: Date;
|
|
86
|
+
/**
|
|
87
|
+
* The document this is a retry OF — present on every attempt after the
|
|
88
|
+
* first, and absent on the original.
|
|
89
|
+
*
|
|
90
|
+
* 🛑 Points at the ORIGINAL, not at the immediately previous attempt. A
|
|
91
|
+
* flat chain means "show me every attempt of this job" is one equality
|
|
92
|
+
* query, where a linked list would need N reads to walk; and the question
|
|
93
|
+
* anyone actually asks is "did this job eventually work", which is about the
|
|
94
|
+
* original.
|
|
95
|
+
*/
|
|
96
|
+
parentJobId?: string;
|
|
97
|
+
/** Why this attempt failed, kept per attempt so a pattern is visible. */
|
|
98
|
+
lastError?: string;
|
|
55
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Attempts before a job is declared dead.
|
|
102
|
+
*
|
|
103
|
+
* Three, with `JOB_BACKOFF_MS` below: a transient fault usually clears in
|
|
104
|
+
* minutes, and a fourth attempt an hour later is far more likely to be the
|
|
105
|
+
* same permanent fault again than a different outcome.
|
|
106
|
+
*/
|
|
107
|
+
export declare const JOB_MAX_ATTEMPTS = 3;
|
|
108
|
+
/**
|
|
109
|
+
* Delay before attempt 2 and attempt 3.
|
|
110
|
+
*
|
|
111
|
+
* 🛑 Indexed by `attempt - 1`, so the array length must be at least
|
|
112
|
+
* `JOB_MAX_ATTEMPTS - 1`. The last value is reused if it is shorter, rather
|
|
113
|
+
* than producing `undefined` — a `nextAttemptAt` of `Invalid Date` would make
|
|
114
|
+
* the sweep's `<= now` comparison false forever and the retry would never run,
|
|
115
|
+
* which is the silent-stall shape this whole phase exists to remove.
|
|
116
|
+
*/
|
|
117
|
+
export declare const JOB_BACKOFF_MS: readonly [number, number];
|
|
56
118
|
export declare const JOB_FIELDS: {
|
|
57
119
|
readonly JOB_TYPE: "jobType";
|
|
58
120
|
readonly STATUS: "status";
|
|
@@ -63,10 +125,28 @@ export declare const JOB_FIELDS: {
|
|
|
63
125
|
readonly FINISHED_AT: "finishedAt";
|
|
64
126
|
readonly CREATED_AT: "createdAt";
|
|
65
127
|
readonly UPDATED_AT: "updatedAt";
|
|
128
|
+
readonly ATTEMPT: "attempt";
|
|
129
|
+
readonly MAX_ATTEMPTS: "maxAttempts";
|
|
130
|
+
readonly NEXT_ATTEMPT_AT: "nextAttemptAt";
|
|
131
|
+
readonly PARENT_JOB_ID: "parentJobId";
|
|
66
132
|
readonly STATUS_VALUES: {
|
|
67
133
|
readonly PENDING: "pending";
|
|
68
134
|
readonly PROCESSING: "processing";
|
|
69
135
|
readonly DONE: "done";
|
|
70
136
|
readonly FAILED: "failed";
|
|
137
|
+
/**
|
|
138
|
+
* Phase 8 — attempts exhausted. The dead-letter state.
|
|
139
|
+
*
|
|
140
|
+
* 🛑 `failed` and `dead` are DIFFERENT facts and conflating them is what
|
|
141
|
+
* made a failed job terminal with no signal. `failed` now means "this
|
|
142
|
+
* attempt failed, and a retry is scheduled or the failure was permanent";
|
|
143
|
+
* `dead` means "we have stopped trying" and is the only one that opens a
|
|
144
|
+
* case.
|
|
145
|
+
*
|
|
146
|
+
* A reader that treats every non-`done` as hopeless loses that distinction —
|
|
147
|
+
* which is why the retention sweep excludes `dead` while an open case
|
|
148
|
+
* references it: the row is the evidence.
|
|
149
|
+
*/
|
|
150
|
+
readonly DEAD: "dead";
|
|
71
151
|
};
|
|
72
152
|
};
|
|
@@ -21,7 +21,39 @@ export const JobStatusValues = {
|
|
|
21
21
|
PROCESSING: "processing",
|
|
22
22
|
DONE: "done",
|
|
23
23
|
FAILED: "failed",
|
|
24
|
+
/**
|
|
25
|
+
* Phase 8 — attempts exhausted. The dead-letter state.
|
|
26
|
+
*
|
|
27
|
+
* 🛑 `failed` and `dead` are DIFFERENT facts and conflating them is what
|
|
28
|
+
* made a failed job terminal with no signal. `failed` now means "this
|
|
29
|
+
* attempt failed, and a retry is scheduled or the failure was permanent";
|
|
30
|
+
* `dead` means "we have stopped trying" and is the only one that opens a
|
|
31
|
+
* case.
|
|
32
|
+
*
|
|
33
|
+
* A reader that treats every non-`done` as hopeless loses that distinction —
|
|
34
|
+
* which is why the retention sweep excludes `dead` while an open case
|
|
35
|
+
* references it: the row is the evidence.
|
|
36
|
+
*/
|
|
37
|
+
DEAD: "dead",
|
|
24
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Attempts before a job is declared dead.
|
|
41
|
+
*
|
|
42
|
+
* Three, with `JOB_BACKOFF_MS` below: a transient fault usually clears in
|
|
43
|
+
* minutes, and a fourth attempt an hour later is far more likely to be the
|
|
44
|
+
* same permanent fault again than a different outcome.
|
|
45
|
+
*/
|
|
46
|
+
export const JOB_MAX_ATTEMPTS = 3;
|
|
47
|
+
/**
|
|
48
|
+
* Delay before attempt 2 and attempt 3.
|
|
49
|
+
*
|
|
50
|
+
* 🛑 Indexed by `attempt - 1`, so the array length must be at least
|
|
51
|
+
* `JOB_MAX_ATTEMPTS - 1`. The last value is reused if it is shorter, rather
|
|
52
|
+
* than producing `undefined` — a `nextAttemptAt` of `Invalid Date` would make
|
|
53
|
+
* the sweep's `<= now` comparison false forever and the retry would never run,
|
|
54
|
+
* which is the silent-stall shape this whole phase exists to remove.
|
|
55
|
+
*/
|
|
56
|
+
export const JOB_BACKOFF_MS = [5 * 60 * 1000, 30 * 60 * 1000];
|
|
25
57
|
export const JOB_FIELDS = {
|
|
26
58
|
JOB_TYPE: "jobType",
|
|
27
59
|
STATUS: "status",
|
|
@@ -32,5 +64,10 @@ export const JOB_FIELDS = {
|
|
|
32
64
|
FINISHED_AT: "finishedAt",
|
|
33
65
|
CREATED_AT: "createdAt",
|
|
34
66
|
UPDATED_AT: "updatedAt",
|
|
67
|
+
// Phase 8 retry/DLQ. `NEXT_ATTEMPT_AT` is the sweep's query field.
|
|
68
|
+
ATTEMPT: "attempt",
|
|
69
|
+
MAX_ATTEMPTS: "maxAttempts",
|
|
70
|
+
NEXT_ATTEMPT_AT: "nextAttemptAt",
|
|
71
|
+
PARENT_JOB_ID: "parentJobId",
|
|
35
72
|
STATUS_VALUES: JobStatusValues,
|
|
36
73
|
};
|
|
@@ -3,6 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
import { useContext, useMemo } from "react";
|
|
4
4
|
import { FormShellContext } from "../../../ui/forms/FormShell.js";
|
|
5
5
|
import { FormErrorList } from "../../../ui/forms/FormErrorList.js";
|
|
6
|
+
import { Div } from "../../../ui/components/Div.js";
|
|
6
7
|
import { useIsInsideOverlay } from "../../../ui/components/overlay-context.js";
|
|
7
8
|
import { useBottomActions } from "./useBottomActions.js";
|
|
8
9
|
/**
|
|
@@ -98,7 +99,25 @@ export function useFormBottomActions({ onSubmit, onCancel, submitLabel = "Save",
|
|
|
98
99
|
infoLabel: showErrors
|
|
99
100
|
? `Fix ${errorCount} ${errorCount === 1 ? "issue" : "issues"}`
|
|
100
101
|
: undefined,
|
|
101
|
-
|
|
102
|
+
/*
|
|
103
|
+
* 🛑 The alert region lives HERE, not in `FormErrorList`.
|
|
104
|
+
*
|
|
105
|
+
* Without it a failed submit on a phone announced nothing at all: the
|
|
106
|
+
* list has no role, `InfoPanel` has no role, and the only signal was the
|
|
107
|
+
* disclosure button's text changing — which a screen reader does not read
|
|
108
|
+
* out unprompted. This is the whole error surface on mobile, since
|
|
109
|
+
* `useBottomActions` defaults to `desktop: "hidden"`.
|
|
110
|
+
*
|
|
111
|
+
* It cannot go on `FormErrorList` itself: the list's other consumer is
|
|
112
|
+
* `<FormErrorSummary>`, whose wrapper is already `role="alert"`, and
|
|
113
|
+
* nesting one alert region inside another makes the announcement
|
|
114
|
+
* ambiguous and duplicated.
|
|
115
|
+
*
|
|
116
|
+
* `aria-live="polite"` rather than `role="alert"`'s implicit assertive —
|
|
117
|
+
* the user has just pressed Save and is expecting a response, so there is
|
|
118
|
+
* nothing to interrupt. Same choice `FormErrorSummary` makes.
|
|
119
|
+
*/
|
|
120
|
+
infoPanel: showErrors && ctx ? (_jsx(Div, { role: "alert", "aria-live": "polite", children: _jsx(FormErrorList, { ctx: ctx }) })) : undefined,
|
|
102
121
|
// Bumped by every submit attempt, so a second failed Save re-opens a
|
|
103
122
|
// sheet the user collapsed — while an ordinary re-render does not.
|
|
104
123
|
infoOpenSignal: showErrors ? ctx?.submitAttemptCount : undefined,
|
|
@@ -42,12 +42,12 @@ export declare const reopenLotterySlotSchema: z.ZodObject<{
|
|
|
42
42
|
adminUserId: z.ZodString;
|
|
43
43
|
}, "strip", z.ZodTypeAny, {
|
|
44
44
|
slotNumber: number;
|
|
45
|
-
sourceType: "
|
|
45
|
+
sourceType: "product" | "event";
|
|
46
46
|
sourceId: string;
|
|
47
47
|
adminUserId: string;
|
|
48
48
|
}, {
|
|
49
49
|
slotNumber: number;
|
|
50
|
-
sourceType: "
|
|
50
|
+
sourceType: "product" | "event";
|
|
51
51
|
sourceId: string;
|
|
52
52
|
adminUserId: string;
|
|
53
53
|
}>;
|
|
@@ -68,7 +68,7 @@ export declare const submitLotteryPullSchema: z.ZodEffects<z.ZodObject<{
|
|
|
68
68
|
}, "strip", z.ZodTypeAny, {
|
|
69
69
|
userId: string;
|
|
70
70
|
transactionId: string;
|
|
71
|
-
sourceType: "
|
|
71
|
+
sourceType: "product" | "event";
|
|
72
72
|
paymentTime: string;
|
|
73
73
|
purchasedItemNumber: number;
|
|
74
74
|
userPhone: string;
|
|
@@ -79,7 +79,7 @@ export declare const submitLotteryPullSchema: z.ZodEffects<z.ZodObject<{
|
|
|
79
79
|
}, {
|
|
80
80
|
userId: string;
|
|
81
81
|
transactionId: string;
|
|
82
|
-
sourceType: "
|
|
82
|
+
sourceType: "product" | "event";
|
|
83
83
|
paymentTime: string;
|
|
84
84
|
purchasedItemNumber: number;
|
|
85
85
|
userPhone: string;
|
|
@@ -90,7 +90,7 @@ export declare const submitLotteryPullSchema: z.ZodEffects<z.ZodObject<{
|
|
|
90
90
|
}>, {
|
|
91
91
|
userId: string;
|
|
92
92
|
transactionId: string;
|
|
93
|
-
sourceType: "
|
|
93
|
+
sourceType: "product" | "event";
|
|
94
94
|
paymentTime: string;
|
|
95
95
|
purchasedItemNumber: number;
|
|
96
96
|
userPhone: string;
|
|
@@ -101,7 +101,7 @@ export declare const submitLotteryPullSchema: z.ZodEffects<z.ZodObject<{
|
|
|
101
101
|
}, {
|
|
102
102
|
userId: string;
|
|
103
103
|
transactionId: string;
|
|
104
|
-
sourceType: "
|
|
104
|
+
sourceType: "product" | "event";
|
|
105
105
|
paymentTime: string;
|
|
106
106
|
purchasedItemNumber: number;
|
|
107
107
|
userPhone: string;
|
|
@@ -113,8 +113,29 @@ export function MediaImage({ src, alt, size = "card", priority = false, loading,
|
|
|
113
113
|
* the placeholder, the `<picture>` and the plain `<Image>` cannot disagree
|
|
114
114
|
* about their dimensions — a fallback that occupies a different box from the
|
|
115
115
|
* image it replaces makes a grid jump as things load.
|
|
116
|
+
*
|
|
117
|
+
* 🛑 `max-h-full` IS THE OTHER HALF OF SIZE_BOX, added 2026-10-23. SIZE_BOX
|
|
118
|
+
* alone fixed the auto-height parent and broke the ALREADY-SIZED one: a
|
|
119
|
+
* parent that is 320x128 got a 320x320 box, so `overflow-hidden` cropped it
|
|
120
|
+
* to the top 40% of a letterboxed square. Measured 89 of 143 call sites in
|
|
121
|
+
* that shape — every image preview on the review, document and site-settings
|
|
122
|
+
* surfaces. `max-height:100%` resolves to `none` against an auto-height
|
|
123
|
+
* parent, so it clamps ONLY in the broken case.
|
|
124
|
+
*
|
|
125
|
+
* Measured in a browser (box for each parent shape), not argued:
|
|
126
|
+
*
|
|
127
|
+
* parent w-full aspect +max-h-full +h-full
|
|
128
|
+
* 320x128 definite 320x320 ✗ 320x128 ✓ 320x128 ✓
|
|
129
|
+
* h128, width shrink 320x320 ✗ 320x128 ✓ 320x128 ✓
|
|
130
|
+
* width 200, h AUTO 200x200 ✓ 200x200 ✓ 200x784 ✗
|
|
131
|
+
* 64px thumb stretched
|
|
132
|
+
* by a flex row 64x64 ✓ 64x64 ✓ 64x90 ✗
|
|
133
|
+
*
|
|
134
|
+
* So `h-full` is NOT the fix — it wins over `aspect-ratio` whenever the
|
|
135
|
+
* parent's height is definite, including the height a flex/grid row hands a
|
|
136
|
+
* thumbnail by stretching it. That is why this is a MAX and not a height.
|
|
116
137
|
*/
|
|
117
|
-
const boxClass = fill ? "h-full" : SIZE_BOX[size]
|
|
138
|
+
const boxClass = fill ? "h-full" : `${SIZE_BOX[size]} max-h-full`;
|
|
118
139
|
// Reset error/load/retry state whenever the resolved src actually changes.
|
|
119
140
|
// Without this, a component instance that hit a load failure once (e.g.
|
|
120
141
|
// lost the post-upload propagation race above) latches hasError forever,
|
|
@@ -222,7 +222,7 @@ accept = IMAGE_ACCEPT_ATTR, maxSizeMB = MAX_IMAGE_BYTES / MEGABYTE, label = "Upl
|
|
|
222
222
|
fileInputRef.current.value = "";
|
|
223
223
|
onChange?.("");
|
|
224
224
|
};
|
|
225
|
-
return (_jsxs(Stack, { gap: "3", children: [label && (_jsx(Label, { className: "block", color: "muted", size: "sm", weight: "medium", children: label })), _jsx(Div, { className: "relative", children: preview ? (_jsxs(Stack, { gap: "sm", children: [_jsxs(Div, { className: `relative h-32 max-w-xs mx-auto ${__O.hidden}`, rounded: "lg", border: "default", children: [_jsx(MediaImage, { src: preview, alt: "Preview", size: "card" }), uploading && progress > 0 && (_jsx(Div, { className: "absolute inset-x-0 bottom-0", children: _jsx(Progress, { value: progress, size: "sm" }) }))] }), !uploading && (_jsxs(Row, { wrap: true, gap: "sm", children: [(enableCrop || enableAdvancedCrop) && (_jsx(Button, { type: "button", onClick: () => openEditor(preview), variant: "secondary", size: "sm", children: tMediaEditor("editImage") })), _jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "outline", size: "sm", children: tUpload("change") }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] })) : (_jsxs(_Fragment, { children: [showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: "photo", facingMode: "environment", onCapture: handleCameraCapture, onError: setError })), !showCamera && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { gap: "sm", wrap: true, children: [showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), disabled: uploading, variant: "outline", size: "sm", children: uploading ? tUpload("uploading") : tUpload("clickToUpload") })), captureSource !== "file-only" && isCameraSupported && (_jsx(Button, { type: "button", onClick: () => setCaptureMode("camera"), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") })), captureSource !== "file-only" && !isCameraSupported && (_jsx(Button, { type: "button", onClick: () => mobileCaptureRef.current?.click(), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") }))] }), _jsxs(Text, { size: "xs", variant: "secondary", children: [accept === "image/*"
|
|
225
|
+
return (_jsxs(Stack, { gap: "3", children: [label && (_jsx(Label, { className: "block", color: "muted", size: "sm", weight: "medium", children: label })), _jsx(Div, { className: "relative", children: preview ? (_jsxs(Stack, { gap: "sm", children: [_jsxs(Div, { className: `relative w-full h-32 max-w-xs mx-auto ${__O.hidden}`, rounded: "lg", border: "default", children: [_jsx(MediaImage, { src: preview, alt: "Preview", size: "card" }), uploading && progress > 0 && (_jsx(Div, { className: "absolute inset-x-0 bottom-0", children: _jsx(Progress, { value: progress, size: "sm" }) }))] }), !uploading && (_jsxs(Row, { wrap: true, gap: "sm", children: [(enableCrop || enableAdvancedCrop) && (_jsx(Button, { type: "button", onClick: () => openEditor(preview), variant: "secondary", size: "sm", children: tMediaEditor("editImage") })), _jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), variant: "outline", size: "sm", children: tUpload("change") }), _jsx(Button, { type: "button", onClick: handleRemove, variant: "danger", size: "sm", children: tUpload("remove") })] }))] })) : (_jsxs(_Fragment, { children: [showCamera && isCameraSupported && (_jsx(CameraCapture, { mode: "photo", facingMode: "environment", onCapture: handleCameraCapture, onError: setError })), !showCamera && (_jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { gap: "sm", wrap: true, children: [showFileInput && (_jsx(Button, { type: "button", onClick: () => fileInputRef.current?.click(), disabled: uploading, variant: "outline", size: "sm", children: uploading ? tUpload("uploading") : tUpload("clickToUpload") })), captureSource !== "file-only" && isCameraSupported && (_jsx(Button, { type: "button", onClick: () => setCaptureMode("camera"), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") })), captureSource !== "file-only" && !isCameraSupported && (_jsx(Button, { type: "button", onClick: () => mobileCaptureRef.current?.click(), disabled: uploading, variant: "ghost", size: "sm", children: t("switchToCamera") }))] }), _jsxs(Text, { size: "xs", variant: "secondary", children: [accept === "image/*"
|
|
226
226
|
? `JPG PNG GIF WebP`
|
|
227
227
|
: accept.split(",").map((a) => a.split("/")[1]?.toUpperCase() ?? a).join(" "), " ", "\u2014 max ", maxSizeMB, "MB"] })] }))] })) }), _jsx("input", { ref: fileInputRef, type: "file", accept: accept, onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-input" }), showCamera && !isCameraSupported && (_jsx("input", { ref: mobileCaptureRef, type: "file", accept: IMAGE_ACCEPT_ATTR, capture: "environment", onChange: handleFileChange, className: "hidden", "aria-hidden": "true", "data-testid": "media-upload-capture-input" })), helperText && !error && (_jsx(Text, { variant: "secondary", size: "xs", children: helperText })), error && _jsx(Alert, { variant: "error", children: error }), uploading && (_jsxs(Row, { gap: "sm", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { size: "sm", variant: "secondary", children: tUpload("uploadingProgress", { progress }) })] })), cropPreviewUrl && !enableAdvancedCrop && (_jsx(ImageCropModal, { isOpen: showCropModal, imageUrl: cropPreviewUrl, onClose: handleCropClose, onSave: handleCropSave })), cropPreviewUrl && enableAdvancedCrop && (_jsx(ImageEditor, { isOpen: showAdvancedEditor, imageUrl: cropPreviewUrl, onClose: handleAdvancedEditorClose, onSave: handleAdvancedEditorSave, aspectRatio: cropAspectRatio }))] }));
|
|
228
228
|
}
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
import { ORDER_RETURN_WINDOW_DAYS, ORDER_AUTO_CONFIRM_DAYS, } from "../../../_internal/shared/features/orders/config.js";
|
|
38
38
|
import { PROTECTION_CLAIM_WINDOW_HOURS } from "../../../_internal/shared/features/shipping/protections.js";
|
|
39
39
|
import { PAYMENT_WINDOW_MINUTES } from "../../orders/constants/payment-window.js";
|
|
40
|
+
import { hasOpenCourierBlock } from "../../orders/utils/courier-block.js";
|
|
40
41
|
const isPaid = (o) => o.paymentStatus === "paid";
|
|
41
42
|
const notTerminal = (o) => o.status !== "cancelled" && o.status !== "refunded";
|
|
42
43
|
/**
|
|
@@ -87,7 +88,29 @@ export const OBLIGATION_POLICIES = {
|
|
|
87
88
|
anchor: (o) => o.acceptedAt,
|
|
88
89
|
duration: { value: 2, unit: "days", calendar: "business" },
|
|
89
90
|
warnAt: [{ value: 8, unit: "hours", calendar: "calendar" }],
|
|
90
|
-
|
|
91
|
+
/*
|
|
92
|
+
* 🛑 `!hasOpenCourierBlock(o)` — DO NOT REMOVE, and do not "simplify" it
|
|
93
|
+
* into the stored field.
|
|
94
|
+
*
|
|
95
|
+
* This is `onDue: "act"`, so firing cancels the order and refunds the
|
|
96
|
+
* buyer with the seller recorded as having failed to dispatch. When the
|
|
97
|
+
* platform's own courier wallet is empty, nothing the seller can do makes
|
|
98
|
+
* the booking succeed — so without this guard our funding state cancels a
|
|
99
|
+
* paid order and blames them for it.
|
|
100
|
+
*
|
|
101
|
+
* The engine re-evaluates `precondition` at fire time and closes
|
|
102
|
+
* `superseded` when it returns false, so the clock PAUSES while blocked and
|
|
103
|
+
* resumes when the block clears. No new mechanism, no stored timer.
|
|
104
|
+
*
|
|
105
|
+
* 🛑 This is the ONLY `onDue: "act"` policy that needs the guard, and the
|
|
106
|
+
* others were checked rather than assumed: `hasDispatched` is
|
|
107
|
+
* `Boolean(o.shippingDate)`, so `delivery-confirm` (which REQUIRES
|
|
108
|
+
* `hasDispatched`) and everything post-delivery can never fire on an order
|
|
109
|
+
* that was never booked. `seller-accept` requires `!o.acceptedAt`, and a
|
|
110
|
+
* booking is only attempted after acceptance. Adding the guard there too
|
|
111
|
+
* would be redundant and would imply nobody traced it.
|
|
112
|
+
*/
|
|
113
|
+
precondition: (o) => isPaid(o) && notTerminal(o) && !hasDispatched(o) && !hasOpenCourierBlock(o),
|
|
91
114
|
blockedBy: ["seller-accept"],
|
|
92
115
|
onDue: "act",
|
|
93
116
|
publishedCopyKey: "obligations.sellerDispatch",
|
|
@@ -4,6 +4,7 @@ import { useState } from "react";
|
|
|
4
4
|
import { Badge, Button, Div, Form, Row, Stack, Text, useToast } from "../../../ui/index.js";
|
|
5
5
|
import { FieldInput } from "../../../ui/forms/index.js";
|
|
6
6
|
import { formatCurrency } from "../../../utils/number.formatter.js";
|
|
7
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
7
8
|
import { normalizeError } from "../../../errors/normalize.js";
|
|
8
9
|
function stateOf(i) {
|
|
9
10
|
if (i.status === "paid")
|
|
@@ -28,19 +29,34 @@ export function EmiInstallmentsPanel({ orderId, installments, remainingBalance,
|
|
|
28
29
|
const money = (value) => formatCurrency(value, currency);
|
|
29
30
|
const ordered = [...installments].sort((a, b) => a.index - b.index);
|
|
30
31
|
const outstanding = ordered.filter((i) => i.status !== "paid").length;
|
|
31
|
-
|
|
32
|
+
/*
|
|
33
|
+
* Takes `setFieldError` from the <Form> render prop rather than closing
|
|
34
|
+
* over state, because the failure belongs ON the reference field.
|
|
35
|
+
*
|
|
36
|
+
* This panel used to be toast-only AND gate submission by disabling the
|
|
37
|
+
* button on an empty reference — so an incomplete form could not be sent
|
|
38
|
+
* and said nothing about why, and a rejected reference produced a toast
|
|
39
|
+
* that was gone before the buyer looked back at the input. Both are money
|
|
40
|
+
* surfaces on somebody's order.
|
|
41
|
+
*/
|
|
42
|
+
const submit = async (index, fieldName, setFieldError) => {
|
|
43
|
+
const trimmed = reference.trim();
|
|
44
|
+
if (trimmed.length === 0) {
|
|
45
|
+
setFieldError(fieldName, "Enter the UTR or reference number from your transfer.");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
32
48
|
setIsSubmitting(true);
|
|
33
49
|
try {
|
|
34
|
-
await onSubmit({ installmentIndex: index, transactionId:
|
|
50
|
+
await onSubmit({ installmentIndex: index, transactionId: trimmed });
|
|
35
51
|
showToast("Payment reference submitted — the seller will confirm it.", "success");
|
|
36
52
|
setOpenIndex(null);
|
|
37
53
|
setReference("");
|
|
38
54
|
}
|
|
39
55
|
catch (err) {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
56
|
+
const normalized = normalizeError(err);
|
|
57
|
+
setFieldError(fieldName, toUserMessage(normalized.code, undefined, {
|
|
58
|
+
fallback: "Couldn't submit that payment reference. Please try again.",
|
|
59
|
+
}));
|
|
44
60
|
}
|
|
45
61
|
finally {
|
|
46
62
|
setIsSubmitting(false);
|
|
@@ -49,9 +65,11 @@ export function EmiInstallmentsPanel({ orderId, installments, remainingBalance,
|
|
|
49
65
|
return (_jsx(Div, { surface: "card", border: "default", rounded: "lg", padding: "md", children: _jsxs(Stack, { gap: "sm", children: [_jsxs(Row, { align: "center", justify: "between", gap: "sm", children: [_jsx(Text, { weight: "semibold", children: "Your EMI plan" }), remainingBalance !== undefined && remainingBalance > 0 && (_jsxs(Text, { size: "sm", color: "muted", children: [money(remainingBalance), " outstanding across ", outstanding, " instalment", outstanding === 1 ? "" : "s"] }))] }), _jsx(Text, { size: "xs", color: "muted", children: "You can pay any instalment whenever you like, including ahead of its due date \u2014 paying early settles the plan sooner and does not cost more." }), _jsx(Stack, { gap: "xs", children: ordered.map((inst) => {
|
|
50
66
|
const state = stateOf(inst);
|
|
51
67
|
const due = inst.dueDate ? new Date(inst.dueDate) : null;
|
|
68
|
+
// Per-installment, so each open row's error is keyed to its own field.
|
|
69
|
+
const fieldName = `emi-reference-${orderId}-${inst.index}`;
|
|
52
70
|
return (_jsxs(Div, { border: "default", rounded: "md", padding: "sm", children: [_jsxs(Row, { align: "center", justify: "between", gap: "sm", className: "min-w-0", children: [_jsxs(Stack, { gap: "xs", className: "min-w-0", children: [_jsxs(Text, { size: "sm", weight: "medium", children: ["Instalment ", inst.index, " \u00B7 ", money(inst.amount)] }), _jsx(Text, { size: "xs", color: "muted", children: due ? `Due ${due.toLocaleDateString()}` : "No due date recorded" })] }), _jsxs(Row, { align: "center", gap: "xs", className: "flex-shrink-0", children: [_jsx(Badge, { variant: state === "paid" ? "success" : state === "awaiting-review" ? "warning" : "default", children: STATE_LABEL[state] }), state === "payable" && (_jsx(Button, { type: "button", size: "sm", variant: "secondary", onClick: () => {
|
|
53
71
|
setOpenIndex(openIndex === inst.index ? null : inst.index);
|
|
54
72
|
setReference("");
|
|
55
|
-
}, children: openIndex === inst.index ? "Cancel" : "Pay now" }))] })] }), openIndex === inst.index && (_jsx(Div, { className: "mt-3", children: _jsx(Form, { onSubmit: (e) => e.preventDefault(), children: () => (_jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Transfer ", money(inst.amount), " to the seller, then enter the UTR or reference number so they can confirm it."] }), _jsx(FieldInput, { name:
|
|
73
|
+
}, children: openIndex === inst.index ? "Cancel" : "Pay now" }))] })] }), openIndex === inst.index && (_jsx(Div, { className: "mt-3", children: _jsx(Form, { onSubmit: (e) => e.preventDefault(), children: ({ setFieldError, clearErrors }) => (_jsxs(Stack, { gap: "sm", children: [_jsxs(Text, { size: "xs", color: "muted", children: ["Transfer ", money(inst.amount), " to the seller, then enter the UTR or reference number so they can confirm it."] }), _jsx(FieldInput, { name: fieldName, label: "UTR / reference number", required: true, value: reference, onChange: (v) => { clearErrors(); setReference(v); } }), _jsx(Button, { type: "submit", size: "sm", isLoading: isSubmitting, disabled: isSubmitting, onClick: () => { void submit(inst.index, fieldName, setFieldError); }, children: "Submit payment reference" })] })) }) }))] }, inst.index));
|
|
56
74
|
}) })] }) }));
|
|
57
75
|
}
|
|
@@ -32,7 +32,16 @@ function legacyFallback(order) {
|
|
|
32
32
|
method,
|
|
33
33
|
transactionId: order.paymentTransactionId ?? order.paymentId,
|
|
34
34
|
proofUrl: order.paymentProofUrl,
|
|
35
|
-
|
|
35
|
+
/*
|
|
36
|
+
* 🛑 D46/D47 — `amountDue`, falling back to `totalPrice`. This figure is
|
|
37
|
+
* what a MANUAL payer is told to transfer and what an admin verifies the
|
|
38
|
+
* proof against, so on a points-funded order `totalPrice` would ask the
|
|
39
|
+
* buyer for money the order does not owe — and then the admin would reject
|
|
40
|
+
* a correct ₹750 UTR for not matching ₹1,000.
|
|
41
|
+
*
|
|
42
|
+
* `?? totalPrice`, never `?? 0`: absent means no points were used.
|
|
43
|
+
*/
|
|
44
|
+
amount: order.amountDue ?? order.totalPrice,
|
|
36
45
|
verificationMethod: method === "cod" ? "cod_collection" : method === "manual" ? "manual_review" : "order_status",
|
|
37
46
|
};
|
|
38
47
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { OrderSourceContext } from "../schemas/firestore";
|
|
2
|
+
export interface OrderProvenanceCardProps {
|
|
3
|
+
sourceContext?: OrderSourceContext;
|
|
4
|
+
/** Falls back to the configured default currency when omitted. */
|
|
5
|
+
currency?: string;
|
|
6
|
+
className?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function OrderProvenanceCard({ sourceContext, currency, className, }: OrderProvenanceCardProps): import("react").JSX.Element | null;
|