mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
refactor(freight-backoffice): review one identity and the eTrade owner match
- Replace the general-manager rows with owner rows across the customer
detail, contract cards, types and service mappings.
- The identity card names its subject ("Verifies for this company") and shows
whether the asserted owner matches the manager on the eTrade licence:
amber when it does not, green when it does, dimmed when eTrade named
nobody. Advisory only, since the comparison is a fuzzy transliteration
match.
- A missing delegation letter now keys on poaDeclared === "yes".
- Keep the three general-manager labels in the change-request label map so
historical requests still render readable field names.
This commit is contained in:
@@ -170,12 +170,13 @@ export function ContractCustomerCard({
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
<SectionCard icon={User} title="General manager" accent="grape">
|
||||
{/* Whoever the eTrade licence names as the business's manager. */}
|
||||
<SectionCard icon={User} title="Owner" accent="grape">
|
||||
<InfoRows
|
||||
rows={[
|
||||
{ icon: User, label: "Name", value: company.generalManagerName },
|
||||
{ icon: Mail, label: "Email", value: company.generalManagerEmail },
|
||||
{ icon: Phone, label: "Phone", value: company.generalManagerPhone },
|
||||
{ icon: User, label: "Name", value: company.ownerName },
|
||||
{ icon: Mail, label: "Email", value: company.ownerEmail },
|
||||
{ icon: Phone, label: "Phone", value: company.ownerPhone },
|
||||
]}
|
||||
/>
|
||||
</SectionCard>
|
||||
|
||||
@@ -43,9 +43,17 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
contactPersonPosition: "Contact position",
|
||||
contactPersonEmail: "Contact email",
|
||||
contactPersonPhone: "Contact phone",
|
||||
generalManagerName: "General manager",
|
||||
generalManagerEmail: "GM email",
|
||||
generalManagerPhone: "GM phone",
|
||||
ownerName: "Owner name",
|
||||
ownerEmail: "Owner email",
|
||||
ownerPhone: "Owner phone",
|
||||
poaDeclared: "Has a Power of Attorney",
|
||||
poaPassportNumber: "PoA passport number",
|
||||
// Nothing writes these any more — the general manager was removed — but
|
||||
// change requests filed before that still carry them, and without a label
|
||||
// the reviewer sees a raw attribute key.
|
||||
generalManagerName: "General manager (retired)",
|
||||
generalManagerEmail: "GM email (retired)",
|
||||
generalManagerPhone: "GM phone (retired)",
|
||||
poaName: "PoA name",
|
||||
poaPhone: "PoA phone",
|
||||
poaEmail: "PoA email",
|
||||
@@ -79,9 +87,9 @@ export function currentValue(company: Company, key: string): string {
|
||||
nationality: c.nationality,
|
||||
contactPersonName: c.contactPersonName ?? attrs.contactPersonName,
|
||||
contactPersonPhone: c.contactPersonPhone ?? attrs.contactPersonPhone,
|
||||
generalManagerName: c.generalManagerName ?? attrs.generalManagerName,
|
||||
generalManagerEmail: c.generalManagerEmail ?? attrs.generalManagerEmail,
|
||||
generalManagerPhone: c.generalManagerPhone ?? attrs.generalManagerPhone,
|
||||
ownerName: c.ownerName ?? attrs.ownerName,
|
||||
ownerEmail: c.ownerEmail ?? attrs.ownerEmail,
|
||||
ownerPhone: c.ownerPhone ?? attrs.ownerPhone,
|
||||
};
|
||||
const v = key in map ? map[key] : (c[key] ?? attrs[key]);
|
||||
return v === null || v === undefined || v === "" ? "—" : String(v);
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
Text,
|
||||
} from "@mantine/core";
|
||||
import {
|
||||
AlertTriangle,
|
||||
ArrowLeft,
|
||||
ArrowRight,
|
||||
Banknote,
|
||||
@@ -638,8 +639,9 @@ export default function CustomerDetailPage() {
|
||||
const hasPoaDetails = poaFields.some((f) => f.value?.trim());
|
||||
// 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 identityState = company?.identity;
|
||||
const ownerIdentity = identityState?.owner;
|
||||
const poaIdentity = identityState?.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.
|
||||
@@ -647,7 +649,7 @@ export default function CustomerDetailPage() {
|
||||
(p) => p.type === "freight_forwarder",
|
||||
);
|
||||
const delegationMissing =
|
||||
(hasPoaDetails || poaMandatory) && poaLive.length === 0;
|
||||
company?.identity?.poaDeclared === "yes" && poaLive.length === 0;
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
@@ -834,18 +836,9 @@ export default function CustomerDetailPage() {
|
||||
value={company.contactPersonPhone}
|
||||
/>
|
||||
<Box />
|
||||
<InfoField
|
||||
label="General manager"
|
||||
value={company.generalManagerName}
|
||||
/>
|
||||
<InfoField
|
||||
label="GM email"
|
||||
value={company.generalManagerEmail}
|
||||
/>
|
||||
<InfoField
|
||||
label="GM phone"
|
||||
value={company.generalManagerPhone}
|
||||
/>
|
||||
<InfoField label="Owner" value={company.ownerName} />
|
||||
<InfoField label="Owner email" value={company.ownerEmail} />
|
||||
<InfoField label="Owner phone" value={company.ownerPhone} />
|
||||
</SimpleGrid>
|
||||
</Stack>
|
||||
</Card>
|
||||
@@ -912,6 +905,11 @@ export default function CustomerDetailPage() {
|
||||
<Text fw={600} c="edr-text">
|
||||
Owner identity
|
||||
</Text>
|
||||
{identityState?.subject === "owner" && (
|
||||
<Badge size="sm" color="blue" variant="light">
|
||||
Verifies for this company
|
||||
</Badge>
|
||||
)}
|
||||
{ownerIdentity?.verified ? (
|
||||
<Badge size="sm" color="edr-green" variant="light">
|
||||
Fayda verified
|
||||
@@ -922,6 +920,36 @@ export default function CustomerDetailPage() {
|
||||
</Badge>
|
||||
)}
|
||||
</Group>
|
||||
|
||||
{/* THE check: is the owner the company put forward the person
|
||||
the eTrade licence actually names? Advisory — eTrade and
|
||||
Fayda transliterate Amharic names differently, so this is a
|
||||
prompt to look, not a verdict. */}
|
||||
{identityState?.ownerMatchesEtrade === false ? (
|
||||
<Alert
|
||||
color="amber"
|
||||
variant="light"
|
||||
icon={<AlertTriangle size={16} />}
|
||||
title="Does not match the eTrade licence"
|
||||
>
|
||||
The licence names{" "}
|
||||
<strong>{identityState.etradeManagerName}</strong>, but this
|
||||
company recorded <strong>{company.ownerName}</strong>.
|
||||
</Alert>
|
||||
) : identityState?.ownerMatchesEtrade === true ? (
|
||||
<Badge
|
||||
size="sm"
|
||||
color="edr-green"
|
||||
variant="light"
|
||||
style={{ alignSelf: "flex-start" }}
|
||||
>
|
||||
Matches the eTrade licence
|
||||
</Badge>
|
||||
) : (
|
||||
<Text size="xs" c="dimmed">
|
||||
No eTrade manager name on file to compare against.
|
||||
</Text>
|
||||
)}
|
||||
{ownerIdentity?.verified ? (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2, lg: 3 }} spacing="lg">
|
||||
<InfoField label="Name" value={ownerIdentity.name} />
|
||||
|
||||
@@ -24,7 +24,7 @@ const cleanParams = (params: object) =>
|
||||
),
|
||||
);
|
||||
|
||||
/** Lift attributes JSONB into the flat contact/manager fields the UI reads. */
|
||||
/** Lift attributes JSONB into the flat contact/owner fields the UI reads. */
|
||||
function mapCompany(dto: Record<string, unknown>): Company {
|
||||
const attrs = (dto.attributes as Record<string, unknown> | null) ?? {};
|
||||
return {
|
||||
@@ -32,9 +32,9 @@ function mapCompany(dto: Record<string, unknown>): Company {
|
||||
companyProfiles: (dto.companyProfiles as Company["companyProfiles"]) ?? [],
|
||||
contactPersonName: (attrs.contactPersonName as string | null) ?? null,
|
||||
contactPersonPhone: (attrs.contactPersonPhone as string | null) ?? null,
|
||||
generalManagerName: (attrs.generalManagerName as string | null) ?? null,
|
||||
generalManagerEmail: (attrs.generalManagerEmail as string | null) ?? null,
|
||||
generalManagerPhone: (attrs.generalManagerPhone as string | null) ?? null,
|
||||
ownerName: (attrs.ownerName as string | null) ?? null,
|
||||
ownerEmail: (attrs.ownerEmail as string | null) ?? null,
|
||||
ownerPhone: (attrs.ownerPhone as string | null) ?? null,
|
||||
poaName: (attrs.poaName as string | null) ?? null,
|
||||
poaEmail: (attrs.poaEmail as string | null) ?? null,
|
||||
poaPhone: (attrs.poaPhone as string | null) ?? null,
|
||||
|
||||
@@ -64,9 +64,9 @@ export interface BookingCompany {
|
||||
email?: string | null;
|
||||
contactPersonName?: string | null;
|
||||
contactPersonPhone?: string | null;
|
||||
generalManagerName?: string | null;
|
||||
generalManagerEmail?: string | null;
|
||||
generalManagerPhone?: string | null;
|
||||
ownerName?: string | null;
|
||||
ownerEmail?: string | null;
|
||||
ownerPhone?: string | null;
|
||||
website?: string | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -179,23 +179,34 @@ export interface IdentityVerificationState {
|
||||
verifiedAt: string | null;
|
||||
birthdate: string | null;
|
||||
gender: string | null;
|
||||
}
|
||||
|
||||
/** Mirrors `OwnerIdentityStateDto`. */
|
||||
export interface OwnerIdentityState extends IdentityVerificationState {
|
||||
/** Typed passport number — the foreign-company alternative to Fayda. */
|
||||
passportNumber: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owner/PoA Fayda verification, shared with the portal's derivation
|
||||
* The company's single identity 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;
|
||||
/** Foreign company: a passport number proves the person as Fayda would. */
|
||||
passportAccepted: boolean;
|
||||
/** Whether the company named a representative. Null = never answered. */
|
||||
poaDeclared: "yes" | "no" | null;
|
||||
/** Whose verification the company is gated on — PoA if declared, else owner. */
|
||||
subject: "owner" | "poa" | null;
|
||||
owner: IdentityVerificationState;
|
||||
poa: IdentityVerificationState;
|
||||
identityProven: boolean;
|
||||
/** The manager named on the eTrade licence, captured at lookup. */
|
||||
etradeManagerName: string | null;
|
||||
/**
|
||||
* Does the owner the company put forward match the eTrade licence?
|
||||
* THE reviewer check. Null when there is nothing to compare. Advisory —
|
||||
* eTrade and Fayda transliterate Amharic names differently, so a `false` is
|
||||
* "look at this", not "reject this".
|
||||
*/
|
||||
ownerMatchesEtrade: boolean | null;
|
||||
complete: boolean;
|
||||
}
|
||||
|
||||
@@ -216,9 +227,10 @@ export interface Company {
|
||||
email?: string | null;
|
||||
contactPersonName?: string | null;
|
||||
contactPersonPhone?: string | null;
|
||||
generalManagerName?: string | null;
|
||||
generalManagerEmail?: string | null;
|
||||
generalManagerPhone?: string | null;
|
||||
/** The owner — whoever the eTrade licence names as the business's manager. */
|
||||
ownerName?: string | null;
|
||||
ownerEmail?: string | null;
|
||||
ownerPhone?: string | null;
|
||||
poaName?: string | null;
|
||||
poaEmail?: string | null;
|
||||
poaPhone?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user