@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.
Files changed (263) hide show
  1. package/dist/.build-stamp.json +3 -3
  2. package/dist/_internal/client/features/layout/DashboardLayoutClient.js +8 -29
  3. package/dist/_internal/client/features/layout/filterNavItems.d.ts +28 -0
  4. package/dist/_internal/client/features/layout/filterNavItems.js +35 -0
  5. package/dist/_internal/server/features/ads/booking.d.ts +118 -0
  6. package/dist/_internal/server/features/ads/booking.js +326 -0
  7. package/dist/_internal/server/features/checkout/actions.d.ts +51 -11
  8. package/dist/_internal/server/features/checkout/actions.js +335 -80
  9. package/dist/_internal/server/features/checkout/expand-order-items.d.ts +1 -2
  10. package/dist/_internal/server/features/checkout/expand-order-items.js +32 -0
  11. package/dist/_internal/server/features/checkout/freeze-parts.d.ts +50 -0
  12. package/dist/_internal/server/features/checkout/freeze-parts.js +94 -0
  13. package/dist/_internal/server/features/checkout/value-otp-gate.d.ts +55 -0
  14. package/dist/_internal/server/features/checkout/value-otp-gate.js +60 -0
  15. package/dist/_internal/server/features/dashboard/attention.d.ts +128 -0
  16. package/dist/_internal/server/features/dashboard/attention.js +183 -0
  17. package/dist/_internal/server/features/dashboard/data.d.ts +47 -0
  18. package/dist/_internal/server/features/dashboard/data.js +82 -0
  19. package/dist/_internal/server/features/inventory/reservations.d.ts +99 -0
  20. package/dist/_internal/server/features/inventory/reservations.js +273 -0
  21. package/dist/_internal/server/features/offers/destinations.d.ts +40 -0
  22. package/dist/_internal/server/features/offers/destinations.js +65 -0
  23. package/dist/_internal/server/features/orders/adapters.js +21 -0
  24. package/dist/_internal/server/features/orders/amend-delivery.d.ts +56 -0
  25. package/dist/_internal/server/features/orders/amend-delivery.js +124 -0
  26. package/dist/_internal/server/features/orders/balance.d.ts +89 -0
  27. package/dist/_internal/server/features/orders/balance.js +256 -0
  28. package/dist/_internal/server/features/pricing/actions.d.ts +58 -0
  29. package/dist/_internal/server/features/pricing/actions.js +144 -0
  30. package/dist/_internal/server/features/shipping/book-split.d.ts +86 -0
  31. package/dist/_internal/server/features/shipping/book-split.js +130 -0
  32. package/dist/_internal/server/features/shipping/book.d.ts +61 -0
  33. package/dist/_internal/server/features/shipping/book.js +176 -0
  34. package/dist/_internal/server/features/shipping/courier-block.js +26 -1
  35. package/dist/_internal/server/features/site-settings/adapters.d.ts +1 -1
  36. package/dist/_internal/server/features/site-settings/adapters.js +14 -0
  37. package/dist/_internal/server/features/stores/adapters.d.ts +1 -1
  38. package/dist/_internal/server/features/stores/adapters.js +14 -0
  39. package/dist/_internal/server/functions/scheduled.d.ts +2 -1
  40. package/dist/_internal/server/functions/scheduled.js +57 -1
  41. package/dist/_internal/server/jobs/core/autoPayoutEligibility.js +2 -0
  42. package/dist/_internal/server/jobs/core/jobRunners.js +45 -0
  43. package/dist/_internal/server/jobs/core/onOrderStatusChange.js +4 -1
  44. package/dist/_internal/server/jobs/core/reservationExpiry.d.ts +17 -0
  45. package/dist/_internal/server/jobs/core/reservationExpiry.js +40 -0
  46. package/dist/_internal/server/jobs/core/weeklyPayoutEligibility.js +15 -0
  47. package/dist/_internal/server/jobs/handlers/index.d.ts +1 -0
  48. package/dist/_internal/server/jobs/handlers/index.js +1 -0
  49. package/dist/_internal/server/jobs/handlers/reservationExpiry.d.ts +2 -0
  50. package/dist/_internal/server/jobs/handlers/reservationExpiry.js +2 -0
  51. package/dist/_internal/shared/actions/action-registry.d.ts +1 -1
  52. package/dist/_internal/shared/actions/action-registry.js +268 -0
  53. package/dist/_internal/shared/checkout/order-math.d.ts +31 -2
  54. package/dist/_internal/shared/checkout/order-math.js +22 -36
  55. package/dist/_internal/shared/checkout/rules/index.d.ts +1 -1
  56. package/dist/_internal/shared/checkout/rules/index.js +1 -1
  57. package/dist/_internal/shared/checkout/rules/payment-gate.d.ts +18 -0
  58. package/dist/_internal/shared/checkout/rules/payment-gate.js +25 -0
  59. package/dist/_internal/shared/checkout/split.d.ts +0 -12
  60. package/dist/_internal/shared/checkout/split.js +22 -2
  61. package/dist/_internal/shared/content/disclaimers.js +7 -7
  62. package/dist/_internal/shared/content/published-values.d.ts +6 -1
  63. package/dist/_internal/shared/content/published-values.js +8 -0
  64. package/dist/_internal/shared/features/admin/integrations.d.ts +32 -2
  65. package/dist/_internal/shared/features/admin/integrations.js +19 -9
  66. package/dist/_internal/shared/features/dashboard/hub-sections.d.ts +72 -0
  67. package/dist/_internal/shared/features/dashboard/hub-sections.js +75 -0
  68. package/dist/_internal/shared/features/emi/choice.d.ts +98 -0
  69. package/dist/_internal/shared/features/emi/choice.js +69 -0
  70. package/dist/_internal/shared/features/emi/group.d.ts +84 -0
  71. package/dist/_internal/shared/features/emi/group.js +126 -0
  72. package/dist/_internal/shared/features/ledger/posting.d.ts +107 -0
  73. package/dist/_internal/shared/features/ledger/posting.js +165 -0
  74. package/dist/_internal/shared/features/ledger/reports.d.ts +76 -0
  75. package/dist/_internal/shared/features/ledger/reports.js +127 -0
  76. package/dist/_internal/shared/features/ledger/rules.d.ts +170 -0
  77. package/dist/_internal/shared/features/ledger/rules.js +221 -0
  78. package/dist/_internal/shared/features/offers/coordination.d.ts +122 -0
  79. package/dist/_internal/shared/features/offers/coordination.js +80 -0
  80. package/dist/_internal/shared/features/orders/delivery-snapshot.d.ts +114 -0
  81. package/dist/_internal/shared/features/orders/delivery-snapshot.js +116 -0
  82. package/dist/_internal/shared/features/payouts/pay-period.d.ts +91 -0
  83. package/dist/_internal/shared/features/payouts/pay-period.js +104 -0
  84. package/dist/_internal/shared/features/points/accepts.d.ts +1 -8
  85. package/dist/_internal/shared/features/points/accepts.js +12 -6
  86. package/dist/_internal/shared/features/pricing/rate-registry.d.ts +150 -0
  87. package/dist/_internal/shared/features/pricing/rate-registry.js +246 -0
  88. package/dist/_internal/shared/features/shipping/parcel.d.ts +123 -0
  89. package/dist/_internal/shared/features/shipping/parcel.js +139 -0
  90. package/dist/_internal/shared/features/site-settings/fees.d.ts +16 -8
  91. package/dist/_internal/shared/features/site-settings/fees.js +12 -8
  92. package/dist/_internal/shared/fees/explain.d.ts +81 -0
  93. package/dist/_internal/shared/fees/explain.js +101 -0
  94. package/dist/_internal/shared/history/index.d.ts +1 -1
  95. package/dist/_internal/shared/history/index.js +1 -1
  96. package/dist/_internal/shared/history/types.d.ts +7 -1
  97. package/dist/_internal/shared/history/types.js +25 -0
  98. package/dist/client.d.ts +11 -1
  99. package/dist/client.js +13 -0
  100. package/dist/constants/api-endpoints.d.ts +72 -0
  101. package/dist/constants/api-endpoints.js +41 -0
  102. package/dist/constants/field-names.d.ts +41 -0
  103. package/dist/constants/field-names.js +47 -0
  104. package/dist/features/account/components/UserOrdersView.js +28 -2
  105. package/dist/features/admin/components/AdminIntegrationsView.d.ts +55 -0
  106. package/dist/features/admin/components/AdminIntegrationsView.js +74 -0
  107. package/dist/features/admin/components/AdminOrderEditorView.d.ts +16 -1
  108. package/dist/features/admin/components/AdminOrderEditorView.js +72 -3
  109. package/dist/features/admin/components/AdminOrdersView.js +20 -1
  110. package/dist/features/admin/schemas/firestore.d.ts +32 -0
  111. package/dist/features/ads/schemas/booking-write.d.ts +157 -0
  112. package/dist/features/ads/schemas/booking-write.js +88 -0
  113. package/dist/features/ads/schemas/creative-form.d.ts +18 -0
  114. package/dist/features/ads/schemas/creative-form.js +51 -0
  115. package/dist/features/ads/schemas/firestore.d.ts +140 -0
  116. package/dist/features/ads/schemas/firestore.js +109 -0
  117. package/dist/features/auctions/repository/bid.repository.js +3 -1
  118. package/dist/features/audit-log/schemas/firestore.d.ts +110 -0
  119. package/dist/features/audit-log/schemas/firestore.js +74 -0
  120. package/dist/features/auth/actions/profile-actions.d.ts +69 -1
  121. package/dist/features/auth/actions/profile-actions.js +62 -1
  122. package/dist/features/auth/schemas/firestore.d.ts +16 -2
  123. package/dist/features/auth/schemas/firestore.js +2 -1
  124. package/dist/features/blog/actions/blog-actions.d.ts +6 -0
  125. package/dist/features/blog/actions/blog-actions.js +17 -2
  126. package/dist/features/blog/api/route.js +4 -1
  127. package/dist/features/blog/columns/index.js +5 -0
  128. package/dist/features/blog/components/BlogIndexPageView.js +2 -1
  129. package/dist/features/blog/repository/blog.repository.d.ts +4 -0
  130. package/dist/features/blog/repository/blog.repository.js +5 -0
  131. package/dist/features/blog/schemas/blog-form.d.ts +3 -0
  132. package/dist/features/blog/schemas/blog-form.js +1 -0
  133. package/dist/features/blog/schemas/firestore.d.ts +38 -1
  134. package/dist/features/blog/schemas/firestore.js +23 -0
  135. package/dist/features/blog/schemas/index.d.ts +6 -0
  136. package/dist/features/blog/schemas/index.js +2 -0
  137. package/dist/features/blog/types/index.d.ts +2 -0
  138. package/dist/features/cart/components/CartPriceBreakdown.d.ts +12 -1
  139. package/dist/features/cart/components/CartPriceBreakdown.js +24 -5
  140. package/dist/features/cart/repository/cart.repository.d.ts +14 -0
  141. package/dist/features/cart/repository/cart.repository.js +20 -0
  142. package/dist/features/cart/schemas/firestore.d.ts +24 -0
  143. package/dist/features/dashboard/components/DashboardHubView.d.ts +83 -0
  144. package/dist/features/dashboard/components/DashboardHubView.js +56 -0
  145. package/dist/features/dashboard/components/DashboardSectionsClient.d.ts +23 -0
  146. package/dist/features/dashboard/components/DashboardSectionsClient.js +66 -0
  147. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.d.ts +55 -0
  148. package/dist/features/dispute-reasons/components/AdminDisputeReasonsView.js +175 -0
  149. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.d.ts +24 -0
  150. package/dist/features/dispute-reasons/repository/dispute-reasons.repository.js +28 -0
  151. package/dist/features/dispute-reasons/schemas/firestore.d.ts +3 -5
  152. package/dist/features/events/components/EventSaleBanner.js +1 -1
  153. package/dist/features/faq/components/FAQPageContent.js +2 -2
  154. package/dist/features/gateway-events/schemas/index.d.ts +2 -2
  155. package/dist/features/homepage/schemas/index.d.ts +8 -8
  156. package/dist/features/inventory/schemas/stock-reservation.d.ts +114 -0
  157. package/dist/features/inventory/schemas/stock-reservation.js +103 -0
  158. package/dist/features/layout/AppLayoutShell.d.ts +9 -1
  159. package/dist/features/layout/AppLayoutShell.js +12 -4
  160. package/dist/features/layout/BottomActions.js +2 -1
  161. package/dist/features/layout/BottomNavbar.d.ts +10 -2
  162. package/dist/features/layout/SideActionDock.js +52 -2
  163. package/dist/features/layout/TitleBarLayout.js +13 -4
  164. package/dist/features/layout/TitleBarLayout.style.css +59 -0
  165. package/dist/features/layout/useCtaReveal.d.ts +16 -1
  166. package/dist/features/layout/useCtaReveal.js +17 -26
  167. package/dist/features/ledger/repository/journal.repository.d.ts +110 -0
  168. package/dist/features/ledger/repository/journal.repository.js +312 -0
  169. package/dist/features/ledger/schemas/expenses.d.ts +87 -0
  170. package/dist/features/ledger/schemas/expenses.js +27 -0
  171. package/dist/features/ledger/schemas/firestore.d.ts +183 -0
  172. package/dist/features/ledger/schemas/firestore.js +69 -0
  173. package/dist/features/orders/components/BalancePaymentPanel.d.ts +12 -0
  174. package/dist/features/orders/components/BalancePaymentPanel.js +94 -0
  175. package/dist/features/orders/components/RaiseDisputeForm.js +19 -11
  176. package/dist/features/orders/repository/orders.repository.d.ts +28 -1
  177. package/dist/features/orders/repository/orders.repository.js +60 -6
  178. package/dist/features/orders/schemas/balance.d.ts +40 -0
  179. package/dist/features/orders/schemas/balance.js +47 -0
  180. package/dist/features/orders/schemas/firestore.d.ts +202 -0
  181. package/dist/features/orders/schemas/firestore.js +43 -0
  182. package/dist/features/orders/schemas/index.d.ts +2 -2
  183. package/dist/features/orders/types/index.d.ts +21 -0
  184. package/dist/features/payments/repository/payout.repository.js +3 -2
  185. package/dist/features/payments/schemas/firestore.d.ts +92 -1
  186. package/dist/features/payments/schemas/firestore.js +23 -0
  187. package/dist/features/pricing/components/AdminPricingView.d.ts +13 -0
  188. package/dist/features/pricing/components/AdminPricingView.js +157 -0
  189. package/dist/features/products/schemas/firestore.d.ts +26 -1
  190. package/dist/features/products/schemas/firestore.js +2 -0
  191. package/dist/features/products/schemas/index.d.ts +2 -2
  192. package/dist/features/reviews/repository/reviews.repository.d.ts +14 -0
  193. package/dist/features/reviews/repository/reviews.repository.js +16 -0
  194. package/dist/features/scams/repository/scammer.repository.js +2 -1
  195. package/dist/features/scams/schemas/firestore.d.ts +2 -1
  196. package/dist/features/scams/schemas/firestore.js +2 -1
  197. package/dist/features/seller/actions/offer-actions.js +56 -0
  198. package/dist/features/seller/actions/seller-actions.d.ts +1 -1
  199. package/dist/features/seller/actions/seller-actions.js +4 -0
  200. package/dist/features/seller/components/DispatchForm.d.ts +11 -0
  201. package/dist/features/seller/components/DispatchForm.js +190 -0
  202. package/dist/features/seller/components/SellerOrdersView.js +94 -2
  203. package/dist/features/seller/components/index.d.ts +1 -0
  204. package/dist/features/seller/components/index.js +1 -0
  205. package/dist/features/seller/repository/offer.repository.js +4 -3
  206. package/dist/features/seller/schemas/firestore.d.ts +23 -2
  207. package/dist/features/seller/schemas/firestore.js +3 -2
  208. package/dist/features/seller/schemas/index.d.ts +4 -4
  209. package/dist/features/seller/schemas/order-forms.d.ts +61 -4
  210. package/dist/features/seller/schemas/order-forms.js +41 -8
  211. package/dist/features/shipments/schemas/firestore.d.ts +25 -0
  212. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.d.ts +39 -0
  213. package/dist/features/shipping-boxes/repository/shipping-boxes.repository.js +55 -0
  214. package/dist/features/shipping-boxes/schemas/firestore.d.ts +2 -4
  215. package/dist/features/stores/repository/store.repository.d.ts +18 -0
  216. package/dist/features/stores/repository/store.repository.js +27 -1
  217. package/dist/features/stores/schemas/firestore.d.ts +41 -0
  218. package/dist/features/stores/schemas/firestore.js +20 -0
  219. package/dist/features/stores/schemas/index.d.ts +6 -6
  220. package/dist/features/support/repository/support.repository.js +72 -6
  221. package/dist/features/support/schemas/firestore.d.ts +17 -0
  222. package/dist/features/support/schemas/index.d.ts +2 -2
  223. package/dist/index.d.ts +5 -0
  224. package/dist/index.js +19 -0
  225. package/dist/next/routing/route-map.d.ts +92 -0
  226. package/dist/next/routing/route-map.js +53 -0
  227. package/dist/react/contexts/SessionContext.d.ts +17 -1
  228. package/dist/schemas/registry.d.ts +22 -16
  229. package/dist/security/pii-schemas.d.ts +1 -1
  230. package/dist/security/pii-schemas.js +45 -0
  231. package/dist/server-entry.d.ts +1 -1
  232. package/dist/server-entry.js +10 -1
  233. package/dist/server.d.ts +29 -1
  234. package/dist/server.js +158 -0
  235. package/dist/styles.css +349 -1
  236. package/dist/tailwind-utilities.css +1 -1
  237. package/dist/ui/components/Button.d.ts +10 -1
  238. package/dist/ui/components/Button.js +25 -3
  239. package/dist/ui/components/Chart.d.ts +63 -0
  240. package/dist/ui/components/Chart.js +169 -0
  241. package/dist/ui/components/Chart.style.css +214 -0
  242. package/dist/ui/components/Disclaimer.js +10 -1
  243. package/dist/ui/components/OverflowRow.js +54 -2
  244. package/dist/ui/components/OverflowRow.style.css +38 -0
  245. package/dist/ui/components/RichTextEditor.js +99 -8
  246. package/dist/ui/components/chart-spec.d.ts +68 -0
  247. package/dist/ui/components/chart-spec.js +140 -0
  248. package/dist/ui/components/index.style.css +1 -0
  249. package/dist/ui/components/surface-tokens.d.ts +1 -1
  250. package/dist/ui/components/surface-tokens.js +9 -0
  251. package/dist/ui/components/toolbar-collapse.d.ts +13 -0
  252. package/dist/ui/components/toolbar-collapse.js +10 -1
  253. package/dist/ui/index.d.ts +6 -0
  254. package/dist/ui/index.js +3 -0
  255. package/dist/ui/rich-text/RichText.d.ts +10 -1
  256. package/dist/ui/rich-text/RichText.js +33 -2
  257. package/dist/ui/rich-text/RichText.style.css +35 -0
  258. package/dist/ui/rich-text/RichTextRenderer.d.ts +30 -1
  259. package/dist/ui/rich-text/RichTextRenderer.js +34 -2
  260. package/dist/ui/rich-text/chart-blocks.d.ts +62 -0
  261. package/dist/ui/rich-text/chart-blocks.js +87 -0
  262. package/package.json +1 -1
  263. 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` never descends into arrays.
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` never descends into arrays, and history is an array.
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` never descends into arrays, and history is an array.
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").PayoutDocument[];
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
+ }