mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 12:18:11 +00:00
366 lines
9.9 KiB
TypeScript
366 lines
9.9 KiB
TypeScript
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 8–9 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;
|
||
}
|