Files
edr-platform/apps/edr-freight-web/backoffice/src/complaints/services/etradeTinService.ts
natib21 e6e44e773b fix ui
2026-07-10 11:25:59 +00:00

366 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

export type EtradeLanguage = "en" | "am";
export interface EtradeBusinessSubGroup {
Code?: number;
Description?: string;
}
export interface EtradeBusiness {
MainGuid?: string;
OwnerTIN?: string;
DateRegistered?: string;
TradeNameAmh?: string;
TradesName?: string;
LicenceNumber?: string;
LicenseNumber?: string;
RenewalDate?: string;
RenewedFrom?: string;
RenewedTo?: string;
SubGroups?: EtradeBusinessSubGroup[];
}
export interface EtradeBusinessLicenseOption {
mainGuid: string;
licenseNumber: string;
tradeName: string;
activities: string[];
renewedTo?: string;
}
export interface EtradeRegistrationInfo {
Tin?: string;
BusinessName?: string;
BusinessNameAmh?: string;
RegNo?: string;
Businesses?: EtradeBusiness[];
tin?: string;
businessName?: string;
businessNameAmh?: string;
regNo?: string;
businesses?: EtradeBusiness[];
[key: string]: unknown;
}
export class TinRegistrationNotFoundError extends Error {
readonly code = "TIN_NOT_FOUND" as const;
constructor() {
super("TIN_NOT_FOUND");
this.name = "TinRegistrationNotFoundError";
}
}
const ETRADE_API_BASE =
import.meta.env.VITE_ETRADE_API_BASE?.trim() || "/api/etrade";
export function resolveEtradeLanguage(language: string): EtradeLanguage {
return language.toLowerCase().startsWith("am") ? "am" : "en";
}
function hasRegistrationData(data: unknown): data is EtradeRegistrationInfo {
if (data == null) return false;
if (typeof data !== "object") return false;
if (Array.isArray(data)) return data.length > 0;
const record = data as Record<string, unknown>;
const keys = Object.keys(record);
if (keys.length === 0) return false;
const tin = String(record.Tin ?? record.tin ?? "").trim();
const businessName = String(
record.BusinessName ?? record.businessName ?? "",
).trim();
return Boolean(tin || businessName);
}
function normalizeEtradeBusinesses(
data: EtradeRegistrationInfo,
): EtradeBusiness[] {
const businesses = data.Businesses ?? data.businesses;
return Array.isArray(businesses) ? businesses : [];
}
export function mapEtradeBusinessLicenses(
data: EtradeRegistrationInfo,
language: EtradeLanguage,
): EtradeBusinessLicenseOption[] {
const companyName = String(
data.BusinessName ?? data.businessName ?? "",
).trim();
const companyNameAmh = String(
data.BusinessNameAmh ?? data.businessNameAmh ?? "",
).trim();
return normalizeEtradeBusinesses(data).flatMap((business) => {
const licenseNumber = String(
business.LicenceNumber ?? business.LicenseNumber ?? "",
).trim();
if (!licenseNumber) return [];
const tradeName = String(business.TradesName ?? "").trim();
const tradeNameAmh = String(business.TradeNameAmh ?? "").trim();
const activities = (business.SubGroups ?? [])
.map((group) => String(group.Description ?? "").trim())
.filter(Boolean);
const displayTradeName =
language === "am"
? tradeNameAmh || tradeName || companyNameAmh || companyName
: tradeName || tradeNameAmh || companyName || companyNameAmh;
const option: EtradeBusinessLicenseOption = {
mainGuid: String(business.MainGuid ?? licenseNumber).trim(),
licenseNumber,
tradeName: displayTradeName || licenseNumber,
activities,
};
const renewedTo = String(business.RenewedTo ?? "").trim();
if (renewedTo) {
option.renewedTo = renewedTo;
}
return [option];
});
}
export function mapEtradeRegistration(
data: EtradeRegistrationInfo,
fallbackTin: string,
) {
const tin = String(data.Tin ?? data.tin ?? fallbackTin).trim();
const organizationName = String(
data.BusinessName ?? data.businessName ?? "",
).trim();
const regNo = String(data.RegNo ?? data.regNo ?? "").trim();
return {
organizationName: organizationName || tin,
tin,
...(regNo ? { regNo } : {}),
};
}
export interface EtradeBusinessByLicense {
MainGuid?: string;
OwnerTIN?: string;
TradeName?: string;
LicenceNumber?: string;
LicenseNumber?: string;
AssociateShortInfos?: Array<{
Position?: string;
ManagerName?: string;
ManagerNameEng?: string;
MobilePhone?: string;
RegularPhone?: string;
}>;
AddressInfo?: {
Region?: string;
Zone?: string;
Woreda?: string;
Kebele?: string;
HouseNo?: string;
MobilePhone?: string;
RegularPhone?: string;
};
[key: string]: unknown;
}
/**
* Normalize Ethiopian phone numbers for IAM auth.
* Accepts 09/07 mobiles and common 0X landline forms; returns +251...
*/
export function normalizeEtradePhoneNumber(
raw: string | null | undefined,
): string | null {
if (!raw?.trim()) return null;
let digits = raw.trim().replace(/[^\d+]/g, "");
if (digits.startsWith("+")) {
digits = digits.slice(1);
}
// Already international without +
if (digits.startsWith("251") && digits.length >= 12) {
return `+${digits}`;
}
// Local with leading 0 (mobile or landline): 09xxxxxxxx / 07xxxxxxxx / 0Xxxxxxxx
if (digits.startsWith("0") && digits.length >= 9) {
return `+251${digits.slice(1)}`;
}
// Mobile without leading 0: 9xxxxxxxx / 7xxxxxxxx
if (/^[97]\d{8}$/.test(digits)) {
return `+251${digits}`;
}
// Bare landline-ish digits (e.g. 52543000) — prefix country code only if 89 digits
if (/^\d{8,9}$/.test(digits)) {
return `+251${digits}`;
}
return null;
}
function pickRawPhoneFromBusiness(data: EtradeBusinessByLicense): string | null {
const associates = Array.isArray(data.AssociateShortInfos)
? data.AssociateShortInfos
: [];
// Prefer RegularPhone — eTrade MobilePhone is often a landline/invalid value
// (e.g. "52543000", "222400000") while RegularPhone holds the mobile (09...).
for (const associate of associates) {
const regular = String(associate.RegularPhone ?? "").trim();
if (regular) return regular;
}
const addressRegular = String(data.AddressInfo?.RegularPhone ?? "").trim();
if (addressRegular) return addressRegular;
for (const associate of associates) {
const mobile = String(associate.MobilePhone ?? "").trim();
if (mobile) return mobile;
}
const addressMobile = String(data.AddressInfo?.MobilePhone ?? "").trim();
if (addressMobile) return addressMobile;
return null;
}
export function extractEtradePhoneNumber(
data: EtradeBusinessByLicense,
): string | null {
return normalizeEtradePhoneNumber(pickRawPhoneFromBusiness(data));
}
export async function fetchBusinessByLicenseNo(
licenseNo: string,
tin: string,
language: EtradeLanguage,
): Promise<EtradeBusinessByLicense> {
const params = new URLSearchParams({
LicenseNo: licenseNo.trim(),
Tin: tin.trim(),
Lang: language,
});
const url = `${ETRADE_API_BASE}/BusinessMain/GetBusinessByLicenseNo?${params.toString()}`;
let response: Response;
try {
response = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
},
});
} catch (error) {
console.error("[eTrade:License] Network request failed", error);
throw error;
}
const responseText = (await response.text()).trim();
if (response.status === 417) {
console.error(
"[eTrade:License] Referer rejected by eTrade. Ensure /api/etrade proxy is configured.",
responseText,
);
throw new Error("ETRADE_REFERER_REJECTED");
}
if (!response.ok && response.status !== 404) {
throw new Error(`ETRADE_HTTP_${response.status}`);
}
if (!responseText) {
throw new Error("ETRADE_BUSINESS_NOT_FOUND");
}
let data: unknown;
try {
data = JSON.parse(responseText);
} catch (parseError) {
console.error(
"[eTrade:License] Invalid JSON response",
parseError,
responseText,
);
throw new Error("INVALID_ETRADE_RESPONSE");
}
if (data == null || typeof data !== "object") {
throw new Error("ETRADE_BUSINESS_NOT_FOUND");
}
return data as EtradeBusinessByLicense;
}
/**
* Looks up business details by license + TIN and returns a normalized phone
* suitable for IAM signup (`+251...`).
*/
export async function resolveEtradePhoneForSignup(
licenseNo: string,
tin: string,
language: EtradeLanguage = "en",
): Promise<string | null> {
const business = await fetchBusinessByLicenseNo(licenseNo, tin, language);
return extractEtradePhoneNumber(business);
}
export async function fetchRegistrationByTin(
tin: string,
language: EtradeLanguage,
): Promise<EtradeRegistrationInfo> {
const normalizedTin = tin.trim();
const url = `${ETRADE_API_BASE}/Registration/GetRegistrationInfoByTin/${encodeURIComponent(normalizedTin)}/${language}`;
let response: Response;
try {
response = await fetch(url, {
method: "GET",
headers: {
Accept: "application/json",
},
});
} catch (error) {
console.error("[eTrade:TIN] Network request failed", error);
throw error;
}
const responseText = (await response.text()).trim();
if (response.status === 417) {
console.error(
"[eTrade:TIN] Referer rejected by eTrade. Ensure /api/etrade proxy is configured.",
responseText,
);
throw new Error("ETRADE_REFERER_REJECTED");
}
if (!response.ok && response.status !== 404) {
throw new Error(`ETRADE_HTTP_${response.status}`);
}
if (!responseText) {
throw new TinRegistrationNotFoundError();
}
let data: unknown;
try {
data = JSON.parse(responseText);
} catch (parseError) {
console.error("[eTrade:TIN] Invalid JSON response", parseError, responseText);
throw new Error("INVALID_ETRADE_RESPONSE");
}
if (data == null || !hasRegistrationData(data)) {
throw new TinRegistrationNotFoundError();
}
return data;
}