From d4767e0d0656ab09f218403ce66ed0c46555b4cd Mon Sep 17 00:00:00 2001 From: Nathnael Date: Wed, 12 Aug 2026 07:11:27 +0000 Subject: [PATCH] fix: an issue --- .../modules/companies/companies.service.ts | 78 ++++++++++--------- .../onboarding-requirements-response.dto.ts | 11 +-- .../file-upload-settings.service.ts | 5 -- .../poa-delegation.constants.ts | 9 ++- .../src/seed/file-upload-settings.seeder.ts | 45 ++++++++--- .../onboarding/OnboardingWizardDialog.tsx | 26 +++---- .../src/pages/accounts/CompanyProfileForm.tsx | 36 ++------- .../new-booking-form/step-documents.tsx | 21 +++-- .../resubmit/useBookingDocumentSetting.ts | 23 +++--- .../new-contract-form/ContractDocsEditor.tsx | 21 +++-- .../new-contract-form/step-documents.tsx | 20 +++-- .../src/pages/settings/NationalitySelect.tsx | 21 +++-- .../src/pages/settings/TabDocuments.tsx | 13 ++-- .../portal/src/services/companies.service.ts | 8 +- .../src/utils/documentSettingCode.test.ts | 28 +++++++ .../portal/src/utils/documentSettingCode.ts | 18 +++++ 16 files changed, 206 insertions(+), 177 deletions(-) create mode 100644 apps/edr-freight-web/portal/src/utils/documentSettingCode.test.ts create mode 100644 apps/edr-freight-web/portal/src/utils/documentSettingCode.ts 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 cf2f2e84d..98b30f006 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -256,11 +256,15 @@ export class CompaniesService { }, ]; - /** The nationality-based document setting code for a company. */ - private documentSettingCodeFor( - nationality: CompanyNationality | null | undefined, - ): string { - return nationality === CompanyNationality.Foreign + /** + * The document setting code for a company: one of three mutually exclusive + * sets. A co-operative union or farm resolves to its own set regardless of + * nationality — it holds no business licence, so it owes a different list of + * papers rather than the nationality list plus extras. + */ + private documentSettingCodeFor(company: Company): string { + if (isCooperative(company)) return COOPERATIVE_ONBOARDING_CODE; + return company.nationality === CompanyNationality.Foreign ? "company_onboarding_documents_foreign" : "company_onboarding_documents_ethiopian"; } @@ -387,13 +391,16 @@ export class CompaniesService { const current = needsCompany ? await this.companiesRepo.findById(companyId) : null; - this.assertRolesAllowedForCooperative( - cooperative ?? isCooperative(current), - roles, - ); + const isCoop = cooperative ?? isCooperative(current); + this.assertRolesAllowedForCooperative(isCoop, roles); + this.assertNationalityAllowedForCooperative(isCoop, nationality); await this.syncCompanyProfiles(companyId, companyType, roles); const updates: Partial = {}; if (nationality) updates.nationality = nationality; + // Ticking the box on a draft that was saved as foreign has to correct the + // stored nationality too, or the company keeps resolving to the foreign + // document set. + if (isCoop) updates.nationality = CompanyNationality.Ethiopian; if (cooperative !== undefined) { updates.attributes = { ...(current?.attributes ?? {}), @@ -407,6 +414,7 @@ export class CompaniesService { } this.assertRolesAllowedForCooperative(cooperative === true, roles); + this.assertNationalityAllowedForCooperative(cooperative === true, nationality); const allowedTypes = this.getProfileTypeForCompanyType(companyType); const chosenTypes = roles.filter((t) => allowedTypes.includes(t)); @@ -458,6 +466,24 @@ export class CompaniesService { } } + /** + * A co-operative union or farm is registered in Ethiopia by the co-operative + * promotion agency, so it is always an Ethiopian company — "foreign" is not a + * combination that exists, and allowing it would resolve the company to a + * document set built around an investment licence it cannot hold. + */ + private assertNationalityAllowedForCooperative( + cooperative: boolean, + nationality: CompanyNationality | undefined, + ): void { + if (!cooperative) return; + if (nationality === CompanyNationality.Foreign) { + throw new BadRequestException( + "A co-operative union or farm is registered in Ethiopia — it cannot onboard as a foreign company.", + ); + } + } + /** * Reconcile the company's operational profiles with the roles the user has * selected: create the missing ones, drop the ones they deselected. @@ -1252,7 +1278,7 @@ export class CompaniesService { uploaded: FileRecord[], ): Promise { const setting = await this.fileUploadSettingsService - .getByCode(this.documentSettingCodeFor(company.nationality)) + .getByCode(this.documentSettingCodeFor(company)) .catch(() => null); const fields = setting?.fields ?? []; const singleFileCodes = new Set( @@ -2036,35 +2062,20 @@ export class CompaniesService { .filter((f) => !f.get(company)) .map((f) => ({ key: f.key, label: f.label })); - // 2. Nationality-based company documents + which are already uploaded. A - // co-operative adds its own set on top: it provides everything its - // nationality demands, plus the papers standing in for the business licence - // it does not hold. + // 2. Company documents + which are already uploaded. One set applies: the + // company's nationality set, or the co-operative set in its place — a union + // or farm holds no business licence, so it owes its own list rather than the + // nationality list plus extras. const cooperative = isCooperative(company); - const documentSettingCode = this.documentSettingCodeFor( - company.nationality, - ); - const [setting, coopSetting, uploadedFiles] = await Promise.all([ + const documentSettingCode = this.documentSettingCodeFor(company); + const [setting, uploadedFiles] = await Promise.all([ this.fileUploadSettingsService .getByCode(documentSettingCode) .catch(() => null), - cooperative - ? this.fileUploadSettingsService - .getByCode(COOPERATIVE_ONBOARDING_CODE) - .catch(() => null) - : Promise.resolve(null), this.filesService.findByResource(company.id, "companies"), ]); const uploadedCodes = new Set(uploadedFiles.map((f) => f.code)); - // The co-op set is admin-managed and could name a fileKey the nationality - // set already carries; the nationality field wins so the same slot is never - // rendered (or required) twice. - const baseFields = setting?.fields ?? []; - const baseKeys = new Set(baseFields.map((f) => f.fileKey)); - const documents = [ - ...baseFields, - ...(coopSetting?.fields ?? []).filter((f) => !baseKeys.has(f.fileKey)), - ] + const documents = (setting?.fields ?? []) .slice() .sort((a, b) => a.displayOrder - b.displayOrder) .map((f) => ({ @@ -2203,9 +2214,6 @@ export class CompaniesService { return new OnboardingRequirementsResponseDto({ documentSettingCode, - cooperativeDocumentSettingCode: cooperative - ? COOPERATIVE_ONBOARDING_CODE - : null, nationality: company.nationality ?? CompanyNationality.Ethiopian, cooperative, companyInfo: { diff --git a/apps/edr-freight-api/src/modules/companies/dto/onboarding-requirements-response.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/onboarding-requirements-response.dto.ts index 8d47dbb4e..20bd0ab06 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/onboarding-requirements-response.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/onboarding-requirements-response.dto.ts @@ -66,15 +66,11 @@ export interface OnboardingPoaState { } export class OnboardingRequirementsResponseDto { - /** Resolved document setting code (by nationality) the docs were drawn from. */ - documentSettingCode: string; /** - * The co-operative document set, merged on top of the nationality one — null - * for every other company. `documents` below already carries the merged - * result; this is only so the portal can fetch the same extra fields when it - * renders the pickers from the file-settings endpoint. + * Resolved document setting code the docs were drawn from: the company's + * nationality set, or the co-operative set in its place. */ - cooperativeDocumentSettingCode: string | null; + documentSettingCode: string; nationality: string; /** * The company trades as a co-operative: no business licence, so no eTrade @@ -118,7 +114,6 @@ export class OnboardingRequirementsResponseDto { constructor(init: Omit) { this.documentSettingCode = init.documentSettingCode; - this.cooperativeDocumentSettingCode = init.cooperativeDocumentSettingCode; this.nationality = init.nationality; this.cooperative = init.cooperative; this.companyInfo = init.companyInfo; diff --git a/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.service.ts b/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.service.ts index 21ddc4c91..9651d4f37 100644 --- a/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.service.ts +++ b/apps/edr-freight-api/src/modules/file-upload-settings/file-upload-settings.service.ts @@ -17,7 +17,6 @@ import { } from "./interfaces/file-upload-settings.repository.interface"; import { COMPANY_ONBOARDING_CODE_PREFIX, - COOPERATIVE_ONBOARDING_CODE, POA_DELEGATION_FILE_KEY, poaDelegationField, } from "./poa-delegation.constants"; @@ -57,10 +56,6 @@ export class FileUploadSettingsService { */ private withPoaDelegationField(setting: FileUploadSetting): FileUploadSetting { if (!setting.code.startsWith(COMPANY_ONBOARDING_CODE_PREFIX)) return setting; - // The co-operative set is merged ON TOP of a nationality set that already - // carries the paper; injecting it here too would hand the portal the same - // slot twice. - if (setting.code === COOPERATIVE_ONBOARDING_CODE) return setting; const fields = setting.fields ?? []; if (fields.some((f) => f.fileKey === POA_DELEGATION_FILE_KEY)) return setting; diff --git a/apps/edr-freight-api/src/modules/file-upload-settings/poa-delegation.constants.ts b/apps/edr-freight-api/src/modules/file-upload-settings/poa-delegation.constants.ts index e8d593ded..e23a87818 100644 --- a/apps/edr-freight-api/src/modules/file-upload-settings/poa-delegation.constants.ts +++ b/apps/edr-freight-api/src/modules/file-upload-settings/poa-delegation.constants.ts @@ -27,10 +27,11 @@ export const POA_DELEGATION_LABEL = "DARS Delegation Paper"; export const COMPANY_ONBOARDING_CODE_PREFIX = "company_onboarding_documents_"; /** - * The co-operative onboarding set. Unlike the nationality sets it is ADDITIVE — - * merged on top of the company's `_ethiopian`/`_foreign` set rather than - * replacing it — which is why the delegation paper is not injected into it: the - * set it is merged onto already carries one. + * The co-operative onboarding set — the third alternative to `_ethiopian` and + * `_foreign`, not an addition to them: a union or farm resolves to this set + * INSTEAD of its nationality's, because it holds no business licence and so + * owes a different list of papers. The delegation paper is injected into it + * like any other company onboarding set. */ export const COOPERATIVE_ONBOARDING_CODE = `${COMPANY_ONBOARDING_CODE_PREFIX}cooperative`; diff --git a/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts b/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts index abc4e613c..4f314453a 100644 --- a/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts +++ b/apps/edr-freight-api/src/seed/file-upload-settings.seeder.ts @@ -155,16 +155,27 @@ const FOREIGN_ONBOARDING_FIELDS: OnboardingField[] = [ // ]; /** - * Extra documents a co-operative union or farm provides, merged on top of its - * nationality set. It has a TIN but no business licence, so the papers that - * evidence the co-operative itself stand in for the trade licence every other - * company uploads. + * Documents required from a co-operative union or farm — the third alternative + * to the two nationality sets, not an addition to them. A co-op is always + * registered in Ethiopia and has a TIN but no business licence, so its + * registration certificate stands in for the commercial registration every + * other Ethiopian company uploads. * - * Only the registration certificate is seeded, and the set is admin-managed - * like every other onboarding set — what these members must actually produce - * is a backoffice decision, edited in the file-settings editor. + * Admin-managed like every other onboarding set: what these members must + * actually produce is a backoffice decision, edited in the file-settings editor. */ const COOPERATIVE_ONBOARDING_FIELDS: OnboardingField[] = [ + { + fileKey: "tin_certificate", + fileLabel: "TIN Certificate", + helpText: "Verified against the TIN registry during registration.", + isRequired: true, + isMultiple: false, + maxFiles: 1, + allowedExtensions: DOC_EXTENSIONS, + maxSizeMb: 50, + displayOrder: 1, + }, { fileKey: "cooperative_registration_certificate", fileLabel: "Co-operative Union / Farm Registration Certificate", @@ -175,8 +186,20 @@ const COOPERATIVE_ONBOARDING_FIELDS: OnboardingField[] = [ maxFiles: 1, allowedExtensions: DOC_EXTENSIONS, maxSizeMb: 50, - displayOrder: 1, + displayOrder: 2, }, + { + fileKey: "national_id", + fileLabel: "National ID", + helpText: "Verified against the National ID API during registration.", + isRequired: true, + isMultiple: false, + maxFiles: 1, + allowedExtensions: DOC_EXTENSIONS, + maxSizeMb: 50, + displayOrder: 3, + }, + poaDelegationDefault(4), ]; interface OnboardingDocumentSetting { @@ -201,11 +224,11 @@ const COMPANY_ONBOARDING_DOCUMENTS: OnboardingDocumentSetting[] = [ entity: "customer", fields: FOREIGN_ONBOARDING_FIELDS, }, - // Additive, not a nationality of its own: a union or farm still uploads - // everything its nationality set demands, and these on top. + // The third set: a union or farm resolves here INSTEAD of a nationality set + // (it is always Ethiopian, and holds no business licence). { code: "company_onboarding_documents_cooperative", - label: "Co-operative union / farm onboarding documents (additional)", + label: "Co-operative union / farm onboarding documents", entity: "customer", fields: COOPERATIVE_ONBOARDING_FIELDS, }, diff --git a/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx b/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx index 6d75e502f..998f733da 100644 --- a/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx +++ b/apps/edr-freight-web/portal/src/components/onboarding/OnboardingWizardDialog.tsx @@ -39,6 +39,7 @@ import type { } from "@/services/companies.service"; import { companiesService } from "@/services/companies.service"; import type { UpdateProfilePayload } from "@/types/profile"; +import { documentSettingCode } from "@/utils/documentSettingCode"; import { extractApiError } from "@/utils/result"; /** Form steps rendered by CompanyProfileForm. */ @@ -116,13 +117,6 @@ function companyTypeForRoles(_roles: string[]): string { return "customer"; } -/** Document upload setting code per company nationality. */ -function documentSettingCode(nationality: CompanyNationality): string { - return nationality === "foreign" - ? "company_onboarding_documents_foreign" - : "company_onboarding_documents_ethiopian"; -} - /** * First-run onboarding wizard with a "draft-first" flow: picking the role(s) * immediately creates a draft company + profile on the backend, so every @@ -168,11 +162,15 @@ export default function OnboardingWizardDialog({ const [cooperative, setCooperative] = useState( company?.company?.attributes?.cooperative === true, ); - // Ticking the box drops a role the company can no longer hold, rather than - // letting Continue fail on a selection the API refuses. + // Ticking the box drops the selections the company can no longer hold, rather + // than letting Continue fail on ones the API refuses: a co-op cannot forward + // freight, and is registered in Ethiopia so it is never foreign. const handleCooperativeChange = useCallback((checked: boolean) => { setCooperative(checked); - if (checked) setRoles((prev) => prev.filter((r) => r !== "freight_forwarder")); + if (checked) { + setRoles((prev) => prev.filter((r) => r !== "freight_forwarder")); + setNationality((prev) => (prev === "foreign" ? "ethiopian" : prev)); + } }, []); const [documentFiles, setDocumentFiles] = useState< Record @@ -419,7 +417,7 @@ export default function OnboardingWizardDialog({ // after the draft — and thus the requirements — exist). const resolvedDocumentSettingCode = requirementsQuery.data?.documentSettingCode ?? - documentSettingCode(effectiveNationality); + documentSettingCode(effectiveNationality, cooperative); // Server-confirmed document state, used both to badge already-uploaded fields // and to keep a refreshed resume from over-shooting the documents step. @@ -480,8 +478,6 @@ export default function OnboardingWizardDialog({ // startOnboarding has persisted it, and the form's whole company step // branches on it. cooperative: requirementsQuery.data?.cooperative ?? cooperative, - extraDocumentSettingCode: - requirementsQuery.data?.cooperativeDocumentSettingCode ?? null, // A freight forwarder cannot answer the power-of-attorney question — the // API forces "yes" — so the step offers no way to change it. declarationLocked: requirementsQuery.data?.poa?.locked ?? false, @@ -551,6 +547,10 @@ export default function OnboardingWizardDialog({ value={nationality} onChange={setNationality} embedded + // A co-op is registered in Ethiopia by the co-operative + // promotion agency — foreign is not on offer rather than + // refused later. + excludeForeign={cooperative} /> {/* A co-operative union or farm registers on a TIN alone. It changes what the next step asks for (typed registration, no diff --git a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx index 59fcc2223..aa445974f 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx @@ -66,7 +66,6 @@ export default function CompanyProfileForm({ onIdentityChange, cooperative = false, declarationLocked = false, - extraDocumentSettingCode, }: { documentSettingCode: string; documentFiles?: Record; @@ -115,8 +114,8 @@ export default function CompanyProfileForm({ /** * The company trades as a co-operative: a TIN but no business licence, so the * eTrade lookup is replaced by typed registration details, the per-role - * licence upload is not owed, and its own document set applies on top of the - * nationality one. + * licence upload is not owed, and its own document set applies instead of the + * nationality one (resolved by the caller into `documentSettingCode`). */ cooperative?: boolean; /** @@ -124,8 +123,6 @@ export default function CompanyProfileForm({ * answer is forced to "yes" and cannot be changed here. */ declarationLocked?: boolean; - /** Additional document set merged in (the co-operative one), if any. */ - extraDocumentSettingCode?: string | null; }) { // A Fayda claim carries the phone as the national registry holds it, which is // often a local number the form's E.164 validation (and the API's @@ -206,36 +203,15 @@ export default function CompanyProfileForm({ const documentFiles = controlledFiles ?? internalFiles; const setDocumentFiles = onDocumentFilesChange ?? setInternalFiles; - const { data: nationalitySetting, isLoading: loadingDocuments } = useQuery( + // One set applies: the company's nationality set, or the co-operative one in + // its place — the caller resolves which (the API resolves the same way when + // it decides what is outstanding). + const { data: uploadSetting, isLoading: loadingDocuments } = useQuery( api.fileUploadSettings.getByCode.queryOptions({ input: { code: documentSettingCode }, refetchOnMount: false, }), ); - // A co-operative's own documents come as a second, additive set — it uploads - // everything its nationality demands, plus the papers standing in for the - // business licence it does not hold. The API merges the same two sets when it - // decides what is outstanding. - const { data: extraSetting } = useQuery( - api.fileUploadSettings.getByCode.queryOptions({ - input: { code: extraDocumentSettingCode ?? "" }, - enabled: Boolean(extraDocumentSettingCode), - refetchOnMount: false, - }), - ); - const uploadSetting = useMemo(() => { - if (!nationalitySetting) return nationalitySetting; - if (!extraSetting?.fields?.length) return nationalitySetting; - // Nationality wins a fileKey collision, so a slot is never rendered twice. - const seen = new Set(nationalitySetting.fields.map((f) => f.fileKey)); - return { - ...nationalitySetting, - fields: [ - ...nationalitySetting.fields, - ...extraSetting.fields.filter((f) => !seen.has(f.fileKey)), - ], - }; - }, [nationalitySetting, extraSetting]); // Which fields the current step renders an input for and therefore requires. // Filled in further down (it depends on values this form owns), and read at 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 de105bf21..d75092655 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 @@ -6,6 +6,7 @@ import { type UseFormReturn } from "react-hook-form"; import useAuth from "@/hooks/useAuth"; import { api } from "@/services/api"; +import { documentSettingCode } from "@/utils/documentSettingCode"; import { operationToProfileType, type BookingDocuments, @@ -20,13 +21,6 @@ type BookingForm = UseFormReturn< BookingFormValues >; -/** Onboarding document setting code for the company's nationality. */ -function documentSettingCode(nationality: string | null | undefined): string { - return nationality === "foreign" - ? "company_onboarding_documents_foreign" - : "company_onboarding_documents_ethiopian"; -} - function formatSize(bytes?: number): string { if (!bytes) return ""; if (bytes < 1024) return `${bytes} B`; @@ -48,14 +42,17 @@ function formatSize(bytes?: number): string { export function StepDocuments({ form }: { form: BookingForm }) { const auth = useAuth(); - const nationality = auth.company?.company?.nationality as - | string - | null - | undefined; + const company = auth.company?.company; + const nationality = company?.nationality as string | null | undefined; const docSettingQuery = useQuery( api.fileUploadSettings.getByCode.queryOptions({ - input: { code: documentSettingCode(nationality) }, + input: { + code: documentSettingCode( + nationality, + company?.attributes?.cooperative === true, + ), + }, }), ); diff --git a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/useBookingDocumentSetting.ts b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/useBookingDocumentSetting.ts index 65774f97f..31d42f6cd 100644 --- a/apps/edr-freight-web/portal/src/pages/bookings/resubmit/useBookingDocumentSetting.ts +++ b/apps/edr-freight-web/portal/src/pages/bookings/resubmit/useBookingDocumentSetting.ts @@ -2,15 +2,9 @@ import { useQuery } from "@tanstack/react-query"; import useAuth from "@/hooks/useAuth"; import { api } from "@/services/api"; +import { documentSettingCode } from "@/utils/documentSettingCode"; -/** Onboarding document setting code for the company's nationality. */ -export function documentSettingCode( - nationality: string | null | undefined, -): string { - return nationality === "foreign" - ? "company_onboarding_documents_foreign" - : "company_onboarding_documents_ethiopian"; -} +export { documentSettingCode }; /** * Fetches the FileUploadSetting that describes the documents a booking requires @@ -22,14 +16,17 @@ export function documentSettingCode( */ export function useBookingDocumentSetting() { const auth = useAuth(); - const nationality = auth.company?.company?.nationality as - | string - | null - | undefined; + const company = auth.company?.company; + const nationality = company?.nationality as string | null | undefined; return useQuery( api.fileUploadSettings.getByCode.queryOptions({ - input: { code: documentSettingCode(nationality) }, + input: { + code: documentSettingCode( + nationality, + company?.attributes?.cooperative === true, + ), + }, }), ); } diff --git a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/ContractDocsEditor.tsx b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/ContractDocsEditor.tsx index 4d966e010..9d045c4e7 100644 --- a/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/ContractDocsEditor.tsx +++ b/apps/edr-freight-web/portal/src/pages/contracts/new-contract-form/ContractDocsEditor.tsx @@ -7,6 +7,7 @@ import type { Freight } from "@edr/types"; import { useQuery } from "@tanstack/react-query"; import useAuth from "@/hooks/useAuth"; import { api } from "@/services/api"; +import { documentSettingCode } from "@/utils/documentSettingCode"; import type { CompanyDocument } from "@/services/companies.service"; import { downloadStoredFile } from "@/services/files.service"; import { labelForDocCode } from "@/pages/bookings/resubmit/resubmitDocs"; @@ -15,13 +16,6 @@ import { BORDER, GREEN, INK } from "../contract-ui"; type DocumentsValue = Record; type ContractFile = NonNullable[number]; -/** Onboarding document setting code for the company's nationality. */ -function documentSettingCode(nationality: string | null | undefined): string { - return nationality === "foreign" - ? "company_onboarding_documents_foreign" - : "company_onboarding_documents_ethiopian"; -} - function hasFile(value: File | File[] | null | undefined): boolean { if (!value) return false; return Array.isArray(value) ? value.length > 0 : true; @@ -95,13 +89,16 @@ export function ContractDocsEditor({ }) { const auth = useAuth(); - const nationality = auth.company?.company?.nationality as - | string - | null - | undefined; + const company = auth.company?.company; + const nationality = company?.nationality as string | null | undefined; const settingQuery = useQuery( api.fileUploadSettings.getByCode.queryOptions({ - input: { code: documentSettingCode(nationality) }, + input: { + code: documentSettingCode( + nationality, + company?.attributes?.cooperative === true, + ), + }, }), ); 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 de4b2bd66..b0b870f72 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 @@ -7,6 +7,7 @@ import { type UseFormReturn } from "react-hook-form"; import useAuth from "@/hooks/useAuth"; import { api } from "@/services/api"; +import { documentSettingCode } from "@/utils/documentSettingCode"; import { type ContractDocuments, type ContractFormInputValues, @@ -21,12 +22,6 @@ type ContractForm = UseFormReturn< ContractFormValues >; -function documentSettingCode(nationality: string | null | undefined): string { - return nationality === "foreign" - ? "company_onboarding_documents_foreign" - : "company_onboarding_documents_ethiopian"; -} - function formatSize(bytes?: number): string { if (!bytes) return ""; if (bytes < 1024) return `${bytes} B`; @@ -61,14 +56,17 @@ export function StepDocuments({ const auth = useAuth(); const [errors, setErrors] = useState>({}); - const nationality = auth.company?.company?.nationality as - | string - | null - | undefined; + const company = auth.company?.company; + const nationality = company?.nationality as string | null | undefined; const docSettingQuery = useQuery( api.fileUploadSettings.getByCode.queryOptions({ - input: { code: documentSettingCode(nationality) }, + input: { + code: documentSettingCode( + nationality, + company?.attributes?.cooperative === true, + ), + }, }), ); diff --git a/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx b/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx index 2115ec083..99569e748 100644 --- a/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx +++ b/apps/edr-freight-web/portal/src/pages/settings/NationalitySelect.tsx @@ -9,6 +9,8 @@ interface NationalitySelectProps { onChange: (next: CompanyNationality) => void; /** Render only the option grid — the wizard supplies its own header/card. */ embedded?: boolean; + /** Hide the foreign option (a co-operative union or farm is always Ethiopian). */ + excludeForeign?: boolean; } /** @@ -21,9 +23,10 @@ export default function NationalitySelect({ value, onChange, embedded = false, + excludeForeign = false, }: NationalitySelectProps) { const grid = ( - + onChange("ethiopian")} /> - } - selected={value === "foreign"} - onClick={() => onChange("foreign")} - /> + {!excludeForeign && ( + } + selected={value === "foreign"} + onClick={() => onChange("foreign")} + /> + )} ); diff --git a/apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx b/apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx index 72d6738c8..584fb70d6 100644 --- a/apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx +++ b/apps/edr-freight-web/portal/src/pages/settings/TabDocuments.tsx @@ -6,6 +6,7 @@ import { type LicenseFileStatus, } from "@/services/companies.service"; import { getMinFiles } from "@/types/fileUploadSettings"; +import { documentSettingCode } from "@/utils/documentSettingCode"; import type { ProfileResponse } from "@/types/profile"; import { SmartFileInput, @@ -57,14 +58,8 @@ interface TabDocumentsProps { onContinue?: () => void; } -function documentSettingCode(nationality: string | null | undefined): string { - return nationality === "foreign" - ? "company_onboarding_documents_foreign" - : "company_onboarding_documents_ethiopian"; -} - /** - * The DARS delegation paper ships in the same nationality document set, but it is + * The DARS delegation paper ships in the same document set, but it is * edited on the Power of Attorney tab (where it is staged for review alongside * the PoA details), so it is excluded from this tab's uploader. */ @@ -83,7 +78,9 @@ export default function TabDocuments({ const docSettingQuery = useQuery( api.fileUploadSettings.getByCode.queryOptions({ - input: { code: documentSettingCode(profile.nationality) }, + input: { + code: documentSettingCode(profile.nationality, profile.cooperative), + }, }), ); 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 e9cf478c3..4c1b93713 100644 --- a/apps/edr-freight-web/portal/src/services/companies.service.ts +++ b/apps/edr-freight-web/portal/src/services/companies.service.ts @@ -183,14 +183,8 @@ export interface OnboardingPoaState { * outstanding, so the client never hardcodes required fields or document sets. */ export interface OnboardingRequirements { + /** The set the docs came from: the nationality one, or the co-operative one. */ documentSettingCode: string; - /** - * Extra document set merged on top of the nationality one for a co-operative, - * null otherwise. `documents` already carries the merged list; this is only - * so the pickers, which render from the file-settings endpoint, can fetch the - * same extra fields. - */ - cooperativeDocumentSettingCode: string | null; nationality: string; /** No business licence: registration typed by hand, no eTrade lookup. */ cooperative: boolean; diff --git a/apps/edr-freight-web/portal/src/utils/documentSettingCode.test.ts b/apps/edr-freight-web/portal/src/utils/documentSettingCode.test.ts new file mode 100644 index 000000000..6063f1b87 --- /dev/null +++ b/apps/edr-freight-web/portal/src/utils/documentSettingCode.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vitest"; + +import { documentSettingCode } from "./documentSettingCode"; + +describe("documentSettingCode", () => { + it("resolves the nationality set", () => { + expect(documentSettingCode("ethiopian")).toBe( + "company_onboarding_documents_ethiopian", + ); + expect(documentSettingCode("foreign")).toBe( + "company_onboarding_documents_foreign", + ); + expect(documentSettingCode(null)).toBe( + "company_onboarding_documents_ethiopian", + ); + }); + + it("replaces the nationality set for a co-operative", () => { + expect(documentSettingCode("ethiopian", true)).toBe( + "company_onboarding_documents_cooperative", + ); + // A co-op is never foreign, but a stale flag must not fall back to the + // foreign set — the co-op answer wins. + expect(documentSettingCode("foreign", true)).toBe( + "company_onboarding_documents_cooperative", + ); + }); +}); diff --git a/apps/edr-freight-web/portal/src/utils/documentSettingCode.ts b/apps/edr-freight-web/portal/src/utils/documentSettingCode.ts new file mode 100644 index 000000000..e27407c27 --- /dev/null +++ b/apps/edr-freight-web/portal/src/utils/documentSettingCode.ts @@ -0,0 +1,18 @@ +/** + * The company document set a company resolves to — one of three, never a + * combination: a co-operative union or farm uploads its own papers INSTEAD of + * its nationality's (it holds no business licence), and is always Ethiopian. + * + * Mirrors `CompaniesService.documentSettingCodeFor` on the API; prefer the + * server-resolved `documentSettingCode` from onboarding requirements where one + * is available, and use this where only the company is at hand. + */ +export function documentSettingCode( + nationality: string | null | undefined, + cooperative?: boolean | null, +): string { + if (cooperative) return "company_onboarding_documents_cooperative"; + return nationality === "foreign" + ? "company_onboarding_documents_foreign" + : "company_onboarding_documents_ethiopian"; +}