feat(freight-api): gate poa paper, fayda identity, foreign passport

- dars delegation paper mandatory wherever poa state changes (named,
  removed, forwarder role applied for/approved), not just onboarding
- ethiopian companies verify owner (and poa, once named) via fayda;
  identity, not general manager, is the verified subject
- foreign companies require a typed owner passport number instead,
  independent of an optional fayda verification
- fanNumber removed from client-writable dtos; server-derived only

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Nathnael
2026-07-28 13:44:23 +00:00
parent dcda8d7d37
commit 68f6ec8c5f
10 changed files with 871 additions and 64 deletions

View File

@@ -15,6 +15,11 @@ import {
FILE_UPLOAD_SETTINGS_REPOSITORY,
IFileUploadSettingsRepository,
} from "./interfaces/file-upload-settings.repository.interface";
import {
COMPANY_ONBOARDING_CODE_PREFIX,
POA_DELEGATION_FILE_KEY,
poaDelegationField,
} from "./poa-delegation.constants";
@Injectable()
export class FileUploadSettingsService {
@@ -40,6 +45,22 @@ export class FileUploadSettingsService {
async getByCode(code: string): Promise<FileUploadSetting> {
const setting = await this.repository.findByCode(code);
if (!setting) throw new NotFoundException(`Setting "${code}" not found`);
return this.withPoaDelegationField(setting);
}
/**
* Company onboarding sets always carry the DARS delegation paper, whether or
* not anyone configured a row for it — see poa-delegation.constants.ts. Every
* consumer (the portal's PoA step, the onboarding gate) reads the set through
* here, so this is the single place the field can be guaranteed.
*/
private withPoaDelegationField(setting: FileUploadSetting): FileUploadSetting {
if (!setting.code.startsWith(COMPANY_ONBOARDING_CODE_PREFIX)) return setting;
const fields = setting.fields ?? [];
if (fields.some((f) => f.fileKey === POA_DELEGATION_FILE_KEY)) return setting;
const lastOrder = fields.reduce((max, f) => Math.max(max, f.displayOrder), 0);
setting.fields = [...fields, poaDelegationField(lastOrder + 1)];
return setting;
}

View File

@@ -0,0 +1,52 @@
import { FileUploadField } from "./entities/file-upload-field.entity";
/**
* The DARS delegation paper — the document that evidences a company's Power of
* Attorney (EDRFREIGHT-358).
*
* Every other onboarding document is admin-managed: the rows in
* `file_upload_fields` are edited from the backoffice file-settings editor and
* the seeder deliberately inserts none. This one is different — a company that
* names a PoA must produce a delegation paper authenticated by the Documents
* Authentication and Registration Service, and that is a legal requirement
* rather than a configuration choice. So the field is defined here in code and
* injected into the company onboarding sets on read: no row to forget to seed,
* and deleting one in the editor cannot silently switch the requirement off.
*/
/** FileRecord `code` (and upload field key) of the live delegation paper. */
export const POA_DELEGATION_FILE_KEY = "poa_delegation_letter";
/** Code for a delegation paper staged in an open change request (not yet live). */
export const POA_DELEGATION_PENDING_CODE = "poa_delegation_letter_pending";
/** Customer-facing name of the document, used by the API and both web apps. */
export const POA_DELEGATION_LABEL = "DARS Delegation Paper";
/** Prefix of the setting codes the field is injected into. */
export const COMPANY_ONBOARDING_CODE_PREFIX = "company_onboarding_documents_";
const POA_DELEGATION_HELP =
"Delegation paper issued by the Documents Authentication and Registration " +
"Service (DARS) delegating the representative named above. Upload the " +
"authenticated copy — a plain letter is not accepted.";
/**
* The field descriptor. `isRequired` stays false because the paper is only due
* once a PoA has actually been named (or the company operates as a freight
* forwarder) — a rule that spans form fields as well as files, so it is
* enforced in CompaniesService rather than by this flag.
*/
export function poaDelegationField(displayOrder: number): FileUploadField {
return {
fileKey: POA_DELEGATION_FILE_KEY,
fileLabel: POA_DELEGATION_LABEL,
helpText: POA_DELEGATION_HELP,
isRequired: false,
isMultiple: false,
maxFiles: 1,
allowedExtensions: ["pdf", "jpg", "jpeg", "png"],
maxSizeMb: 10,
displayOrder,
} as FileUploadField;
}