feat(WIP): require the gm for fayda on ethiopian companies

This commit is contained in:
Nathnael
2026-08-04 12:40:57 +00:00
parent 415ae52143
commit 7393034188
12 changed files with 820 additions and 178 deletions

View File

@@ -437,6 +437,10 @@ export default function OnboardingWizardDialog({
// stays a plain typed role. Mandatory (Fayda) for an Ethiopian company;
// a foreign one requires a typed passport number for the owner instead.
identity: requirementsQuery.data?.identity,
onIdentityChange: () => {
void profileQuery.refetch();
void requirementsQuery.refetch();
},
// Surface a failed final submit (license/document upload or complete) inside
// the form — otherwise the server message (e.g. a 500) would be invisible on
// the submit step.

View File

@@ -82,6 +82,12 @@ function tabIncomplete(tabId: SettingsTab, profile: ProfileResponse): boolean {
case "contact":
return !profile.contactPersonName || !profile.contactPersonPhone;
case "gm":
// The GM is established through Fayda — verified in their own right or
// declared the same person as the owner — so the identity answers this,
// not the typed columns. A company that may still type them (foreign,
// whose manager may hold no Fayda ID) is judged on those instead.
if (profile.identity?.gm.verified) return false;
if (profile.identity?.faydaRequired) return true;
return (
!profile.generalManagerName ||
!profile.generalManagerEmail ||

View File

@@ -42,6 +42,7 @@ import {
toFormValues,
} from "./companyProfileForm/helpers";
import FaydaVerifyPanel from "@/components/FaydaVerifyPanel";
import { verifaydaService } from "@/services/verifayda.service";
import type { CompanyIdentityState } from "@/services/verifayda.service";
import { LinkCheckboxCard } from "./companyProfileForm/LinkCheckboxCard";
import ETradeCompanyCard from "./companyProfileForm/ETradeCompanyCard";
@@ -67,6 +68,7 @@ export default function CompanyProfileForm({
uploadedDocumentKeys,
onUploadDocuments,
identity,
onIdentityChange,
}: {
documentSettingCode: string;
documentFiles?: Record<string, File | File[] | null>;
@@ -106,6 +108,12 @@ export default function CompanyProfileForm({
>;
/** Fayda verification state for the owner and the PoA (undefined until loaded). */
identity?: CompanyIdentityState;
/**
* Refetch the profile + requirements. Only the in-page identity actions need
* this — a Fayda verification navigates the whole tab away and comes back to
* a freshly booted app, so it has nothing to notify.
*/
onIdentityChange?: () => void;
}) {
const [step, setStep] = useState<CompanyStep>(initialStep ?? "company");
const [saving, setSaving] = useState(false);
@@ -339,7 +347,11 @@ export default function CompanyProfileForm({
// "Same as …" links. A checked card prefills the target step's fields from the
// source step and disables them (kept mirrored while linked); unchecking clears
// them and re-enables editing.
const [gmSameAsOwner, setGmSameAsOwner] = useState(false);
// Seeded from the server so a resumed draft reopens with the declaration the
// company already made, rather than an unticked box over a linked GM.
const [gmSameAsOwner, setGmSameAsOwner] = useState(
identity?.gmSameAsOwner ?? false,
);
const [contactSameAsGm, setContactSameAsGm] = useState(false);
// General Manager source. The company step's email/phone are seeded from
@@ -366,6 +378,10 @@ export default function CompanyProfileForm({
useEffect(() => {
if (!gmSameAsOwner) return;
// A verified owner's identity is copied server-side and read back from
// `identity.gm`; mirroring it into form fields here would send typed
// values for something the API already owns.
if (identity?.owner.verified) return;
setValue("generalManagerName", gmSourceName, { shouldValidate: true });
setValue("generalManagerEmail", gmSourceEmail, { shouldValidate: true });
setValue("generalManagerPhone", gmSourcePhone, {
@@ -374,18 +390,78 @@ export default function CompanyProfileForm({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [gmSameAsOwner, gmSourceName, gmSourceEmail, gmSourcePhone]);
const toggleGmSameAsOwner = (checked: boolean) => {
/**
* "Same as owner" has two meanings depending on what backs the owner.
*
* A Fayda-verified owner is a proven identity, so the declaration is made
* server-side: the API copies that identity onto the GM and records what it
* did. Anything typed here would arrive wearing a verified badge it hadn't
* earned, which is exactly what the verification exists to prevent.
*
* A foreign company's owner is backed by a typed passport instead, so there
* is nothing proven to copy — that stays the local field-mirroring it has
* always been.
*/
const [gmLinkPending, setGmLinkPending] = useState(false);
const toggleGmSameAsOwner = async (checked: boolean) => {
setGmSameAsOwner(checked);
if (!checked) {
setValue("generalManagerName", "");
setValue("generalManagerEmail", "");
setValue("generalManagerPhone", "");
if (!identity?.owner.verified) {
if (!checked) {
setValue("generalManagerName", "");
setValue("generalManagerEmail", "");
setValue("generalManagerPhone", "");
}
return;
}
setGmLinkPending(true);
try {
if (checked) await verifaydaService.setGmSameAsOwner();
else await verifaydaService.clearGmIdentity();
onIdentityChange?.();
} catch (err) {
setGmSameAsOwner(!checked);
setSaveError(
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message ??
(err instanceof Error ? err.message : "Could not update the general manager"),
);
} finally {
setGmLinkPending(false);
}
};
const gmName = watch("generalManagerName");
const gmEmail = watch("generalManagerEmail");
const gmPhone = watch("generalManagerPhone");
// Where the GM's details come from depends on how they were established: a
// Fayda verification (or a "same as owner" declaration) owns them outright,
// and only a company that may still type them falls back to form state.
const gmVerified = identity?.gm.verified ?? false;
const gmName = gmVerified
? (identity?.gm.name ?? "")
: watch("generalManagerName");
const gmEmail = gmVerified
? (identity?.gm.email ?? "")
: watch("generalManagerEmail");
const gmPhone = gmVerified
? (identity?.gm.phone ?? "")
: watch("generalManagerPhone");
/**
* Whether the GM has been established at all — by verification, by the
* "same as owner" declaration, or (only where Fayda is optional) by typing.
* Fayda is an Ethiopian national ID, so a foreign company's GM may hold none.
*/
const gmTyped = Boolean(
watch("generalManagerName")?.trim() &&
watch("generalManagerEmail")?.trim() &&
watch("generalManagerPhone")?.trim(),
);
const gmEstablished =
gmVerified || (identity ? !identity.faydaRequired && gmTyped : gmTyped);
/** Same rule for the representative: verified, or typed where Fayda is optional. */
const poaEstablished =
(identity?.poa.verified ?? false) ||
(identity ? !identity.faydaRequired && Boolean(watch("poaName")?.trim()) : false);
// While linked, mirror the source values into the (disabled) target fields so
// the copy stays current even if the user goes back and edits the source.
@@ -627,9 +703,23 @@ export default function CompanyProfileForm({
setSaveError("Verify the company owner's identity with Fayda before continuing.");
return;
}
if (step === "poa" && requirePoa && !identity?.poa.verified) {
// The GM is established through Fayda now, so the step gates on the
// identity rather than on typed text — same strength as the old required
// fields, different evidence. A foreign company's GM may hold no Fayda ID,
// so typed details still satisfy it there.
if (step === "personnel" && !gmEstablished) {
setSaveError(
"Freight forwarders act on other companies' behalf, so the Power of Attorney's identity must be verified with Fayda.",
identity?.faydaRequired
? "Verify the general manager with Fayda, or tick “same as owner” if they are the company's owner."
: "Add the general manager's details, or verify them with Fayda.",
);
return;
}
if (step === "poa" && requirePoa && !poaEstablished) {
setSaveError(
identity?.faydaRequired
? "Freight forwarders act on other companies' behalf, so the Power of Attorney's identity must be verified with Fayda."
: "Freight forwarders act on other companies' behalf, so a Power of Attorney is required.",
);
return;
}
@@ -764,10 +854,11 @@ export default function CompanyProfileForm({
<Text fw={600} size="sm" c="edr-text">
General Manager
</Text>
{/* GM is a plain typed role, not the person the Fayda
verification proves — the owner is (see the Company step).
They're very often the same human, which "same as owner" is
for once the owner has verified. */}
{/* The GM is very often the owner. Where the owner is
Fayda-verified this reuses that proven identity outright
rather than making the same human verify twice; where the
owner is backed by a typed passport there is nothing proven
to copy, so it stays a local prefill. */}
<LinkCheckboxCard
checked={gmSameAsOwner}
onToggle={toggleGmSameAsOwner}
@@ -778,33 +869,53 @@ export default function CompanyProfileForm({
}
description={
identity?.owner.verified
? "Reuse the Fayda-verified owner's name, email and phone. Uncheck to enter different details."
? "Reuse the Fayda-verified owner's identity for the general manager. Uncheck to verify a different person."
: etradeOwner
? "Reuse the eTrade-registered owner's name, plus the company email and phone as you entered them. Uncheck to enter different details."
: "Reuse your account's name and the company email and phone as you entered them. Uncheck to enter different details."
}
/>
<TextInput
label="Name"
placeholder="Abebe Bikila"
error={errors.generalManagerName?.message}
{...register("generalManagerName")}
/>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="Email"
type="email"
placeholder="gm@company.com"
error={errors.generalManagerEmail?.message}
{...register("generalManagerEmail")}
{/* Verifying a second person is only meaningful when the GM is
someone other than the owner. */}
{!gmSameAsOwner && identity && (
<FaydaVerifyPanel
subject="gm"
title="General Manager"
state={identity.gm}
required={identity.faydaRequired}
disabled={gmLinkPending}
/>
<ControlledPhoneField
control={control}
name="generalManagerPhone"
label="Phone"
required
/>
</SimpleGrid>
)}
{/* Typed details survive only where Fayda cannot be required —
a foreign company's manager may hold no Fayda ID. Once
verified the API owns these fields, so they go away. */}
{!gmSameAsOwner && !gmVerified && !identity?.faydaRequired && (
<>
<TextInput
label="Name"
placeholder="Abebe Bikila"
error={errors.generalManagerName?.message}
{...register("generalManagerName")}
/>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="Email"
type="email"
placeholder="gm@company.com"
error={errors.generalManagerEmail?.message}
{...register("generalManagerEmail")}
/>
<ControlledPhoneField
control={control}
name="generalManagerPhone"
label="Phone"
required
/>
</SimpleGrid>
</>
)}
</>
)}
@@ -872,14 +983,17 @@ export default function CompanyProfileForm({
required={requirePoa}
/>
)}
{/* The city is the one field the Fayda address claim does not
reliably decompose into, so it stays typed. */}
<TextInput
label="PoA Location"
placeholder="City, Country"
error={errors.poaLocation?.message}
{...register("poaLocation")}
/>
{/* The address comes from the Fayda claim along with the name,
so it is shown on the panel rather than typed. Only a company
whose representative may hold no Fayda ID still types it. */}
{!identity?.poa.verified && !identity?.faydaRequired && (
<TextInput
label="PoA Location"
placeholder="City, Country"
error={errors.poaLocation?.message}
{...register("poaLocation")}
/>
)}
{/* The paper authorises the representative the verification
named, so it only has meaning once one exists. */}

View File

@@ -69,12 +69,24 @@ export const onboardingSchema = z.object({
.string()
.min(1, "Contact person phone is required")
.refine(isValidPhone, "Enter a valid phone number"),
generalManagerName: z.string().min(1, "Manager name is required"),
generalManagerEmail: z.string().email("Invalid Manager email"),
// Optional here, not unrequired: the GM is now established by Fayda — either
// verified in their own right or declared the same person as the owner — so
// for an Ethiopian company these fields are never typed and would fail a
// blanket `min(1)`. Presence is gated per nationality in the step's own
// check, where the identity state is available; zod only polices format for
// the foreign companies that still type them.
generalManagerName: z.string().optional(),
generalManagerEmail: z
.string()
.optional()
.refine(
(v) => !v || z.string().email().safeParse(v).success,
"Invalid Manager email",
),
generalManagerPhone: z
.string()
.min(1, "Manager phone is required")
.refine(isValidPhone, "Enter a valid phone number"),
.optional()
.refine((v) => !v || isValidPhone(v), "Enter a valid phone number"),
poaName: z.string().optional(),
poaPhone: z
.string()

View File

@@ -1,10 +1,11 @@
import { useEffect, useMemo, useState } from "react";
import { useMemo, useState } from "react";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { Briefcase, CheckCircle2, Save, XCircle } from "lucide-react";
import {
Alert,
Card,
Group,
Stack,
@@ -15,17 +16,29 @@ import {
Grid,
} from "@mantine/core";
import { api } from "@/services/api";
import { verifaydaService } from "@/services/verifayda.service";
import { ControlledPhoneField, isValidPhone } from "@/components/PhoneField";
import { LinkCheckboxCard } from "@/pages/accounts/companyProfileForm/LinkCheckboxCard";
import FaydaVerifyPanel from "@/components/FaydaVerifyPanel";
import type { ProfileResponse } from "@/types/profile";
// Optional, not unrequired: an Ethiopian company's GM is established through
// Fayda and never types these, so a blanket `min(1)` would fail a form that is
// correct. Presence is gated below, where the identity state says which route
// applies; zod only polices format for the companies that still type them.
const schema = z.object({
generalManagerName: z.string().min(1, "GM name is required"),
generalManagerEmail: z.string().email("Invalid GM email"),
generalManagerName: z.string().optional(),
generalManagerEmail: z
.string()
.optional()
.refine(
(v) => !v || z.string().email().safeParse(v).success,
"Invalid GM email",
),
generalManagerPhone: z
.string()
.min(1, "GM phone is required")
.refine(isValidPhone, "Enter a valid phone number"),
.optional()
.refine((v) => !v || isValidPhone(v), "Enter a valid phone number"),
});
type FormData = z.infer<typeof schema>;
@@ -37,16 +50,20 @@ interface TabGeneralManagerProps {
}
/**
* The general manager is a plain typed role, not the person the Fayda
* verification proves — the owner is. They're very often the same human,
* which is what "Same as owner" is for: once the owner has verified, this
* copies their name/email/phone in rather than making the customer re-type
* data the company already proved.
* The general manager's identity comes from Fayda: either verified in their
* own right, or declared to be the owner — very often the same human, which is
* what "Same as owner" is for. Typed details survive only for a foreign
* company, whose manager may hold no Fayda ID at all.
*/
export default function TabGeneralManager({ profile, mode = "edit", onContinue }: TabGeneralManagerProps) {
const queryClient = useQueryClient();
const owner = profile.identity?.owner;
const [gmSameAsOwner, setGmSameAsOwner] = useState(false);
const identity = profile.identity;
const gm = identity?.gm;
const faydaRequired = identity?.faydaRequired ?? false;
const [gmSameAsOwner, setGmSameAsOwner] = useState(
identity?.gmSameAsOwner ?? false,
);
const defaultValues = useMemo((): FormData => {
return {
@@ -68,25 +85,45 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue }
values: defaultValues,
});
const toggleGmSameAsOwner = (checked: boolean) => {
/**
* With a Fayda-verified owner the declaration is made server-side — the API
* copies the proven identity onto the GM — so nothing is typed here. Without
* one (a foreign company, whose owner is backed by a passport) there is
* nothing proven to copy and this stays a local prefill.
*/
const [linkPending, setLinkPending] = useState(false);
const [linkError, setLinkError] = useState<string | null>(null);
const toggleGmSameAsOwner = async (checked: boolean) => {
setGmSameAsOwner(checked);
if (checked && owner) {
setValue("generalManagerName", owner.name ?? "", { shouldValidate: true });
setValue("generalManagerEmail", owner.email ?? "", { shouldValidate: true });
setValue("generalManagerPhone", owner.phone ?? "", { shouldValidate: true });
setLinkError(null);
if (!owner?.verified) {
if (checked && owner) {
setValue("generalManagerName", owner.name ?? "", { shouldValidate: true });
setValue("generalManagerEmail", owner.email ?? "", { shouldValidate: true });
setValue("generalManagerPhone", owner.phone ?? "", { shouldValidate: true });
}
return;
}
setLinkPending(true);
try {
if (checked) await verifaydaService.setGmSameAsOwner();
else await verifaydaService.clearGmIdentity();
queryClient.invalidateQueries({
queryKey: api.companies.getProfile.queryKey(),
});
} catch (err) {
setGmSameAsOwner(!checked);
setLinkError(
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message ??
(err instanceof Error ? err.message : "Could not update the general manager"),
);
} finally {
setLinkPending(false);
}
};
// Keep the copy live while the checkbox is on — e.g. the owner re-verifies
// with updated details.
useEffect(() => {
if (!gmSameAsOwner || !owner) return;
setValue("generalManagerName", owner.name ?? "");
setValue("generalManagerEmail", owner.email ?? "");
setValue("generalManagerPhone", owner.phone ?? "");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [gmSameAsOwner, owner?.name, owner?.email, owner?.phone]);
const mutation = useMutation({
mutationFn: (data: FormData) =>
api.companies.updateProfile.call({
@@ -102,6 +139,11 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue }
const onSubmit = (data: FormData) => mutation.mutate(data);
// Nothing to save when Fayda owns the details: the verification and the
// "same as owner" declaration both write server-side, so the form would be
// posting empty strings over a proven identity.
const typedFieldsInUse = !gmSameAsOwner && !gm?.verified && !faydaRequired;
return (
<Card padding="lg">
<Group gap="sm" mb="xs">
@@ -114,40 +156,70 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue }
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
{owner?.verified && (
<LinkCheckboxCard
checked={gmSameAsOwner}
onToggle={toggleGmSameAsOwner}
title="Same as verified owner"
description="Reuse the Fayda-verified owner's name, email and phone. Uncheck to enter different details."
/>
)}
<TextInput
label="Full Name"
placeholder="Abebe Bikila"
error={errors.generalManagerName?.message}
{...register("generalManagerName")}
<LinkCheckboxCard
checked={gmSameAsOwner}
onToggle={toggleGmSameAsOwner}
title={
owner?.verified ? "Same as verified owner" : "Same as business owner"
}
description={
owner?.verified
? "Reuse the Fayda-verified owner's identity for the general manager. Uncheck to verify a different person."
: "Reuse the owner's name, email and phone. Uncheck to enter different details."
}
/>
<Grid>
<Grid.Col span={6}>
{linkError && (
<Alert color="red" variant="light" icon={<XCircle size={18} />}>
{linkError}
</Alert>
)}
{/* Verifying a second person only means something when the manager
is someone other than the owner. */}
{!gmSameAsOwner && gm && (
<FaydaVerifyPanel
subject="gm"
title="General Manager"
state={gm}
required={faydaRequired}
disabled={linkPending || mutation.isPending}
/>
)}
{/* Typed details survive only where Fayda cannot be required — a
foreign company's manager may hold no Fayda ID. Once verified the
API owns these fields and refuses edits, so they go away. */}
{!gmSameAsOwner && !gm?.verified && !faydaRequired && (
<>
<TextInput
label="Email Address"
type="email"
placeholder="gm@company.com"
error={errors.generalManagerEmail?.message}
{...register("generalManagerEmail")}
label="Full Name"
placeholder="Abebe Bikila"
error={errors.generalManagerName?.message}
{...register("generalManagerName")}
/>
</Grid.Col>
<Grid.Col span={6}>
<ControlledPhoneField
control={control}
name="generalManagerPhone"
label="Phone Number"
required
/>
</Grid.Col>
</Grid>
<Grid>
<Grid.Col span={6}>
<TextInput
label="Email Address"
type="email"
placeholder="gm@company.com"
error={errors.generalManagerEmail?.message}
{...register("generalManagerEmail")}
/>
</Grid.Col>
<Grid.Col span={6}>
<ControlledPhoneField
control={control}
name="generalManagerPhone"
label="Phone Number"
required
/>
</Grid.Col>
</Grid>
</>
)}
</Stack>
<Group
@@ -171,7 +243,7 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue }
)}
</Group>
<Group gap="md">
{mode === "edit" && (
{mode === "edit" && typedFieldsInUse && (
<Button
type="button"
variant="outline"
@@ -181,13 +253,26 @@ export default function TabGeneralManager({ profile, mode = "edit", onContinue }
Reset
</Button>
)}
<Button
type="submit"
leftSection={<Save size={16} />}
loading={mutation.isPending}
>
{mode === "onboarding" ? "Continue" : "Save Changes"}
</Button>
{/* Saving only means something while the details are typed: under
Fayda both routes write server-side, so a submit would post
empty strings at an identity the API owns and refuses to
overwrite. Onboarding still needs a way forward, so the button
becomes a plain Continue rather than disappearing. */}
{typedFieldsInUse ? (
<Button
type="submit"
leftSection={<Save size={16} />}
loading={mutation.isPending}
>
{mode === "onboarding" ? "Continue" : "Save Changes"}
</Button>
) : (
mode === "onboarding" && (
<Button type="button" onClick={() => onContinue?.()}>
Continue
</Button>
)
)}
</Group>
</Group>
</form>

View File

@@ -263,19 +263,22 @@ export default function TabPowerOfAttorney({
<form onSubmit={handleSubmit(onSubmit)}>
<Stack gap="md">
{/* Name, email, phone and address are all written by the Fayda
verification, so only the city — which the address claim does
not reliably decompose into — is typed. */}
<Grid>
<Grid.Col span={6}>
<TextInput
label="PoA Location"
placeholder="City, Country"
error={errors.poaLocation?.message}
{...register("poaLocation")}
/>
</Grid.Col>
</Grid>
{/* Name, email, phone and address all come from the Fayda
verification and are shown on the panel above. Only a company
whose representative may hold no Fayda ID still types a
location. */}
{!poaProvided && !(identity?.faydaRequired ?? false) && (
<Grid>
<Grid.Col span={6}>
<TextInput
label="PoA Location"
placeholder="City, Country"
error={errors.poaLocation?.message}
{...register("poaLocation")}
/>
</Grid.Col>
</Grid>
)}
</Stack>
{/* ------------------------ Delegation letter ------------------------ */}

View File

@@ -3,11 +3,12 @@ import { unwrap } from "@/utils/endpoint";
import type { ApiResponse } from "@/types/apiResponse";
/**
* Which of the company's people a verification is for. The owner is NOT the
* general manager — GM is a plain typed role the portal offers a "same as
* owner" copy for, but only the owner and the PoA are ever Fayda-verified.
* Which of the company's people a verification is for. The owner is who the
* company is proven through; the PoA and GM are personnel it names. The GM is
* very often the owner — "same as owner" reuses that verification rather than
* making the same human prove themselves twice.
*/
export type IdentitySubject = "owner" | "poa";
export type IdentitySubject = "owner" | "poa" | "gm";
/** One person's Fayda verification state, as the API reports it. */
export interface IdentityVerificationState {
@@ -29,12 +30,24 @@ export interface OwnerIdentityState extends IdentityVerificationState {
}
export interface CompanyIdentityState {
/** True when Fayda verification of the owner (and PoA) is mandatory — Ethiopian companies only. */
/**
* True when Fayda verification is mandatory — Ethiopian companies only.
* Doubles as "may this person be typed instead": Fayda is an Ethiopian
* national ID, so a foreign company's GM and PoA are offered the
* verification but fall back to typed details when they hold none.
*/
faydaRequired: boolean;
/** True when the owner's passport number is mandatory — foreign companies only. */
passportRequired: boolean;
owner: OwnerIdentityState;
poa: IdentityVerificationState;
/**
* General manager. `verified` covers both routes: the GM verifying in their
* own right, and the company declaring the GM is the owner (in which case
* `gmSameAsOwner` is set and the owner's Fayda sub backs it).
*/
gm: IdentityVerificationState;
gmSameAsOwner: boolean;
complete: boolean;
}
@@ -106,6 +119,30 @@ export const verifaydaService = {
return unwrap(response.data);
},
/**
* Declare the General Manager is the company's owner, reusing the owner's
* verified identity rather than making the same human verify twice. The copy
* happens server-side from the stored owner identity — the portal never
* supplies the values — and is refused until the owner is verified.
*/
setGmSameAsOwner: async (): Promise<CompanyIdentityState> => {
const response = await client.post<ApiResponse<CompanyIdentityState>>(
"/api/companies/identity/gm/same-as-owner",
);
return unwrap(response.data);
},
/**
* Clear the GM's identity — the "same as owner" declaration or a verification
* of their own — leaving them open to be re-established either way.
*/
clearGmIdentity: async (): Promise<CompanyIdentityState> => {
const response = await client.delete<ApiResponse<CompanyIdentityState>>(
"/api/companies/identity/gm",
);
return unwrap(response.data);
},
/**
* Drop the Power of Attorney — verified identity, details and delegation
* paper together. A verified person's fields are locked, so blanking the form

View File

@@ -36,11 +36,15 @@ export interface ProfileResponse {
generalManagerEmail: string | null;
generalManagerPhone: string | null;
/**
* Fayda verification state for the owner and the PoA — not the general
* manager, which stays a plain typed role. `identity.faydaRequired` /
* `identity.passportRequired` is the Ethiopian/foreign switch: an Ethiopian
* company verifies the owner (and PoA) with Fayda; a foreign one requires a
* typed passport number for the owner instead.
* Fayda verification state for the owner, the PoA and the general manager.
* `identity.faydaRequired` / `identity.passportRequired` is the
* Ethiopian/foreign switch: an Ethiopian company verifies all three with
* Fayda, while a foreign one proves its owner with a typed passport number
* and may type its GM and PoA, whose holders may have no Fayda ID.
*
* The `generalManager*` fields above are the same person's details written
* flat — a verification keeps them in step, since the booking, contract and
* train-scheduling notifiers mail `generalManagerEmail` directly.
*/
identity: CompanyIdentityState;
poaName: string | null;