@mohasinac/appkit 4.62.1 → 4.63.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-stamp.json +3 -3
- package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
- package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
- package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
- package/dist/_internal/server/features/ads/booking.d.ts +118 -0
- package/dist/_internal/server/features/ads/booking.js +326 -0
- package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
- package/dist/_internal/server/features/checkout/actions.js +335 -80
- package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
- package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
- package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
- package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
- package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
- package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
- package/dist/_internal/server/features/dashboard/attention.js +183 -0
- package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
- package/dist/_internal/server/features/dashboard/data.js +82 -0
- package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
- package/dist/_internal/server/features/inventory/reservations.js +273 -0
- package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
- package/dist/_internal/server/features/offers/destinations.js +65 -0
- package/dist/_internal/server/features/orders/adapters.js +21 -0
- package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
- package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
- package/dist/_internal/server/features/orders/balance.d.ts +89 -0
- package/dist/_internal/server/features/orders/balance.js +256 -0
- package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
- package/dist/_internal/server/features/pricing/actions.js +144 -0
- package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
- package/dist/_internal/server/features/shipping/book-split.js +130 -0
- package/dist/_internal/server/features/shipping/book.d.ts +61 -0
- package/dist/_internal/server/features/shipping/book.js +176 -0
- package/dist/_internal/server/features/shipping/courier-block.js +26 -1
- package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
- package/dist/_internal/server/features/site-settings/adapters.js +14 -0
- package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
- package/dist/_internal/server/features/stores/adapters.js +14 -0
- package/dist/_internal/server/functions/scheduled.d.ts +2 -1
- package/dist/_internal/server/functions/scheduled.js +57 -1
- package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
- package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
- package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
- package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
- package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
- package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
- package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
- package/dist/_internal/server/jobs/handlers/index.js +1 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
- package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
- package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
- package/dist/_internal/shared/actions/action-registry.js +268 -0
- package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
- package/dist/_internal/shared/checkout/order-math.js +22 -36
- package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
- package/dist/_internal/shared/checkout/rules/index.js +1 -1
- package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
- package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
- package/dist/_internal/shared/checkout/split.d.ts +0 -12
- package/dist/_internal/shared/checkout/split.js +22 -2
- package/dist/_internal/shared/content/disclaimers.js +7 -7
- package/dist/_internal/shared/content/published-values.d.ts +6 -1
- package/dist/_internal/shared/content/published-values.js +8 -0
- package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
- package/dist/_internal/shared/features/admin/integrations.js +19 -9
- package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
- package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
- package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
- package/dist/_internal/shared/features/emi/choice.js +69 -0
- package/dist/_internal/shared/features/emi/group.d.ts +84 -0
- package/dist/_internal/shared/features/emi/group.js +126 -0
- package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
- package/dist/_internal/shared/features/ledger/posting.js +165 -0
- package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
- package/dist/_internal/shared/features/ledger/reports.js +127 -0
- package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
- package/dist/_internal/shared/features/ledger/rules.js +221 -0
- package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
- package/dist/_internal/shared/features/offers/coordination.js +80 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
- package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
- package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
- package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
- package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
- package/dist/_internal/shared/features/points/accepts.js +12 -6
- package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
- package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
- package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
- package/dist/_internal/shared/features/shipping/parcel.js +139 -0
- package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
- package/dist/_internal/shared/features/site-settings/fees.js +12 -8
- package/dist/_internal/shared/fees/explain.d.ts +81 -0
- package/dist/_internal/shared/fees/explain.js +101 -0
- package/dist/_internal/shared/history/index.d.ts +1 -1
- package/dist/_internal/shared/history/index.js +1 -1
- package/dist/_internal/shared/history/types.d.ts +7 -1
- package/dist/_internal/shared/history/types.js +25 -0
- package/dist/client.d.ts +11 -1
- package/dist/client.js +13 -0
- package/dist/constants/api-endpoints.d.ts +72 -0
- package/dist/constants/api-endpoints.js +41 -0
- package/dist/constants/field-names.d.ts +41 -0
- package/dist/constants/field-names.js +47 -0
- package/dist/features/account/components/UserOrdersView.js +28 -2
- package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
- package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
- package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
- package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
- package/dist/features/admin/components/AdminOrdersView.js +20 -1
- package/dist/features/admin/schemas/firestore.d.ts +32 -0
- package/dist/features/ads/schemas/booking-write.d.ts +157 -0
- package/dist/features/ads/schemas/booking-write.js +88 -0
- package/dist/features/ads/schemas/creative-form.d.ts +18 -0
- package/dist/features/ads/schemas/creative-form.js +51 -0
- package/dist/features/ads/schemas/firestore.d.ts +140 -0
- package/dist/features/ads/schemas/firestore.js +109 -0
- package/dist/features/auctions/repository/bid.repository.js +3 -1
- package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
- package/dist/features/audit-log/schemas/firestore.js +74 -0
- package/dist/features/auth/actions/profile-actions.d.ts +69 -1
- package/dist/features/auth/actions/profile-actions.js +62 -1
- package/dist/features/auth/schemas/firestore.d.ts +16 -2
- package/dist/features/auth/schemas/firestore.js +2 -1
- package/dist/features/blog/actions/blog-actions.d.ts +6 -0
- package/dist/features/blog/actions/blog-actions.js +17 -2
- package/dist/features/blog/api/route.js +4 -1
- package/dist/features/blog/columns/index.js +5 -0
- package/dist/features/blog/components/BlogIndexPageView.js +2 -1
- package/dist/features/blog/repository/blog.repository.d.ts +4 -0
- package/dist/features/blog/repository/blog.repository.js +5 -0
- package/dist/features/blog/schemas/blog-form.d.ts +3 -0
- package/dist/features/blog/schemas/blog-form.js +1 -0
- package/dist/features/blog/schemas/firestore.d.ts +38 -1
- package/dist/features/blog/schemas/firestore.js +23 -0
- package/dist/features/blog/schemas/index.d.ts +6 -0
- package/dist/features/blog/schemas/index.js +2 -0
- package/dist/features/blog/types/index.d.ts +2 -0
- package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
- package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
- package/dist/features/cart/repository/cart.repository.d.ts +14 -0
- package/dist/features/cart/repository/cart.repository.js +20 -0
- package/dist/features/cart/schemas/firestore.d.ts +24 -0
- package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
- package/dist/features/dashboard/components/DashboardHubView.js +56 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
- package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
- package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
- package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
- package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
- package/dist/features/events/components/EventSaleBanner.js +1 -1
- package/dist/features/faq/components/FAQPageContent.js +2 -2
- package/dist/features/gateway-events/schemas/index.d.ts +2 -2
- package/dist/features/homepage/schemas/index.d.ts +8 -8
- package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
- package/dist/features/inventory/schemas/stock-reservation.js +103 -0
- package/dist/features/layout/AppLayoutShell.d.ts +9 -1
- package/dist/features/layout/AppLayoutShell.js +12 -4
- package/dist/features/layout/BottomActions.js +2 -1
- package/dist/features/layout/BottomNavbar.d.ts +10 -2
- package/dist/features/layout/SideActionDock.js +52 -2
- package/dist/features/layout/TitleBarLayout.js +13 -4
- package/dist/features/layout/TitleBarLayout.style.css +59 -0
- package/dist/features/layout/useCtaReveal.d.ts +16 -1
- package/dist/features/layout/useCtaReveal.js +17 -26
- package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
- package/dist/features/ledger/repository/journal.repository.js +312 -0
- package/dist/features/ledger/schemas/expenses.d.ts +87 -0
- package/dist/features/ledger/schemas/expenses.js +27 -0
- package/dist/features/ledger/schemas/firestore.d.ts +183 -0
- package/dist/features/ledger/schemas/firestore.js +69 -0
- package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
- package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
- package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
- package/dist/features/orders/repository/orders.repository.d.ts +28 -1
- package/dist/features/orders/repository/orders.repository.js +60 -6
- package/dist/features/orders/schemas/balance.d.ts +40 -0
- package/dist/features/orders/schemas/balance.js +47 -0
- package/dist/features/orders/schemas/firestore.d.ts +202 -0
- package/dist/features/orders/schemas/firestore.js +43 -0
- package/dist/features/orders/schemas/index.d.ts +2 -2
- package/dist/features/orders/types/index.d.ts +21 -0
- package/dist/features/payments/repository/payout.repository.js +3 -2
- package/dist/features/payments/schemas/firestore.d.ts +92 -1
- package/dist/features/payments/schemas/firestore.js +23 -0
- package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
- package/dist/features/pricing/components/AdminPricingView.js +157 -0
- package/dist/features/products/schemas/firestore.d.ts +26 -1
- package/dist/features/products/schemas/firestore.js +2 -0
- package/dist/features/products/schemas/index.d.ts +2 -2
- package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
- package/dist/features/reviews/repository/reviews.repository.js +16 -0
- package/dist/features/scams/repository/scammer.repository.js +2 -1
- package/dist/features/scams/schemas/firestore.d.ts +2 -1
- package/dist/features/scams/schemas/firestore.js +2 -1
- package/dist/features/seller/actions/offer-actions.js +56 -0
- package/dist/features/seller/actions/seller-actions.d.ts +1 -1
- package/dist/features/seller/actions/seller-actions.js +4 -0
- package/dist/features/seller/components/DispatchForm.d.ts +11 -0
- package/dist/features/seller/components/DispatchForm.js +190 -0
- package/dist/features/seller/components/SellerOrdersView.js +94 -2
- package/dist/features/seller/components/index.d.ts +1 -0
- package/dist/features/seller/components/index.js +1 -0
- package/dist/features/seller/repository/offer.repository.js +4 -3
- package/dist/features/seller/schemas/firestore.d.ts +23 -2
- package/dist/features/seller/schemas/firestore.js +3 -2
- package/dist/features/seller/schemas/index.d.ts +4 -4
- package/dist/features/seller/schemas/order-forms.d.ts +61 -4
- package/dist/features/seller/schemas/order-forms.js +41 -8
- package/dist/features/shipments/schemas/firestore.d.ts +25 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
- package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
- package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
- package/dist/features/stores/repository/store.repository.d.ts +18 -0
- package/dist/features/stores/repository/store.repository.js +27 -1
- package/dist/features/stores/schemas/firestore.d.ts +41 -0
- package/dist/features/stores/schemas/firestore.js +20 -0
- package/dist/features/stores/schemas/index.d.ts +6 -6
- package/dist/features/support/repository/support.repository.js +72 -6
- package/dist/features/support/schemas/firestore.d.ts +17 -0
- package/dist/features/support/schemas/index.d.ts +2 -2
- package/dist/index.d.ts +5 -0
- package/dist/index.js +19 -0
- package/dist/next/routing/route-map.d.ts +92 -0
- package/dist/next/routing/route-map.js +53 -0
- package/dist/react/contexts/SessionContext.d.ts +17 -1
- package/dist/schemas/registry.d.ts +22 -16
- package/dist/security/pii-schemas.d.ts +1 -1
- package/dist/security/pii-schemas.js +45 -0
- package/dist/server-entry.d.ts +1 -1
- package/dist/server-entry.js +10 -1
- package/dist/server.d.ts +29 -1
- package/dist/server.js +158 -0
- package/dist/styles.css +349 -1
- package/dist/tailwind-utilities.css +1 -1
- package/dist/ui/components/Button.d.ts +10 -1
- package/dist/ui/components/Button.js +25 -3
- package/dist/ui/components/Chart.d.ts +63 -0
- package/dist/ui/components/Chart.js +169 -0
- package/dist/ui/components/Chart.style.css +214 -0
- package/dist/ui/components/Disclaimer.js +10 -1
- package/dist/ui/components/OverflowRow.js +54 -2
- package/dist/ui/components/OverflowRow.style.css +38 -0
- package/dist/ui/components/RichTextEditor.js +99 -8
- package/dist/ui/components/chart-spec.d.ts +68 -0
- package/dist/ui/components/chart-spec.js +140 -0
- package/dist/ui/components/index.style.css +1 -0
- package/dist/ui/components/surface-tokens.d.ts +1 -1
- package/dist/ui/components/surface-tokens.js +9 -0
- package/dist/ui/components/toolbar-collapse.d.ts +13 -0
- package/dist/ui/components/toolbar-collapse.js +10 -1
- package/dist/ui/index.d.ts +6 -0
- package/dist/ui/index.js +3 -0
- package/dist/ui/rich-text/RichText.d.ts +10 -1
- package/dist/ui/rich-text/RichText.js +33 -2
- package/dist/ui/rich-text/RichText.style.css +35 -0
- package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
- package/dist/ui/rich-text/RichTextRenderer.js +34 -2
- package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
- package/dist/ui/rich-text/chart-blocks.js +87 -0
- package/package.json +1 -1
- package/scripts/seed-cli.mjs +3 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
4
|
+
import { Badge, Button, Heading, Row, Stack, Text } from "../../../ui/index.js";
|
|
5
|
+
import { Modal } from "../../../ui/components/Modal.js";
|
|
6
|
+
import { useToast } from "../../../ui/components/Toast.js";
|
|
7
|
+
import { Form } from "../../../ui/components/Form.js";
|
|
8
|
+
import { FieldInput } from "../../../ui/forms/FieldInput.js";
|
|
9
|
+
import { FieldTextarea } from "../../../ui/forms/FieldTextarea.js";
|
|
10
|
+
import { FormErrorSummary } from "../../../ui/forms/FormErrorSummary.js";
|
|
11
|
+
import { apiClient } from "../../../http/ApiClient.js";
|
|
12
|
+
import { normalizeError } from "../../../errors/normalize.js";
|
|
13
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
14
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
15
|
+
import { RATE_TIER_ORDER, } from "../../../_internal/shared/features/pricing/rate-registry.js";
|
|
16
|
+
const TIER_LABEL = {
|
|
17
|
+
statutory: "Statutory",
|
|
18
|
+
policy: "Policy ceiling",
|
|
19
|
+
operational: "Operational",
|
|
20
|
+
};
|
|
21
|
+
const TIER_BLURB = {
|
|
22
|
+
statutory: "Set by law, not by us. Sourced per HSN code in Tax codes — they deliberately do not appear below.",
|
|
23
|
+
policy: "A ceiling the owner decides. Administrator only, and a reason is required.",
|
|
24
|
+
operational: "Tuned, audited and reversible by back-office staff.",
|
|
25
|
+
};
|
|
26
|
+
const TIER_TONE = {
|
|
27
|
+
statutory: "warning",
|
|
28
|
+
policy: "info",
|
|
29
|
+
operational: "default",
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* 🛑 Format by the descriptor's UNIT, never by guessing from the value.
|
|
33
|
+
*
|
|
34
|
+
* `2` is 2% as a commission, ₹2 as a floor and 2 days as a hold, and the three
|
|
35
|
+
* are side by side in one table. A bare number would make the COD ceiling and
|
|
36
|
+
* the COD percentage read as the same kind of thing.
|
|
37
|
+
*/
|
|
38
|
+
function formatRate(value, unit) {
|
|
39
|
+
if (value === null)
|
|
40
|
+
return "—";
|
|
41
|
+
switch (unit) {
|
|
42
|
+
case "percent":
|
|
43
|
+
return `${value}%`;
|
|
44
|
+
case "rupees":
|
|
45
|
+
return `₹${value.toLocaleString("en-IN")}`;
|
|
46
|
+
case "days":
|
|
47
|
+
return value === 1 ? "1 day" : `${value} days`;
|
|
48
|
+
case "minutes":
|
|
49
|
+
return value === 1 ? "1 minute" : `${value} minutes`;
|
|
50
|
+
case "count":
|
|
51
|
+
return String(value);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function formatWhen(iso) {
|
|
55
|
+
const d = new Date(iso);
|
|
56
|
+
return Number.isNaN(d.getTime()) ? "—" : d.toLocaleDateString("en-IN", { dateStyle: "medium" });
|
|
57
|
+
}
|
|
58
|
+
export function AdminPricingView({ endpoint }) {
|
|
59
|
+
const { showToast } = useToast();
|
|
60
|
+
const [rates, setRates] = useState(null);
|
|
61
|
+
const [loadError, setLoadError] = useState(null);
|
|
62
|
+
const [editing, setEditing] = useState(null);
|
|
63
|
+
const [draftValue, setDraftValue] = useState("");
|
|
64
|
+
const [draftReason, setDraftReason] = useState("");
|
|
65
|
+
const [saving, setSaving] = useState(false);
|
|
66
|
+
const load = useCallback(async () => {
|
|
67
|
+
setLoadError(null);
|
|
68
|
+
try {
|
|
69
|
+
const payload = await apiClient.get(endpoint);
|
|
70
|
+
setRates(payload?.rates ?? []);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
/*
|
|
74
|
+
* Surfaced, never swallowed into an empty table. An empty rate list and
|
|
75
|
+
* a failed read look identical, and the first reads as "we charge
|
|
76
|
+
* nothing" (Root Cause #59/#72 — undefined must never render as zero).
|
|
77
|
+
*
|
|
78
|
+
* 🛑 A FIXED string, never `normalized.message`. The server's own text
|
|
79
|
+
* is how a Node `MODULE_NOT_FOUND` with a require stack ended up inside
|
|
80
|
+
* "Place your bid" (Root Cause #86) — and `mapToHttpError` scrubs 5xx
|
|
81
|
+
* messages in production anyway, so the raw value is either useless or
|
|
82
|
+
* dangerous. `normalizeError` still runs: it is what keeps `unknown`
|
|
83
|
+
* out of the catch body.
|
|
84
|
+
*/
|
|
85
|
+
void normalizeError(err);
|
|
86
|
+
setLoadError("Could not load the rate table. Try again.");
|
|
87
|
+
setRates([]);
|
|
88
|
+
}
|
|
89
|
+
}, [endpoint]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
void load();
|
|
92
|
+
}, [load]);
|
|
93
|
+
const byTier = useMemo(() => {
|
|
94
|
+
const groups = new Map();
|
|
95
|
+
for (const tier of RATE_TIER_ORDER)
|
|
96
|
+
groups.set(tier, []);
|
|
97
|
+
for (const rate of rates ?? [])
|
|
98
|
+
groups.get(rate.tier)?.push(rate);
|
|
99
|
+
return groups;
|
|
100
|
+
}, [rates]);
|
|
101
|
+
const openEditor = useCallback((rate) => {
|
|
102
|
+
setEditing(rate);
|
|
103
|
+
setDraftValue(rate.value === null ? "" : String(rate.value));
|
|
104
|
+
setDraftReason("");
|
|
105
|
+
}, []);
|
|
106
|
+
const submit = useCallback(async (setFieldError) => {
|
|
107
|
+
if (!editing)
|
|
108
|
+
return;
|
|
109
|
+
const parsed = Number(draftValue);
|
|
110
|
+
if (!Number.isFinite(parsed)) {
|
|
111
|
+
setFieldError("newValue", "Enter a number.");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
setSaving(true);
|
|
115
|
+
try {
|
|
116
|
+
await apiClient.post(endpoint, {
|
|
117
|
+
path: editing.path,
|
|
118
|
+
newValue: parsed,
|
|
119
|
+
...(draftReason.trim() ? { reason: draftReason.trim() } : {}),
|
|
120
|
+
});
|
|
121
|
+
showToast(`${editing.label} updated.`, "success");
|
|
122
|
+
setEditing(null);
|
|
123
|
+
await load();
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
void normalizeError(err);
|
|
127
|
+
/*
|
|
128
|
+
* 🛑 `toUserMessage`, never the server's own message — and no
|
|
129
|
+
* `?? normalized.message` fallback either. That fallback IS the bug:
|
|
130
|
+
* `toUserMessage` already terminates at `errors.codes.UNKNOWN`, so
|
|
131
|
+
* adding a raw-text branch for the unmapped case reintroduces exactly
|
|
132
|
+
* what Root Cause #86 describes — a require stack rendered inside a
|
|
133
|
+
* modal. The first draft of this file had it.
|
|
134
|
+
*/
|
|
135
|
+
const code = err?.code;
|
|
136
|
+
setFieldError("newValue", toUserMessage(code ?? "UNKNOWN"));
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
setSaving(false);
|
|
140
|
+
}
|
|
141
|
+
}, [draftReason, draftValue, editing, endpoint, load, showToast]);
|
|
142
|
+
return (_jsxs(Stack, { gap: "loose", children: [_jsxs(Stack, { gap: "dense", children: [_jsx(Heading, { level: 1, children: "Pricing" }), _jsx(Text, { color: "muted", children: "Every rate the platform charges, what kind of number it is, and when it last moved. Changes take effect from the moment they are saved and never restate past orders." })] }), loadError ? (_jsxs(Stack, { surface: "danger-surface", padding: "md", rounded: "lg", gap: "dense", children: [_jsx(Text, { weight: "semibold", children: "The rate table could not be loaded." }), _jsx(Text, { size: "sm", children: loadError }), _jsx(Row, { children: _jsx(Button, { variant: "secondary", onClick: () => void load(), children: "Try again" }) })] })) : null, RATE_TIER_ORDER.map((tier) => {
|
|
143
|
+
const rows = byTier.get(tier) ?? [];
|
|
144
|
+
/*
|
|
145
|
+
* The statutory section renders with NO rows and that is the point —
|
|
146
|
+
* it tells a reader the tier exists and where it is set. Hiding an
|
|
147
|
+
* empty section would make statutory rates look like something this
|
|
148
|
+
* platform does not have.
|
|
149
|
+
*/
|
|
150
|
+
if (rows.length === 0 && tier !== "statutory")
|
|
151
|
+
return null;
|
|
152
|
+
return (_jsxs(Stack, { gap: "comfortable", children: [_jsxs(Stack, { gap: "dense", children: [_jsxs(Row, { gap: "dense", align: "center", children: [_jsx(Heading, { level: 2, children: TIER_LABEL[tier] }), _jsx(Badge, { variant: TIER_TONE[tier], children: rows.length || "set elsewhere" })] }), _jsx(Text, { size: "sm", color: "muted", children: TIER_BLURB[tier] })] }), rows.map((rate) => (_jsxs(Row, { surface: "card", padding: "card", rounded: "lg", gap: "comfortable", align: "start", justify: "between", children: [_jsxs(Stack, { gap: "dense", children: [_jsxs(Row, { gap: "dense", align: "center", children: [_jsx(Text, { weight: "semibold", children: rate.label }), rate.sellerOnly ? _jsx(Badge, { variant: "default", children: "Seller-facing" }) : null] }), _jsx(Text, { size: "sm", color: "muted", children: rate.description }), rate.lastChange ? (_jsxs(Text, { size: "xs", color: "muted", children: [formatRate(rate.lastChange.previousValue, rate.unit), " \u2192", " ", formatRate(rate.lastChange.newValue, rate.unit), " on", " ", formatWhen(rate.lastChange.effectiveFrom), rate.lastChange.changedByName ? ` by ${rate.lastChange.changedByName}` : "", rate.lastChange.reason ? ` — ${rate.lastChange.reason}` : ""] })) : (_jsx(Text, { size: "xs", color: "muted", children: "No recorded change \u2014 this is the value it was configured with." }))] }), _jsxs(Row, { gap: "comfortable", align: "center", children: [_jsx(Text, { weight: "bold", numeric: true, children: formatRate(rate.value, rate.unit) }), _jsx(Button, { variant: "secondary", size: "sm", disabled: !rate.editable, onClick: () => openEditor(rate), children: "Change" })] })] }, rate.path)))] }, tier));
|
|
153
|
+
}), _jsx(Modal, { isOpen: editing !== null, onClose: () => setEditing(null), title: editing ? `Change ${editing.label}` : "Change rate", children: editing ? (_jsx(Form, { onSubmit: (e) => e.preventDefault(), children: ({ setFieldError, clearErrors }) => (_jsxs(Stack, { gap: "comfortable", children: [_jsx(FormErrorSummary, {}), _jsx(Text, { size: "sm", color: "muted", children: editing.description }), _jsx(FieldInput, { name: "newValue", label: `New value (${editing.unit})`, required: true, value: draftValue, onChange: setDraftValue }), _jsx(FieldTextarea, { name: "reason", label: editing.tier === "policy" ? "Reason (required)" : "Reason (optional)", value: draftReason, onChange: setDraftReason }), _jsx(Text, { size: "xs", color: "muted", children: "This takes effect now. Orders, invoices and payouts already recorded keep the rate they were charged at \u2014 nothing is restated." }), _jsxs(Row, { gap: "dense", justify: "end", children: [_jsx(Button, { variant: "ghost", onClick: () => setEditing(null), children: "Cancel" }), _jsx(Button, { action: ACTIONS.ADMIN["update-pricing"], isLoading: saving, onClick: () => {
|
|
154
|
+
clearErrors();
|
|
155
|
+
void submit(setFieldError);
|
|
156
|
+
} })] })] })) })) : null })] }));
|
|
157
|
+
}
|
|
@@ -603,6 +603,31 @@ export interface ProductDocument extends BaseDocument {
|
|
|
603
603
|
howToUse?: string[];
|
|
604
604
|
allowOffers?: boolean;
|
|
605
605
|
minOfferPercent?: number;
|
|
606
|
+
/**
|
|
607
|
+
* The seller permits a COORDINATED purchase on this listing.
|
|
608
|
+
*
|
|
609
|
+
* ## 🛑 A coordinated purchase is a SPECIALISED OFFER, not a listing type
|
|
610
|
+
*
|
|
611
|
+
* Owner, 2026-10-22: *"it is not a variant, it is similar to an offer, which
|
|
612
|
+
* the seller enables on the listing, then the customer can coordinate on it,
|
|
613
|
+
* it is a specialized offer."*
|
|
614
|
+
*
|
|
615
|
+
* That framing REMOVES work rather than adding it, and the removals are the
|
|
616
|
+
* part worth knowing — otherwise somebody rebuilds them:
|
|
617
|
+
*
|
|
618
|
+
* | Looks like it needs | Actually |
|
|
619
|
+
* |---|---|
|
|
620
|
+
* | a 7th `OrderSourceContext` variant | **nothing** — it is `offer-accepted`, and the provenance card already renders that branch |
|
|
621
|
+
* | an `orderType` tab | **nothing** — the Offer-wins tab already covers it |
|
|
622
|
+
* | a commit/settlement path | the offer lifecycle, with its chain fields, `checkoutDeadline` and lapse sweep |
|
|
623
|
+
* | a new money term | **none** — `unitPriceFor` already honours `lockedPrice`, so tax, payout base, refund ceiling and the invoice all follow |
|
|
624
|
+
*
|
|
625
|
+
* So this flag is deliberately shaped exactly like `allowOffers` above: same
|
|
626
|
+
* optional boolean, same seller control, same projection list. A listing
|
|
627
|
+
* with `allowOffers: false` and this `true` is coherent — the seller takes
|
|
628
|
+
* no solo offers but will consider a group — and nothing needs to forbid it.
|
|
629
|
+
*/
|
|
630
|
+
allowCoordination?: boolean;
|
|
606
631
|
customFields?: CustomField[];
|
|
607
632
|
customSections?: CustomSection[];
|
|
608
633
|
sublistingCategoryId?: string;
|
|
@@ -837,7 +862,7 @@ export declare const ProductStatusValues: {
|
|
|
837
862
|
export declare const PRODUCT_COLLECTION: "products";
|
|
838
863
|
export declare const PRODUCT_INDEXED_FIELDS: readonly ["storeId", "status", "category", "categoryIds", "featured", "listingType", "isPromoted", "isOnSale", "isSold", "searchTxt", "createdAt", "barcodeId", "physicalLocation.zone"];
|
|
839
864
|
export declare const DEFAULT_PRODUCT_DATA: Partial<ProductDocument>;
|
|
840
|
-
export declare const PRODUCT_PUBLIC_FIELDS: readonly ["allowOffers", "minOfferPercent", "id", "title", "description", "category", "categoryIds", "categoryIdNames", "subcategory", "brand", "price", "originalPrice", "currency", "stockQuantity", "availableQuantity", "images", "status", "storeName", "featured", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "isLot", "lotSize", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "views7d", "views30d", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "listingType", "auctionEndDate", "startingBid", "currentBid", "bidCount", "reservePrice", "buyNowPrice", "minBidIncrement", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderCurrentCount", "preOrderProductionStatus", "preOrderCancellable", "condition", "insurance", "insuranceCost", "shippingPaidBy", "isPromoted", "isOnSale", "isSold", "madeToOrder", "sourceStickerSheetId", "sourceStickerPartOrdinal", "slug", "seoTitle", "seoDescription", "seoKeywords", "viewCount", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "groupMinMembers", "groupMaxMembers", "groupMaxUnitsPerMember", "groupRequiredMemberIds", "groupDiscountPercent", "createdAt"];
|
|
865
|
+
export declare const PRODUCT_PUBLIC_FIELDS: readonly ["allowOffers", "minOfferPercent", "allowCoordination", "id", "title", "description", "category", "categoryIds", "categoryIdNames", "subcategory", "brand", "price", "originalPrice", "currency", "stockQuantity", "availableQuantity", "images", "status", "storeName", "featured", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "isLot", "lotSize", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "views7d", "views30d", "unitLabel", "partGlossaryId", "colourway", "mouldVariant", "assembledFrom", "listingType", "auctionEndDate", "startingBid", "currentBid", "bidCount", "reservePrice", "buyNowPrice", "minBidIncrement", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderCurrentCount", "preOrderProductionStatus", "preOrderCancellable", "condition", "insurance", "insuranceCost", "shippingPaidBy", "isPromoted", "isOnSale", "isSold", "madeToOrder", "sourceStickerSheetId", "sourceStickerPartOrdinal", "slug", "seoTitle", "seoDescription", "seoKeywords", "viewCount", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "groupMinMembers", "groupMaxMembers", "groupMaxUnitsPerMember", "groupRequiredMemberIds", "groupDiscountPercent", "createdAt"];
|
|
841
866
|
export declare const PRODUCT_UPDATABLE_FIELDS: readonly ["title", "description", "category", "categoryIds", "subcategory", "brand", "price", "originalPrice", "stockQuantity", "images", "status", "tags", "specifications", "features", "shippingInfo", "returnPolicy", "finalSale", "isLot", "lotSize", "physicalLocation", "sku", "pickupAddressId", "condition", "insurance", "shippingPaidBy", "autoExtendable", "auctionExtensionMinutes", "auctionShippingPaidBy", "reservePrice", "buyNowPrice", "minBidIncrement", "bidsHaveStarted", "grading", "card", "classified", "digitalCode", "liveItem", "catalogProductId", "listingType", "preOrderDeliveryDate", "preOrderDepositPercent", "preOrderDepositAmount", "preOrderMaxQuantity", "preOrderProductionStatus", "preOrderCancellable", "isOnSale", "isSold", "madeToOrder", "sourceStickerSheetId", "sourceStickerPartOrdinal", "allowShipBeforeEmiComplete", "gstRate", "hsnCode", "authenticity", "printMeta", "seoTitle", "seoDescription", "seoKeywords", "customFields", "customSections", "sublistingCategoryId", "groupId", "isGroupParent", "groupParentSlug", "groupChildSlugs", "groupTitle", "groupMinMembers", "groupMaxMembers", "groupMaxUnitsPerMember", "groupRequiredMemberIds", "groupDiscountPercent", "barcodeId"];
|
|
842
867
|
export type ProductCreateInput = Omit<ProductDocument, "id" | "createdAt" | "updatedAt" | "availableQuantity" | "bidCount" | "currentBid" | "auctionOriginalEndDate">;
|
|
843
868
|
export type ProductUpdateInput = Partial<Pick<ProductDocument, (typeof PRODUCT_UPDATABLE_FIELDS)[number]>>;
|
|
@@ -142,6 +142,8 @@ export const PRODUCT_PUBLIC_FIELDS = [
|
|
|
142
142
|
// the moment something starts projecting through it.
|
|
143
143
|
"allowOffers",
|
|
144
144
|
"minOfferPercent",
|
|
145
|
+
/* Same reasoning as `allowOffers`: the card renders a pill from it. */
|
|
146
|
+
"allowCoordination",
|
|
145
147
|
"id",
|
|
146
148
|
"title",
|
|
147
149
|
"description",
|
|
@@ -449,10 +449,10 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
449
449
|
wonByOrderId?: string | undefined;
|
|
450
450
|
}[] | undefined;
|
|
451
451
|
prizeGithubFileUrl?: string | undefined;
|
|
452
|
+
attributes?: Record<string, string> | undefined;
|
|
452
453
|
inStock?: boolean | undefined;
|
|
453
454
|
sellerAvatar?: string | undefined;
|
|
454
455
|
stockCount?: number | undefined;
|
|
455
|
-
attributes?: Record<string, string> | undefined;
|
|
456
456
|
}, {
|
|
457
457
|
status: "published" | "draft" | "in_review" | "archived";
|
|
458
458
|
id: string;
|
|
@@ -589,10 +589,10 @@ export declare const productItemSchema: z.ZodObject<{
|
|
|
589
589
|
wonByOrderId?: string | undefined;
|
|
590
590
|
}[] | undefined;
|
|
591
591
|
prizeGithubFileUrl?: string | undefined;
|
|
592
|
+
attributes?: Record<string, string> | undefined;
|
|
592
593
|
inStock?: boolean | undefined;
|
|
593
594
|
sellerAvatar?: string | undefined;
|
|
594
595
|
stockCount?: number | undefined;
|
|
595
|
-
attributes?: Record<string, string> | undefined;
|
|
596
596
|
}>;
|
|
597
597
|
/** Base Zod schema for list-query parameters. */
|
|
598
598
|
export declare const productListParamsSchema: z.ZodObject<{
|
|
@@ -52,6 +52,20 @@ declare class ReviewRepository extends BaseRepository<ReviewDocument> {
|
|
|
52
52
|
findApprovedByStore(storeId: string, limit?: number): Promise<ReviewDocument[]>;
|
|
53
53
|
findByUser(userId: string): Promise<ReviewDocument[]>;
|
|
54
54
|
findApprovedByUser(userId: string): Promise<ReviewDocument[]>;
|
|
55
|
+
/**
|
|
56
|
+
* How many reviews this account has WRITTEN.
|
|
57
|
+
*
|
|
58
|
+
* Deliberately every status and both reviewer roles — it counts the same set
|
|
59
|
+
* `GET /api/user/reviews` lists by default (`findByUser`), so the number in
|
|
60
|
+
* the sidebar and the rows on the page it links to agree by construction. A
|
|
61
|
+
* count that quietly excluded pending reviews would read as the author's
|
|
62
|
+
* work having vanished.
|
|
63
|
+
*
|
|
64
|
+
* An aggregation, not `findByUser(...).length`: that sibling downloads every
|
|
65
|
+
* review the account has ever written to learn how many there are, and the
|
|
66
|
+
* caller here runs on every session profile fetch.
|
|
67
|
+
*/
|
|
68
|
+
countByUser(userId: string): Promise<number>;
|
|
55
69
|
findPending(): Promise<ReviewDocument[]>;
|
|
56
70
|
findByStatus(status: ReviewStatus): Promise<ReviewDocument[]>;
|
|
57
71
|
/**
|
|
@@ -175,6 +175,22 @@ class ReviewRepository extends BaseRepository {
|
|
|
175
175
|
.get();
|
|
176
176
|
return snapshot.docs.map((doc) => this.mapDoc(doc));
|
|
177
177
|
}
|
|
178
|
+
/**
|
|
179
|
+
* How many reviews this account has WRITTEN.
|
|
180
|
+
*
|
|
181
|
+
* Deliberately every status and both reviewer roles — it counts the same set
|
|
182
|
+
* `GET /api/user/reviews` lists by default (`findByUser`), so the number in
|
|
183
|
+
* the sidebar and the rows on the page it links to agree by construction. A
|
|
184
|
+
* count that quietly excluded pending reviews would read as the author's
|
|
185
|
+
* work having vanished.
|
|
186
|
+
*
|
|
187
|
+
* An aggregation, not `findByUser(...).length`: that sibling downloads every
|
|
188
|
+
* review the account has ever written to learn how many there are, and the
|
|
189
|
+
* caller here runs on every session profile fetch.
|
|
190
|
+
*/
|
|
191
|
+
async countByUser(userId) {
|
|
192
|
+
return getFirestoreCount(this.getCollection().where(REVIEW_FIELDS.USER_ID, "==", userId));
|
|
193
|
+
}
|
|
178
194
|
async findPending() {
|
|
179
195
|
return this.findBy(REVIEW_FIELDS.STATUS, "pending");
|
|
180
196
|
}
|
|
@@ -142,7 +142,8 @@ class ScammerRepository extends BaseRepository {
|
|
|
142
142
|
reason: ctx?.reason,
|
|
143
143
|
note: ctx?.note,
|
|
144
144
|
// A scammer profile is built entirely from identifying details, and
|
|
145
|
-
// `encryptPiiFields`
|
|
145
|
+
// `encryptPiiFields` covers a field only where it is NAMED. It CAN
|
|
146
|
+
// reach into an array via `array[].key`, but a bare key does not.
|
|
146
147
|
piiFields: SCAMMER_HISTORY_PII_FIELDS,
|
|
147
148
|
});
|
|
148
149
|
const data = prepareForFirestore(withEntry ?? { ...input, updatedAt: new Date() });
|
|
@@ -403,6 +403,7 @@ export declare const SCAMMER_TRACKED_FIELDS: readonly ["status", "verifiedBy", "
|
|
|
403
403
|
/**
|
|
404
404
|
* PII on this document, for `withHistory`'s scrub. A scammer profile is built
|
|
405
405
|
* ENTIRELY out of identifying details, so this list is long on purpose —
|
|
406
|
-
* `encryptPiiFields`
|
|
406
|
+
* `encryptPiiFields` covers only NAMED paths, and nobody is going to declare
|
|
407
|
+
* `statusHistory[].changes.<key>` for every field that might land there.
|
|
407
408
|
*/
|
|
408
409
|
export declare const SCAMMER_HISTORY_PII_FIELDS: readonly ["name", "displayName", "email", "phone", "phoneNumber", "upiId", "bankAccount", "accountNumber", "reporterEmail", "reporterName"];
|
|
@@ -248,7 +248,8 @@ export const SCAMMER_TRACKED_FIELDS = [
|
|
|
248
248
|
/**
|
|
249
249
|
* PII on this document, for `withHistory`'s scrub. A scammer profile is built
|
|
250
250
|
* ENTIRELY out of identifying details, so this list is long on purpose —
|
|
251
|
-
* `encryptPiiFields`
|
|
251
|
+
* `encryptPiiFields` covers only NAMED paths, and nobody is going to declare
|
|
252
|
+
* `statusHistory[].changes.<key>` for every field that might land there.
|
|
252
253
|
*/
|
|
253
254
|
export const SCAMMER_HISTORY_PII_FIELDS = [
|
|
254
255
|
"name",
|
|
@@ -7,6 +7,9 @@
|
|
|
7
7
|
import { serverLogger } from "../../../monitoring/index.js";
|
|
8
8
|
import { offerRepository } from "../repository/offer.repository.js";
|
|
9
9
|
import { productRepository } from "../../products/repository/products.repository.js";
|
|
10
|
+
import { categoriesRepository } from "../../categories/repository/categories.repository.js";
|
|
11
|
+
import { resolveParcel } from "../../../_internal/shared/features/shipping/parcel.js";
|
|
12
|
+
import { checkDestinations, assertAllServiceable, } from "../../../_internal/server/features/shipping/book-split.js";
|
|
10
13
|
import { ProductStatusValues } from "../../products/schemas/index.js";
|
|
11
14
|
import { sendNotification } from "../../admin/actions/notification-actions.js";
|
|
12
15
|
import { userRepository } from "../../auth/repository/user.repository.js";
|
|
@@ -184,6 +187,59 @@ export async function respondToOffer(userId, input) {
|
|
|
184
187
|
throw new ValidationError(ERROR_MESSAGES.OFFER.EXPIRED);
|
|
185
188
|
let updated;
|
|
186
189
|
if (action === "accept") {
|
|
190
|
+
/*
|
|
191
|
+
* ── S5a: a coordinated offer must be SHIPPABLE before it is binding ──
|
|
192
|
+
*
|
|
193
|
+
* Accepting locks a price and starts the buyer's checkout deadline, so it
|
|
194
|
+
* is the last moment at which "the courier cannot reach one of these
|
|
195
|
+
* addresses" costs nobody anything. After it, the coordinator has paid
|
|
196
|
+
* and the only remedies left are a refund and an apology.
|
|
197
|
+
*
|
|
198
|
+
* 🛑 Deliberately NOT at booking time, which is the obvious place and the
|
|
199
|
+
* wrong one: by then the money is taken, and a four-destination order
|
|
200
|
+
* that fails on the fourth has three parcels the carrier already accepted
|
|
201
|
+
* and no rollback. `assertAllServiceable` still guards the booking path —
|
|
202
|
+
* this is the check that keeps it from ever being reached in that state.
|
|
203
|
+
*
|
|
204
|
+
* 🛑 Costs NOTHING on an ordinary offer. `coordination` is absent on
|
|
205
|
+
* every non-coordinated one, so this whole block is skipped — a check
|
|
206
|
+
* that charges every seller for a feature most never use is how Rule #6's
|
|
207
|
+
* round-trip budget gets spent by accident.
|
|
208
|
+
*
|
|
209
|
+
* 🛑 The weight comes from `resolveParcel`, NOT from a literal. That
|
|
210
|
+
* module's whole argument is that an item weight systematically
|
|
211
|
+
* under-estimates a packed box (a bey is ~30 g loose and ~300 g boxed),
|
|
212
|
+
* and a hard-coded `1` here would be a fourth, worse tier beside the
|
|
213
|
+
* three it defines. The shipment tier is deliberately absent — nothing is
|
|
214
|
+
* packed yet at accept time, which is exactly the case tier two and three
|
|
215
|
+
* exist for.
|
|
216
|
+
*
|
|
217
|
+
* 🛑 **No resolvable weight SKIPS the check** rather than guessing one.
|
|
218
|
+
* The carrier is being asked "can you reach this pincode with a parcel of
|
|
219
|
+
* this size", so an invented weight asks a question about a parcel that
|
|
220
|
+
* does not exist and may refuse a destination that is perfectly
|
|
221
|
+
* reachable. Not checking is honest; the booking path still has
|
|
222
|
+
* `assertAllServiceable`, and a listing with no weight at any tier has a
|
|
223
|
+
* data problem this is not the place to report.
|
|
224
|
+
*/
|
|
225
|
+
const members = offer.coordination?.members ?? [];
|
|
226
|
+
if (members.length > 0) {
|
|
227
|
+
const offerProduct = await productRepository.findById(offer.productId);
|
|
228
|
+
/*
|
|
229
|
+
* 🛑 `categorySlugs[0]` is the LEAF, and `findById` is the right lookup
|
|
230
|
+
* for it. Products carry the full ancestor chain `[leaf, …, root]` and
|
|
231
|
+
* categories are pure slugs (`id === slug`), so this is one get rather
|
|
232
|
+
* than the `where("slug","==")` query `findBySlugAndType` would run —
|
|
233
|
+
* and the leaf is the tier whose packed default is actually about this
|
|
234
|
+
* kind of item. A root's default would be the average of the catalogue.
|
|
235
|
+
*/
|
|
236
|
+
const leafSlug = offerProduct?.categorySlugs?.[0];
|
|
237
|
+
const leaf = leafSlug ? await categoriesRepository.findById(leafSlug) : null;
|
|
238
|
+
const parcel = resolveParcel(null, offerProduct, leaf?.productDefaults ?? null);
|
|
239
|
+
if (parcel) {
|
|
240
|
+
assertAllServiceable(await checkDestinations(members, parcel.weightG / 1000));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
187
243
|
updated = await offerRepository.accept(offerId, offer.offerAmount, sellerNote, new Date(Date.now() + OFFER_CHECKOUT_WINDOW_MS));
|
|
188
244
|
}
|
|
189
245
|
else if (action === "decline") {
|
|
@@ -181,7 +181,7 @@ export declare function listSellerPayouts(userId: string, params?: {
|
|
|
181
181
|
page?: number;
|
|
182
182
|
pageSize?: number;
|
|
183
183
|
}): Promise<{
|
|
184
|
-
items: import("../../payments").
|
|
184
|
+
items: import("../../payments").StorePayoutDocument[];
|
|
185
185
|
total: number;
|
|
186
186
|
page: number;
|
|
187
187
|
pageSize: number;
|
|
@@ -299,6 +299,8 @@ export async function requestPayout(userId, userName, userEmail, input) {
|
|
|
299
299
|
if (earnings.netAmount <= 0 || earnings.eligibleOrders.length === 0)
|
|
300
300
|
throw new ValidationError("No eligible earnings available for payout.");
|
|
301
301
|
const payout = await payoutRepository.createPayout({
|
|
302
|
+
/* A seller requesting their own earnings — a liability settlement. */
|
|
303
|
+
payeeType: "store",
|
|
302
304
|
storeId: userId,
|
|
303
305
|
sellerName: userName,
|
|
304
306
|
sellerEmail: userEmail,
|
|
@@ -391,6 +393,8 @@ export async function bulkSellerOrder(userId, userRole, userDisplayName, userEma
|
|
|
391
393
|
// The store-funded points, both ends — see the preview path above.
|
|
392
394
|
ordersPointsBorneByStore(eligible));
|
|
393
395
|
const payoutDoc = await payoutRepository.createPayout({
|
|
396
|
+
/* A seller requesting their own earnings — a liability settlement. */
|
|
397
|
+
payeeType: "store",
|
|
394
398
|
storeId: userId,
|
|
395
399
|
sellerName: userDisplayName,
|
|
396
400
|
sellerEmail: userEmail,
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
export declare function DispatchForm({ orderId,
|
|
3
|
+
/** Already dispatched? Then there is nothing to book and nothing to record. */
|
|
4
|
+
trackingNumber,
|
|
5
|
+
/** Set by `bookShipment`; a booked parcel must not be re-booked. */
|
|
6
|
+
platformBooked, onDispatched, }: {
|
|
7
|
+
orderId: string;
|
|
8
|
+
trackingNumber?: string;
|
|
9
|
+
platformBooked?: boolean;
|
|
10
|
+
onDispatched: () => void;
|
|
11
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
/*
|
|
4
|
+
* WHY: Bug 0e — a seller could not express "book this through us". The write
|
|
5
|
+
* shape said `method: z.literal("custom")`, so there was no option to
|
|
6
|
+
* offer; `platformBookedShipment`, which `order-math.ts` gates shipping
|
|
7
|
+
* reimbursement on, had ZERO writers.
|
|
8
|
+
*
|
|
9
|
+
* The schema now carries a discriminated union and the server does the
|
|
10
|
+
* booking. This is the control that reaches it.
|
|
11
|
+
*
|
|
12
|
+
* WHAT: One dispatch panel with two branches — post it yourself, or book a
|
|
13
|
+
* pickup on the platform's carrier account.
|
|
14
|
+
*
|
|
15
|
+
* EXPORTS: DispatchForm
|
|
16
|
+
*
|
|
17
|
+
* @tag domain:seller,orders,shipping
|
|
18
|
+
* @tag layer:component
|
|
19
|
+
* @tag pattern:form
|
|
20
|
+
* @tag access:client
|
|
21
|
+
* @tag consumers:SellerOrderDetailPanel
|
|
22
|
+
* @tag sideEffects:network
|
|
23
|
+
*/
|
|
24
|
+
import React, { useState } from "react";
|
|
25
|
+
import { ACTIONS } from "../../../_internal/shared/actions/action-registry.js";
|
|
26
|
+
import { SELLER_ENDPOINTS } from "../../../constants/api-endpoints.js";
|
|
27
|
+
import { apiClient, ApiClientError } from "../../../http/index.js";
|
|
28
|
+
import { useApiMutation } from "../../../client/api/useApiMutation.js";
|
|
29
|
+
import { useQuery } from "@tanstack/react-query";
|
|
30
|
+
import { normalizeError } from "../../../errors/normalize.js";
|
|
31
|
+
import { toUserMessage } from "../../../errors/error-display-map.js";
|
|
32
|
+
import { Button } from "../../../ui/components/Button.js";
|
|
33
|
+
import { Div } from "../../../ui/components/Div.js";
|
|
34
|
+
import { Row } from "../../../ui/components/Layout.js";
|
|
35
|
+
import { Stack } from "../../../ui/components/Layout.js";
|
|
36
|
+
import { Text } from "../../../ui/components/Typography.js";
|
|
37
|
+
import { Form } from "../../../ui/components/Form.js";
|
|
38
|
+
import { FieldInput } from "../../../ui/forms/FieldInput.js";
|
|
39
|
+
import { FieldSelect } from "../../../ui/forms/FieldSelect.js";
|
|
40
|
+
import { shipOrderSchema } from "../schemas/order-forms.js";
|
|
41
|
+
import { applyZodIssues, rootLevelMessages } from "../../../ui/forms/apply-zod-issues.js";
|
|
42
|
+
/**
|
|
43
|
+
* Box presets, in centimetres and kilograms.
|
|
44
|
+
*
|
|
45
|
+
* 🛑 A PRESET rather than four number inputs, and that is the whole UX
|
|
46
|
+
* argument: a seller packing a Beyblade does not know the parcel's
|
|
47
|
+
* dimensions, and a carrier rejects a wrong one with an opaque error at
|
|
48
|
+
* pickup time rather than at booking time. Four free fields produce four
|
|
49
|
+
* chances to guess. "Custom" exists for the genuinely odd parcel.
|
|
50
|
+
*/
|
|
51
|
+
const BOX_PRESETS = [
|
|
52
|
+
{ id: "small", label: "Small — single top (0.3 kg)", weight: 0.3, length: 20, width: 15, height: 10 },
|
|
53
|
+
{ id: "medium", label: "Medium — a few items (1 kg)", weight: 1, length: 30, width: 25, height: 15 },
|
|
54
|
+
{ id: "large", label: "Large — stadium or bundle (3 kg)", weight: 3, length: 45, width: 35, height: 25 },
|
|
55
|
+
];
|
|
56
|
+
export function DispatchForm({ orderId,
|
|
57
|
+
/** Already dispatched? Then there is nothing to book and nothing to record. */
|
|
58
|
+
trackingNumber,
|
|
59
|
+
/** Set by `bookShipment`; a booked parcel must not be re-booked. */
|
|
60
|
+
platformBooked, onDispatched, }) {
|
|
61
|
+
const [method, setMethod] = useState("custom");
|
|
62
|
+
const [carrier, setCarrier] = useState("");
|
|
63
|
+
const [tracking, setTracking] = useState("");
|
|
64
|
+
const [trackingUrl, setTrackingUrl] = useState("");
|
|
65
|
+
const [pickupId, setPickupId] = useState("");
|
|
66
|
+
const [boxId, setBoxId] = useState(BOX_PRESETS[1].id);
|
|
67
|
+
const [error, setError] = useState(null);
|
|
68
|
+
/*
|
|
69
|
+
* 🛑 Fetched ONLY when the platform branch is open. The custom branch is
|
|
70
|
+
* the common case and needs no address at all, so loading this on mount
|
|
71
|
+
* would be a request on every order panel open for a field most sellers
|
|
72
|
+
* never reach (Rule #6).
|
|
73
|
+
*/
|
|
74
|
+
/*
|
|
75
|
+
* 🛑 `toUserMessage(code)` on failure, never the server's own `error`
|
|
76
|
+
* string — an unmapped code would print server internals into the panel,
|
|
77
|
+
* which is literally how a Node `MODULE_NOT_FOUND` with a require stack
|
|
78
|
+
* reached a buyer-facing modal (Root Cause #86). `ApiClientError` carries
|
|
79
|
+
* the stable code, so the mapping survives the move off raw `fetch`.
|
|
80
|
+
*/
|
|
81
|
+
/*
|
|
82
|
+
* 🛑 A REF, because `setFieldError` comes from `<Form>`'s render-prop and
|
|
83
|
+
* therefore does not exist in component scope where the mutation is
|
|
84
|
+
* declared. The mutation needs a STABLE sink — passing the render-prop
|
|
85
|
+
* helper directly is impossible, and re-creating the mutation per render to
|
|
86
|
+
* close over it would reset its pending state mid-request.
|
|
87
|
+
*
|
|
88
|
+
* It is assigned inside the render-prop below, so by the time any submit
|
|
89
|
+
* can fire it is populated. The `?.` is not defensive decoration: the first
|
|
90
|
+
* render completes before `<Form>` calls its child.
|
|
91
|
+
*/
|
|
92
|
+
const fieldErrorSink = React.useRef(null);
|
|
93
|
+
const shipMutation = useApiMutation({
|
|
94
|
+
/*
|
|
95
|
+
* 🛑 Passed so the server's own per-field `issues` land on the fields
|
|
96
|
+
* they name. The route parses the same `shipOrderSchema` this form does,
|
|
97
|
+
* so a rejection it produces that the client parse did not is real news
|
|
98
|
+
* — a carrier the courier does not recognise, a pickup address that
|
|
99
|
+
* stopped being serviceable — and collapsing that into one generic
|
|
100
|
+
* sentence hides which input to fix.
|
|
101
|
+
*/
|
|
102
|
+
setFieldError: (field, message) => fieldErrorSink.current?.(field, message),
|
|
103
|
+
mutationFn: (payload) => apiClient.post(SELLER_ENDPOINTS.ORDER_SHIP(orderId), payload),
|
|
104
|
+
onSuccess: () => onDispatched(),
|
|
105
|
+
onError: (err) => {
|
|
106
|
+
void normalizeError(err);
|
|
107
|
+
setError(toUserMessage(err instanceof ApiClientError ? err.code : undefined));
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
/*
|
|
111
|
+
* 🛑 `enabled` is what keeps this request off the common path. The custom
|
|
112
|
+
* branch needs no address at all, so loading pickups on mount would fire a
|
|
113
|
+
* request on every order panel a seller opens for a field most of them
|
|
114
|
+
* never reach (Rule #6). `useQuery` also dedupes and caches it, which the
|
|
115
|
+
* hand-rolled effect could not — it refetched on every branch toggle.
|
|
116
|
+
*/
|
|
117
|
+
const pickupsQuery = useQuery({
|
|
118
|
+
queryKey: ["store", "addresses", "pickup"],
|
|
119
|
+
enabled: method === "platform",
|
|
120
|
+
queryFn: () => apiClient.get(SELLER_ENDPOINTS.STORE_ADDRESSES),
|
|
121
|
+
});
|
|
122
|
+
const pickups = Array.isArray(pickupsQuery.data?.items)
|
|
123
|
+
? pickupsQuery.data.items
|
|
124
|
+
: [];
|
|
125
|
+
/*
|
|
126
|
+
* 🛑 A FAILED request and an empty list must not render the same. An empty
|
|
127
|
+
* dropdown reads as "this store has no pickup addresses" — a fact about the
|
|
128
|
+
* store — when it may be a network error, and the two need different
|
|
129
|
+
* actions from the seller.
|
|
130
|
+
*/
|
|
131
|
+
const pickupsFailed = pickupsQuery.isError;
|
|
132
|
+
React.useEffect(() => {
|
|
133
|
+
if (pickups.length === 1 && !pickupId)
|
|
134
|
+
setPickupId(pickups[0].id);
|
|
135
|
+
}, [pickups, pickupId]);
|
|
136
|
+
if (trackingNumber) {
|
|
137
|
+
return (_jsxs(Stack, { gap: "sm", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Dispatched" }), _jsx(Text, { size: "xs", color: "muted", children: platformBooked
|
|
138
|
+
? `Booked through LetItRip · ${trackingNumber}`
|
|
139
|
+
: `Posted by you · ${trackingNumber}` })] }));
|
|
140
|
+
}
|
|
141
|
+
const submit = async (
|
|
142
|
+
/* The `<Form>` render-prop's own signature — `applyZodIssues` clears a
|
|
143
|
+
* field by passing `null`, so a `string`-only sink is not assignable. */
|
|
144
|
+
setFieldError, clearErrors) => {
|
|
145
|
+
clearErrors();
|
|
146
|
+
setError(null);
|
|
147
|
+
const box = BOX_PRESETS.find((b) => b.id === boxId) ?? BOX_PRESETS[1];
|
|
148
|
+
const payload = method === "platform"
|
|
149
|
+
? {
|
|
150
|
+
method: "platform",
|
|
151
|
+
pickupAddressId: pickupId,
|
|
152
|
+
dimensions: {
|
|
153
|
+
weight: box.weight,
|
|
154
|
+
length: box.length,
|
|
155
|
+
width: box.width,
|
|
156
|
+
height: box.height,
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
: {
|
|
160
|
+
method: "custom",
|
|
161
|
+
shippingCarrier: carrier.trim(),
|
|
162
|
+
trackingNumber: tracking.trim(),
|
|
163
|
+
trackingUrl: trackingUrl.trim(),
|
|
164
|
+
};
|
|
165
|
+
/*
|
|
166
|
+
* Parsed against the SAME schema the route and the action use, so the
|
|
167
|
+
* branch's required fields are stated once. `applyZodIssues` can only
|
|
168
|
+
* attach path-bearing issues, so root-level refinements are surfaced
|
|
169
|
+
* separately or a refused save shows nothing at all.
|
|
170
|
+
*/
|
|
171
|
+
const parsed = shipOrderSchema.safeParse(payload);
|
|
172
|
+
if (!parsed.success) {
|
|
173
|
+
applyZodIssues(parsed.error.issues, setFieldError);
|
|
174
|
+
const root = rootLevelMessages(parsed.error.issues);
|
|
175
|
+
if (root.length > 0)
|
|
176
|
+
setError(root.join(" "));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
shipMutation.mutate(parsed.data);
|
|
180
|
+
};
|
|
181
|
+
return (_jsxs(Stack, { gap: "3", children: [_jsx(Text, { size: "sm", weight: "medium", children: "Dispatch" }), _jsxs(Row, { gap: "sm", children: [_jsx(Button, { size: "sm", variant: method === "custom" ? "primary" : "outline", "aria-pressed": method === "custom", onClick: () => setMethod("custom"), children: "I posted it" }), _jsx(Button, { size: "sm", variant: method === "platform" ? "primary" : "outline", "aria-pressed": method === "platform", onClick: () => setMethod("platform"), children: "Book through LetItRip" })] }), _jsx(Form, { onSubmit: (e) => e.preventDefault(), children: ({ setFieldError, clearErrors }) => {
|
|
182
|
+
fieldErrorSink.current = setFieldError;
|
|
183
|
+
return (_jsxs(Stack, { gap: "3", children: [method === "custom" ? (_jsxs(_Fragment, { children: [_jsx(FieldInput, { name: "shippingCarrier", label: "Carrier", required: true, value: carrier, onChange: setCarrier }), _jsx(FieldInput, { name: "trackingNumber", label: "Tracking number", required: true, value: tracking, onChange: setTracking }), _jsx(FieldInput, { name: "trackingUrl", label: "Tracking URL", required: true, type: "url", value: trackingUrl, onChange: setTrackingUrl })] })) : (_jsxs(_Fragment, { children: [pickupsFailed ? (_jsx(Text, { size: "xs", color: "error", role: "alert", children: "Could not load your pickup addresses. Reopen this panel to try again." })) : pickups.length === 0 ? (_jsx(Text, { size: "xs", color: "muted", children: "Loading your pickup addresses\u2026" })) : (_jsx(FieldSelect, { name: "pickupAddressId", label: "Collect from", required: true, value: pickupId, onChange: setPickupId, options: pickups.map((p) => ({
|
|
184
|
+
value: p.id,
|
|
185
|
+
label: [p.label, p.city, p.postalCode].filter(Boolean).join(" · ") || p.id,
|
|
186
|
+
})) })), _jsx(FieldSelect, { name: "box", label: "Parcel size", required: true, value: boxId, onChange: setBoxId, options: BOX_PRESETS.map((b) => ({ value: b.id, label: b.label })) }), _jsx(Text, { size: "xs", color: "muted", children: "The courier is booked on the platform's account and the tracking id comes back from them \u2014 you do not enter one." })] })), error && (_jsx(Div, { textSize: "xs", className: "border border-error/20", color: "error", surface: "danger-surface", padding: "inlineSm", rounded: "lg", role: "alert", children: error })), _jsx(Button, { type: "submit", action: method === "platform"
|
|
187
|
+
? ACTIONS.STORE["dispatch-platform"]
|
|
188
|
+
: ACTIONS.STORE["dispatch-custom"], isLoading: shipMutation.isPending, onClick: () => void submit(setFieldError, clearErrors) })] }));
|
|
189
|
+
} })] }));
|
|
190
|
+
}
|