diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts index d9dd53f94..25c5a461a 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -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; } 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 73e2fdc00..fd352c959 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -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 { 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.`, + ); } } diff --git a/apps/edr-freight-api/src/modules/contracts/contracts.service.ts b/apps/edr-freight-api/src/modules/contracts/contracts.service.ts index c98e24b19..d5fcf5395 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.service.ts @@ -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; } diff --git a/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx b/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx index d060b7651..da03064be 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx @@ -124,6 +124,30 @@ export default function NewContractPage({ ); } + // A suspended/blacklisted account must be told its real status — falling + // through to the wizard here only to fail at submit read as "pending" before. + if ( + !auth.isPending && + (auth.companyStatus === "suspended" || auth.companyStatus === "blacklisted") + ) { + return ( + navigate("/contracts")} + /> + ); + } + // Profile changes pending review lock out new contract creation too. if (!auth.isPending && auth.isUnderReview) { return ( @@ -349,13 +373,19 @@ export default function NewContractPage({ // badges. Intercity rides any customer profile, so always "approved". const operationStatus = useMemo( () => - (op: OperationType): "approved" | "pending" | "rejected" | "missing" => { + ( + op: OperationType, + ): "approved" | "pending" | "rejected" | "suspended" | "missing" => { if (op === "intercity") return "approved"; const target = operationToProfileType(op, profileTypes); const status = profileStatusByType.get(target); if (!status) return "missing"; if (status === "active") return "approved"; if (status === "rejected") return "rejected"; + // Suspension is per-role: the customer keeps working under their other + // roles, so this operation must say "suspended", not "pending". + if (status === "suspended" || status === "blacklisted") + return "suspended"; return "pending"; }, [profileStatusByType, profileTypes], @@ -1171,6 +1201,8 @@ export default function NewContractPage({ ? profileByType.get(pendingApprovalProfile) : undefined; const isRejected = target?.status === "rejected"; + const isSuspended = + target?.status === "suspended" || target?.status === "blacklisted"; const label = pendingApprovalProfile ? (PROFILE_TYPE_LABELS[pendingApprovalProfile] ?? pendingApprovalProfile) @@ -1179,12 +1211,42 @@ export default function NewContractPage({ setPendingApprovalProfile(null)} - title={isRejected ? "Profile not approved" : "Awaiting approval"} + title={ + isRejected + ? "Profile not approved" + : isSuspended + ? "Role suspended" + : "Awaiting approval" + } centered radius="lg" > - {isRejected ? ( + {isSuspended ? ( + <> + + Your {label} role has been suspended by EDR staff, so you + can't start a contract under it. Your other roles are + unaffected. Contact EDR support to resolve this. + + {target?.reviewNote && ( + + + Message from EDR staff:{" "} + {target.reviewNote} + + + )} + + + + + ) : isRejected ? ( <> Your {label} profile was not approved. Fix the issue below diff --git a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step1-contract-type.tsx b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step1-contract-type.tsx index 2bacf8074..f66ee3a15 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step1-contract-type.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step1-contract-type.tsx @@ -45,7 +45,7 @@ export function Step1ContractType({ /** Approval state of the profile each operation maps to (for the badges). */ operationStatus?: ( op: OperationType, - ) => "approved" | "pending" | "rejected" | "missing"; + ) => "approved" | "pending" | "rejected" | "suspended" | "missing"; }) { const contractType = form.watch("contractType"); @@ -229,6 +229,11 @@ export function Step1ContractType({ Rejected )} + {status === "suspended" && ( + + Suspended + + )} {status === "missing" && ( Add license