fix: onboarding validation

This commit is contained in:
Nathnael
2026-08-06 18:54:54 +00:00
parent ab7069e335
commit dd2b624aa6
11 changed files with 804 additions and 165 deletions

View File

@@ -26,9 +26,22 @@ interface ETradeInfoProps {
onStatusChange?: (status: ETradeStatus) => void;
/** Called when the TIN changes away from the last fetched value — clear whatever it filled in. */
onReset?: () => void;
/**
* This TIN already passed eTrade in an earlier session (the saved profile
* carries its registration details), so adopt it on arrival instead of
* re-querying. Rehydration lands the TIN after the first render, which used
* to look exactly like the customer typing a new one: every reopen fired a
* live lookup that could fail on an outage, and re-marked eTrade's fields as
* freshly verified so they were resubmitted on the next save. "Get Data"
* stays available for a deliberate re-verify.
*/
alreadyVerified?: boolean;
}
const isValidTin = (tin: string) => tin.length === 10;
// Digits, not just length: a 10-character non-numeric TIN used to fire a lookup
// that could only fail, and the failure was then reported as "this TIN isn't
// registered with eTrade" instead of "that isn't a TIN".
const isValidTin = (tin: string) => /^\d{10}$/.test(tin);
export default function ETradeInfo({
tin,
@@ -37,6 +50,7 @@ export default function ETradeInfo({
onDataLoaded,
onStatusChange,
onReset,
alreadyVerified,
}: ETradeInfoProps) {
const mutation = useETradeData();
const isLoading = mutation.isPending;
@@ -63,6 +77,13 @@ export default function ETradeInfo({
// doesn't refire the lookup the moment this mounts.
const lastFetchedTin = useRef<string | null>(tin || null);
useEffect(() => {
// A rehydrated TIN that eTrade already accepted: adopt it silently. Doing
// this before the change-detection below also keeps `onReset` from firing,
// which would wipe the very registration details that prove it passed.
if (alreadyVerified && lastFetchedTin.current === null && isValidTin(tin)) {
lastFetchedTin.current = tin;
return;
}
if (tin !== lastFetchedTin.current) {
// TIN moved away from whatever we last fetched — that result (verified
// data, "taken", or an error) no longer describes this TIN. Drop it so
@@ -82,12 +103,17 @@ export default function ETradeInfo({
const apiError =
mutation.isError && mutation.error ? extractApiError(mutation.error) : null;
// A 400 here means eTrade simply has no record for this TIN.
const notFound = apiError?.statusCode === 400;
// A 400 here usually means eTrade has no record for this TIN — but the API
// also wraps its own transport failures as a 400 ("Failed to fetch company
// info from eTrade: …"), and reporting an outage as "this TIN isn't
// registered" sends the customer off to re-check a number that was fine.
const unreachable = /failed to fetch/i.test(apiError?.message ?? "");
const notFound = apiError?.statusCode === 400 && !unreachable;
const errorMessage =
apiError && !notFound
? apiError.message ||
"We couldn't reach eTrade to fetch your company information. Please try again."
? unreachable || !apiError.message
? "We couldn't reach eTrade to fetch your company information. Please try again in a moment."
: apiError.message
: null;
const status: ETradeStatus = isLoading