Files
edr-platform/apps/edr-freight-web/portal/src/services/verifayda.service.ts

160 lines
5.8 KiB
TypeScript

import { client } from "@/utils/api";
import { unwrap } from "@/utils/endpoint";
import type { ApiResponse } from "@/types/apiResponse";
/**
* Which of the company's two people a verification is for.
*
* The **owner** is whoever the eTrade licence names as the business's manager
* — not necessarily the legal owner, but the person the record has to match.
* The **PoA** is who the company delegates to act for it.
*
* Exactly one of them is verified, chosen by the company's own answer to "does
* anyone hold power of attorney for you?" — see `poaDeclared`.
*/
export type IdentitySubject = "owner" | "poa";
/** Whether the company named a representative. Null until it answers. */
export type PoaDeclaration = "yes" | "no";
/** One person's identity state, as the API reports it. */
export interface IdentityVerificationState {
verified: boolean;
name: string | null;
phone: string | null;
email: string | null;
address: string | null;
verifiedAt: string | null;
/**
* Typed passport number — the ALTERNATIVE to Fayda for a foreign company,
* never written by a verification. Only asked of whichever person carries
* the company's identity, and only when `passportAccepted`.
*/
passportNumber: string | null;
}
export interface CompanyIdentityState {
/**
* True for a foreign company: a typed passport number proves the identity
* just as a Fayda verification does. Fayda is an Ethiopian national ID, so an
* Ethiopian company has no alternative to it.
*/
passportAccepted: boolean;
/**
* The company's answer to the power-of-attorney question. Null until it
* answers — which is itself outstanding, since the answer decides who
* verifies. Always "yes" for a freight forwarder, which cannot operate
* without a representative and is never asked.
*/
poaDeclared: PoaDeclaration | null;
/** Whose verification the company is gated on. Null while undeclared. */
subject: IdentitySubject | null;
owner: IdentityVerificationState;
poa: IdentityVerificationState;
/** True once `subject` is proven — Fayda-verified, or passport where accepted. */
identityProven: boolean;
/** The manager named on the eTrade licence, captured at lookup. */
etradeManagerName: string | null;
/**
* That manager's phone (E.164), from the same lookup. Together with the name
* this is what survives a refresh: the live lookup result does not, so
* without these two a resumed wizard cannot tell an eTrade-sourced owner from
* a typed one, and offers the licence's own data back as editable inputs.
*/
etradeManagerPhone: string | null;
/**
* Does the owner the company put forward match the eTrade licence? This is
* the backoffice's check. Null when there is nothing to compare. Advisory:
* eTrade's and Fayda's transliterations rarely agree exactly.
*/
ownerMatchesEtrade: boolean | null;
complete: boolean;
}
/**
* What the panel was doing when it handed the tab over to eSignet. The
* verification is a full-page redirect, so the page that started it is gone by
* the time /fayda/callback runs — this is how that page knows whose identity
* the code+state belongs to and where to put the user back.
*
* sessionStorage, not localStorage: it is scoped to this tab, so two tabs
* verifying different people can't overwrite each other, and it dies with the
* tab rather than outliving an abandoned verification.
*/
const PENDING_KEY = "fayda-pending-verification";
export interface PendingVerification {
subject: IdentitySubject;
/** Path to return to once the verification completes. */
returnTo: string;
}
export function stashPendingVerification(pending: PendingVerification): void {
sessionStorage.setItem(PENDING_KEY, JSON.stringify(pending));
}
/** Read and clear — the code+state are single-use, so a retry needs a fresh start. */
export function takePendingVerification(): PendingVerification | null {
const raw = sessionStorage.getItem(PENDING_KEY);
sessionStorage.removeItem(PENDING_KEY);
if (!raw) return null;
try {
const parsed = JSON.parse(raw) as PendingVerification;
return parsed.subject ? parsed : null;
} catch {
return null;
}
}
export const verifaydaService = {
/**
* Returns the eSignet authorize URL to navigate the tab to. `PORTAL` selects
* the portal's own registered redirect_uri — the backoffice and mobile
* clients have their own.
*/
start: async (): Promise<string> => {
const response = await client.post<
ApiResponse<{ authorizationUrl: string }>
>("/api/fayda/verification/start", {
purpose: "VERIFY",
platform: "PORTAL",
});
return unwrap(response.data).authorizationUrl;
},
/**
* Exchange the callback code+state for a verified identity and bind it to one
* of the company's people. The API writes that person's name, phone, email
* and address from the Fayda payload — none of it is typed here.
*/
completeIdentity: async (
subject: IdentitySubject,
code: string,
state: string,
): Promise<CompanyIdentityState> => {
const response = await client.post<ApiResponse<CompanyIdentityState>>(
"/api/companies/identity/fayda/complete",
{ subject, code, state },
);
return unwrap(response.data);
},
/**
* Answer whether anyone holds power of attorney for this company — the
* question that decides whose identity is verified.
*
* Answering "no" tears the representative down server-side: their details,
* their verification, their passport number and the DARS delegation paper.
* Refused for a freight forwarder, which cannot operate without one.
*/
setPoaDeclared: async (
declared: PoaDeclaration,
): Promise<CompanyIdentityState> => {
const response = await client.patch<ApiResponse<CompanyIdentityState>>(
"/api/companies/identity/poa-declared",
{ declared },
);
return unwrap(response.data);
},
};