From c7fdb8fd20c100f90d57f9cbfb4273c2971a088e Mon Sep 17 00:00:00 2001
From: BADRI NARAYANAN
Date: Mon, 7 Sep 2026 00:04:31 -0700
Subject: [PATCH] fix(orders): show the real order-rejection reason; fix input
a11y and layout
Diagnosed "some error occurred" on order placement: verified live (fresh
KYC/account-approved, funded test customer) that the exact wire path --
frontend payload construction, CreateOrderRequest schema, Units/Price
decimal-string convention -- works correctly end to end (a direct POST
with the exact VTI/Buy/2/$20 payload succeeded, 201). The real bug is
that frontend/src/utils/apiErrorMessage.js has zero mappings for any
order-specific rejection code, so every real, specific reason the backend
returns (insufficient_investable_cash, kyc_verification_pending,
kyc_verification_rejected, account_approval_pending,
account_approval_rejected, customer_not_eligible) collapsed into the
generic "Something went wrong" fallback. Reproduced live against an
approved-but-unfunded account: before this fix, "Something went wrong";
after, "You don't have enough investable cash for this order."
New ordersErrorMessage.js mirrors funding's existing
fundingErrorMessage.js pattern; wired into OrderForm, OrderList, and
ApprovalBanner (all three previously called the bare utility).
Also, per a careful read of every input field: wired validation/server
errors into Input's `error` prop (fixes a real accessibility gap --
aria-invalid/aria-describedby never fired before, message rendered
through a disconnected sibling paragraph); trimmed the reference-price
value before validating and sending (quantity already did this via
parseQuantity, reference price did not); added an explicit empty-state
message if the securities list is ever empty instead of letting the
selected security silently go undefined; added `required` to both
selects for parity with the inputs.
Layout: OrderForm.css hard-coded max-width: 360px with no breakpoint,
and the form wasn't wrapped in a Card -- confirmed visually (a live
1280px snapshot showed the four fields as a narrow login-form-sized
column with the rest of the page empty). Replaced with a responsive
2-column field grid (Security+Side, Quantity+Reference price) at 768px+
matching FeesPage.css/FundingPage.css's established breakpoint
convention, collapsing to one column below it; wrapped in a Card at the
page's --reading-max-width (640px) -- verified both breakpoints live.
No backend changes -- the wire contract was already correct.
---
.../orders/components/ApprovalBanner.jsx | 4 +-
.../features/orders/components/OrderForm.css | 28 +++++++++--
.../features/orders/components/OrderForm.jsx | 49 ++++++++++++-------
.../features/orders/components/OrderList.jsx | 4 +-
.../src/features/orders/ordersErrorMessage.js | 19 +++++++
frontend/src/pages/OrderNewPage.css | 7 +++
frontend/src/pages/OrderNewPage.jsx | 8 ++-
7 files changed, 92 insertions(+), 27 deletions(-)
create mode 100644 frontend/src/features/orders/ordersErrorMessage.js
create mode 100644 frontend/src/pages/OrderNewPage.css
diff --git a/frontend/src/features/orders/components/ApprovalBanner.jsx b/frontend/src/features/orders/components/ApprovalBanner.jsx
index 652b6d1..481c90e 100644
--- a/frontend/src/features/orders/components/ApprovalBanner.jsx
+++ b/frontend/src/features/orders/components/ApprovalBanner.jsx
@@ -1,6 +1,6 @@
import { useState } from 'react'
import { Button } from '../../../components/Button'
-import { getErrorMessage } from '../../../utils/apiErrorMessage.js'
+import { getOrdersErrorMessage } from '../ordersErrorMessage.js'
import { formatUnitsString } from '../../../utils/format.js'
import { useApproveOrder } from '../hooks/useApproveOrder.js'
import './ApprovalBanner.css'
@@ -26,7 +26,7 @@ export function ApprovalBanner({ order, onApproved }) {