refactor(companies): one verified identity per company, no general manager

Replaces the owner/general-manager/PoA trio with a single identity whose
subject is the PoA when the company declares one and the owner otherwise.

- Drop the general manager everywhere: entity columns, DTOs, required-field
  list, self-service attributes, gm* identity handling.
- New explicit poaDeclared answer ("yes"/"no") replaces poaSameAsOwner. A
  DARS delegation letter is required iff it is "yes"; a freight forwarder is
  forced to "yes" server-side and gets no waiver.
- Owner name/email/phone become typeable and required, prefilled from the
  eTrade lookup, never falling back to the authenticated account.
- Store eTrade's manager separately (etradeManagerName/Phone) and expose
  ownerMatchesEtrade so backoffice compares the asserted owner against the
  licence instead of against itself.
- Foreign companies satisfy the identity with Fayda or a passport number, on
  whichever subject is verifying (poaPassportNumber added).
- Write companies.email/phone from the owner unconditionally, so a company
  without a Fayda-verified owner still has a notification address.
This commit is contained in:
Nathnael
2026-08-11 11:53:26 +00:00
parent e59e0324e0
commit a1bcdfb692
15 changed files with 659 additions and 827 deletions

View File

@@ -39,6 +39,7 @@ import {
CompleteIdentityVerificationDto,
} from "./dto/complete-identity-verification.dto";
import { SetOnboardingStepDto } from "./dto/set-onboarding-step.dto";
import { SetPoaDeclaredDto } from "./dto/set-poa-declared.dto";
import { StartOnboardingDto } from "./dto/start-onboarding.dto";
import { DashboardQueryDto } from "./dto/dashboard-query.dto";
import {
@@ -415,90 +416,31 @@ export class CompaniesController {
@PortalCustomer()
@ApiOperation({
summary:
"Bind a completed Fayda verification to the company's owner or Power of Attorney. " +
"Bind a completed Fayda verification to the company's single identity. " +
"Start the flow with POST /fayda/verification/start (platform=PORTAL), then post the returned code+state here. " +
"`subject` must match the company's PoA declaration — the representative when one is named, otherwise the owner. " +
"The verified name, phone, email and address are written from the Fayda payload; on an approved company the change is staged for backoffice review.",
})
async completeIdentityVerification(
@CurrentUser() user: CurrentIamUser,
@Body() dto: CompleteIdentityVerificationDto,
): Promise<CompanyIdentityStateDto> {
return this.companiesService.completeIdentityVerification(user.id, dto, {
email: user.email,
phoneNumber: user.phoneNumber,
});
return this.companiesService.completeIdentityVerification(user.id, dto);
}
@Post("identity/gm/same-as-owner")
@Patch("identity/poa-declared")
@PortalCustomer()
@ApiOperation({
summary:
"Declare the General Manager is the company's owner, copying the owner's verified identity across. " +
"Refused until the owner is Fayda-verified — there would be nothing proven to copy.",
"Answer whether anyone holds power of attorney for this company — the question that decides whose identity is verified. " +
'Answering "no" removes the representative entirely: their details, their verification, their passport number and the DARS delegation paper. ' +
'Refused for a freight forwarder, which cannot operate without a representative (its answer is always "yes").',
})
async setGmSameAsOwner(
async setPoaDeclared(
@CurrentUser() user: CurrentIamUser,
@Body() dto: SetPoaDeclaredDto,
): Promise<CompanyIdentityStateDto> {
return this.companiesService.setGmSameAsOwner(user.id, {
email: user.email,
phoneNumber: user.phoneNumber,
});
}
@Delete("identity/gm")
@PortalCustomer()
@ApiOperation({
summary:
"Clear the General Manager's identity — the \"same as owner\" declaration or a verification, and the details either wrote. " +
"Leaves the GM open to be verified in their own right, or typed where Fayda is optional.",
})
async clearGmIdentity(
@CurrentUser() user: CurrentIamUser,
): Promise<CompanyIdentityStateDto> {
return this.companiesService.clearGmIdentity(user.id);
}
@Post("identity/poa/same-as-owner")
@PortalCustomer()
@ApiOperation({
summary:
"Declare the Power of Attorney is the company's owner, copying the owner's identity across. " +
"Waives the DARS delegation paper — nobody delegates to themselves. " +
"Refused for an Ethiopian company whose owner is not Fayda-verified yet: its representative must be verified, and there would be nothing proven to copy.",
})
async setPoaSameAsOwner(
@CurrentUser() user: CurrentIamUser,
): Promise<CompanyIdentityStateDto> {
return this.companiesService.setPoaSameAsOwner(user.id, {
email: user.email,
phoneNumber: user.phoneNumber,
});
}
@Delete("identity/poa/same-as-owner")
@PortalCustomer()
@ApiOperation({
summary:
"Undo the Power of Attorney \"same as owner\" declaration and the identity it copied, leaving the representative open to be verified in their own right. " +
"Unlike DELETE identity/fayda/poa this is allowed for a freight forwarder — it is how they change who represents them — and leaves the delegation paper on file.",
})
async clearPoaSameAsOwner(
@CurrentUser() user: CurrentIamUser,
): Promise<CompanyIdentityStateDto> {
return this.companiesService.clearPoaSameAsOwner(user.id);
}
@Delete("identity/fayda/poa")
@PortalCustomer()
@ApiOperation({
summary:
"Remove the company's Power of Attorney — the verified identity, its details and the delegation paper together. " +
"Refused while the company holds a freight forwarder role, which cannot operate without a representative.",
})
async removePoaIdentity(
@CurrentUser() user: CurrentIamUser,
): Promise<CompanyIdentityStateDto> {
return this.companiesService.removePoaIdentity(user.id);
return this.companiesService.setPoaDeclared(user.id, dto.declared);
}
@Patch("onboarding-step")