mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 01:18:18 +00:00
fix: an issue
This commit is contained in:
@@ -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<boolean>(
|
||||
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<string, File | File[] | null>
|
||||
@@ -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
|
||||
|
||||
@@ -66,7 +66,6 @@ export default function CompanyProfileForm({
|
||||
onIdentityChange,
|
||||
cooperative = false,
|
||||
declarationLocked = false,
|
||||
extraDocumentSettingCode,
|
||||
}: {
|
||||
documentSettingCode: string;
|
||||
documentFiles?: Record<string, File | File[] | null>;
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<string, File | File[] | null>;
|
||||
type ContractFile = NonNullable<Freight.IContract["files"]>[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,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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<Record<string, string>>({});
|
||||
|
||||
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,
|
||||
),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
<SimpleGrid cols={{ base: 1, sm: 2 }} spacing="md">
|
||||
<SimpleGrid cols={{ base: 1, sm: excludeForeign ? 1 : 2 }} spacing="md">
|
||||
<RoleCard
|
||||
label="Ethiopian Company"
|
||||
description="Registered in Ethiopia. You'll provide a TIN certificate, commercial registration and national ID."
|
||||
@@ -31,13 +34,15 @@ export default function NationalitySelect({
|
||||
selected={value === "ethiopian"}
|
||||
onClick={() => onChange("ethiopian")}
|
||||
/>
|
||||
<RoleCard
|
||||
label="Foreign Company"
|
||||
description="Registered abroad. You'll provide a passport and investment license."
|
||||
icon={<Globe2 size={22} />}
|
||||
selected={value === "foreign"}
|
||||
onClick={() => onChange("foreign")}
|
||||
/>
|
||||
{!excludeForeign && (
|
||||
<RoleCard
|
||||
label="Foreign Company"
|
||||
description="Registered abroad. You'll provide a passport and investment license."
|
||||
icon={<Globe2 size={22} />}
|
||||
selected={value === "foreign"}
|
||||
onClick={() => onChange("foreign")}
|
||||
/>
|
||||
)}
|
||||
</SimpleGrid>
|
||||
);
|
||||
|
||||
|
||||
@@ -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),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
});
|
||||
18
apps/edr-freight-web/portal/src/utils/documentSettingCode.ts
Normal file
18
apps/edr-freight-web/portal/src/utils/documentSettingCode.ts
Normal file
@@ -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";
|
||||
}
|
||||
Reference in New Issue
Block a user