diff --git a/apps/edr-freight-api/src/migrations/2450000000000-DropActiveProfileTypeFromExternalProfiles.ts b/apps/edr-freight-api/src/migrations/2450000000000-DropActiveProfileTypeFromExternalProfiles.ts new file mode 100644 index 000000000..d8119930f --- /dev/null +++ b/apps/edr-freight-api/src/migrations/2450000000000-DropActiveProfileTypeFromExternalProfiles.ts @@ -0,0 +1,45 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +/** + * Drop the `active_profile_type` "active mode" column. A booking/contract now + * resolves its company_profile from the trade direction at creation time (with + * a forwarder passing an explicit companyProfileId), so no per-user active mode + * is stored. `onboarding_step` / `onboarding_completed` are unaffected. + */ +export class DropActiveProfileTypeFromExternalProfiles2450000000000 + implements MigrationInterface +{ + name = 'DropActiveProfileTypeFromExternalProfiles2450000000000'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.external_profiles + DROP COLUMN IF EXISTS active_profile_type; + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE freight.external_profiles + ADD COLUMN IF NOT EXISTS active_profile_type varchar(32); + `); + // Rebuild the mode the same way the original column was backfilled: + // importer first, then exporter, then whichever profile the company has. + await queryRunner.query(` + UPDATE freight.external_profiles ep + SET active_profile_type = cp.type + FROM ( + SELECT DISTINCT ON (company_id) company_id, type + FROM freight.company_profiles + ORDER BY company_id, + CASE type + WHEN 'importer' THEN 0 + WHEN 'exporter' THEN 1 + ELSE 2 + END + ) cp + WHERE ep.company_id = cp.company_id + AND ep.active_profile_type IS NULL; + `); + } +} 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 25c5a461a..7f8718480 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.service.ts @@ -12,7 +12,6 @@ import { Freight, SchedulingStatus } from '@edr/types'; import { insertWithGeneratedReference } from '@edr/api-common'; // import { CustomersService } from '../customers/customers.service'; import { CompaniesService } from '../companies/companies.service'; -import { ProfileType } from '../companies/entities/company-profile.entity'; import { CompanyKind, CompanyStatus } from '../companies/entities/company.entity'; import { TrainSchedulingService } from '../train-scheduling/train-scheduling.service'; import { eatDay } from '../train-scheduling/batch-window.util'; @@ -743,21 +742,13 @@ export class BookingsService { ); companyProfileId = profile.id; } else if (companyId) { - let fallbackType: ProfileType | null = null; - if (userId) { - try { - const { profile } = - await this.companiesService.getCompanyInfoByUserId(userId); - fallbackType = profile.activeProfileType ?? null; - } catch { - // No profile (e.g. staff creating on behalf) — fall back to mapping. - } - } + // No explicit profile pin: resolve from the booking's trade direction + // (import→importer, export→exporter; otherwise the first profile). A + // forwarder booking sends dto.companyProfileId and takes the branch above. companyProfileId = await this.companiesService.resolveCompanyProfileIdForBooking( companyId, tradeDirection, - fallbackType, ); // A customer booking under their own account may only do so once the @@ -1065,9 +1056,6 @@ export class BookingsService { await this.companiesService.resolveCompanyProfileIdForBooking( existing.companyId, tradeDirection, - existing.companyProfileId - ? undefined - : (existing.companyProfile?.type as ProfileType | undefined), ); } if (dto.scheduledDate) updates.scheduledDate = new Date(dto.scheduledDate); @@ -1389,15 +1377,6 @@ export class BookingsService { } } - /** - * Resolve the active company_profile id a customer's bookings should be - * scoped to (importer/exporter mode). Null when not onboarded — callers fall - * back to company-level scoping. - */ - async resolveActiveCompanyProfileId(userId: string): Promise { - return this.companiesService.resolveActiveCompanyProfileId(userId); - } - /** * Authorize a customer's access to a single booking. Staff are scoped at the * controller (they pass `isStaff`); for a customer, the booking must belong 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 6111bd32a..43d70ce19 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -26,7 +26,6 @@ import { CreateExternalProfileDto } from "./dto/create-external-profile.dto"; import { CreateCompanyWithProfileDto } from "./dto/create-company-with-profile.dto"; import { AddCompanyProfilesDto } from "./dto/add-company-profiles.dto"; import { CreateCompanyProfileDto } from "./dto/create-company-profile.dto"; -import { SetActiveModeDto } from "./dto/set-active-mode.dto"; import { SetOnboardingStepDto } from "./dto/set-onboarding-step.dto"; import { StartOnboardingDto } from "./dto/start-onboarding.dto"; import { DashboardQueryDto } from "./dto/dashboard-query.dto"; @@ -353,21 +352,6 @@ export class CompaniesController { return this.companiesService.removePoaDelegationLetter(user.id, fileId); } - @Patch("active-mode") - @ApiOperation({ - summary: "Switch the current user's active operational mode (importer/exporter)", - }) - async setActiveMode( - @CurrentUser() user: CurrentIamUser, - @Body() dto: SetActiveModeDto, - ): Promise { - const { profile, company } = await this.companiesService.setActiveMode( - user.id, - dto.type, - ); - return new CompanyInfoResponseDto(profile, company); - } - @Patch("onboarding-step") @ApiOperation({ summary: "Persist the user's current onboarding wizard step" }) @HttpCode(HttpStatus.NO_CONTENT) 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 fd352c959..3867bef3a 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -201,18 +201,6 @@ export class CompaniesService { attributes: dto.attributes ?? null, }); - // Default active mode from the chosen role(s): importer wins when both are - // picked, otherwise the first allowed type chosen. - const allowedTypes = this.getProfileTypeForCompanyType(company.type); - const chosenTypes = (dto.companyProfiles ?? []) - .map((p) => p.type) - .filter((t) => allowedTypes.includes(t)); - const activeProfileType = - chosenTypes.find((t) => t === ProfileType.importer) ?? - chosenTypes[0] ?? - allowedTypes[0] ?? - null; - const profile = await this.profilesRepo.create({ userId: identity.userId, companyId: company.id, @@ -220,7 +208,6 @@ export class CompaniesService { lastName: identity.lastName, jobTitle: dto.jobTitle ?? null, isPrimaryContact: dto.isPrimaryContact ?? true, - activeProfileType, onboardingStep: "company", }); @@ -293,11 +280,6 @@ export class CompaniesService { const allowedTypes = this.getProfileTypeForCompanyType(companyType); const chosenTypes = roles.filter((t) => allowedTypes.includes(t)); - const activeProfileType = - chosenTypes.find((t) => t === ProfileType.importer) ?? - chosenTypes[0] ?? - allowedTypes[0] ?? - null; const company = await this.companiesRepo.create({ name: identity.firstName @@ -316,7 +298,6 @@ export class CompaniesService { firstName: identity.firstName, lastName: identity.lastName, isPrimaryContact: true, - activeProfileType, onboardingStep: "company", onboardingCompleted: false, }); @@ -1213,40 +1194,50 @@ export class CompaniesService { if (!updated) throw new NotFoundException(`Company profile ${existing.id} not found`); - // Suspension and reactivation lock/unlock a role the customer relies on — - // tell them, and carry the staff message so they know why. - const reactivated = - status === ProfileStatus.Active && - existing.status === ProfileStatus.Suspended; - if (status === ProfileStatus.Suspended || reactivated) { + // Every reviewed transition that changes what the customer can do is told + // to them, carrying the staff message so they know why. Approval has no + // message (the note is cleared); the others require one. + const change = + status === ProfileStatus.Suspended + ? "suspended" + : status === ProfileStatus.Rejected + ? "rejected" + : status === ProfileStatus.Active + ? existing.status === ProfileStatus.Suspended + ? "reactivated" + : "approved" + : null; + if (change) { const company = await this.companiesRepo.findById(updated.companyId); if (company) { this.companyNotifier.profileStatusChanged( company, updated.type, - status === ProfileStatus.Suspended ? "suspended" : "reactivated", + change, note ?? "", ); - } - } - - // Approving any profile promotes a pending company to active, so the - // customer can start working as soon as their first profile is cleared. - if (status === ProfileStatus.Active) { - const company = await this.companiesRepo.findById(updated.companyId); - if (company && company.status === CompanyStatus.Pending) { - await this.companiesRepo.update(updated.companyId, { - status: CompanyStatus.Active, - }); + // The first approved role promotes a pending company to active — a + // bigger event (the account itself goes live), so tell them that too. + if ( + status === ProfileStatus.Active && + company.status === CompanyStatus.Pending + ) { + await this.companiesRepo.update(updated.companyId, { + status: CompanyStatus.Active, + }); + this.companyNotifier.companyApproved(company); + } } } return updated; } /** - * Customer reapplies for a rejected operational role (after fixing whatever the - * reviewer flagged, e.g. re-uploading a license): flip it back to Pending and - * clear the rejection note so it re-enters the approval queue. + * Customer reapplies for a rejected or suspended operational role (after + * fixing whatever the reviewer flagged, e.g. re-uploading a license): flip it + * back to Pending and clear the review note so it re-enters the approval + * queue. Suspension is a staff lockout, so resubmitting is an appeal — the + * backoffice still has to approve before the role goes live again. */ async reapplyCompanyProfile( userId: string, @@ -1261,9 +1252,12 @@ export class CompaniesService { if (!target || target.companyId !== companyId) { throw new NotFoundException(`Company profile ${profileId} not found`); } - if (target.status !== ProfileStatus.Rejected) { + if ( + target.status !== ProfileStatus.Rejected && + target.status !== ProfileStatus.Suspended + ) { throw new BadRequestException( - "Only a rejected role can be resubmitted for approval", + "Only a rejected or suspended role can be resubmitted for approval", ); } @@ -1387,10 +1381,9 @@ export class CompaniesService { /** * Create a single operational profile for the current user's company. The new - * role starts Pending, so it deliberately does NOT become the active mode: - * switching onto an unapproved profile would strip the user of `canBook` and - * block them from creating contracts under the role they already had approved. - * Callers switch explicitly via {@link setActiveMode} once the role is Active. + * role starts Pending and carries no reference until a backoffice reviewer + * approves it; a booking/contract resolves its profile from the trade + * direction at creation time, so no "active mode" is stored. */ async createCompanyProfileForUser( userId: string, @@ -1425,40 +1418,6 @@ export class CompaniesService { return created; } - /** - * Switch the user's active operational mode. The target profile must already - * exist — clients create it first via createCompanyProfileForUser. - */ - async setActiveMode( - userId: string, - type: ProfileType, - ): Promise<{ profile: ExternalProfile; company: Company }> { - const profile = await this.profilesRepo.findByUserId(userId); - if (!profile) - throw new NotFoundException(`Profile for user ${userId} not found`); - - const companyId = profile.company?.id ?? profile.companyId; - const company = await this.findCompanyById(companyId); - - const allowedTypes = this.getProfileTypeForCompanyType(company.type); - if (!allowedTypes.includes(type)) { - throw new BadRequestException( - `Profile type "${type}" is not allowed for company type "${company.type}"`, - ); - } - - const existing = await this.companyProfilesRepo.findByType(companyId, type); - if (!existing) { - throw new ConflictException( - `No ${type} profile exists yet — create it before switching`, - ); - } - - await this.profilesRepo.update(profile.id, { activeProfileType: type }); - - return this.getCompanyInfoByUserId(userId); - } - async setOnboardingStep(userId: string, step: string): Promise { const profile = await this.profilesRepo.findByUserId(userId); if (!profile) @@ -2329,15 +2288,14 @@ export class CompaniesService { /** * Resolve which company_profile a new booking belongs to, from the company * and the booking's trade direction. IMPORT → importer profile, EXPORT → - * exporter profile; for DOMESTIC or a forwarder/single-profile company (or - * when the natural profile doesn't exist) it falls back to the user's active - * profile, then the company's first profile. Returns null when the company - * has no profiles at all. + * exporter profile; for DOMESTIC (or when the natural profile doesn't exist, + * e.g. a freight forwarder) it falls back to the company's first profile. + * Callers that need a specific role (a forwarder) pass an explicit + * companyProfileId instead. Returns null when the company has no profiles. */ async resolveCompanyProfileIdForBooking( companyId: string, tradeDirection: string, - fallbackType?: ProfileType | null, ): Promise { const profiles = await this.companyProfilesRepo.findByCompanyId(companyId); if (profiles.length === 0) return null; @@ -2349,30 +2307,12 @@ export class CompaniesService { ? ProfileType.exporter : null; - const byType = (type?: ProfileType | null) => - type ? profiles.find((p) => p.type === type) : undefined; - - const match = byType(naturalType) ?? byType(fallbackType) ?? profiles[0]; + const match = + (naturalType && profiles.find((p) => p.type === naturalType)) ?? + profiles[0]; return match?.id ?? null; } - /** - * Resolve the company_profile a customer's data should be scoped to, from - * their persisted active mode. Returns null when nothing can be resolved - * (not onboarded yet) so callers can fall back to company-level scoping. - */ - async resolveActiveCompanyProfileId(userId: string): Promise { - try { - const { profile, company } = await this.getCompanyInfoByUserId(userId); - const type = profile.activeProfileType; - if (!type) return null; - const match = company.companyProfiles?.find((p) => p.type === type); - return match?.id ?? null; - } catch { - return null; - } - } - async fetchETradeData(tin: string) { const { businessInfo, companyInfo } = await this.etradeService.resolveCompanyData(tin); diff --git a/apps/edr-freight-api/src/modules/companies/company-notifier.service.ts b/apps/edr-freight-api/src/modules/companies/company-notifier.service.ts index d9aac8e45..66f88e9f8 100644 --- a/apps/edr-freight-api/src/modules/companies/company-notifier.service.ts +++ b/apps/edr-freight-api/src/modules/companies/company-notifier.service.ts @@ -59,23 +59,13 @@ export class CompanyNotifierService { } } - /** - * Tell the customer their account was suspended or blacklisted. Called only on - * a real transition into one of those statuses; other status writes are silent. - */ - statusChanged(company: Company, previous: CompanyStatus): void { - const status = company.status; - if (status === previous) return; - if (!PUNITIVE_STATUSES.includes(status)) return; - - const label = status === CompanyStatus.Suspended ? "suspended" : "blacklisted"; - const title = `Account ${label}`; - const body = - `Your company account has been ${label}. ` + - `You will not be able to submit new contracts or bookings. ` + - `Please contact EDR support for assistance.`; - - this.logger.log(`ACCOUNT_${label.toUpperCase()} — ${company.id}`); + /** SMS + email + in-app account-status item to the company contact. */ + private notifyAccount( + company: Company, + title: string, + body: string, + link = "/settings", + ): void { void this.notifyContact(company, `${title}. ${body}`); void this.inbox.notify({ recipients: { companyId: company.id }, @@ -83,32 +73,88 @@ export class CompanyNotifierService { type: NotificationType.ACCOUNT_STATUS, title, body, - link: "/settings", - data: { companyId: company.id, status }, + link, + data: { companyId: company.id, status: company.status }, priority: NotificationPriority.HIGH, }); } /** - * Tell the customer one of their operational roles was suspended or - * reactivated, quoting the staff message — the service layer requires one for - * both transitions, so the customer always learns why, not just what. + * Tell the customer their account changed status. Fires on the transitions + * that change what they can do: suspended/blacklisted (locked out) and + * reactivated (back to Active from a lockout). Silent otherwise. + */ + statusChanged(company: Company, previous: CompanyStatus): void { + const status = company.status; + if (status === previous) return; + + if (status === CompanyStatus.Active && PUNITIVE_STATUSES.includes(previous)) { + this.logger.log(`ACCOUNT_REACTIVATED — ${company.id}`); + this.notifyAccount( + company, + "Account reactivated", + "Your company account has been reactivated. " + + "You can submit new contracts and bookings again.", + ); + return; + } + + if (!PUNITIVE_STATUSES.includes(status)) return; + + const label = status === CompanyStatus.Suspended ? "suspended" : "blacklisted"; + this.logger.log(`ACCOUNT_${label.toUpperCase()} — ${company.id}`); + this.notifyAccount( + company, + `Account ${label}`, + `Your company account has been ${label}. ` + + `You will not be able to submit new contracts or bookings. ` + + `Please contact EDR support for assistance.`, + ); + } + + /** + * Tell the customer their company account was approved and is now live — the + * first operational role clearing review promotes a pending company to Active. + */ + companyApproved(company: Company): void { + this.logger.log(`ACCOUNT_APPROVED — ${company.id}`); + this.notifyAccount( + company, + "Account approved", + "Your company account has been approved and is now active. " + + "You can start submitting bookings and contracts.", + "/dashboard", + ); + } + + /** + * Tell the customer one of their operational roles changed review status — + * approved, rejected, suspended, or reactivated — quoting the staff message + * when one was given (rejection/suspension/reactivation require one; approval + * carries none). */ profileStatusChanged( company: Company, profileType: string, - change: "suspended" | "reactivated", + change: "approved" | "rejected" | "suspended" | "reactivated", staffMessage: string, ): void { const title = `${profileType} role ${change}`; - const consequence = - change === "suspended" - ? `You will not be able to operate under this role until it is reactivated; ` + - `your other roles are unaffected.` - : `You can operate under this role again.`; + const consequence: Record = { + approved: "You can now operate under this role.", + rejected: + "You will not be able to operate under this role. Amend the required " + + "documents and resubmit it for approval from your settings page.", + suspended: + "You will not be able to operate under this role until it is " + + "reactivated; your other roles are unaffected.", + reactivated: "You can operate under this role again.", + }; + const message = staffMessage.trim(); const body = `Your company's ${profileType} role has been ${change}. ` + - `${consequence} Message from EDR staff: ${staffMessage}`; + `${consequence[change]}` + + (message ? ` Message from EDR staff: ${message}` : ""); this.logger.log( `PROFILE_${change.toUpperCase()} — ${company.id} / ${profileType}`, @@ -121,7 +167,7 @@ export class CompanyNotifierService { title, body, link: "/settings", - data: { companyId: company.id, profileType, change, staffMessage }, + data: { companyId: company.id, profileType, change, staffMessage: message }, priority: NotificationPriority.HIGH, }); } diff --git a/apps/edr-freight-api/src/modules/companies/dto/company-info-response.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/company-info-response.dto.ts index 9a4fb330a..2634e0943 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/company-info-response.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/company-info-response.dto.ts @@ -24,7 +24,7 @@ export class CompanyInfoResponseDto { company: Company, changeRequest?: CompanyChangeRequest | null, ) { - this.profile = new ResponseExternalProfileDto(profile, company); + this.profile = new ResponseExternalProfileDto(profile); this.company = new ResponseCompanyDto(company); const open = diff --git a/apps/edr-freight-api/src/modules/companies/dto/response-external-profile.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/response-external-profile.dto.ts index 256641074..916bb940d 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/response-external-profile.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/response-external-profile.dto.ts @@ -1,8 +1,6 @@ -import { Company } from '../entities/company.entity'; import { ExternalProfile, } from '../entities/external-profile.entity'; -import { ProfileType } from '../entities/company-profile.entity'; export class ResponseExternalProfileDto { id: string; @@ -13,20 +11,12 @@ export class ResponseExternalProfileDto { nationalId?: string | null; jobTitle?: string | null; isPrimaryContact: boolean; - /** The active operational mode (importer/exporter/forwarder). */ - activeProfileType?: ProfileType | null; - /** - * The id of the company_profile matching activeProfileType, resolved - * server-side so the client never re-derives it. Null until a company - * (with profiles) is loaded and a matching profile exists. - */ - activeCompanyProfileId?: string | null; onboardingStep?: string | null; onboardingCompleted: boolean; createdAt: Date; updatedAt: Date; - constructor(profile: ExternalProfile, company?: Company) { + constructor(profile: ExternalProfile) { this.id = profile.id; this.userId = profile.userId; this.companyId = profile.companyId; @@ -35,13 +25,8 @@ export class ResponseExternalProfileDto { this.nationalId = profile.nationalId; this.jobTitle = profile.jobTitle; this.isPrimaryContact = profile.isPrimaryContact; - this.activeProfileType = profile.activeProfileType ?? null; this.onboardingStep = profile.onboardingStep ?? null; this.onboardingCompleted = profile.onboardingCompleted ?? false; - this.activeCompanyProfileId = - company?.companyProfiles?.find( - (p) => p.type === profile.activeProfileType, - )?.id ?? null; this.createdAt = profile.createdAt; this.updatedAt = profile.updatedAt; } diff --git a/apps/edr-freight-api/src/modules/companies/dto/set-active-mode.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/set-active-mode.dto.ts deleted file mode 100644 index ac8f57a93..000000000 --- a/apps/edr-freight-api/src/modules/companies/dto/set-active-mode.dto.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IsEnum } from 'class-validator'; -import { ProfileType } from '../entities/company-profile.entity'; - -export class SetActiveModeDto { - @IsEnum(ProfileType) - type!: ProfileType; -} diff --git a/apps/edr-freight-api/src/modules/companies/entities/external-profile.entity.ts b/apps/edr-freight-api/src/modules/companies/entities/external-profile.entity.ts index 93e499b5e..84f644091 100644 --- a/apps/edr-freight-api/src/modules/companies/entities/external-profile.entity.ts +++ b/apps/edr-freight-api/src/modules/companies/entities/external-profile.entity.ts @@ -1,7 +1,6 @@ import { BaseEntity } from '@edr/api-common'; import { Column, Entity, Index, ManyToOne, JoinColumn } from 'typeorm'; import { Company } from './company.entity'; -import { ProfileType } from './company-profile.entity'; @Entity({ schema: 'freight', name: 'external_profiles' }) @Index(['userId']) @@ -32,21 +31,6 @@ export class ExternalProfile extends BaseEntity { @Column({ name: 'is_primary_contact', type: 'boolean', default: false }) isPrimaryContact!: boolean; - /** - * The operational profile the user is currently "in" (importer vs exporter, - * or the single forwarder profile). Drives header switching and scopes the - * customer's bookings / dashboard to that company_profile. Nullable for - * users who haven't picked a role yet. - */ - @Column({ - name: 'active_profile_type', - type: 'varchar', - length: 32, - nullable: true, - enum: ProfileType, - }) - activeProfileType?: ProfileType | null; - /** Coarse resume point for the onboarding wizard (e.g. 'role', 'company', 'documents', 'done'). */ @Column({ name: 'onboarding_step', 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 d5fcf5395..b51403991 100644 --- a/apps/edr-freight-api/src/modules/contracts/contracts.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/contracts.service.ts @@ -12,7 +12,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 { ServiceType } from '../rule-engine/entities/service-type.entity'; import { Yard } from '../rule-engine/entities/yard.entity'; import { FilesService } from '../files/files.service'; @@ -188,31 +187,31 @@ export class ContractsService { this.assertRouteShape(dto.contractKind, dto.routes); await this.assertRoutesMatchDirection(dto.tradeDirection, dto.routes); - // Stamp the operational profile (importer/exporter) for portal scoping. + // Stamp the operational profile for portal scoping. A forwarder contract + // pins its profile explicitly (trade direction can't tell it apart from a + // direct import/export); everything else resolves from the trade direction. let companyProfileId: string | null = null; if (!isGovernment && companyId) { - let fallbackType: ProfileType | null = null; - if (userId) { - try { - const { profile } = - await this.companiesService.getCompanyInfoByUserId(userId); - fallbackType = profile.activeProfileType ?? null; - } catch { - // No profile (e.g. staff creating on behalf) — fall back to mapping. - } - } - companyProfileId = - await this.companiesService.resolveCompanyProfileIdForBooking( - companyId, - dto.tradeDirection, - fallbackType, - ); + if (dto.companyProfileId) { + const profile = + await this.companiesService.getActiveCompanyProfileForBooking( + companyId, + dto.companyProfileId, + ); + companyProfileId = profile.id; + } else { + companyProfileId = + await this.companiesService.resolveCompanyProfileIdForBooking( + companyId, + dto.tradeDirection, + ); - const customerSelfBooking = !dto.companyId && !!userId; - if (customerSelfBooking && companyProfileId) { - await this.companiesService.assertCompanyProfileApprovedForBooking( - companyProfileId, - ); + const customerSelfBooking = !dto.companyId && !!userId; + if (customerSelfBooking && companyProfileId) { + await this.companiesService.assertCompanyProfileApprovedForBooking( + companyProfileId, + ); + } } } diff --git a/apps/edr-freight-api/src/modules/contracts/dto/create-contract.dto.ts b/apps/edr-freight-api/src/modules/contracts/dto/create-contract.dto.ts index 688e0b4c7..fb4f40654 100644 --- a/apps/edr-freight-api/src/modules/contracts/dto/create-contract.dto.ts +++ b/apps/edr-freight-api/src/modules/contracts/dto/create-contract.dto.ts @@ -124,6 +124,16 @@ export class CreateContractDto { @IsUUID() companyId?: string; + @ApiPropertyOptional({ + format: 'uuid', + description: + 'Explicit company profile to stamp the contract to (a forwarder contract); ' + + 'commercial contracts otherwise auto-resolve from trade direction.', + }) + @IsOptional() + @IsUUID() + companyProfileId?: string; + @ApiProperty({ enum: CONTRACT_KINDS, description: 'ONE_TIME | GENERAL' }) @IsIn([...CONTRACT_KINDS]) contractKind!: string; diff --git a/apps/edr-freight-web/portal/src/components/AppLayout.tsx b/apps/edr-freight-web/portal/src/components/AppLayout.tsx index 7870a56b6..411c00ce0 100644 --- a/apps/edr-freight-web/portal/src/components/AppLayout.tsx +++ b/apps/edr-freight-web/portal/src/components/AppLayout.tsx @@ -1,6 +1,8 @@ import { + Alert, AppShell, Avatar, + Badge, Box, Button, Divider, @@ -19,6 +21,7 @@ import { } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; import { + Ban, ChevronDown, FileSignature, LogOut, @@ -201,29 +204,49 @@ export function AppLayout({ CUSTOMER_SERVICES.includes(p.type as ServiceType), ) : []; + // Suspended services are also hidden by default (the profile exists) — surface + // them so the customer can appeal by resubmitting a fresh business license. + const suspendedServices = isCustomer + ? companyProfiles.filter( + (p) => + p.status === "suspended" && + p.id && + CUSTOMER_SERVICES.includes(p.type as ServiceType), + ) + : []; const canManageServices = - isCustomer && (addableServices.length > 0 || rejectedServices.length > 0); + isCustomer && + (addableServices.length > 0 || + rejectedServices.length > 0 || + suspendedServices.length > 0); const [switching, setSwitching] = useState(false); const [createOpen, setCreateOpen] = useState(false); const [createTarget, setCreateTarget] = useState("importer"); - // Non-null while resubmitting a rejected service; null while creating a new one. + // Non-null while resubmitting a rejected/suspended service; null while creating a new one. const [reapplyId, setReapplyId] = useState(null); + // Status of the profile being resubmitted ("rejected" | "suspended") — drives + // the modal copy; null for a brand-new profile. + const [reapplyStatus, setReapplyStatus] = useState(null); + // Reason the profile was suspended/rejected, surfaced in the modal. + const [reapplyNote, setReapplyNote] = useState(null); const [licenseFiles, setLicenseFiles] = useState([]); const [createError, setCreateError] = useState(null); const openServiceModal = ( type: ServiceType, - profileId: string | null, + profile?: { id?: string; status?: string; reviewNote?: string | null }, ) => { setCreateTarget(type); - setReapplyId(profileId); + setReapplyId(profile?.id ?? null); + setReapplyStatus(profile?.status ?? null); + setReapplyNote(profile?.reviewNote ?? null); setLicenseFiles([]); setCreateError(null); setCreateOpen(true); }; - const handleAddService = (type: ServiceType) => openServiceModal(type, null); + const handleAddService = (type: ServiceType) => openServiceModal(type); const handleCreateConfirm = async () => { const isReapply = reapplyId !== null; @@ -250,6 +273,7 @@ export function AppLayout({ }; const serviceLabel = (m: ServiceType) => PROFILE_TYPE_LABELS[m] ?? m; + const isSuspendedAppeal = reapplyStatus === "suspended"; const isItemActive = (item: SidebarItem) => activePath === item.href.toLowerCase() || @@ -372,7 +396,7 @@ export function AppLayout({ key={p.id} color="red" onClick={() => - openServiceModal(p.type as ServiceType, p.id!) + openServiceModal(p.type as ServiceType, p) } leftSection={} > @@ -381,6 +405,30 @@ export function AppLayout({ ))} )} + {suspendedServices.length > 0 && ( + <> + {(addableServices.length > 0 || + rejectedServices.length > 0) && } + Suspended — appeal + {suspendedServices.map((p) => ( + + openServiceModal(p.type as ServiceType, p) + } + leftSection={} + rightSection={ + + Suspended + + } + > + {serviceLabel(p.type as ServiceType)} + + ))} + + )} )} @@ -849,25 +897,39 @@ export function AppLayout({ opened={createOpen} onClose={() => (switching ? undefined : setCreateOpen(false))} title={ - reapplyId - ? `Resubmit your ${serviceLabel(createTarget)} service` - : `Set up your ${serviceLabel(createTarget)} profile` + isSuspendedAppeal + ? `Appeal suspension — ${serviceLabel(createTarget)}` + : reapplyId + ? `Resubmit your ${serviceLabel(createTarget)} service` + : `Set up your ${serviceLabel(createTarget)} profile` } centered radius="lg" > - {reapplyId + {isSuspendedAppeal ? `Your ${serviceLabel( createTarget, - ).toLowerCase()} service was rejected. Replace the business license if needed, then resubmit for approval.` - : `You don't have a ${serviceLabel( - createTarget, - ).toLowerCase()} profile yet. Add your business license to create one and switch to ${serviceLabel( - createTarget, - ).toLowerCase()}.`} + ).toLowerCase()} service is currently suspended. Replace the business license if needed and resubmit — this sends your appeal back to EDR for review.` + : reapplyId + ? `Your ${serviceLabel( + createTarget, + ).toLowerCase()} service was rejected. Replace the business license if needed, then resubmit for approval.` + : `You don't have a ${serviceLabel( + createTarget, + ).toLowerCase()} profile yet. Add your business license to create one — it goes to EDR for approval before you can operate under it.`} + {isSuspendedAppeal && reapplyNote && ( + } + title="Reason for suspension" + > + {reapplyNote} + + )} - {reapplyId ? "Resubmit" : "Create & switch"} + {isSuspendedAppeal + ? "Submit appeal" + : reapplyId + ? "Resubmit" + : "Create"} diff --git a/apps/edr-freight-web/portal/src/components/NewBookingButton.tsx b/apps/edr-freight-web/portal/src/components/NewBookingButton.tsx index cb0e2e726..79109ce2c 100644 --- a/apps/edr-freight-web/portal/src/components/NewBookingButton.tsx +++ b/apps/edr-freight-web/portal/src/components/NewBookingButton.tsx @@ -11,22 +11,21 @@ interface NewBookingButtonProps { /** * New-booking entry point that respects approval status: a customer can only - * create bookings under a profile once the backoffice has approved it. While the - * active profile is pending the button is disabled with an explanation, so the - * gate is communicated rather than silently failing at submit time. + * create bookings once the backoffice has approved at least one operational + * role. While every role is still pending the button is disabled with an + * explanation, so the gate is communicated rather than failing at submit time. */ export function NewBookingButton({ label = "New booking", size, mt, }: NewBookingButtonProps) { - const { canBook, activeProfileStatus } = useAuth(); + const { canBook, hasPendingProfile } = useAuth(); if (!canBook) { - const message = - activeProfileStatus === "pending" - ? "Your profile is awaiting approval. You'll be able to create bookings as soon as it's approved." - : "Bookings aren't available for this profile yet."; + const message = hasPendingProfile + ? "Your role is awaiting approval. You'll be able to create bookings as soon as it's approved." + : "Bookings aren't available until one of your roles is approved."; return ( diff --git a/apps/edr-freight-web/portal/src/constants/URLS.ts b/apps/edr-freight-web/portal/src/constants/URLS.ts index 090502a3a..451b58390 100644 --- a/apps/edr-freight-web/portal/src/constants/URLS.ts +++ b/apps/edr-freight-web/portal/src/constants/URLS.ts @@ -99,7 +99,6 @@ export const URL_CONSTANTS = { PROFILE: "/api/companies/profile", COMPANY_PROFILES: "/api/companies/company-profiles", COMPANY_PROFILE: "/api/companies/company-profile", - ACTIVE_MODE: "/api/companies/active-mode", ONBOARDING_START: "/api/companies/onboarding/start", ONBOARDING_STEP: "/api/companies/onboarding-step", ONBOARDING_COMPLETE: "/api/companies/onboarding/complete", diff --git a/apps/edr-freight-web/portal/src/hooks/useAuth.ts b/apps/edr-freight-web/portal/src/hooks/useAuth.ts index daeb55600..3573750a9 100644 --- a/apps/edr-freight-web/portal/src/hooks/useAuth.ts +++ b/apps/edr-freight-web/portal/src/hooks/useAuth.ts @@ -158,11 +158,7 @@ const useAuth = () => { } }; - // Active-mode (importer/exporter) state, sourced from the persisted profile. const companyInfo = isAuthenticated ? (companyQuery.data ?? null) : null; - const activeProfileType = companyInfo?.profile?.activeProfileType ?? null; - const activeCompanyProfileId = - companyInfo?.profile?.activeCompanyProfileId ?? null; const companyType = companyInfo?.company?.type ?? null; const companyStatus = companyInfo?.company?.status ?? null; // A company can create bookings only once an admin has approved it (active). @@ -171,14 +167,13 @@ const useAuth = () => { companyInfo?.profile?.onboardingCompleted ?? false; const onboardingStep = companyInfo?.profile?.onboardingStep ?? null; - // Booking is gated on backoffice approval of the active operational profile: - // a customer can only book under a profile once its status is "active". - const activeProfile = - companyInfo?.company?.companyProfiles?.find( - (p) => p.id === activeCompanyProfileId, - ) ?? null; - const activeProfileStatus = activeProfile?.status ?? null; - const canBook = activeProfileStatus === "active"; + // A booking/contract stamps its operational profile from the trade direction + // at creation time, so there's no "active mode": the customer can create work + // as long as they have at least one backoffice-approved operational role. + const companyProfiles = companyInfo?.company?.companyProfiles ?? []; + const hasActiveProfile = companyProfiles.some((p) => p.status === "active"); + const hasPendingProfile = companyProfiles.some((p) => p.status === "pending"); + const canBook = hasActiveProfile; // Profile-edit review: while a change request is pending the customer is // locked out of editing and of creating new contracts/bookings; a rejected @@ -188,7 +183,7 @@ const useAuth = () => { const reviewNote = review?.note ?? null; const isUnderReview = reviewStatus === "pending"; - /** Refetch everything scoped to the active operational profile. */ + /** Refetch company info, dashboard, and bookings after a profile change. */ const invalidateScopedData = async () => { await Promise.all([ queryClient.invalidateQueries({ @@ -201,16 +196,6 @@ const useAuth = () => { ]); }; - const switchMode = async (type: ProfileTypeValue): Promise> => { - try { - await api.companies.setActiveMode.call({ type }); - await invalidateScopedData(); - return { success: true, data: undefined }; - } catch (err) { - return { success: false, error: extractApiError(err) }; - } - }; - /** * Add an operational role. The new role starts pending review, so the active * mode is left untouched — the user keeps working under their approved role. @@ -276,10 +261,9 @@ const useAuth = () => { user: isAuthenticated ? (authQuery.data ?? null) : null, company: isAuthenticated ? (companyQuery.data ?? null) : null, customer: isAuthenticated ? (companyQuery.data ?? null) : null, - activeProfileType, - activeCompanyProfileId, - activeProfileStatus, canBook, + hasActiveProfile, + hasPendingProfile, companyType, companyStatus, isCompanyApproved, @@ -288,7 +272,6 @@ const useAuth = () => { isUnderReview, onboardingCompleted, onboardingStep, - switchMode, createProfile, reapplyProfile, login, diff --git a/apps/edr-freight-web/portal/src/lib/posthog.ts b/apps/edr-freight-web/portal/src/lib/posthog.ts index 59599fbb6..3f1fde723 100644 --- a/apps/edr-freight-web/portal/src/lib/posthog.ts +++ b/apps/edr-freight-web/portal/src/lib/posthog.ts @@ -106,7 +106,6 @@ export function captureApiError(error: unknown): void { /** Company context, as returned by `useAuth().company`. */ interface IdentifyCompany { company?: { id?: string; type?: string | null; status?: string | null } | null; - profile?: { activeProfileType?: string | null } | null; } /** @@ -136,7 +135,6 @@ export function useIdentify( company_id: company?.company?.id, company_type: company?.company?.type, company_status: company?.company?.status, - active_profile_type: company?.profile?.activeProfileType, }); }, [ user?.id, @@ -146,6 +144,5 @@ export function useIdentify( company?.company?.id, company?.company?.type, company?.company?.status, - company?.profile?.activeProfileType, ]); } diff --git a/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx b/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx index 4018d131b..755691607 100644 --- a/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/SettingsPage.tsx @@ -2,6 +2,7 @@ import { api } from "@/services/api"; import type { ProfileResponse } from "@/types/profile"; import { Alert, + Anchor, Badge, Box, Button, @@ -27,6 +28,7 @@ import { Building2, Clock, FileCheck, + FileText, Globe, Layers, RefreshCw, @@ -37,6 +39,8 @@ import { UserCog, } from "lucide-react"; import { companiesService } from "@/services/companies.service"; +import { fetchViewableFile } from "@/services/files.service"; +import { useFileViewer } from "@edr/ui-common"; import { useCallback, useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import useAuth from "@/hooks/useAuth"; @@ -300,8 +304,6 @@ export default function SettingsPage() { )} - - value && setTab(value as SettingsTab)} @@ -354,6 +356,7 @@ export default function SettingsPage() {
+
@@ -404,6 +407,7 @@ const ROLE_STATUS: Record = { */ function OperationalServicesCard({ profile }: { profile: ProfileResponse }) { const queryClient = useQueryClient(); + const { view, viewer } = useFileViewer(); const roles = profile.companyProfiles; const refresh = () => @@ -431,7 +435,8 @@ function OperationalServicesCard({ profile }: { profile: ProfileResponse }) { if (roles.length === 0) return null; return ( - + <> + Operational Services @@ -465,10 +470,62 @@ function OperationalServicesCard({ profile }: { profile: ProfileResponse }) { )} - {r.status === "rejected" && r.reviewNote && ( - - Reviewer note: {r.reviewNote} + {(r.status === "rejected" || r.status === "suspended") && + r.reviewNote && ( + + + {r.status === "suspended" + ? "Suspension reason:" + : "Reviewer note:"} + {" "} + {r.reviewNote} + + )} + + {r.licenseFiles.length === 0 ? ( + + No license document + ) : ( + + {r.licenseFiles.map((f) => ( + + + + void fetchViewableFile(f.id, f.name).then(view) + } + > + {f.name} + + {f.status !== "live" && ( + + {f.status === "pending_remove" + ? "Removal pending" + : "Pending"} + + )} + + ))} + )} @@ -484,7 +541,9 @@ function OperationalServicesCard({ profile }: { profile: ProfileResponse }) { ); })} - + + {viewer} + ); } diff --git a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx index d3808c8ce..700967b68 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/NewBookingPage.tsx @@ -38,6 +38,7 @@ import { bookingFormSchema, getRouteDirection, initialBookingFormValues, + isForwarderOperation, operationToProfileType, operationToTradeDirection, stepFields, @@ -384,25 +385,23 @@ export default function NewBookingPage() { [profileTypes], ); - // Stamp the booking to the right operational profile. Import/Export (and their - // "as FF" variants) switch the active mode so the matching onboarding documents - // are attached; Intercity uses whatever profile is already active. - const handleOperationSelect = (op: OperationType) => { - if (op === "intercity") return; - const target = operationToProfileType(op, profileTypes); - if (auth.activeProfileType !== target) { - void auth.switchMode(target as never); - } - }; - - // Onboarding documents for the active profile — shown read-only in the - // Documents step and attached to the booking on submit by the backend. - const onboardingDocs = useMemo(() => { + // The company_profile this booking belongs to, derived from the selected + // operation: import→importer, export→exporter, and the "as FF" variants → + // freight_forwarder. A forwarder booking is pinned explicitly on submit + // (companyProfileId) because trade direction alone can't distinguish it. + const selectedProfile = useMemo(() => { const profiles = auth.company?.company?.companyProfiles ?? []; - const active = - profiles.find((p) => p.id === auth.activeCompanyProfileId) ?? profiles[0]; - return active?.licenseFiles ?? []; - }, [auth.company, auth.activeCompanyProfileId]); + if (!operationType) return profiles[0] ?? null; + const targetType = operationToProfileType(operationType, profileTypes); + return profiles.find((p) => p.type === targetType) ?? profiles[0] ?? null; + }, [auth.company, operationType, profileTypes]); + + // Onboarding documents for the resolved profile — shown read-only in the + // Documents step and attached to the booking on submit by the backend. + const onboardingDocs = useMemo( + () => selectedProfile?.licenseFiles ?? [], + [selectedProfile], + ); const [pricingData, setPricingData] = useState( null, @@ -483,7 +482,16 @@ export default function NewBookingPage() { (s) => s.id === data.serviceTypeId, )!; + // Pin the profile only for a forwarder booking — trade direction resolves + // importer/exporter on its own, but can't tell a forwarder apart. + const forwarderProfileId = + data.operationType && + isForwarderOperation(data.operationType, profileTypes) + ? selectedProfile?.id + : undefined; + return { + ...(forwarderProfileId ? { companyProfileId: forwarderProfileId } : {}), bookingType: isContract ? Freight.BookingType.GeneralContract : Freight.BookingType.OneTime, @@ -711,7 +719,6 @@ export default function NewBookingPage() { )} {step === 1 && ( diff --git a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-documents.tsx b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-documents.tsx index 880efb846..48671db4c 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-documents.tsx +++ b/apps/edr-freight-web/portal/src/pages/bookings/new-booking-form/step-documents.tsx @@ -7,6 +7,7 @@ import { type UseFormReturn } from "react-hook-form"; import useAuth from "@/hooks/useAuth"; import { api } from "@/services/api"; import { + operationToProfileType, type BookingDocuments, type BookingFormInputValues, type BookingFormValues, @@ -58,11 +59,20 @@ export function StepDocuments({ form }: { form: BookingForm }) { }), ); - // Documents already on file from onboarding (read-only reference). + // Documents already on file from onboarding (read-only reference), for the + // profile this booking's operation resolves to (importer/exporter/forwarder). const onboardingDocs = (() => { const profiles = auth.company?.company?.companyProfiles ?? []; + const operationType = form.watch("operationType"); + const targetType = operationType + ? operationToProfileType( + operationType, + profiles.map((p) => p.type), + ) + : null; const active = - profiles.find((p) => p.id === auth.activeCompanyProfileId) ?? profiles[0]; + (targetType && profiles.find((p) => p.type === targetType)) ?? + profiles[0]; return active?.licenseFiles ?? []; })(); 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 da03064be..11cb955e0 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/NewContractPage.tsx @@ -49,6 +49,7 @@ import { } from "./new-contract-form/schema"; import { getRouteDirection, + isForwarderOperation, operationToProfileType, operationToTradeDirection, } from "./new-contract-form/helpers"; @@ -469,10 +470,8 @@ export default function NewContractPage({ form.setValue("operationType", undefined as never, { shouldDirty: true }); return; } - // Case 1 — approved: proceed, switching the active profile if needed. - if (auth.activeProfileType !== target) { - void auth.switchMode(target as never); - } + // Case 1 — approved: proceed. The profile is resolved from the operation at + // submit time (a forwarder operation pins it explicitly), so nothing to set. }; const handleCreateProfileConfirm = () => { @@ -512,10 +511,14 @@ export default function NewContractPage({ const onboardingDocs = useMemo(() => { const profiles = auth.company?.company?.companyProfiles ?? []; + const targetType = operationType + ? operationToProfileType(operationType, profileTypes) + : null; const active = - profiles.find((p) => p.id === auth.activeCompanyProfileId) ?? profiles[0]; + (targetType && profiles.find((p) => p.type === targetType)) ?? + profiles[0]; return active?.licenseFiles ?? []; - }, [auth.company, auth.activeCompanyProfileId]); + }, [auth.company, operationType, profileTypes]); async function handleContinue() { const fields = contractStepFields[step]; @@ -569,7 +572,20 @@ export default function NewContractPage({ }, ]; + // Pin the profile only for a forwarder contract (trade direction can't tell + // a forwarder apart from a direct import/export). + const forwarderProfileId = + data.operationType && + isForwarderOperation(data.operationType, profileTypes) + ? (auth.company?.company?.companyProfiles ?? []).find( + (p) => + p.type === + operationToProfileType(data.operationType!, profileTypes), + )?.id + : undefined; + return { + ...(forwarderProfileId ? { companyProfileId: forwarderProfileId } : {}), contractKind: isGeneral ? Freight.ContractKind.General : Freight.ContractKind.OneTime, diff --git a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step-documents.tsx b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step-documents.tsx index 86b6f61a2..f2985007f 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step-documents.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/step-documents.tsx @@ -12,6 +12,7 @@ import { type ContractFormInputValues, type ContractFormValues, } from "./schema"; +import { operationToProfileType } from "./helpers"; import { StepCard, StepHeader } from "./shared"; type ContractForm = UseFormReturn< @@ -73,8 +74,16 @@ export function StepDocuments({ const onboardingDocs = (() => { const profiles = auth.company?.company?.companyProfiles ?? []; + const operationType = form.watch("operationType"); + const targetType = operationType + ? operationToProfileType( + operationType, + profiles.map((p) => p.type), + ) + : null; const active = - profiles.find((p) => p.id === auth.activeCompanyProfileId) ?? profiles[0]; + (targetType && profiles.find((p) => p.type === targetType)) ?? + profiles[0]; return active?.licenseFiles ?? []; })(); diff --git a/apps/edr-freight-web/portal/src/services/api.ts b/apps/edr-freight-web/portal/src/services/api.ts index ce13fad55..68276a18c 100644 --- a/apps/edr-freight-web/portal/src/services/api.ts +++ b/apps/edr-freight-web/portal/src/services/api.ts @@ -240,12 +240,6 @@ export const api = { CompanyInfoResponse >("companies", "startOnboarding", companiesService.startOnboarding), - setActiveMode: endpoint<{ type: ProfileTypeValue }, CompanyInfoResponse>( - "companies", - "setActiveMode", - companiesService.setActiveMode, - ), - setOnboardingStep: endpoint<{ step: string }, void>( "companies", "setOnboardingStep", diff --git a/apps/edr-freight-web/portal/src/services/companies.service.ts b/apps/edr-freight-web/portal/src/services/companies.service.ts index 190a806a8..6b8a8be0f 100644 --- a/apps/edr-freight-web/portal/src/services/companies.service.ts +++ b/apps/edr-freight-web/portal/src/services/companies.service.ts @@ -45,10 +45,6 @@ export interface ExternalProfileResponse { nationalId: string | null; jobTitle: string | null; isPrimaryContact: boolean; - /** The active operational mode (importer/exporter/forwarder). */ - activeProfileType: ProfileTypeValue | null; - /** Id of the company_profile matching activeProfileType (server-resolved). */ - activeCompanyProfileId: string | null; onboardingStep: string | null; onboardingCompleted: boolean; createdAt: string; @@ -323,17 +319,6 @@ export const companiesService = { return unwrap(response.data); }, - /** Switch the active operational mode (target profile must already exist). */ - setActiveMode: async (payload: { - type: ProfileTypeValue; - }): Promise => { - const response = await client.patch>( - URL_CONSTANTS.COMPANIES_API.ACTIVE_MODE, - payload, - ); - return unwrap(response.data); - }, - setOnboardingStep: async (payload: { step: string }): Promise => { await client.patch(URL_CONSTANTS.COMPANIES_API.ONBOARDING_STEP, payload); },