feat(portal): offer the investment-licence path in the onboarding wizard

A foreign company can now say it operates on an investment licence, on the
same step as its nationality and roles. The box only appears for a foreign
company, and moving the nationality answer back to Ethiopian drops it — the
API refuses both pairings.

The company step's eTrade gate now reads `manualRegistration`
(co-operative OR investment licence): the TIN lookup still runs, but finding
nothing is an expected outcome rather than a blocker, and the registration
section is typed instead. What stays keyed to `cooperative` alone is the
per-role business licence — an investor holds one, a co-operative does not —
so the licence cards and their validation are unchanged for investors.

Also carries the client plumbing for the revert endpoint the settings card
uses next.
This commit is contained in:
Nathnael
2026-08-18 08:45:29 +00:00
parent a9763a541a
commit d1584ee708
8 changed files with 132 additions and 32 deletions

View File

@@ -162,6 +162,13 @@ export default function OnboardingWizardDialog({
const [cooperative, setCooperative] = useState<boolean>(
company?.company?.attributes?.cooperative === true,
);
// A foreign company operating on an Ethiopian Investment Commission licence.
// eTrade holds nothing for its TIN, so it types the registration exactly as a
// co-operative does — but it still holds a licence per role, so nothing about
// the licence step changes.
const [investorLicence, setInvestorLicence] = useState<boolean>(
company?.company?.attributes?.investorLicence === true,
);
// Ticking the box drops the selections the company can no longer hold, rather
// than letting Continue fail on ones the API refuses: a co-op cannot forward
// freight, and is registered in Ethiopia so it is never foreign.
@@ -171,8 +178,20 @@ export default function OnboardingWizardDialog({
setRoles((prev) => prev.filter((r) => r !== "freight_forwarder"));
// Ethiopian is then the only answer left, so it is made rather than asked.
setNationality("ethiopian");
// Which also rules out the investment licence — that is a foreign
// company's, and the API refuses the pair.
setInvestorLicence(false);
}
}, []);
// The investment licence is a foreign company's document. Moving the answer
// back to Ethiopian drops it rather than sending a pair the API refuses.
const handleNationalityChange = useCallback(
(value: CompanyNationality | null) => {
setNationality(value);
if (value !== "foreign") setInvestorLicence(false);
},
[],
);
const [documentFiles, setDocumentFiles] = useState<
Record<string, File | File[] | null>
>({});
@@ -224,6 +243,7 @@ export default function OnboardingWizardDialog({
roles: ProfileTypeValue[];
nationality?: CompanyNationality;
cooperative?: boolean;
investorLicence?: boolean;
}) => api.companies.startOnboarding.call(vars),
onSuccess: async () => {
// Nationality drives the server-resolved identity requirements (Fayda vs
@@ -307,6 +327,7 @@ export default function OnboardingWizardDialog({
setRoles(existingProfiles.map((p) => p.type));
setNationality(savedNationality);
setCooperative(company?.company?.attributes?.cooperative === true);
setInvestorLicence(company?.company?.attributes?.investorLicence === true);
// Resume into the form only when profiles exist; otherwise send the user to
// role selection so the missing operational profiles get created.
setPhase(hasOperationalProfiles ? "form" : "nationality-role");
@@ -322,8 +343,9 @@ export default function OnboardingWizardDialog({
roles: roles as ProfileTypeValue[],
nationality: nationality ?? undefined,
cooperative,
investorLicence,
});
}, [roles, nationality, cooperative, startMutation]);
}, [roles, nationality, cooperative, investorLicence, startMutation]);
// Back from the form's first step returns to nationality/role selection.
// Safe to re-enter: startOnboarding is idempotent — it reuses the existing
@@ -479,6 +501,10 @@ export default function OnboardingWizardDialog({
// startOnboarding has persisted it, and the form's whole company step
// branches on it.
cooperative: requirementsQuery.data?.cooperative ?? cooperative,
// Same rule, same reason: only a persisted flag changes what the company
// step asks for.
investorLicence:
requirementsQuery.data?.investorLicence ?? investorLicence,
// A freight forwarder cannot answer the power-of-attorney question — the
// API forces "yes" — so the step offers no way to change it.
declarationLocked: requirementsQuery.data?.poa?.locked ?? false,
@@ -546,7 +572,7 @@ export default function OnboardingWizardDialog({
</Text>
<NationalitySelect
value={nationality}
onChange={setNationality}
onChange={handleNationalityChange}
embedded
// A co-op is registered in Ethiopia by the co-operative
// promotion agency — foreign is not on offer rather than
@@ -563,6 +589,22 @@ export default function OnboardingWizardDialog({
label="We're a co-operative union or farm"
description="For members with a TIN but no business licence. You'll type your registration details instead of us pulling them from eTrade, and upload your co-operative papers in place of a trade licence."
/>
{/* Only a foreign company is offered this: the licence is the
Investment Commission's, and it is the reason eTrade has
nothing to look up. Same consequence as the co-operative box —
typed registration instead of a lookup — but the per-role
business licence still applies, so the documents step is
unchanged. */}
{nationality === "foreign" && !cooperative && (
<Checkbox
checked={investorLicence}
onChange={(e) =>
setInvestorLicence(e.currentTarget.checked)
}
label="We operate on a foreign investment licence"
description="For investors registered with the Ethiopian Investment Commission rather than the trade registry. eTrade holds no record of your TIN, so you'll type your registration details instead — and our team reviews them by hand."
/>
)}
<Text fw={600} size="lg" c="edr-text">
What does your company do?(multiple)
</Text>