fix(companies): honest status errors and per-role suspension visibility

A suspended customer trying to create a contract was told their account
was "awaiting approval" - the guards collapsed every non-active status
into the pending message. Contract and booking creation now go through a
shared assertCompanyActiveFor that names the real company status, and
the per-role guard names the role's actual state (suspended,
blacklisted, rejected - with the reviewer note) so a customer suspended
for one operation knows the others still work.

Portal: the operation dropdown gets a Suspended badge, the block modal a
suspended branch quoting the staff message, and the wizard a
suspended/blacklisted gate instead of falling through to a submit error.

EDRFREIGHT-234
This commit is contained in:
Nathnael
2026-07-21 09:09:34 +00:00
parent c8f932f5d5
commit 4afffca3d2
5 changed files with 131 additions and 25 deletions

View File

@@ -635,12 +635,9 @@ export class BookingsService {
);
}
const { company } = await this.companiesService.getCompanyInfoByUserId(userId);
// A customer can only book once their company has been approved.
if (company.status !== CompanyStatus.Active) {
throw new ForbiddenException(
"Your company is awaiting approval — you can't create bookings yet.",
);
}
// A customer can only book once their company has been approved; the
// helper names the real status (suspended/blacklisted) when it isn't.
this.companiesService.assertCompanyActiveFor(company, 'bookings');
companyId = company.id;
}

View File

@@ -1649,21 +1649,68 @@ export class CompaniesService {
}
/**
* Block a customer from booking under a profile that isn't approved yet.
* Called from the booking-create path for self-service bookings; staff- and
* government-initiated bookings bypass this. No-op when the profile can't be
* found (defensive — resolution is best-effort upstream).
* Block a self-service action when the company account isn't active, naming
* the actual status — a suspended customer told "awaiting approval" has no
* idea what happened or who to call.
*/
assertCompanyActiveFor(company: Company, action: string): void {
if (company.status === CompanyStatus.Active) return;
switch (company.status) {
case CompanyStatus.Suspended:
throw new ForbiddenException(
`Your company account is suspended — you can't create ${action} right now. ` +
`Please contact EDR support for details.`,
);
case CompanyStatus.Blacklisted:
throw new ForbiddenException(
`Your company account is blacklisted — you can't create ${action}. ` +
`Please contact EDR support.`,
);
default:
throw new ForbiddenException(
`Your company is awaiting approval — you can't create ${action} yet.`,
);
}
}
/**
* Block a customer from booking under a profile that isn't approved yet — or
* that a reviewer has since suspended. Called from the booking/contract
* create path for self-service actions; staff- and government-initiated ones
* bypass this. No-op when the profile can't be found (defensive — resolution
* is best-effort upstream). The message names the profile's real status:
* suspension in particular is per-role, so the customer must learn which
* operation is blocked (their other roles still work).
*/
async assertCompanyProfileApprovedForBooking(
companyProfileId: string,
): Promise<void> {
const profile = await this.companyProfilesRepo.findById(companyProfileId);
if (!profile) return;
if (profile.status !== ProfileStatus.Active) {
const role = profile.type.replace(/_/g, " ");
throw new ForbiddenException(
`Your ${role} profile is awaiting approval. You'll be able to create bookings once it has been approved.`,
);
if (profile.status === ProfileStatus.Active) return;
const role = profile.type.replace(/_/g, " ");
switch (profile.status) {
case ProfileStatus.Suspended:
throw new ForbiddenException(
`Your ${role} role is suspended${
profile.reviewNote ? `${profile.reviewNote}` : ""
}. Your other roles are unaffected. Please contact EDR support to resolve this.`,
);
case ProfileStatus.Blacklisted:
throw new ForbiddenException(
`Your ${role} role is blacklisted. Please contact EDR support.`,
);
case ProfileStatus.Rejected:
throw new ForbiddenException(
`Your ${role} role was rejected${
profile.reviewNote ? `${profile.reviewNote}` : ""
}. Amend and resubmit it from your settings page.`,
);
default:
throw new ForbiddenException(
`Your ${role} profile is awaiting approval. You'll be able to proceed once it has been approved.`,
);
}
}

View File

@@ -13,7 +13,6 @@ import { YardCountry } from '@edr/types';
import { deriveTradeDirection } from '../../common/derive-trade-direction.util';
import { CompaniesService } from '../companies/companies.service';
import { ProfileType } from '../companies/entities/company-profile.entity';
import { CompanyStatus } from '../companies/entities/company.entity';
import { ServiceType } from '../rule-engine/entities/service-type.entity';
import { Yard } from '../rule-engine/entities/yard.entity';
import { FilesService } from '../files/files.service';
@@ -181,11 +180,7 @@ export class ContractsService {
);
}
const { company } = await this.companiesService.getCompanyInfoByUserId(userId);
if (company.status !== CompanyStatus.Active) {
throw new ForbiddenException(
"Your company is awaiting approval — you can't create contracts yet.",
);
}
this.companiesService.assertCompanyActiveFor(company, 'contracts');
companyId = company.id;
}