feat: add poa and poa delegation file to onboarding

This commit is contained in:
Nathnael
2026-07-10 08:18:37 +00:00
parent e5d2bb2f63
commit 78cb1ac5d3
7 changed files with 256 additions and 18 deletions

View File

@@ -54,6 +54,23 @@ const LICENSE_CODE = "business_license";
/** Code for a license file staged in an open change request (not yet live). */
const LICENSE_PENDING_CODE = "business_license_pending";
/** Mirrors the field seeded in seed/file-upload-settings.seeder.ts. */
const POA_DELEGATION_FILE_KEY = "poa_delegation_letter";
/** company.attributes keys that together mean "a PoA was entered". */
const POA_ATTRIBUTES = [
"poaName",
"poaPhone",
"poaEmail",
"poaLocation",
"poaAddress",
] as const;
/** Mandatory once the company operates as a freight forwarder. */
const REQUIRED_POA_FIELDS: { key: string; label: string }[] = [
{ key: "poaName", label: "PoA name" },
{ key: "poaEmail", label: "PoA email" },
{ key: "poaPhone", label: "PoA phone" },
];
export interface UserIdentity {
userId: string;
firstName: string;
@@ -1240,6 +1257,29 @@ export class CompaniesService {
);
const missingLicenses = licenseProfiles.filter((p) => !p.uploaded);
// 4. Power of Attorney. Optional in general, but a freight forwarder acts on
// other companies' behalf so its PoA is mandatory. Either way, a PoA that
// has been entered must be evidenced by the delegation letter.
const poaRequired = (company.companyProfiles ?? []).some(
(p) => p.type === ProfileType.freightForwarder,
);
const poaProvided = POA_ATTRIBUTES.some((k) =>
(company.attributes?.[k] as string | undefined)?.trim(),
);
const missingPoaFields = poaRequired
? REQUIRED_POA_FIELDS.filter(
(f) => !(company.attributes?.[f.key] as string | undefined)?.trim(),
)
: [];
// Only gate on the letter once the document set actually carries the field.
const delegationField = (setting?.fields ?? []).find(
(f) => f.fileKey === POA_DELEGATION_FILE_KEY,
);
const missingDelegation =
Boolean(delegationField) &&
(poaRequired || poaProvided) &&
!uploadedCodes.has(POA_DELEGATION_FILE_KEY);
const outstanding = [
...missingInfo.map((f) => `Add your ${f.label.toLowerCase()}`),
...missingDocs.map((d) => `Upload your ${d.fileLabel}`),
@@ -1247,18 +1287,31 @@ export class CompaniesService {
(p) =>
`Upload a business license for your ${p.type.replace(/_/g, " ")} profile`,
),
...missingPoaFields.map((f) => `Add your ${f.label.toLowerCase()}`),
...(missingDelegation
? ["Upload the delegation letter for your Power of Attorney"]
: []),
];
// Progress spans every required item the user has to satisfy: company-info
// fields, required documents and one license per operational profile.
// fields, required documents, one license per operational profile, and the
// PoA details/letter whenever those are mandatory.
const requiredDocCount = documents.filter((d) => d.isRequired).length;
const poaItemCount =
(poaRequired ? REQUIRED_POA_FIELDS.length : 0) +
(delegationField && (poaRequired || poaProvided) ? 1 : 0);
const total =
this.REQUIRED_COMPANY_INFO.length +
requiredDocCount +
licenseProfiles.length;
licenseProfiles.length +
poaItemCount;
const completed =
total -
(missingInfo.length + missingDocs.length + missingLicenses.length);
(missingInfo.length +
missingDocs.length +
missingLicenses.length +
missingPoaFields.length +
(missingDelegation ? 1 : 0));
return new OnboardingRequirementsResponseDto({
documentSettingCode,
@@ -1266,6 +1319,13 @@ export class CompaniesService {
companyInfo: { complete: missingInfo.length === 0, missingFields: missingInfo },
documents,
licenseProfiles,
poa: {
required: poaRequired,
provided: poaProvided,
delegationLetterUploaded: uploadedCodes.has(POA_DELEGATION_FILE_KEY),
missingFields: missingPoaFields,
complete: missingPoaFields.length === 0 && !missingDelegation,
},
progress: { completed, total },
isComplete: outstanding.length === 0,
onboardingCompleted: profile.onboardingCompleted,

View File

@@ -35,6 +35,19 @@ export interface OnboardingLicenseProfile {
uploaded: boolean;
}
export interface OnboardingPoaState {
/** True when the company operates as a freight forwarder — PoA is mandatory. */
required: boolean;
/** True once any PoA detail has been entered. */
provided: boolean;
/** True when the delegation letter is stored for the company. */
delegationLetterUploaded: boolean;
/** PoA details still missing (only populated when `required`). */
missingFields: OnboardingInfoField[];
/** False while the PoA step still owes details or a delegation letter. */
complete: boolean;
}
export class OnboardingRequirementsResponseDto {
/** Resolved document setting code (by nationality) the docs were drawn from. */
documentSettingCode: string;
@@ -52,6 +65,9 @@ export class OnboardingRequirementsResponseDto {
/** Per-operational-profile business-license requirements. */
licenseProfiles: OnboardingLicenseProfile[];
/** Power of Attorney state, so the wizard needn't re-derive the rule. */
poa: OnboardingPoaState;
/** Overall setup progress across fields + documents + licenses. */
progress: { completed: number; total: number };
@@ -70,6 +86,7 @@ export class OnboardingRequirementsResponseDto {
this.companyInfo = init.companyInfo;
this.documents = init.documents;
this.licenseProfiles = init.licenseProfiles;
this.poa = init.poa;
this.progress = init.progress;
this.isComplete = init.isComplete;
this.onboardingCompleted = init.onboardingCompleted;