mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-07 21:15:41 +00:00
feat: add etrade precheck
This commit is contained in:
@@ -1183,9 +1183,11 @@ export class CompaniesService {
|
|||||||
const { businessInfo } = await this.etradeService.resolveCompanyData(tin);
|
const { businessInfo } = await this.etradeService.resolveCompanyData(tin);
|
||||||
if (!businessInfo) {
|
if (!businessInfo) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
"No business license found for this TIN. Please check the number and try again.",
|
"We couldn't find a business license for this TIN with eTrade. Please double-check the number and try again.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return this.etradeService.extractRegistrationData(businessInfo);
|
const registrationData = this.etradeService.extractRegistrationData(businessInfo);
|
||||||
|
const tinTaken = await this.companiesRepo.existsByTin(tin);
|
||||||
|
return { ...registrationData, tinTaken };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsString, IsNotEmpty, IsOptional, IsEnum, MaxLength, Length, Matches, IsEmail } from 'class-validator';
|
import { IsString, IsNotEmpty, IsOptional, IsEnum, MaxLength, Length, IsEmail } from 'class-validator';
|
||||||
import { CompanyType, CompanyStatus } from '../entities/company.entity';
|
import { CompanyType, CompanyStatus } from '../entities/company.entity';
|
||||||
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
|
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
|
||||||
|
|
||||||
@@ -17,10 +17,7 @@ export class CreateCompanyDto {
|
|||||||
|
|
||||||
@IsString()
|
@IsString()
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@Length(10, 10)
|
@Length(10, 10, { message: 'TIN must be exactly 10 digits' })
|
||||||
@Matches(/^00\d{8}$/, {
|
|
||||||
message: 'TIN must be 10 digits starting with 00',
|
|
||||||
})
|
|
||||||
tin!: string;
|
tin!: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export class ETradeResponseDto implements CompanyRegistrationData {
|
|||||||
managerName!: string;
|
managerName!: string;
|
||||||
managerEmail?: string;
|
managerEmail?: string;
|
||||||
managerPhone!: string;
|
managerPhone!: string;
|
||||||
|
tinTaken?: boolean;
|
||||||
|
|
||||||
constructor(data: CompanyRegistrationData) {
|
constructor(data: CompanyRegistrationData) {
|
||||||
this.licenceNumber = data.licenceNumber;
|
this.licenceNumber = data.licenceNumber;
|
||||||
@@ -35,5 +36,6 @@ export class ETradeResponseDto implements CompanyRegistrationData {
|
|||||||
this.managerName = data.managerName;
|
this.managerName = data.managerName;
|
||||||
this.managerEmail = data.managerEmail;
|
this.managerEmail = data.managerEmail;
|
||||||
this.managerPhone = data.managerPhone;
|
this.managerPhone = data.managerPhone;
|
||||||
|
this.tinTaken = data.tinTaken;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { IsString, IsOptional, IsEmail, MaxLength, Length, Matches, IsEnum } from 'class-validator';
|
import { IsString, IsOptional, IsEmail, MaxLength, Length, IsEnum } from 'class-validator';
|
||||||
import { CompanyNationality } from '../entities/company.entity';
|
import { CompanyNationality } from '../entities/company.entity';
|
||||||
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
|
import { IsValidPhone } from '../../../common/validators/is-phone-number.validator';
|
||||||
|
|
||||||
@@ -34,10 +34,7 @@ export class UpdateProfileDto {
|
|||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsString()
|
@IsString()
|
||||||
@Length(10, 10)
|
@Length(10, 10, { message: 'TIN must be exactly 10 digits' })
|
||||||
@Matches(/^00\d{8}$/, {
|
|
||||||
message: 'TIN must be 10 digits starting with 00',
|
|
||||||
})
|
|
||||||
tin?: string;
|
tin?: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
import type { UseFormRegisterReturn } from "react-hook-form";
|
import type { UseFormRegisterReturn } from "react-hook-form";
|
||||||
import { AlertCircle, CheckCircle2, Download } from "lucide-react";
|
import { AlertCircle, CheckCircle2, Download, Info } from "lucide-react";
|
||||||
import { useETradeData } from "@/hooks/useETradeData";
|
import { useETradeData } from "@/hooks/useETradeData";
|
||||||
|
import { extractApiError } from "@/utils/result";
|
||||||
import type { CompanyRegistrationData } from "@edr/types";
|
import type { CompanyRegistrationData } from "@edr/types";
|
||||||
|
|
||||||
interface ETradeInfoProps {
|
interface ETradeInfoProps {
|
||||||
@@ -22,6 +24,8 @@ interface ETradeInfoProps {
|
|||||||
onDataLoaded: (data: CompanyRegistrationData) => void;
|
onDataLoaded: (data: CompanyRegistrationData) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isValidTin = (tin: string) => tin.length === 10;
|
||||||
|
|
||||||
export default function ETradeInfo({
|
export default function ETradeInfo({
|
||||||
tin,
|
tin,
|
||||||
register,
|
register,
|
||||||
@@ -30,53 +34,100 @@ export default function ETradeInfo({
|
|||||||
}: ETradeInfoProps) {
|
}: ETradeInfoProps) {
|
||||||
const mutation = useETradeData();
|
const mutation = useETradeData();
|
||||||
const isLoading = mutation.isPending;
|
const isLoading = mutation.isPending;
|
||||||
const hasData = mutation.data;
|
const tinTaken = mutation.data?.tinTaken;
|
||||||
|
const hasData =
|
||||||
|
mutation.data && !mutation.data.tinTaken ? mutation.data : null;
|
||||||
|
|
||||||
const handleFetch = async () => {
|
const handleFetch = async () => {
|
||||||
if (!tin || tin.length !== 10 || !tin.startsWith("00")) return;
|
if (!isValidTin(tin)) return;
|
||||||
const result = await mutation.mutateAsync(tin);
|
const result = await mutation.mutateAsync(tin);
|
||||||
if (result) {
|
if (result && !result.tinTaken) {
|
||||||
onDataLoaded(result);
|
onDataLoaded(result);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const errorMessage =
|
// Auto-fetch as soon as the TIN reaches its full 10-digit length — only
|
||||||
|
// once per distinct value, so retyping the same TIN doesn't refetch.
|
||||||
|
const lastFetchedTin = useRef<string | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (isValidTin(tin) && lastFetchedTin.current !== tin) {
|
||||||
|
lastFetchedTin.current = tin;
|
||||||
|
handleFetch();
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [tin]);
|
||||||
|
|
||||||
|
const apiError =
|
||||||
mutation.isError && mutation.error
|
mutation.isError && mutation.error
|
||||||
? (mutation.error as any).message ||
|
? extractApiError(mutation.error)
|
||||||
"Failed to fetch company information. Please try again."
|
: null;
|
||||||
|
// A 400 here means eTrade simply has no record for this TIN — not a
|
||||||
|
// failure. Soft-pedal it as an FYI, not a red error, so filling in
|
||||||
|
// manually doesn't feel like something went wrong.
|
||||||
|
const notFound = apiError?.statusCode === 400;
|
||||||
|
const errorMessage =
|
||||||
|
apiError && !notFound
|
||||||
|
? apiError.message ||
|
||||||
|
"We couldn't reach eTrade to fetch your company information. Please try again, or fill in the details manually below."
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
<Group align="flex-start" grow>
|
<Group align="flex-start" grow>
|
||||||
<TextInput
|
<TextInput
|
||||||
label={<>TIN Number (10 digits) <span style={{ color: "var(--mantine-color-red-6)" }}>*</span></>}
|
label={
|
||||||
|
<>
|
||||||
|
TIN Number (10 digits){" "}
|
||||||
|
<span style={{ color: "var(--mantine-color-red-6)" }}>*</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
placeholder="0012345678"
|
placeholder="0012345678"
|
||||||
maxLength={10}
|
maxLength={10}
|
||||||
error={error}
|
error={error}
|
||||||
{...register}
|
{...register}
|
||||||
/>
|
/>
|
||||||
<Button
|
{errorMessage && (
|
||||||
variant="filled"
|
<Button
|
||||||
color="edr-green"
|
variant="filled"
|
||||||
onClick={handleFetch}
|
color="edr-green"
|
||||||
disabled={!tin || tin.length !== 10 || !tin.startsWith("00") || isLoading}
|
onClick={handleFetch}
|
||||||
leftSection={
|
disabled={!isValidTin(tin) || isLoading}
|
||||||
isLoading ? <Loader size={16} /> : <Download size={16} />
|
leftSection={
|
||||||
}
|
isLoading ? <Loader size={16} /> : <Download size={16} />
|
||||||
mt="24px"
|
}
|
||||||
>
|
mt="24px"
|
||||||
{isLoading ? "Getting..." : "Get Data"}
|
>
|
||||||
</Button>
|
{isLoading ? "Getting..." : "Get Data"}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
|
{notFound && (
|
||||||
|
<Alert icon={<Info size={16} />} color="gray">
|
||||||
|
We couldn't find a matching business record for this TIN — no
|
||||||
|
problem, just fill in the details below.
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<Alert
|
<Alert
|
||||||
icon={<AlertCircle size={16} />}
|
icon={<AlertCircle size={16} />}
|
||||||
color="red"
|
color="red"
|
||||||
title="Failed to fetch data"
|
title="Couldn't fetch eTrade data"
|
||||||
>
|
>
|
||||||
{errorMessage} You can still fill in the details manually below.
|
{errorMessage}
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{tinTaken && (
|
||||||
|
<Alert
|
||||||
|
icon={<AlertCircle size={16} />}
|
||||||
|
color="red"
|
||||||
|
title="TIN already registered"
|
||||||
|
>
|
||||||
|
This TIN is already registered to another company account. Please
|
||||||
|
double-check the number, or contact support if you believe this is a
|
||||||
|
mistake.
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -450,8 +450,6 @@ export default function CompanyProfileForm({
|
|||||||
onDataLoaded={handleETradeDataLoaded}
|
onDataLoaded={handleETradeDataLoaded}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Divider my="sm" />
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Company Name"
|
label="Company Name"
|
||||||
placeholder="Global Logistics Ltd"
|
placeholder="Global Logistics Ltd"
|
||||||
@@ -507,7 +505,7 @@ export default function CompanyProfileForm({
|
|||||||
from eTrade · read-only
|
from eTrade · read-only
|
||||||
</Text>
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
<SimpleGrid cols={2} spacing="md">
|
<SimpleGrid cols={2} spacing="sm">
|
||||||
<ReadOnlyField
|
<ReadOnlyField
|
||||||
label="License Number"
|
label="License Number"
|
||||||
value={watch("licenceNumber")}
|
value={watch("licenceNumber")}
|
||||||
|
|||||||
@@ -76,4 +76,6 @@ export interface CompanyRegistrationData {
|
|||||||
managerName: string;
|
managerName: string;
|
||||||
managerEmail?: string;
|
managerEmail?: string;
|
||||||
managerPhone: string;
|
managerPhone: string;
|
||||||
|
/** True when this TIN is already registered to an existing company. */
|
||||||
|
tinTaken?: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user