mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 06:00:55 +00:00
feat: add choice to etrade fetcher
This commit is contained in:
@@ -210,6 +210,7 @@ export class CompaniesController {
|
||||
const data = await this.companiesService.fetchETradeData(
|
||||
dto.tin,
|
||||
companyId,
|
||||
dto.licenceNumber,
|
||||
);
|
||||
return new ETradeResponseDto(data);
|
||||
}
|
||||
|
||||
@@ -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<CompanyRegistrationData> {
|
||||
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>
|
||||
> = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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 || "",
|
||||
})),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)",
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user