feat: expose eTrade registration and fayda birthdate/gender in company profile

This commit is contained in:
Nathnael
2026-07-29 13:38:39 +00:00
parent 0165a64e76
commit 16ef64fae2
5 changed files with 200 additions and 18 deletions

View File

@@ -2380,9 +2380,7 @@ export class CompaniesService {
...(result.fullName ? { [`${prefix}Name`]: result.fullName } : {}),
...(result.email ? { [`${prefix}Email`]: result.email } : {}),
...(result.phoneNumber ? { [`${prefix}Phone`]: result.phoneNumber } : {}),
...(dto.subject === "poa" && result.address
? { poaAddress: result.address }
: {}),
...(result.address ? { [`${prefix}Address`]: result.address } : {}),
};
// An approved company's profile edits are staged for backoffice review, and

View File

@@ -41,6 +41,8 @@ export class IdentityVerificationStateDto {
@ApiProperty({ nullable: true }) email!: string | null;
@ApiProperty({ nullable: true }) address!: string | null;
@ApiProperty({ nullable: true }) verifiedAt!: string | null;
@ApiProperty({ nullable: true }) birthdate!: string | null;
@ApiProperty({ nullable: true }) gender!: string | null;
}
export class OwnerIdentityStateDto extends IdentityVerificationStateDto {
@@ -106,6 +108,8 @@ function stateFor(
email: read(`${p}Email`),
address: read(`${p}Address`),
verifiedAt: read(`${p}FaydaVerifiedAt`),
birthdate: read(`${p}Birthdate`),
gender: read(`${p}Gender`),
};
}

View File

@@ -9,6 +9,10 @@ import {
ProfileLicenseFileView,
} from '../entities/company-profile.entity';
import { ResponseExternalProfileDto } from './response-external-profile.dto';
import {
buildCompanyIdentityState,
CompanyIdentityStateDto,
} from './complete-identity-verification.dto';
export class ResponseCompanyProfileDto {
id: string;
@@ -69,6 +73,28 @@ export class ResponseCompanyDto {
* external profiles weren't loaded.
*/
onboardingCompleted?: boolean;
// eTrade-sourced registration record — populated by the onboarding TIN
// lookup, locked/read-only on the portal from the moment it's fetched.
licenceNumber?: string | null;
statusDescription?: string | null;
dateRegistered?: string | null;
renewedFrom?: string | null;
renewalDate?: string | null;
renewedTo?: string | null;
region?: string | null;
zone?: string | null;
woreda?: string | null;
kebele?: string | null;
houseNo?: string | null;
/**
* Owner/PoA Fayda verification state, shared with the portal
* (`buildCompanyIdentityState`) so backoffice never re-derives — or
* disagrees with — the rule the API actually enforces.
*/
identity: CompanyIdentityStateDto;
createdAt: Date;
updatedAt: Date;
@@ -95,6 +121,18 @@ export class ResponseCompanyDto {
? company.profiles.length === 0 ||
company.profiles.some((p) => p.onboardingCompleted)
: undefined;
this.licenceNumber = company.licenceNumber;
this.statusDescription = company.statusDescription;
this.dateRegistered = company.dateRegistered;
this.renewedFrom = company.renewedFrom;
this.renewalDate = company.renewalDate;
this.renewedTo = company.renewedTo;
this.region = company.region;
this.zone = company.zone;
this.woreda = company.woreda;
this.kebele = company.kebele;
this.houseNo = company.houseNo;
this.identity = buildCompanyIdentityState(company);
this.createdAt = company.createdAt;
this.updatedAt = company.updatedAt;
}

View File

@@ -607,17 +607,11 @@ export default function CustomerDetailPage() {
{ label: "PoA address", value: company?.poaAddress },
];
const hasPoaDetails = poaFields.some((f) => f.value?.trim());
// Fayda verification is written into the company attributes by the portal's
// verification flow.
const attrs = (company?.attributes ?? {}) as Record<string, unknown>;
const faydaVerified = (prefix: "owner" | "poa") => ({
verified: Boolean(attrs[`${prefix}FaydaSub`]),
at: (attrs[`${prefix}FaydaVerifiedAt`] as string | undefined) ?? null,
});
const ownerIdentity = faydaVerified("owner");
const ownerPassportNumber =
(attrs.ownerPassportNumber as string | undefined) ?? null;
const poaIdentity = faydaVerified("poa");
// Shared with the portal (buildCompanyIdentityState) — same derivation, so
// this page can never disagree with the rule the API actually enforces.
const ownerIdentity = company?.identity?.owner;
const poaIdentity = company?.identity?.poa;
const hasEtradeRecord = Boolean(company?.licenceNumber?.trim());
// A freight forwarder acts on other companies' behalf, so its PoA — details
// and DARS delegation paper both — is mandatory rather than optional.
const poaMandatory = (company?.companyProfiles ?? []).some(
@@ -766,10 +760,10 @@ export default function CustomerDetailPage() {
<InfoField
label="Owner identity"
value={
ownerIdentity.verified
ownerIdentity?.verified
? "Fayda verified"
: ownerPassportNumber
? `Passport ${ownerPassportNumber}`
: ownerIdentity?.passportNumber
? `Passport ${ownerIdentity.passportNumber}`
: "Not verified"
}
/>
@@ -804,6 +798,97 @@ export default function CustomerDetailPage() {
</Stack>
</Card>
<Card>
<Stack gap="lg">
<Group gap="xs" wrap="nowrap">
<Text fw={600} c="edr-text">
eTrade registration
</Text>
{hasEtradeRecord ? (
<Badge size="sm" color="edr-green" variant="light">
Verified with eTrade
</Badge>
) : (
<Badge size="sm" color="gray" variant="light">
No eTrade record
</Badge>
)}
</Group>
{hasEtradeRecord ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
<InfoField
label="License number"
value={company.licenceNumber}
/>
<InfoField label="Status" value={company.statusDescription} />
<InfoField
label="Date registered"
value={company.dateRegistered}
/>
<InfoField label="Renewed from" value={company.renewedFrom} />
<InfoField label="Renewal date" value={company.renewalDate} />
<InfoField label="Renewed to" value={company.renewedTo} />
<InfoField label="Region" value={company.region} />
<InfoField label="Zone" value={company.zone} />
<InfoField label="Woreda" value={company.woreda} />
<InfoField label="Kebele" value={company.kebele} />
<InfoField label="House No" value={company.houseNo} />
</SimpleGrid>
) : (
<Text size="sm" c="dimmed">
No eTrade registration record on file for this customer's
TIN.
</Text>
)}
</Stack>
</Card>
<Card>
<Stack gap="lg">
<Group gap="xs" wrap="nowrap">
<Text fw={600} c="edr-text">
Owner identity
</Text>
{ownerIdentity?.verified ? (
<Badge size="sm" color="edr-green" variant="light">
Fayda verified
</Badge>
) : (
<Badge size="sm" color="gray" variant="light">
Not verified
</Badge>
)}
</Group>
{ownerIdentity?.verified ? (
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
<InfoField label="Name" value={ownerIdentity.name} />
<InfoField label="Phone" value={ownerIdentity.phone} />
<InfoField label="Email" value={ownerIdentity.email} />
<InfoField label="Address" value={ownerIdentity.address} />
<InfoField
label="Verified at"
value={formatDate(ownerIdentity.verifiedAt)}
/>
<InfoField
label="Birthdate"
value={ownerIdentity.birthdate}
/>
<InfoField label="Gender" value={ownerIdentity.gender} />
<InfoField
label="Passport number"
value={ownerIdentity.passportNumber}
/>
</SimpleGrid>
) : (
<Text size="sm" c="dimmed">
{ownerIdentity?.passportNumber
? `Not Fayda verified — identified by passport ${ownerIdentity.passportNumber}.`
: "The company owner has not verified their identity with Fayda."}
</Text>
)}
</Stack>
</Card>
<Card>
<Stack gap="lg">
<Group justify="space-between" wrap="nowrap">
@@ -844,9 +929,22 @@ export default function CustomerDetailPage() {
<InfoField
label="PoA Fayda"
value={
poaIdentity.verified ? "Verified" : "Not verified"
poaIdentity?.verified ? "Verified" : "Not verified"
}
/>
{poaIdentity?.verified && (
<>
<InfoField
label="PoA verified at"
value={formatDate(poaIdentity.verifiedAt)}
/>
<InfoField
label="PoA birthdate"
value={poaIdentity.birthdate}
/>
<InfoField label="PoA gender" value={poaIdentity.gender} />
</>
)}
</SimpleGrid>
) : (
<Text size="sm" c="dimmed">

View File

@@ -135,6 +135,36 @@ export interface CustomerResetTarget {
phoneIsDomestic: boolean | null;
}
/** One person's Fayda verification state — mirrors `IdentityVerificationStateDto`. */
export interface IdentityVerificationState {
verified: boolean;
name: string | null;
phone: string | null;
email: string | null;
address: string | null;
verifiedAt: string | null;
birthdate: string | null;
gender: string | null;
}
/** Mirrors `OwnerIdentityStateDto`. */
export interface OwnerIdentityState extends IdentityVerificationState {
passportNumber: string | null;
}
/**
* Owner/PoA Fayda verification, shared with the portal's derivation
* (`buildCompanyIdentityState`) so backoffice never re-derives — or
* disagrees with — the rule the API actually enforces.
*/
export interface CompanyIdentityState {
faydaRequired: boolean;
passportRequired: boolean;
owner: OwnerIdentityState;
poa: IdentityVerificationState;
complete: boolean;
}
/** Mirrors backend `Company` (+ its `companyProfiles`). */
export interface Company {
id: string;
@@ -161,6 +191,20 @@ export interface Company {
poaAddress?: string | null;
website?: string | null;
attributes?: Record<string, unknown> | null;
// eTrade-sourced registration record — populated by the onboarding TIN
// lookup, locked/read-only on the portal from the moment it's fetched.
licenceNumber?: string | null;
statusDescription?: string | null;
dateRegistered?: string | null;
renewedFrom?: string | null;
renewalDate?: string | null;
renewedTo?: string | null;
region?: string | null;
zone?: string | null;
woreda?: string | null;
kebele?: string | null;
houseNo?: string | null;
identity?: CompanyIdentityState;
companyProfiles: CompanyProfile[];
/**
* Whether the customer submitted their onboarding application. A company row