From 615f7e679818d288c3a89208a1e81e61ea1d3f42 Mon Sep 17 00:00:00 2001 From: Nathnael Date: Mon, 10 Aug 2026 14:49:17 +0000 Subject: [PATCH] feat: add choice to etrade fetcher --- .../modules/companies/companies.controller.ts | 1 + .../modules/companies/companies.service.ts | 22 ++- .../companies/dto/etrade-response.dto.ts | 4 +- .../modules/companies/dto/fetch-etrade.dto.ts | 12 +- .../etrade-business-selection.spec.ts | 87 +++++++++ .../companies/services/etrade.service.ts | 29 ++- .../src/modules/payment/payments.dto.ts | 11 +- .../src/components/onboarding/ETradeInfo.tsx | 184 ++++++++++++++++-- .../portal/src/hooks/useETradeData.ts | 11 +- .../src/pages/accounts/CompanyProfileForm.tsx | 4 +- .../companyProfileForm/ETradeCompanyCard.tsx | 126 ++---------- .../accounts/companyProfileForm/helpers.ts | 7 +- .../companyProfileForm/schema.test.ts | 23 ++- .../accounts/companyProfileForm/schema.ts | 46 ++--- .../steps/CompanyInfoStep.tsx | 10 +- .../src/pages/settings/TabCompanyProfile.tsx | 168 +++------------- .../portal/src/services/companies.service.ts | 5 +- packages/types/src/freight/etrade.ts | 23 ++- 18 files changed, 443 insertions(+), 330 deletions(-) create mode 100644 apps/edr-freight-api/src/modules/companies/services/etrade-business-selection.spec.ts 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 78f8db410..de7a01484 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.controller.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.controller.ts @@ -210,6 +210,7 @@ export class CompaniesController { const data = await this.companiesService.fetchETradeData( dto.tin, companyId, + dto.licenceNumber, ); return new ETradeResponseDto(data); } 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 0ed91c9e3..5dd55e704 100644 --- a/apps/edr-freight-api/src/modules/companies/companies.service.ts +++ b/apps/edr-freight-api/src/modules/companies/companies.service.ts @@ -3541,9 +3541,10 @@ export class CompaniesService { /** Resolve a TIN's live eTrade registration data. Throws when eTrade has no matching business licence. */ private async resolveEtradeRegistration( tin: string, + licenceNumber?: string, ): Promise { const { businessInfo, companyInfo } = - await this.etradeService.resolveCompanyData(tin); + await this.etradeService.resolveCompanyData(tin, licenceNumber); if (!businessInfo) { throw new BadRequestException( "We couldn't find a business license for this TIN with eTrade. Please double-check the number and try again.", @@ -3555,8 +3556,15 @@ export class CompaniesService { ); } - async fetchETradeData(tin: string, excludeCompanyId?: string) { - const registrationData = await this.resolveEtradeRegistration(tin); + async fetchETradeData( + tin: string, + excludeCompanyId?: string, + licenceNumber?: string, + ) { + const registrationData = await this.resolveEtradeRegistration( + tin, + licenceNumber, + ); const tinTaken = await this.companiesRepo.existsByTin( tin, excludeCompanyId, @@ -3583,7 +3591,13 @@ export class CompaniesService { if (!touched) return; const tin = dto.tin ?? company.tin; - const registration = await this.resolveEtradeRegistration(tin); + // Re-verify the licence the customer actually chose. Without it a TIN + // holding several licences would silently snap back to eTrade's first one on + // every save, overwriting the selection with a different business's record. + const registration = await this.resolveEtradeRegistration( + tin, + dto.licenceNumber ?? company.licenceNumber ?? undefined, + ); const fresh: Partial< Record<(typeof ETRADE_SOURCED_FIELDS)[number], string> > = { diff --git a/apps/edr-freight-api/src/modules/companies/dto/etrade-response.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/etrade-response.dto.ts index bfe1f9b72..40b1f308e 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/etrade-response.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/etrade-response.dto.ts @@ -1,4 +1,4 @@ -import { CompanyRegistrationData } from "@edr/types"; +import { CompanyRegistrationData, ETradeBusinessOption } from "@edr/types"; export class ETradeResponseDto implements CompanyRegistrationData { companyName!: string; @@ -19,6 +19,7 @@ export class ETradeResponseDto implements CompanyRegistrationData { managerEmail?: string; managerPhone!: string; tinTaken?: boolean; + businesses?: ETradeBusinessOption[]; constructor(data: CompanyRegistrationData) { this.companyName = data.companyName; @@ -39,5 +40,6 @@ export class ETradeResponseDto implements CompanyRegistrationData { this.managerEmail = data.managerEmail; this.managerPhone = data.managerPhone; this.tinTaken = data.tinTaken; + this.businesses = data.businesses; } } diff --git a/apps/edr-freight-api/src/modules/companies/dto/fetch-etrade.dto.ts b/apps/edr-freight-api/src/modules/companies/dto/fetch-etrade.dto.ts index 466c03ed6..9ca533835 100644 --- a/apps/edr-freight-api/src/modules/companies/dto/fetch-etrade.dto.ts +++ b/apps/edr-freight-api/src/modules/companies/dto/fetch-etrade.dto.ts @@ -1,4 +1,4 @@ -import { IsString, IsNotEmpty } from "class-validator"; +import { IsString, IsNotEmpty, IsOptional, MaxLength } from "class-validator"; import { IsTin } from "../../../common/validators/is-tin.validator"; export class FetchETradeDto { @@ -6,4 +6,14 @@ export class FetchETradeDto { @IsNotEmpty() @IsTin({ message: "TIN must be exactly 10 digits" }) tin!: string; + + /** + * Which of the TIN's business licences to resolve. Omitted on the first + * lookup — the response lists them all so the customer can pick, and the pick + * comes back here. + */ + @IsOptional() + @IsString() + @MaxLength(100) + licenceNumber?: string; } diff --git a/apps/edr-freight-api/src/modules/companies/services/etrade-business-selection.spec.ts b/apps/edr-freight-api/src/modules/companies/services/etrade-business-selection.spec.ts new file mode 100644 index 000000000..0719d7fe8 --- /dev/null +++ b/apps/edr-freight-api/src/modules/companies/services/etrade-business-selection.spec.ts @@ -0,0 +1,87 @@ +import { ETradeService } from './etrade.service'; +import type { ETradeBusinessInfo, ETradeCompanyInfo } from '@edr/types'; + +/** + * A TIN routinely holds several business licences (import of vehicles, export of + * coffee, freight forwarding…), all under the same trade name. The customer + * picks one, and every later lookup has to resolve that same licence — snapping + * back to eTrade's first would silently swap their company record. + */ +const companyInfo = (): ETradeCompanyInfo => + ({ + Tin: '0045014036', + BusinessName: 'PAVE LOGISTICS AND TRADING P L C', + Businesses: [ + { + LicenceNumber: 'MT/AA/14/670/11551235/2017', + TradesName: 'PAVE LOGISTICS AND TRADING P L C', + RenewedTo: '7/7/2026', + SubGroups: [ + { Code: 66331, Description: 'Export trade in minerals' }, + ], + }, + { + LicenceNumber: 'MT/AA/14/670/128936/2007', + TradesName: 'PAVE LOGISTICS AND TRADING P L C', + RenewedTo: '7/7/2026', + SubGroups: [{ Code: 72131, Description: '(72131)Freight Forwarders' }], + }, + ], + }) as unknown as ETradeCompanyInfo; + +describe('ETradeService business selection', () => { + const build = () => { + const service = new ETradeService({} as never); + const fetched: string[] = []; + jest + .spyOn(service, 'getCompanyInfoByTin') + .mockResolvedValue(companyInfo()); + jest + .spyOn(service, 'getBusinessByLicenseNo') + .mockImplementation(async (licenceNo: string) => { + fetched.push(licenceNo); + return { LicenceNumber: licenceNo } as ETradeBusinessInfo; + }); + return { service, fetched }; + }; + + it('defaults to the first licence when none is chosen', async () => { + const { service, fetched } = build(); + await service.resolveCompanyData('0045014036'); + expect(fetched).toEqual(['MT/AA/14/670/11551235/2017']); + }); + + it('resolves the chosen licence', async () => { + const { service, fetched } = build(); + await service.resolveCompanyData('0045014036', 'MT/AA/14/670/128936/2007'); + expect(fetched).toEqual(['MT/AA/14/670/128936/2007']); + }); + + it('falls back to the first licence when the chosen one is gone', async () => { + const { service, fetched } = build(); + await service.resolveCompanyData('0045014036', 'NO/SUCH/LICENCE'); + expect(fetched).toEqual(['MT/AA/14/670/11551235/2017']); + }); + + it('lists every licence for the picker, code prefixes stripped', () => { + const { service } = build(); + const data = service.extractRegistrationData( + { LicenceNumber: 'x' } as ETradeBusinessInfo, + companyInfo(), + ); + expect(data.businesses).toEqual([ + { + licenceNumber: 'MT/AA/14/670/11551235/2017', + tradeName: 'PAVE LOGISTICS AND TRADING P L C', + activity: 'Export trade in minerals', + renewedTo: '7/7/2026', + }, + { + licenceNumber: 'MT/AA/14/670/128936/2007', + tradeName: 'PAVE LOGISTICS AND TRADING P L C', + activity: 'Freight Forwarders', + renewedTo: '7/7/2026', + }, + ]); + }); +}); diff --git a/apps/edr-freight-api/src/modules/companies/services/etrade.service.ts b/apps/edr-freight-api/src/modules/companies/services/etrade.service.ts index 3292ee2d3..45cc8e840 100644 --- a/apps/edr-freight-api/src/modules/companies/services/etrade.service.ts +++ b/apps/edr-freight-api/src/modules/companies/services/etrade.service.ts @@ -66,7 +66,15 @@ export class ETradeService { } } - async resolveCompanyData(tin: string): Promise<{ + /** + * @param licenceNumber which of the TIN's licences to resolve. Defaults to the + * first one — a TIN with several licences is only unambiguous once the + * customer has picked one (see {@link ETradeBusinessOption}). + */ + async resolveCompanyData( + tin: string, + licenceNumber?: string, + ): Promise<{ companyInfo: ETradeCompanyInfo; businessInfo: ETradeBusinessInfo | null; }> { @@ -76,10 +84,15 @@ export class ETradeService { return { companyInfo, businessInfo: null }; } - const latestBusiness = companyInfo.Businesses[0]; + // An unknown licence falls back to the first rather than 400-ing: eTrade can + // drop or renumber a licence between the customer picking it and the save + // that re-verifies it, and that must not lock them out of their own profile. + const selected = + companyInfo.Businesses.find((b) => b.LicenceNumber === licenceNumber) ?? + companyInfo.Businesses[0]; try { const businessInfo = await this.getBusinessByLicenseNo( - latestBusiness.LicenceNumber, + selected.LicenceNumber, tin, ); return { companyInfo, businessInfo }; @@ -124,6 +137,16 @@ export class ETradeService { regularPhone: businessInfo.AddressInfo?.RegularPhone || "", managerName: primaryManager?.ManagerNameEng || "", managerPhone: primaryManager?.RegularPhone || "", + businesses: (companyInfo?.Businesses ?? []).map((b) => ({ + licenceNumber: b.LicenceNumber, + tradeName: b.TradesName?.trim() || "", + activity: (b.SubGroups ?? []) + // Some descriptions repeat the code inline ("(65611)Import trade …"). + .map((g) => g.Description?.replace(/^\(\d+\)\s*/, "").trim()) + .filter(Boolean) + .join(", "), + renewedTo: b.RenewedTo || "", + })), }; } } diff --git a/apps/edr-freight-api/src/modules/payment/payments.dto.ts b/apps/edr-freight-api/src/modules/payment/payments.dto.ts index ee7b703d2..92ff7dc38 100644 --- a/apps/edr-freight-api/src/modules/payment/payments.dto.ts +++ b/apps/edr-freight-api/src/modules/payment/payments.dto.ts @@ -69,6 +69,7 @@ export class ClientActionDto { "LAUNCH_APP", "INVOKE_BRIDGE", "COLLECT_OTP", + "AWAIT_PUSH", "SHOW_BILL_REFERENCE", ], }) @@ -77,6 +78,7 @@ export class ClientActionDto { | "LAUNCH_APP" | "INVOKE_BRIDGE" | "COLLECT_OTP" + | "AWAIT_PUSH" | "SHOW_BILL_REFERENCE"; @ApiPropertyOptional({ description: "Set when type=REDIRECT (web flow)" }) @@ -104,9 +106,16 @@ export class ClientActionDto { @ApiPropertyOptional({ description: "Set when type=COLLECT_OTP (e.g. CAC Bank)" }) providerOrderId?: string; - @ApiPropertyOptional({ description: "Set when type=COLLECT_OTP" }) + @ApiPropertyOptional({ + description: "Set when type=COLLECT_OTP or type=AWAIT_PUSH", + }) message?: string; + @ApiPropertyOptional({ + description: "Set when type=AWAIT_PUSH (masked MSISDN the push prompt went to)", + }) + payerAccountMasked?: string; + @ApiPropertyOptional({ description: "Set when type=SHOW_BILL_REFERENCE (CBE bill payment)", }) diff --git a/apps/edr-freight-web/portal/src/components/onboarding/ETradeInfo.tsx b/apps/edr-freight-web/portal/src/components/onboarding/ETradeInfo.tsx index 74e267a30..56273351d 100644 --- a/apps/edr-freight-web/portal/src/components/onboarding/ETradeInfo.tsx +++ b/apps/edr-freight-web/portal/src/components/onboarding/ETradeInfo.tsx @@ -1,7 +1,17 @@ -import { Alert, Button, Loader, Stack, TextInput } from "@mantine/core"; -import { useEffect, useRef } from "react"; +import { + Alert, + Button, + Card, + Group, + Loader, + Radio, + Stack, + Text, + TextInput, +} from "@mantine/core"; +import { useEffect, useRef, useState } from "react"; import type { UseFormRegisterReturn } from "react-hook-form"; -import { AlertCircle, Download } from "lucide-react"; +import { AlertCircle, Building2, Download } from "lucide-react"; import { useETradeData } from "@/hooks/useETradeData"; import { extractApiError } from "@/utils/result"; import type { CompanyRegistrationData } from "@edr/types"; @@ -12,6 +22,8 @@ export type ETradeStatus = | "verified" | "not-found" | "taken" + /** eTrade returned several business licences; the customer must pick one. */ + | "choose-business" | "error"; interface ETradeInfoProps { @@ -36,6 +48,12 @@ interface ETradeInfoProps { * stays available for a deliberate re-verify. */ alreadyVerified?: boolean; + /** + * The licence this company already operates under, if any. Pre-selects it in + * the picker so a deliberate re-verify refreshes that same business rather + * than silently snapping to eTrade's first one. + */ + selectedLicenceNumber?: string; } // Digits, not just length: a 10-character non-numeric TIN used to fire a lookup @@ -51,6 +69,7 @@ export default function ETradeInfo({ onStatusChange, onReset, alreadyVerified, + selectedLicenceNumber, }: ETradeInfoProps) { const mutation = useETradeData(); const isLoading = mutation.isPending; @@ -61,14 +80,39 @@ export default function ETradeInfo({ // user has typed a different TIN and overwrite its fields with stale data. const requestIdRef = useRef(0); - const handleFetch = async () => { + // Which of the TIN's licences the customer operates as. A ref alongside the + // state because handleFetch is called from an effect that doesn't re-run on + // this value. + const [licence, setLicence] = useState( + selectedLicenceNumber || null, + ); + const licenceRef = useRef(licence); + licenceRef.current = licence; + + const handleFetch = async (chosen = licenceRef.current) => { if (!isValidTin(tin)) return; const requestId = ++requestIdRef.current; - const result = await mutation.mutateAsync(tin); + const result = await mutation.mutateAsync({ + tin, + licenceNumber: chosen ?? undefined, + }); if (requestIdRef.current !== requestId) return; - if (result && !result.tinTaken) { - onDataLoaded(result); - } + if (!result || result.tinTaken) return; + // Several licences and no pick yet: the registration data describes only + // eTrade's first one, so it must not be adopted as this company's record + // until the customer says which business they're acting as. + if (!chosen && (result.businesses?.length ?? 0) > 1) return; + onDataLoaded(result); + }; + + // The picker collapses to a one-line summary + "Change" once a business is + // settled on; it only stays open while the choice is still outstanding. + const [pickerOpen, setPickerOpen] = useState(false); + + const handleChooseBusiness = (value: string) => { + setLicence(value); + setPickerOpen(false); + handleFetch(value); }; // Auto-fetch as soon as the TIN reaches its full 10-digit length — only @@ -87,8 +131,12 @@ export default function ETradeInfo({ if (tin !== lastFetchedTin.current) { // TIN moved away from whatever we last fetched — that result (verified // data, "taken", or an error) no longer describes this TIN. Drop it so - // the UI doesn't keep showing the previous TIN's outcome. + // the UI doesn't keep showing the previous TIN's outcome. The licence pick + // belongs to the old TIN too, so it goes with it (via the ref as well, so + // the fetch below doesn't reuse it before the state lands). requestIdRef.current++; + licenceRef.current = null; + setLicence(null); if (mutation.data || mutation.error) { mutation.reset(); onReset?.(); @@ -116,17 +164,28 @@ export default function ETradeInfo({ : apiError.message : null; + const businesses = mutation.data?.businesses ?? []; + const chosenBusiness = businesses.find((b) => b.licenceNumber === licence); + // More than one licence and none of them picked: the lookup succeeded but + // this company's record is still undecided, so it must not read as verified. + // Matched against the list rather than `licence` alone — a saved licence + // eTrade no longer lists is not a choice among what it offers today. + const needsChoice = businesses.length > 1 && !chosenBusiness; + const showPicker = needsChoice || pickerOpen; + const status: ETradeStatus = isLoading ? "loading" : tinTaken ? "taken" - : mutation.isSuccess && mutation.data && !mutation.data.tinTaken - ? "verified" - : notFound - ? "not-found" - : errorMessage - ? "error" - : "idle"; + : needsChoice + ? "choose-business" + : mutation.isSuccess && mutation.data && !mutation.data.tinTaken + ? "verified" + : notFound + ? "not-found" + : errorMessage + ? "error" + : "idle"; const lastReportedStatus = useRef(null); useEffect(() => { @@ -142,7 +201,11 @@ export default function ETradeInfo({ const willAutoFetch = isValidTin(tin) && lastFetchedTin.current !== tin; const showLoading = isLoading || willAutoFetch; - const showRetry = isValidTin(tin) && status !== "verified" && !showLoading; + const showRetry = + isValidTin(tin) && + status !== "verified" && + status !== "choose-business" && + !showLoading; return ( @@ -170,7 +233,7 @@ export default function ETradeInfo({ className="max-w-none" variant="filled" color="edr-green" - onClick={handleFetch} + onClick={() => handleFetch()} disabled={!isValidTin(tin)} leftSection={} > @@ -179,6 +242,91 @@ export default function ETradeInfo({ )} + {businesses.length > 1 && !showPicker && chosenBusiness && ( + + + + + + + {chosenBusiness.activity || + chosenBusiness.tradeName || + chosenBusiness.licenceNumber} + + + + {chosenBusiness.licenceNumber} + + {chosenBusiness.renewedTo && ( + + · valid to {chosenBusiness.renewedTo} + + )} + + + + + + + )} + + {businesses.length > 1 && showPicker && ( + + } + color={needsChoice ? "yellow" : "blue"} + title={ + needsChoice + ? `This TIN holds ${businesses.length} business licences` + : "Change business" + } + > + Pick the business you're registering as — its licence and registered + address become this account's record. + + + + {businesses.map((b) => ( + + + + {b.activity || b.tradeName || b.licenceNumber} + + + + {b.licenceNumber} + + {b.renewedTo && ( + + · valid to {b.renewedTo} + + )} + + + } + /> + + ))} + + + + )} + {notFound && ( } diff --git a/apps/edr-freight-web/portal/src/hooks/useETradeData.ts b/apps/edr-freight-web/portal/src/hooks/useETradeData.ts index 9fbce53d9..e764ecad5 100644 --- a/apps/edr-freight-web/portal/src/hooks/useETradeData.ts +++ b/apps/edr-freight-web/portal/src/hooks/useETradeData.ts @@ -3,10 +3,17 @@ import { companiesService } from "@/services/companies.service"; import { extractApiError } from "@/utils/result"; import type { CompanyRegistrationData } from "@edr/types"; +/** + * `licenceNumber` picks which of the TIN's business licences to resolve — a TIN + * routinely holds several, and the customer says which one they operate as. + */ export function useETradeData() { return useMutation({ - mutationFn: async (tin: string): Promise => { - return companiesService.fetchETradeInfo({ tin }); + mutationFn: async (vars: { + tin: string; + licenceNumber?: string; + }): Promise => { + return companiesService.fetchETradeInfo(vars); }, onError: (error) => { const { message } = extractApiError(error); 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 21956c9a1..92e1a7118 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/CompanyProfileForm.tsx @@ -989,7 +989,9 @@ export default function CompanyProfileForm({ } if (step === "company" && !tinVerified) { setSaveError( - "We need to confirm your TIN with eTrade before continuing.", + tinStatus === "choose-business" + ? "This TIN holds more than one business licence — pick the one you're registering as." + : "We need to confirm your TIN with eTrade before continuing.", ); return; } diff --git a/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/ETradeCompanyCard.tsx b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/ETradeCompanyCard.tsx index 2634fed0d..4469009ed 100644 --- a/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/ETradeCompanyCard.tsx +++ b/apps/edr-freight-web/portal/src/pages/accounts/companyProfileForm/ETradeCompanyCard.tsx @@ -1,70 +1,27 @@ -import { Badge, Card, Group, Select, SimpleGrid, Text, TextInput } from "@mantine/core"; +import { Badge, Card, Group, SimpleGrid, Text } from "@mantine/core"; import { CheckCircle2 } from "lucide-react"; -import { Controller } from "react-hook-form"; -import type { - Control, - FieldErrors, - UseFormRegister, - UseFormWatch, -} from "react-hook-form"; -import { ETHIOPIAN_REGIONS } from "@edr/types"; +import type { UseFormWatch } from "react-hook-form"; import type { FormData } from "./schema"; import { ReadOnlyField } from "./ReadOnlyField"; /** - * One field of the verified-registration card: locked read-only once eTrade - * supplied a value, but falls back to an editable input when eTrade left it - * blank — otherwise a gap in eTrade's own data would leave the field - * permanently empty and the user stuck (zod requires all of these). + * The verified eTrade record, rendered strictly read-only. * - * A value that fails validation unlocks the same way. eTrade (or a row saved - * before the current rules) can supply something the schema rejects, and a - * rejected value rendered read-only is a step that can never be completed and - * never says why. + * Nothing here is typeable — not even a field eTrade left blank. These values + * are the government's record of the company, so a customer-typed substitute + * would be an unverified claim wearing the badge of a verified one. A gap stays + * a visible gap ("—"), and the schema no longer requires these fields, so it + * cannot block the step either. */ -function LockedField({ - label, - name, - register, - watch, - errors, -}: { - label: string; - name: keyof FormData; - register: UseFormRegister; - watch: UseFormWatch; - errors: FieldErrors; -}) { - const value = watch(name) as string | undefined; - if (value && value.trim() && !errors[name]) { - return ; - } - return ( - - ); -} - export default function ETradeCompanyCard({ tin, - register, watch, - errors, - control, }: { tin: string; - register: UseFormRegister; watch: UseFormWatch; - errors: FieldErrors; - control: Control; }) { const companyName = watch("companyName"); - const region = watch("region"); return ( @@ -88,73 +45,18 @@ export default function ETradeCompanyCard({ - + - {/* Membership of the catalog, not mere presence: eTrade's normalizer - returns null for a region it doesn't recognise, and older rows can - hold a spelling that isn't in the list. Showing such a value - read-only left the customer with a required field they could not - correct. */} - {(ETHIOPIAN_REGIONS as readonly string[]).includes(region ?? "") ? ( - - ) : ( - ( - ({ value: r, label: r }))} - error={error} - value={field.value || null} - onChange={(v) => field.onChange(v ?? "")} - onBlur={field.onBlur} - /> - )} - /> - ); -} 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 58de2bd16..d6a78f1af 100644 --- a/apps/edr-freight-web/portal/src/services/companies.service.ts +++ b/apps/edr-freight-web/portal/src/services/companies.service.ts @@ -480,7 +480,10 @@ export const companiesService = { }, /** Fetch company registration data from eTrade by TIN. */ - fetchETradeInfo: async (payload: { tin: string }): Promise => { + fetchETradeInfo: async (payload: { + tin: string; + licenceNumber?: string; + }): Promise => { const response = await client.post>( URL_CONSTANTS.COMPANIES_API.FETCH_ETRADE_INFO, payload, diff --git a/packages/types/src/freight/etrade.ts b/packages/types/src/freight/etrade.ts index 67a7de28b..6f33567e4 100644 --- a/packages/types/src/freight/etrade.ts +++ b/packages/types/src/freight/etrade.ts @@ -55,10 +55,24 @@ export interface ETradeCompanyInfo { RenewedFrom: string; RenewedTo: string; BusinessLicensingGroupMain: string | null; - SubGroups: string | null; + SubGroups: Array<{ Code: number; Description: string }> | null; }>; } +/** + * One business licence held under a TIN. A single owner routinely holds many + * (import of vehicles, export of coffee, freight forwarding…), all sharing the + * same trade name — the licensed activity is what tells them apart, so that is + * what the customer picks by. + */ +export interface ETradeBusinessOption { + licenceNumber: string; + tradeName: string; + /** The licensed activities ("Import trade in …"), joined. May be empty. */ + activity: string; + renewedTo: string; +} + export interface CompanyRegistrationData { /** * The registered organization name — `ETradeCompanyInfo.BusinessName`, falling @@ -84,4 +98,11 @@ export interface CompanyRegistrationData { managerPhone: string; /** True when this TIN is already registered to an existing company. */ tinTaken?: boolean; + /** + * Every licence this TIN holds. More than one means the customer has to say + * which business they are acting as before the registration data above can be + * trusted — it describes whichever licence was selected (the first, by + * default). + */ + businesses?: ETradeBusinessOption[]; }