diff --git a/apps/edr-freight-api/src/modules/companies/companies.controller.ts b/apps/edr-freight-api/src/modules/companies/companies.controller.ts index 810d4d46a..1dd28ca9c 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -421,7 +421,10 @@ export class CompaniesController { @CurrentUser() user: CurrentIamUser, @Body() dto: CompleteIdentityVerificationDto, ): Promise { - return this.companiesService.completeIdentityVerification(user.id, dto); + return this.companiesService.completeIdentityVerification(user.id, dto, { + email: user.email, + phoneNumber: user.phoneNumber, + }); } @Post("identity/gm/same-as-owner") @@ -434,7 +437,10 @@ export class CompaniesController { async setGmSameAsOwner( @CurrentUser() user: CurrentIamUser, ): Promise { - return this.companiesService.setGmSameAsOwner(user.id); + return this.companiesService.setGmSameAsOwner(user.id, { + email: user.email, + phoneNumber: user.phoneNumber, + }); } @Delete("identity/gm") diff --git a/apps/edr-freight-api/src/modules/companies/companies.service.ts b/apps/edr-freight-api/src/modules/companies/companies.service.ts index 9f0a03bdd..2cddb2974 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -2749,6 +2749,12 @@ export class CompaniesService { async completeIdentityVerification( userId: string, dto: CompleteIdentityVerificationDto, + /** + * The signed-in account, used as the owner's fallback contact details. + * Optional so the callers that only have a user id keep compiling — they + * simply get no fallback. + */ + account?: { email?: string; phoneNumber?: string }, ): Promise { const { company } = await this.getCompanyInfoByUserId(userId); const prefix = IDENTITY_PREFIX[dto.subject]; @@ -2780,6 +2786,22 @@ export class CompaniesService { } const now = new Date().toISOString(); + + // Fayda's email and phone claims are optional and routinely come back empty. + // For the owner that leaves the company with no contact details at all: the + // step renders no input for them (they are the verification's output), and + // "same as owner" then copies those blanks onto `generalManagerEmail` / + // `generalManagerPhone`, which `REQUIRED_COMPANY_INFO` demands at submit — + // an unfixable dead end. The account doing the onboarding is the one contact + // we always have, and it is already OTP-proven, so it stands in. + // + // Owner only: the PoA and the GM are other people, and the registering + // account's address is not theirs to wear. + const isOwner = dto.subject === "owner"; + const email = result.email || (isOwner ? account?.email : undefined); + const phone = + result.phoneNumber || (isOwner ? account?.phoneNumber : undefined); + const identity: VerifiedIdentityAttributes = { [`${prefix}FaydaSub`]: result.sub, [`${prefix}FaydaVerifiedAt`]: now, @@ -2787,14 +2809,12 @@ export class CompaniesService { [`${prefix}Gender`]: result.gender ?? null, // The verified payload owns the person's details from here on. ...(result.fullName ? { [`${prefix}Name`]: result.fullName } : {}), - ...(result.email ? { [`${prefix}Email`]: result.email } : {}), + ...(email ? { [`${prefix}Email`]: email } : {}), // Fayda returns whatever the national registry holds, which is routinely a // local number ("0911223344"). Every typed phone in this service is stored // E.164, and `@IsValidPhone()` rejects anything else — so a raw claim here // becomes a value the portal reads back and cannot resubmit. - ...(result.phoneNumber - ? { [`${prefix}Phone`]: normalizeE164(result.phoneNumber) } - : {}), + ...(phone ? { [`${prefix}Phone`]: normalizeE164(phone) } : {}), ...(result.address ? { [`${prefix}Address`]: result.address } : {}), }; @@ -2844,7 +2864,13 @@ export class CompaniesService { * proven identity to copy, only typed text that would arrive wearing a * verified badge. */ - async setGmSameAsOwner(userId: string): Promise { + async setGmSameAsOwner( + userId: string, + /** Same fallback as {@link completeIdentityVerification}, for owners + * verified before that fallback existed — their stored contact details are + * blank, and copying blanks here would block the submit. */ + account?: { email?: string; phoneNumber?: string }, + ): Promise { const { company } = await this.getCompanyInfoByUserId(userId); const attrs = company.attributes ?? {}; const ownerSub = attrs.ownerFaydaSub as string | undefined; @@ -2854,20 +2880,23 @@ export class CompaniesService { ); } + const ownerEmail = (attrs.ownerEmail as string | undefined) || account?.email || null; + const ownerPhone = (attrs.ownerPhone as string | undefined) || account?.phoneNumber || null; + const copied: Record = { gmSameAsOwner: true, gmFaydaSub: ownerSub, gmFaydaVerifiedAt: attrs.ownerFaydaVerifiedAt ?? new Date().toISOString(), gmName: attrs.ownerName ?? null, - gmEmail: attrs.ownerEmail ?? null, - gmPhone: attrs.ownerPhone ?? null, + gmEmail: ownerEmail, + gmPhone: ownerPhone ? normalizeE164(ownerPhone) : null, gmAddress: attrs.ownerAddress ?? null, gmBirthdate: attrs.ownerBirthdate ?? null, gmGender: attrs.ownerGender ?? null, // Kept in step for the notifiers, same as a GM verification does. generalManagerName: attrs.ownerName ?? null, - generalManagerEmail: attrs.ownerEmail ?? null, - generalManagerPhone: attrs.ownerPhone ?? null, + generalManagerEmail: ownerEmail, + generalManagerPhone: ownerPhone ? normalizeE164(ownerPhone) : null, }; const updated = await this.companiesRepo.update(company.id, {