mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
feat: expose eTrade registration and fayda birthdate/gender in company profile
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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`),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user