diff --git a/apps/edr-freight-api/src/modules/companies/companies.service.ts b/apps/edr-freight-api/src/modules/companies/companies.service.ts index cf03d8d50..b792ed4ec 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -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 diff --git a/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts index dbcc76471..6e7468c8a 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/complete-identity-verification.dto.ts @@ -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`), }; } diff --git a/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts index a05812558..60f9f9a34 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/response-company.dto.ts @@ -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; } diff --git a/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx b/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx index aa4042bbd..666069d98 100644 --- a/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx +++ b/apps/edr-freight-web/backoffice/src/pages/customers/CustomerDetailPage.tsx @@ -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; - 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() { @@ -804,6 +798,97 @@ export default function CustomerDetailPage() { + + + + + eTrade registration + + {hasEtradeRecord ? ( + + Verified with eTrade + + ) : ( + + No eTrade record + + )} + + {hasEtradeRecord ? ( + + + + + + + + + + + + + + ) : ( + + No eTrade registration record on file for this customer's + TIN. + + )} + + + + + + + + Owner identity + + {ownerIdentity?.verified ? ( + + Fayda verified + + ) : ( + + Not verified + + )} + + {ownerIdentity?.verified ? ( + + + + + + + + + + + ) : ( + + {ownerIdentity?.passportNumber + ? `Not Fayda verified — identified by passport ${ownerIdentity.passportNumber}.` + : "The company owner has not verified their identity with Fayda."} + + )} + + + @@ -844,9 +929,22 @@ export default function CustomerDetailPage() { + {poaIdentity?.verified && ( + <> + + + + + )} ) : ( diff --git a/apps/edr-freight-web/backoffice/src/types/customer.ts b/apps/edr-freight-web/backoffice/src/types/customer.ts index d95084851..7e69b19d7 100644 --- a/apps/edr-freight-web/backoffice/src/types/customer.ts +++ b/apps/edr-freight-web/backoffice/src/types/customer.ts @@ -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 | 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