feat: add eTrade fields to company entity and onboarding process

- Added new fields to the Company entity: licenceNumber, statusDescription, dateRegistered, renewedFrom, renewalDate, renewedTo, region, zone, woreda, kebele, houseNo, and etradePhone.
- Updated onboarding wizard to include a new contact step and fetch company information from eTrade using TIN.
- Created ETradeInfo component to handle fetching and displaying eTrade data.
- Implemented ETradeService to interact with eTrade API and extract relevant company registration data.
- Added new DTOs for fetching eTrade data and handling responses.
- Updated CompanyProfileForm to integrate new fields and handle eTrade data.
- Created hooks for managing eTrade data fetching and error handling.
This commit is contained in:
Marshal
2026-06-20 10:27:41 +00:00
parent 4a0085ef57
commit 1f92a04ecf
19 changed files with 1088 additions and 75 deletions

View File

@@ -0,0 +1,109 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddETradeFieldsToCompanies1791000000003
implements MigrationInterface
{
name = "AddETradeFieldsToCompanies1791000000003";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS licence_number varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS status_description text;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS date_registered varchar(50);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS renewed_from varchar(50);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS renewal_date varchar(50);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS renewed_to varchar(50);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS region varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS zone varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS woreda varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS kebele varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS house_no varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS etrade_phone varchar(20);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS licence_number;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS status_description;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS date_registered;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS renewed_from;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS renewal_date;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS renewed_to;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS region;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS zone;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS woreda;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS kebele;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS house_no;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS etrade_phone;
`);
}
}

View File

@@ -38,6 +38,8 @@ import { CompanyInfoResponseDto } from "./dto/company-info-response.dto";
import { UpdateProfileDto } from "./dto/update-profile.dto"; import { UpdateProfileDto } from "./dto/update-profile.dto";
import { ProfileResponseDto } from "./dto/profile-response.dto"; import { ProfileResponseDto } from "./dto/profile-response.dto";
import { DashboardSummaryResponseDto } from "./dto/dashboard-summary-response.dto"; import { DashboardSummaryResponseDto } from "./dto/dashboard-summary-response.dto";
import { FetchETradeDto } from "./dto/fetch-etrade.dto";
import { ETradeResponseDto } from "./dto/etrade-response.dto";
interface CurrentIamUser { interface CurrentIamUser {
id: string; id: string;
@@ -85,6 +87,15 @@ export class CompaniesController {
return this.companiesService.getDashboardSummary(user.id); return this.companiesService.getDashboardSummary(user.id);
} }
@Post("fetch-etrade-info")
@ApiOperation({ summary: "Fetch company info from eTrade by TIN" })
async fetchETradeInfo(
@Body() dto: FetchETradeDto,
): Promise<ETradeResponseDto> {
const data = await this.companiesService.fetchETradeData(dto.tin);
return new ETradeResponseDto(data);
}
@Patch("profile") @Patch("profile")
@ApiOperation({ summary: "Update profile (flattened settings page)" }) @ApiOperation({ summary: "Update profile (flattened settings page)" })
async updateProfile( async updateProfile(

View File

@@ -1,5 +1,6 @@
import { Module } from "@nestjs/common"; import { Module } from "@nestjs/common";
import { TypeOrmModule } from "@nestjs/typeorm"; import { TypeOrmModule } from "@nestjs/typeorm";
import { HttpModule } from "@nestjs/axios";
import { FilesModule } from "../files/files.module"; import { FilesModule } from "../files/files.module";
import { MinioModule } from "../minio/minio.module"; import { MinioModule } from "../minio/minio.module";
import { CompaniesController } from "./companies.controller"; import { CompaniesController } from "./companies.controller";
@@ -12,10 +13,12 @@ import { ExternalProfile } from "./entities/external-profile.entity";
import { CompanyProfile } from "./entities/company-profile.entity"; import { CompanyProfile } from "./entities/company-profile.entity";
import { Booking } from "../bookings/entities/booking.entity"; import { Booking } from "../bookings/entities/booking.entity";
import { CompanyProfileRepository } from "./company-profile.repository"; import { CompanyProfileRepository } from "./company-profile.repository";
import { ETradeService } from "./services/etrade.service";
@Module({ @Module({
imports: [ imports: [
TypeOrmModule.forFeature([Company, ExternalProfile, CompanyProfile, Booking]), TypeOrmModule.forFeature([Company, ExternalProfile, CompanyProfile, Booking]),
HttpModule,
FilesModule, FilesModule,
MinioModule, MinioModule,
], ],
@@ -26,6 +29,7 @@ import { CompanyProfileRepository } from "./company-profile.repository";
ExternalProfileRepository, ExternalProfileRepository,
CompanyProfileRepository, CompanyProfileRepository,
CompanyDashboardRepository, CompanyDashboardRepository,
ETradeService,
], ],
exports: [CompaniesService], exports: [CompaniesService],
}) })

View File

@@ -9,6 +9,7 @@ import { CompanyProfileRepository } from "./company-profile.repository";
import { ExternalProfileRepository } from "./external-profile.repository"; import { ExternalProfileRepository } from "./external-profile.repository";
import { CompanyDashboardRepository } from "./company-dashboard.repository"; import { CompanyDashboardRepository } from "./company-dashboard.repository";
import { MinioService } from "../minio/minio.service"; import { MinioService } from "../minio/minio.service";
import { ETradeService } from "./services/etrade.service";
import { CreateCompanyDto } from "./dto/create-company.dto"; import { CreateCompanyDto } from "./dto/create-company.dto";
import { UpdateCompanyDto } from "./dto/update-company.dto"; import { UpdateCompanyDto } from "./dto/update-company.dto";
import { CreateExternalProfileDto } from "./dto/create-external-profile.dto"; import { CreateExternalProfileDto } from "./dto/create-external-profile.dto";
@@ -46,6 +47,7 @@ export class CompaniesService {
private readonly profilesRepo: ExternalProfileRepository, private readonly profilesRepo: ExternalProfileRepository,
private readonly dashboardRepo: CompanyDashboardRepository, private readonly dashboardRepo: CompanyDashboardRepository,
private readonly minioService: MinioService, private readonly minioService: MinioService,
private readonly etradeService: ETradeService,
) { } ) { }
async createCompany(dto: CreateCompanyDto): Promise<Company> { async createCompany(dto: CreateCompanyDto): Promise<Company> {
@@ -496,6 +498,10 @@ export class CompaniesService {
if (dto.contactPersonName !== undefined) if (dto.contactPersonName !== undefined)
attrUpdates.contactPersonName = dto.contactPersonName; attrUpdates.contactPersonName = dto.contactPersonName;
if (dto.contactPersonPosition !== undefined)
attrUpdates.contactPersonPosition = dto.contactPersonPosition;
if (dto.contactPersonEmail !== undefined)
attrUpdates.contactPersonEmail = dto.contactPersonEmail;
if (dto.contactPersonPhone !== undefined) if (dto.contactPersonPhone !== undefined)
attrUpdates.contactPersonPhone = dto.contactPersonPhone; attrUpdates.contactPersonPhone = dto.contactPersonPhone;
if (dto.generalManagerName !== undefined) if (dto.generalManagerName !== undefined)
@@ -511,6 +517,31 @@ export class CompaniesService {
attrUpdates.poaLocation = dto.poaLocation; attrUpdates.poaLocation = dto.poaLocation;
if (dto.poaAddress !== undefined) attrUpdates.poaAddress = dto.poaAddress; if (dto.poaAddress !== undefined) attrUpdates.poaAddress = dto.poaAddress;
if (dto.licenceNumber !== undefined)
companyUpdates.licenceNumber = dto.licenceNumber;
if (dto.statusDescription !== undefined)
companyUpdates.statusDescription = dto.statusDescription;
if (dto.dateRegistered !== undefined)
companyUpdates.dateRegistered = dto.dateRegistered;
if (dto.renewedFrom !== undefined)
companyUpdates.renewedFrom = dto.renewedFrom;
if (dto.renewalDate !== undefined)
companyUpdates.renewalDate = dto.renewalDate;
if (dto.renewedTo !== undefined)
companyUpdates.renewedTo = dto.renewedTo;
if (dto.region !== undefined)
companyUpdates.region = dto.region;
if (dto.zone !== undefined)
companyUpdates.zone = dto.zone;
if (dto.woreda !== undefined)
companyUpdates.woreda = dto.woreda;
if (dto.kebele !== undefined)
companyUpdates.kebele = dto.kebele;
if (dto.houseNo !== undefined)
companyUpdates.houseNo = dto.houseNo;
if (dto.etradePhone !== undefined)
companyUpdates.etradePhone = dto.etradePhone;
companyUpdates.attributes = attrUpdates; companyUpdates.attributes = attrUpdates;
const updated = await this.companiesRepo.update(company.id, companyUpdates); const updated = await this.companiesRepo.update(company.id, companyUpdates);
@@ -892,4 +923,14 @@ export class CompaniesService {
return null; return null;
} }
} }
async fetchETradeData(tin: string) {
const { businessInfo } = await this.etradeService.resolveCompanyData(tin);
if (!businessInfo) {
throw new BadRequestException(
"No business license found for this TIN. Please check the number and try again.",
);
}
return this.etradeService.extractRegistrationData(businessInfo);
}
} }

View File

@@ -0,0 +1,39 @@
import { CompanyRegistrationData } from "@edr/types";
export class ETradeResponseDto implements CompanyRegistrationData {
licenceNumber!: string;
statusDescription!: string;
dateRegistered!: string;
renewedFrom!: string;
renewalDate!: string;
renewedTo!: string;
region!: string;
zone!: string;
woreda!: string;
kebele!: string;
houseNo!: string;
mobilePhone!: string;
regularPhone!: string;
managerName!: string;
managerEmail?: string;
managerPhone!: string;
constructor(data: CompanyRegistrationData) {
this.licenceNumber = data.licenceNumber;
this.statusDescription = data.statusDescription;
this.dateRegistered = data.dateRegistered;
this.renewedFrom = data.renewedFrom;
this.renewalDate = data.renewalDate;
this.renewedTo = data.renewedTo;
this.region = data.region;
this.zone = data.zone;
this.woreda = data.woreda;
this.kebele = data.kebele;
this.houseNo = data.houseNo;
this.mobilePhone = data.mobilePhone;
this.regularPhone = data.regularPhone;
this.managerName = data.managerName;
this.managerEmail = data.managerEmail;
this.managerPhone = data.managerPhone;
}
}

View File

@@ -0,0 +1,8 @@
import { IsString, IsNotEmpty, Length } from "class-validator";
export class FetchETradeDto {
@IsString()
@IsNotEmpty()
@Length(10, 10, { message: "TIN must be exactly 10 digits" })
tin!: string;
}

View File

@@ -17,7 +17,22 @@ export class ProfileResponseDto {
companyProfiles: ResponseCompanyProfileDto[]; companyProfiles: ResponseCompanyProfileDto[];
licenceNumber: string | null;
statusDescription: string | null;
dateRegistered: string | null;
renewedFrom: string | null;
renewalDate: string | null;
renewedTo: string | null;
region: string | null;
zone: string | null;
woreda: string | null;
kebele: string | null;
houseNo: string | null;
etradePhone: string | null;
contactPersonName: string | null; contactPersonName: string | null;
contactPersonPosition: string | null;
contactPersonEmail: string | null;
contactPersonPhone: string | null; contactPersonPhone: string | null;
generalManagerName: string | null; generalManagerName: string | null;
generalManagerEmail: string | null; generalManagerEmail: string | null;
@@ -48,8 +63,23 @@ export class ProfileResponseDto {
this.fanNumber = company.fanNumber ?? null; this.fanNumber = company.fanNumber ?? null;
this.profileId = profile.id; this.profileId = profile.id;
this.licenceNumber = company.licenceNumber ?? null;
this.statusDescription = company.statusDescription ?? null;
this.dateRegistered = company.dateRegistered ?? null;
this.renewedFrom = company.renewedFrom ?? null;
this.renewalDate = company.renewalDate ?? null;
this.renewedTo = company.renewedTo ?? null;
this.region = company.region ?? null;
this.zone = company.zone ?? null;
this.woreda = company.woreda ?? null;
this.kebele = company.kebele ?? null;
this.houseNo = company.houseNo ?? null;
this.etradePhone = company.etradePhone ?? null;
const attrs = company.attributes ?? {}; const attrs = company.attributes ?? {};
this.contactPersonName = attrs.contactPersonName ?? null; this.contactPersonName = attrs.contactPersonName ?? null;
this.contactPersonPosition = attrs.contactPersonPosition ?? null;
this.contactPersonEmail = attrs.contactPersonEmail ?? null;
this.contactPersonPhone = attrs.contactPersonPhone ?? null; this.contactPersonPhone = attrs.contactPersonPhone ?? null;
this.generalManagerName = attrs.generalManagerName ?? null; this.generalManagerName = attrs.generalManagerName ?? null;
this.generalManagerEmail = attrs.generalManagerEmail ?? null; this.generalManagerEmail = attrs.generalManagerEmail ?? null;

View File

@@ -50,6 +50,14 @@ export class UpdateProfileDto {
@IsString() @IsString()
contactPersonName?: string; contactPersonName?: string;
@IsOptional()
@IsString()
contactPersonPosition?: string;
@IsOptional()
@IsEmail()
contactPersonEmail?: string;
@IsOptional() @IsOptional()
@IsString() @IsString()
contactPersonPhone?: string; contactPersonPhone?: string;
@@ -85,4 +93,63 @@ export class UpdateProfileDto {
@IsOptional() @IsOptional()
@IsString() @IsString()
poaAddress?: string; poaAddress?: string;
@IsOptional()
@IsString()
@MaxLength(100)
licenceNumber?: string;
@IsOptional()
@IsString()
statusDescription?: string;
@IsOptional()
@IsString()
@MaxLength(50)
dateRegistered?: string;
@IsOptional()
@IsString()
@MaxLength(50)
renewedFrom?: string;
@IsOptional()
@IsString()
@MaxLength(50)
renewalDate?: string;
@IsOptional()
@IsString()
@MaxLength(50)
renewedTo?: string;
@IsOptional()
@IsString()
@MaxLength(100)
region?: string;
@IsOptional()
@IsString()
@MaxLength(100)
zone?: string;
@IsOptional()
@IsString()
@MaxLength(100)
woreda?: string;
@IsOptional()
@IsString()
@MaxLength(100)
kebele?: string;
@IsOptional()
@IsString()
@MaxLength(100)
houseNo?: string;
@IsOptional()
@IsString()
@MaxLength(20)
etradePhone?: string;
} }

View File

@@ -117,6 +117,67 @@ export class Company extends BaseEntity {
@Column({ name: "attributes", type: "jsonb", nullable: true }) @Column({ name: "attributes", type: "jsonb", nullable: true })
attributes?: Record<string, any> | null; attributes?: Record<string, any> | null;
@Column({
name: "licence_number",
type: "varchar",
length: 100,
nullable: true,
})
licenceNumber?: string | null;
@Column({ name: "status_description", type: "text", nullable: true })
statusDescription?: string | null;
@Column({
name: "date_registered",
type: "varchar",
length: 50,
nullable: true,
})
dateRegistered?: string | null;
@Column({
name: "renewed_from",
type: "varchar",
length: 50,
nullable: true,
})
renewedFrom?: string | null;
@Column({
name: "renewal_date",
type: "varchar",
length: 50,
nullable: true,
})
renewalDate?: string | null;
@Column({
name: "renewed_to",
type: "varchar",
length: 50,
nullable: true,
})
renewedTo?: string | null;
@Column({ name: "region", type: "varchar", length: 100, nullable: true })
region?: string | null;
@Column({ name: "zone", type: "varchar", length: 100, nullable: true })
zone?: string | null;
@Column({ name: "woreda", type: "varchar", length: 100, nullable: true })
woreda?: string | null;
@Column({ name: "kebele", type: "varchar", length: 100, nullable: true })
kebele?: string | null;
@Column({ name: "house_no", type: "varchar", length: 100, nullable: true })
houseNo?: string | null;
@Column({ name: "etrade_phone", type: "varchar", length: 20, nullable: true })
etradePhone?: string | null;
@OneToMany(() => ExternalProfile, (profile) => profile.company) @OneToMany(() => ExternalProfile, (profile) => profile.company)
profiles?: ExternalProfile[]; profiles?: ExternalProfile[];

View File

@@ -0,0 +1,102 @@
import { Injectable, BadRequestException } from "@nestjs/common";
import { HttpService } from "@nestjs/axios";
import { firstValueFrom } from "rxjs";
import {
ETradeCompanyInfo,
ETradeBusinessInfo,
CompanyRegistrationData,
} from "@edr/types";
@Injectable()
export class ETradeService {
private readonly baseUrl = "https://etrade.gov.et/api";
private readonly referer = "https://etrade.gov.et/business-license-checker";
constructor(private readonly httpService: HttpService) {}
async getCompanyInfoByTin(tin: string): Promise<ETradeCompanyInfo> {
const url = `${this.baseUrl}/Registration/GetRegistrationInfoByTin/${tin}/en`;
try {
const response = await firstValueFrom(
this.httpService.get<ETradeCompanyInfo>(url, {
headers: { Referer: this.referer },
}),
);
return response.data;
} catch (error: any) {
throw new BadRequestException(
`Failed to fetch company info from eTrade: ${error.message}`,
);
}
}
async getBusinessByLicenseNo(
licenseNo: string,
tin: string,
): Promise<ETradeBusinessInfo> {
const url = `${this.baseUrl}/BusinessMain/GetBusinessByLicenseNo`;
try {
const response = await firstValueFrom(
this.httpService.get<ETradeBusinessInfo>(url, {
params: {
LicenseNo: licenseNo,
Tin: tin,
Lang: "en",
},
headers: { Referer: this.referer },
}),
);
return response.data;
} catch (error: any) {
throw new BadRequestException(
`Failed to fetch business info from eTrade: ${error.message}`,
);
}
}
async resolveCompanyData(tin: string): Promise<{
companyInfo: ETradeCompanyInfo;
businessInfo: ETradeBusinessInfo | null;
}> {
const companyInfo = await this.getCompanyInfoByTin(tin);
if (!companyInfo.Businesses || companyInfo.Businesses.length === 0) {
return { companyInfo, businessInfo: null };
}
const latestBusiness = companyInfo.Businesses[0];
try {
const businessInfo = await this.getBusinessByLicenseNo(
latestBusiness.LicenceNumber,
tin,
);
return { companyInfo, businessInfo };
} catch {
return { companyInfo, businessInfo: null };
}
}
extractRegistrationData(
businessInfo: ETradeBusinessInfo,
): CompanyRegistrationData {
const primaryManager = businessInfo.AssociateShortInfos?.[0];
return {
licenceNumber: businessInfo.LicenceNumber,
statusDescription: businessInfo.StatusDescription,
dateRegistered: businessInfo.DateRegistered,
renewedFrom: businessInfo.RenewedFrom,
renewalDate: businessInfo.RenewalDate,
renewedTo: businessInfo.RenewedTo,
region: businessInfo.AddressInfo?.Region || "",
zone: businessInfo.AddressInfo?.Zone || "",
woreda: businessInfo.AddressInfo?.Woreda || "",
kebele: businessInfo.AddressInfo?.Kebele || "",
houseNo: businessInfo.AddressInfo?.HouseNo || "",
mobilePhone: businessInfo.AddressInfo?.MobilePhone || "",
regularPhone: businessInfo.AddressInfo?.RegularPhone || "",
managerName: primaryManager?.ManagerNameEng || "",
managerPhone: primaryManager?.RegularPhone || "",
};
}
}

View File

@@ -0,0 +1,96 @@
import {
Alert,
Button,
Group,
Loader,
SimpleGrid,
Stack,
Text,
TextInput,
} from "@mantine/core";
import { AlertCircle, CheckCircle2, RefreshCw } from "lucide-react";
import { useETradeData } from "@/hooks/useETradeData";
import type { CompanyRegistrationData } from "@edr/types";
interface ETradeInfoProps {
tin: string;
onDataLoaded: (data: CompanyRegistrationData) => void;
}
export default function ETradeInfo({ tin, onDataLoaded }: ETradeInfoProps) {
const mutation = useETradeData();
const isLoading = mutation.isPending;
const hasError = mutation.isError;
const hasData = mutation.data;
const handleFetch = async () => {
if (!tin || tin.length !== 10) return;
const result = await mutation.mutateAsync(tin);
if (result) {
onDataLoaded(result);
}
};
const errorMessage =
hasError && mutation.error
? (mutation.error as any).message ||
"Failed to fetch company information. Please try again."
: null;
return (
<Stack gap="md">
<Group grow>
<TextInput
label="TIN Number"
placeholder="1234567890"
value={tin}
disabled
readOnly
/>
<Button
variant="filled"
color="edr-green"
onClick={handleFetch}
disabled={!tin || tin.length !== 10 || isLoading}
leftSection={isLoading ? <Loader size={16} /> : <RefreshCw size={16} />}
mt="24px"
>
{isLoading ? "Fetching..." : "Fetch Info from eTrade"}
</Button>
</Group>
{hasError && errorMessage && (
<Alert
icon={<AlertCircle size={16} />}
color="red"
title="Failed to fetch data"
>
{errorMessage}
</Alert>
)}
{hasData && (
<Alert
icon={<CheckCircle2 size={16} />}
color="green"
title="Company information loaded"
>
<Stack gap={0}>
<Text size="sm">
<strong>License:</strong> {hasData.licenceNumber}
</Text>
<Text size="sm">
<strong>Status:</strong> {hasData.statusDescription}
</Text>
{hasData.region && (
<Text size="sm">
<strong>Location:</strong> {hasData.kebele}, {hasData.woreda},{" "}
{hasData.zone}, {hasData.region}
</Text>
)}
</Stack>
</Alert>
)}
</Stack>
);
}

View File

@@ -20,10 +20,17 @@ import NationalitySelect from "@/pages/settings/NationalitySelect";
import OnboardingRoleSelect from "@/pages/settings/OnboardingRoleSelect"; import OnboardingRoleSelect from "@/pages/settings/OnboardingRoleSelect";
/** Form steps shared by CompanyProfileForm and ForwarderForm. */ /** Form steps shared by CompanyProfileForm and ForwarderForm. */
type FormStep = "company" | "personnel" | "poa" | "documents" | "additional"; type FormStep =
| "company"
| "personnel"
| "contact"
| "poa"
| "documents"
| "additional";
const FORM_STEPS: FormStep[] = [ const FORM_STEPS: FormStep[] = [
"company", "company",
"personnel", "personnel",
"contact",
"poa", "poa",
"documents", "documents",
"additional", "additional",

View File

@@ -90,6 +90,7 @@ export const URL_CONSTANTS = {
ONBOARDING_STEP: "/api/companies/onboarding-step", ONBOARDING_STEP: "/api/companies/onboarding-step",
ONBOARDING_COMPLETE: "/api/companies/onboarding/complete", ONBOARDING_COMPLETE: "/api/companies/onboarding/complete",
DASHBOARD: "/api/companies/dashboard", DASHBOARD: "/api/companies/dashboard",
FETCH_ETRADE_INFO: "/api/companies/fetch-etrade-info",
DOCUMENTS: (id: string) => `/api/companies/${id}/documents`, DOCUMENTS: (id: string) => `/api/companies/${id}/documents`,
PROFILE_LICENSE: (profileId: string) => PROFILE_LICENSE: (profileId: string) =>
`/api/companies/company-profiles/${profileId}/license`, `/api/companies/company-profiles/${profileId}/license`,

View File

@@ -0,0 +1,16 @@
import { useMutation } from "@tanstack/react-query";
import { companiesService } from "@/services/companies.service";
import { extractApiError } from "@/utils/result";
import type { CompanyRegistrationData } from "@edr/types";
export function useETradeData() {
return useMutation({
mutationFn: async (tin: string): Promise<CompanyRegistrationData> => {
return companiesService.fetchETradeInfo({ tin });
},
onError: (error) => {
const { message } = extractApiError(error);
console.error("eTrade fetch error:", message);
},
});
}

View File

@@ -2,6 +2,7 @@ import {
Alert, Alert,
Box, Box,
Button, Button,
Checkbox,
Divider, Divider,
Group, Group,
Loader, Loader,
@@ -23,6 +24,7 @@ import {
FileText, FileText,
UploadCloud, UploadCloud,
User, User,
UserCheck,
} from "lucide-react"; } from "lucide-react";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@@ -31,6 +33,7 @@ import { z } from "zod";
import type { AuthUser } from "@/types/auth"; import type { AuthUser } from "@/types/auth";
import type { CreateCompanyPayload } from "@/services/companies.service"; import type { CreateCompanyPayload } from "@/services/companies.service";
import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile"; import type { ProfileResponse, UpdateProfilePayload } from "@/types/profile";
import type { CompanyRegistrationData } from "@edr/types";
import PhoneInput from "@/components/auth/PhoneInput"; import PhoneInput from "@/components/auth/PhoneInput";
import { SmartFileInput } from "@edr/ui-common"; import { SmartFileInput } from "@edr/ui-common";
import { api } from "@/services/api"; import { api } from "@/services/api";
@@ -38,8 +41,15 @@ import { splitPhone } from "@/utils/phone";
import RoleLicenseStep, { import RoleLicenseStep, {
type RoleLicenseProfile, type RoleLicenseProfile,
} from "@/components/onboarding/RoleLicenseStep"; } from "@/components/onboarding/RoleLicenseStep";
import ETradeInfo from "@/components/onboarding/ETradeInfo";
type CompanyStep = "company" | "personnel" | "poa" | "documents" | "additional"; type CompanyStep =
| "company"
| "personnel"
| "contact"
| "poa"
| "documents"
| "additional";
const onboardingSchema = z.object({ const onboardingSchema = z.object({
companyName: z.string().min(1, "Company name is required"), companyName: z.string().min(1, "Company name is required"),
@@ -54,7 +64,25 @@ const onboardingSchema = z.object({
.min(1, "VAT number is required") .min(1, "VAT number is required")
.length(10, "VAT number must be exactly 10 digits"), .length(10, "VAT number must be exactly 10 digits"),
fanNumber: z.string().length(16, "FAN must be exactly 16 digits"), fanNumber: z.string().length(16, "FAN must be exactly 16 digits"),
licenceNumber: z.string().optional(),
statusDescription: z.string().optional(),
dateRegistered: z.string().optional(),
renewedFrom: z.string().optional(),
renewalDate: z.string().optional(),
renewedTo: z.string().optional(),
region: z.string().optional(),
zone: z.string().optional(),
woreda: z.string().optional(),
kebele: z.string().optional(),
houseNo: z.string().optional(),
etradePhone: z.string().optional(),
contactPersonName: z.string().min(1, "Contact person name is required"), contactPersonName: z.string().min(1, "Contact person name is required"),
contactPersonPosition: z.string().optional(),
contactPersonEmail: z
.string()
.email("Invalid email address")
.optional()
.or(z.literal("")),
contactPersonPhone: z.string().min(1, "Contact person phone is required"), contactPersonPhone: z.string().min(1, "Contact person phone is required"),
contactPersonPhoneCountryCode: z.string().min(1, "Country code is required"), contactPersonPhoneCountryCode: z.string().min(1, "Country code is required"),
generalManagerName: z.string().min(1, "GM name is required"), generalManagerName: z.string().min(1, "GM name is required"),
@@ -82,16 +110,32 @@ const stepFields: Record<CompanyStep, (keyof FormData)[]> = {
"tinNumber", "tinNumber",
"vatNumber", "vatNumber",
"fanNumber", "fanNumber",
"licenceNumber",
"statusDescription",
"dateRegistered",
"renewedFrom",
"renewalDate",
"renewedTo",
"region",
"zone",
"woreda",
"kebele",
"houseNo",
"etradePhone",
], ],
personnel: [ personnel: [
"contactPersonName",
"contactPersonPhone",
"contactPersonPhoneCountryCode",
"generalManagerName", "generalManagerName",
"generalManagerEmail", "generalManagerEmail",
"generalManagerPhone", "generalManagerPhone",
"generalManagerPhoneCountryCode", "generalManagerPhoneCountryCode",
], ],
contact: [
"contactPersonName",
"contactPersonPosition",
"contactPersonEmail",
"contactPersonPhone",
"contactPersonPhoneCountryCode",
],
poa: [], poa: [],
documents: [], documents: [],
additional: [], additional: [],
@@ -109,6 +153,8 @@ function buildPayload(data: FormData, _user: AuthUser): CreateCompanyPayload {
fanNumber: data.fanNumber, fanNumber: data.fanNumber,
attributes: { attributes: {
contactPersonName: data.contactPersonName, contactPersonName: data.contactPersonName,
contactPersonPosition: data.contactPersonPosition || undefined,
contactPersonEmail: data.contactPersonEmail || undefined,
contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`, contactPersonPhone: `${data.contactPersonPhoneCountryCode}${data.contactPersonPhone}`,
generalManagerName: data.generalManagerName, generalManagerName: data.generalManagerName,
generalManagerEmail: data.generalManagerEmail, generalManagerEmail: data.generalManagerEmail,
@@ -138,15 +184,32 @@ function stepPayload(step: CompanyStep, d: FormData): Partial<UpdateProfilePaylo
tin: d.tinNumber, tin: d.tinNumber,
vatNumber: d.vatNumber, vatNumber: d.vatNumber,
fanNumber: d.fanNumber, fanNumber: d.fanNumber,
licenceNumber: d.licenceNumber,
statusDescription: d.statusDescription,
dateRegistered: d.dateRegistered,
renewedFrom: d.renewedFrom,
renewalDate: d.renewalDate,
renewedTo: d.renewedTo,
region: d.region,
zone: d.zone,
woreda: d.woreda,
kebele: d.kebele,
houseNo: d.houseNo,
etradePhone: d.etradePhone,
}; };
case "personnel": case "personnel":
return { return {
contactPersonName: d.contactPersonName,
contactPersonPhone: `${d.contactPersonPhoneCountryCode}${d.contactPersonPhone}`,
generalManagerName: d.generalManagerName, generalManagerName: d.generalManagerName,
generalManagerEmail: d.generalManagerEmail, generalManagerEmail: d.generalManagerEmail,
generalManagerPhone: `${d.generalManagerPhoneCountryCode}${d.generalManagerPhone}`, generalManagerPhone: `${d.generalManagerPhoneCountryCode}${d.generalManagerPhone}`,
}; };
case "contact":
return {
contactPersonName: d.contactPersonName,
contactPersonPosition: d.contactPersonPosition || undefined,
contactPersonEmail: d.contactPersonEmail || undefined,
contactPersonPhone: `${d.contactPersonPhoneCountryCode}${d.contactPersonPhone}`,
};
case "poa": case "poa":
return { return {
poaName: d.poaName || undefined, poaName: d.poaName || undefined,
@@ -181,7 +244,21 @@ function toFormValues(p: ProfileResponse): FormData {
tinNumber: tin, tinNumber: tin,
vatNumber: p.vatNumber ?? "", vatNumber: p.vatNumber ?? "",
fanNumber: p.fanNumber ?? "", fanNumber: p.fanNumber ?? "",
licenceNumber: p.licenceNumber ?? "",
statusDescription: p.statusDescription ?? "",
dateRegistered: p.dateRegistered ?? "",
renewedFrom: p.renewedFrom ?? "",
renewalDate: p.renewalDate ?? "",
renewedTo: p.renewedTo ?? "",
region: p.region ?? "",
zone: p.zone ?? "",
woreda: p.woreda ?? "",
kebele: p.kebele ?? "",
houseNo: p.houseNo ?? "",
etradePhone: p.etradePhone ?? "",
contactPersonName: p.contactPersonName ?? "", contactPersonName: p.contactPersonName ?? "",
contactPersonPosition: p.contactPersonPosition ?? "",
contactPersonEmail: p.contactPersonEmail ?? "",
contactPersonPhone: contactPhone.number, contactPersonPhone: contactPhone.number,
contactPersonPhoneCountryCode: contactPhone.countryCode, contactPersonPhoneCountryCode: contactPhone.countryCode,
generalManagerName: p.generalManagerName ?? "", generalManagerName: p.generalManagerName ?? "",
@@ -281,6 +358,7 @@ export default function CompanyProfileForm({
handleSubmit, handleSubmit,
trigger, trigger,
watch, watch,
setValue,
formState: { errors }, formState: { errors },
} = useForm<FormData>({ } = useForm<FormData>({
resolver: zodResolver(onboardingSchema), resolver: zodResolver(onboardingSchema),
@@ -294,7 +372,21 @@ export default function CompanyProfileForm({
tinNumber: "", tinNumber: "",
vatNumber: "", vatNumber: "",
fanNumber: "", fanNumber: "",
licenceNumber: "",
statusDescription: "",
dateRegistered: "",
renewedFrom: "",
renewalDate: "",
renewedTo: "",
region: "",
zone: "",
woreda: "",
kebele: "",
houseNo: "",
etradePhone: "",
contactPersonName: "", contactPersonName: "",
contactPersonPosition: "",
contactPersonEmail: "",
contactPersonPhone: "", contactPersonPhone: "",
contactPersonPhoneCountryCode: "+251", contactPersonPhoneCountryCode: "+251",
generalManagerName: "", generalManagerName: "",
@@ -312,8 +404,83 @@ export default function CompanyProfileForm({
values: rehydrate ? toFormValues(rehydrate) : undefined, values: rehydrate ? toFormValues(rehydrate) : undefined,
}); });
// The business owner/manager pulled from eTrade — powers "Use owner as
// manager" on the General Manager step. Null until a TIN lookup succeeds.
const [etradeOwner, setEtradeOwner] = useState<{
name: string;
phone: string;
} | null>(null);
// Mirror the two "copy from previous person" checkboxes so they can be
// re-toggled (re-checking re-pulls the latest values).
const [gmIsContact, setGmIsContact] = useState(false);
const [contactIsPoa, setContactIsPoa] = useState(false);
const handleETradeDataLoaded = (data: CompanyRegistrationData) => {
setValue("licenceNumber", data.licenceNumber);
setValue("statusDescription", data.statusDescription);
setValue("dateRegistered", data.dateRegistered);
setValue("renewedFrom", data.renewedFrom);
setValue("renewalDate", data.renewalDate);
setValue("renewedTo", data.renewedTo);
setValue("region", data.region);
setValue("zone", data.zone);
setValue("woreda", data.woreda);
setValue("kebele", data.kebele);
setValue("houseNo", data.houseNo);
setValue("etradePhone", data.regularPhone || data.mobilePhone);
setEtradeOwner({
name: data.managerName,
phone: data.managerPhone || data.regularPhone || data.mobilePhone,
});
};
/** Fill the General Manager from the eTrade business owner. */
const useOwnerAsManager = () => {
if (!etradeOwner) return;
setValue("generalManagerName", etradeOwner.name);
const { number, countryCode } = splitPhone(etradeOwner.phone);
setValue("generalManagerPhone", number);
setValue("generalManagerPhoneCountryCode", countryCode);
};
/** Copy the General Manager into the Contact Person fields (toggleable). */
const toggleGmAsContact = (checked: boolean) => {
setGmIsContact(checked);
if (!checked) return;
setValue("contactPersonName", watch("generalManagerName"));
setValue("contactPersonEmail", watch("generalManagerEmail"));
setValue("contactPersonPhone", watch("generalManagerPhone"));
setValue(
"contactPersonPhoneCountryCode",
watch("generalManagerPhoneCountryCode"),
);
};
/** Copy the Contact Person into the PoA fields (toggleable, still editable). */
const toggleContactAsPoa = (checked: boolean) => {
setContactIsPoa(checked);
if (!checked) return;
setValue("poaName", watch("contactPersonName"));
setValue("poaEmail", watch("contactPersonEmail"));
setValue("poaPhone", watch("contactPersonPhone"));
setValue("poaPhoneCountryCode", watch("contactPersonPhoneCountryCode"));
};
const hasDocuments = Boolean(uploadSetting?.fields?.length); const hasDocuments = Boolean(uploadSetting?.fields?.length);
const totalSteps = 5;
// Single source of truth for step sequence — navigation, labels and the
// progress bar all derive from this so adding/removing a step is one edit.
const stepOrder: CompanyStep[] = [
"company",
"personnel",
"contact",
"poa",
"documents",
"additional",
];
const totalSteps = stepOrder.length;
const currentIdx = stepOrder.indexOf(step);
/** Validate + persist the current step, returning whether we may advance. */ /** Validate + persist the current step, returning whether we may advance. */
const saveCurrentStep = async (): Promise<boolean> => { const saveCurrentStep = async (): Promise<boolean> => {
@@ -351,55 +518,44 @@ export default function CompanyProfileForm({
handleSubmit((data) => onSubmit(buildPayload(data, user)))(); handleSubmit((data) => onSubmit(buildPayload(data, user)))();
return; return;
} }
if (step === "documents") { // The documents step has nothing to persist; field steps validate + save
setStep("additional"); // before advancing.
return; if (step !== "documents") {
const ok = await saveCurrentStep();
if (!ok) return;
} }
// company / personnel / poa: validate + save before advancing. setStep(stepOrder[currentIdx + 1]);
const ok = await saveCurrentStep();
if (!ok) return;
setStep(
step === "company" ? "personnel" : step === "personnel" ? "poa" : "documents",
);
}; };
const prevStep = () => { const prevStep = () => {
setSaveError(null); setSaveError(null);
if (step === "company") onBack(); if (currentIdx === 0) onBack();
else if (step === "personnel") setStep("company"); else setStep(stepOrder[currentIdx - 1]);
else if (step === "poa") setStep("personnel");
else if (step === "documents") setStep("poa");
else setStep("documents");
}; };
// Back is hidden on the first step during onboarding (can't return to role // Back is hidden on the first step during onboarding (can't return to role
// selection); otherwise always available. // selection); otherwise always available.
const showBack = !(hideFirstStepBack && step === "company"); const showBack = !(hideFirstStepBack && step === "company");
const STEPS: { key: CompanyStep; icon: React.ReactNode }[] = [ const STEP_ICONS: Record<CompanyStep, React.ReactNode> = {
{ key: "company", icon: <Building2 size={18} /> }, company: <Building2 size={18} />,
{ key: "personnel", icon: <User size={18} /> }, personnel: <User size={18} />,
{ key: "poa", icon: <FileText size={18} /> }, contact: <UserCheck size={18} />,
{ key: "documents", icon: <UploadCloud size={18} /> }, poa: <FileText size={18} />,
{ key: "additional", icon: <CheckCircle2 size={18} /> }, documents: <UploadCloud size={18} />,
]; additional: <CheckCircle2 size={18} />,
const STEP_LABELS: Record<CompanyStep, string> = {
company: `Step 1 of ${totalSteps} — Company Information`,
personnel: `Step 2 of ${totalSteps} — Personnel Details`,
poa: `Step 3 of ${totalSteps} — Power of Attorney (Optional)`,
documents: `Step 4 of ${totalSteps} — Upload Documents`,
additional: `Step 5 of ${totalSteps} — Business License`,
}; };
const stepOrder: CompanyStep[] = [ const STEP_TITLES: Record<CompanyStep, string> = {
"company", company: "Company Information",
"personnel", personnel: "General Manager",
"poa", contact: "Contact Person",
"documents", poa: "Power of Attorney (Optional)",
"additional", documents: "Upload Documents",
]; additional: "Business License",
const currentIdx = stepOrder.indexOf(step); };
const stepLabel = `Step ${currentIdx + 1} of ${totalSteps}${STEP_TITLES[step]}`;
return ( return (
<> <>
@@ -421,7 +577,7 @@ export default function CompanyProfileForm({
className="relative max-w-lg mx-auto px-2" className="relative max-w-lg mx-auto px-2"
> >
<Box className="absolute top-1/2 left-2 right-2 h-0.5 bg-edr-border -translate-y-1/2 z-0" /> <Box className="absolute top-1/2 left-2 right-2 h-0.5 bg-edr-border -translate-y-1/2 z-0" />
{STEPS.map(({ key, icon }, i) => { {stepOrder.map((key, i) => {
const done = i < currentIdx; const done = i < currentIdx;
const active = i === currentIdx; const active = i === currentIdx;
return done || active ? ( return done || active ? (
@@ -433,7 +589,7 @@ export default function CompanyProfileForm({
color="edr-green" color="edr-green"
className="relative z-10" className="relative z-10"
> >
{done ? <CheckCircle2 size={18} /> : icon} {done ? <CheckCircle2 size={18} /> : STEP_ICONS[key]}
</ThemeIcon> </ThemeIcon>
) : ( ) : (
<Box <Box
@@ -443,14 +599,14 @@ export default function CompanyProfileForm({
c="edr-slate" c="edr-slate"
className="relative z-10 flex items-center justify-center rounded-full border-2 border-edr-border bg-edr-card" className="relative z-10 flex items-center justify-center rounded-full border-2 border-edr-border bg-edr-card"
> >
{icon} {STEP_ICONS[key]}
</Box> </Box>
); );
})} })}
</Group> </Group>
<Text size="sm" c="edr-muted" ta="center" mt="sm"> <Text size="sm" c="edr-muted" ta="center" mt="sm">
{STEP_LABELS[step]} {stepLabel}
</Text> </Text>
</Box> </Box>
@@ -520,38 +676,133 @@ export default function CompanyProfileForm({
error={errors.fanNumber?.message} error={errors.fanNumber?.message}
{...register("fanNumber")} {...register("fanNumber")}
/> />
<Divider my="sm" />
<Text fw={600} size="sm" c="edr-text">
Fetch Company Information from eTrade
</Text>
<ETradeInfo
tin={watch("tinNumber")}
onDataLoaded={handleETradeDataLoaded}
/>
{watch("licenceNumber") && (
<>
<Divider my="sm" />
<Text fw={600} size="sm" c="edr-text">
Registration Details from eTrade
</Text>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="License Number"
placeholder="01/23/01/19786/2006"
error={errors.licenceNumber?.message}
{...register("licenceNumber")}
/>
<TextInput
label="Status"
placeholder="Not renewed for 2 years"
error={errors.statusDescription?.message}
{...register("statusDescription")}
/>
</SimpleGrid>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="Date Registered"
placeholder="12/17/2013"
error={errors.dateRegistered?.message}
{...register("dateRegistered")}
/>
<TextInput
label="Renewal Date"
placeholder="3/17/2016"
error={errors.renewalDate?.message}
{...register("renewalDate")}
/>
</SimpleGrid>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="Renewed From"
placeholder="3/17/2016"
error={errors.renewedFrom?.message}
{...register("renewedFrom")}
/>
<TextInput
label="Renewed To"
placeholder="7/7/2016"
error={errors.renewedTo?.message}
{...register("renewedTo")}
/>
</SimpleGrid>
<Text fw={600} size="sm" c="edr-text" mt="md">
Address Information
</Text>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="Region"
placeholder="Tigray"
error={errors.region?.message}
{...register("region")}
/>
<TextInput
label="Zone"
placeholder="EASTERN TIGRAY"
error={errors.zone?.message}
{...register("zone")}
/>
</SimpleGrid>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="Woreda"
placeholder="EROB"
error={errors.woreda?.message}
{...register("woreda")}
/>
<TextInput
label="Kebele"
placeholder="ARAS"
error={errors.kebele?.message}
{...register("kebele")}
/>
</SimpleGrid>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="House No"
placeholder="House Number"
error={errors.houseNo?.message}
{...register("houseNo")}
/>
<TextInput
label="Phone"
placeholder="0355235416"
error={errors.etradePhone?.message}
{...register("etradePhone")}
/>
</SimpleGrid>
</>
)}
</> </>
)} )}
{step === "personnel" && ( {step === "personnel" && (
<> <>
<Text fw={600} size="sm" c="edr-text"> <Group justify="space-between" align="center">
Contact Person <Text fw={600} size="sm" c="edr-text">
</Text> General Manager
<SimpleGrid cols={2} spacing="md"> </Text>
<TextInput {etradeOwner && (
label="Name" <Button
placeholder="Jane Smith" variant="light"
error={errors.contactPersonName?.message} color="edr-green"
{...register("contactPersonName")} size="xs"
/> leftSection={<UserCheck size={14} />}
<PhoneInput onClick={useOwnerAsManager}
countryCode={{ ...register("contactPersonPhoneCountryCode") }} >
phone={{ Use owner as manager
...register("contactPersonPhone"), </Button>
placeholder: "912345678", )}
}} </Group>
countryCodeError={errors.contactPersonPhoneCountryCode}
phoneError={errors.contactPersonPhone}
label="Phone"
/>
</SimpleGrid>
<Divider color="edr-border" />
<Text fw={600} size="sm" c="edr-text">
General Manager
</Text>
<TextInput <TextInput
label="Name" label="Name"
placeholder="Abebe Bikila" placeholder="Abebe Bikila"
@@ -582,12 +833,65 @@ export default function CompanyProfileForm({
</> </>
)} )}
{step === "contact" && (
<>
<Text fw={600} size="sm" c="edr-text">
Contact Person
</Text>
<Checkbox
color="edr-green"
label="Use General Manager as contact person"
checked={gmIsContact}
onChange={(e) => toggleGmAsContact(e.currentTarget.checked)}
/>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="Name"
placeholder="Jane Smith"
error={errors.contactPersonName?.message}
{...register("contactPersonName")}
/>
<TextInput
label="Position (Optional)"
placeholder="Operations Lead"
error={errors.contactPersonPosition?.message}
{...register("contactPersonPosition")}
/>
</SimpleGrid>
<SimpleGrid cols={2} spacing="md">
<TextInput
label="Email (Optional)"
type="email"
placeholder="contact@company.com"
error={errors.contactPersonEmail?.message}
{...register("contactPersonEmail")}
/>
<PhoneInput
countryCode={{ ...register("contactPersonPhoneCountryCode") }}
phone={{
...register("contactPersonPhone"),
placeholder: "912345678",
}}
countryCodeError={errors.contactPersonPhoneCountryCode}
phoneError={errors.contactPersonPhone}
label="Phone"
/>
</SimpleGrid>
</>
)}
{step === "poa" && ( {step === "poa" && (
<> <>
<Text size="sm" c="edr-muted"> <Text size="sm" c="edr-muted">
Power of Attorney details are optional. Fill them in if you have Power of Attorney details are optional. Fill them in if you have
them, or skip to continue. them, or skip to continue.
</Text> </Text>
<Checkbox
color="edr-green"
label="Use contact person as Power of Attorney"
checked={contactIsPoa}
onChange={(e) => toggleContactAsPoa(e.currentTarget.checked)}
/>
<TextInput <TextInput
label="PoA Name" label="PoA Name"
placeholder="Authorized Representative Name" placeholder="Authorized Representative Name"

View File

@@ -266,4 +266,13 @@ export const companiesService = {
); );
return unwrap(response.data); return unwrap(response.data);
}, },
/** Fetch company registration data from eTrade by TIN. */
fetchETradeInfo: async (payload: { tin: string }): Promise<any> => {
const response = await client.post<ApiResponse<any>>(
URL_CONSTANTS.COMPANIES_API.FETCH_ETRADE_INFO,
payload,
);
return unwrap(response.data);
},
}; };

View File

@@ -13,7 +13,21 @@ export interface ProfileResponse {
tinNumber: string; tinNumber: string;
vatNumber: string | null; vatNumber: string | null;
fanNumber: string | null; fanNumber: string | null;
licenceNumber?: string | null;
statusDescription?: string | null;
dateRegistered?: string | null;
renewedFrom?: string | null;
renewalDate?: string | null;
renewedTo?: string | null;
region?: string | null;
zone?: string | null;
woreda?: string | null;
kebele?: string | null;
houseNo?: string | null;
etradePhone?: string | null;
contactPersonName: string | null; contactPersonName: string | null;
contactPersonPosition: string | null;
contactPersonEmail: string | null;
contactPersonPhone: string | null; contactPersonPhone: string | null;
generalManagerName: string | null; generalManagerName: string | null;
generalManagerEmail: string | null; generalManagerEmail: string | null;
@@ -36,7 +50,21 @@ export interface UpdateProfilePayload {
tin?: string; tin?: string;
vatNumber?: string; vatNumber?: string;
fanNumber?: string; fanNumber?: string;
licenceNumber?: string;
statusDescription?: string;
dateRegistered?: string;
renewedFrom?: string;
renewalDate?: string;
renewedTo?: string;
region?: string;
zone?: string;
woreda?: string;
kebele?: string;
houseNo?: string;
etradePhone?: string;
contactPersonName?: string; contactPersonName?: string;
contactPersonPosition?: string;
contactPersonEmail?: string;
contactPersonPhone?: string; contactPersonPhone?: string;
generalManagerName?: string; generalManagerName?: string;
generalManagerEmail?: string; generalManagerEmail?: string;

View File

@@ -0,0 +1,79 @@
export interface ETradeAddressInfo {
Region: string;
Zone: string;
Woreda: string;
Kebele: string;
HouseNo: string;
MobilePhone: string;
RegularPhone: string;
}
export interface ETradeAssociateInfo {
Position: string | null;
ManagerName: string;
ManagerNameEng: string;
Photo: string | null;
MobilePhone: string | null;
RegularPhone: string | null;
}
export interface ETradeBusinessInfo {
MainGuid: string;
OwnerTIN: string;
DateRegistered: string;
TradeName: string;
LicenceNumber: string;
Status: number;
StatusDescription: string;
Capital: number;
AssociateShortInfos: ETradeAssociateInfo[];
AddressInfo: ETradeAddressInfo;
RenewedTo: string;
RenewedToDateString: string;
RenewalDate: string;
RenewedFrom: string;
CancellationDate: string | null;
}
export interface ETradeCompanyInfo {
Tin: string;
LegalCondtion: string;
RegNo: string;
RegDate: string;
BusinessName: string;
BusinessNameAmh: string;
PaidUpCapital: number;
AssociateShortInfos: ETradeAssociateInfo[];
Businesses: Array<{
MainGuid: string;
OwnerTIN: string;
DateRegistered: string;
TradeNameAmh: string;
TradesName: string;
LicenceNumber: string;
RenewalDate: string;
RenewedFrom: string;
RenewedTo: string;
BusinessLicensingGroupMain: string | null;
SubGroups: string | null;
}>;
}
export interface CompanyRegistrationData {
licenceNumber: string;
statusDescription: string;
dateRegistered: string;
renewedFrom: string;
renewalDate: string;
renewedTo: string;
region: string;
zone: string;
woreda: string;
kebele: string;
houseNo: string;
mobilePhone: string;
regularPhone: string;
managerName: string;
managerEmail?: string;
managerPhone: string;
}

View File

@@ -3,6 +3,7 @@ import type { BaseEntity } from "../common";
export * from "./dropdown_settings"; export * from "./dropdown_settings";
export * from "./file_upload_settings"; export * from "./file_upload_settings";
export * from "./overview"; export * from "./overview";
export * from "./etrade";
export enum TradeDirection { export enum TradeDirection {
IMPORT = "IMPORT", IMPORT = "IMPORT",