fix: etrade inconsistencies

This commit is contained in:
Nathnael
2026-08-02 18:33:38 +00:00
parent 7762d6c2d3
commit 3a3c309a5b
3 changed files with 21 additions and 24 deletions

View File

@@ -928,7 +928,7 @@ export class CompaniesService {
): Promise<ProfileResponseDto> {
const { profile, company } = await this.getCompanyInfoByUserId(userId);
await this.assertEtradeFieldsAuthentic(company, dto);
await this.applyEtradeSourcedFields(company, dto);
// Naming (or renaming) a Power of Attorney is one of the writes that can
// leave the company with a representative and nothing evidencing them, so
@@ -3216,22 +3216,24 @@ export class CompaniesService {
/**
* An eTrade-sourced field can only ever hold what a fresh eTrade lookup for
* this TIN actually returns — the portal never lets the customer type these
* once eTrade has supplied them, so a mismatch here means either stale
* client state or a hand-crafted request, and either way the write is
* refused rather than silently trusting it.
* once eTrade has supplied them. Rather than trust the client's copy (stale
* cache, hand-crafted request, or just a formatting mismatch) and reject it,
* refetch eTrade ourselves and overwrite the touched fields with whatever it
* says now — the client's submitted values for these keys only matter as a
* "this field is part of the save" flag, never as data we persist.
*/
private async assertEtradeFieldsAuthentic(
private async applyEtradeSourcedFields(
company: Company,
dto: UpdateProfileDto,
): Promise<void> {
const touched = ETRADE_SOURCED_FIELDS.some(
(key) => dto[key] !== undefined,
(key) => key !== "tin" && dto[key] !== undefined,
);
if (!touched) return;
const tin = dto.tin ?? company.tin;
const registration = await this.resolveEtradeRegistration(tin);
const expected: Partial<Record<(typeof ETRADE_SOURCED_FIELDS)[number], string>> = {
const fresh: Partial<Record<(typeof ETRADE_SOURCED_FIELDS)[number], string>> = {
companyName: registration.companyName,
licenceNumber: registration.licenceNumber,
statusDescription: registration.statusDescription,
@@ -3251,21 +3253,11 @@ export class CompaniesService {
};
for (const key of ETRADE_SOURCED_FIELDS) {
const submitted = dto[key];
if (submitted === undefined) continue;
const source = expected[key];
// eTrade left this field blank — the onboarding/settings card falls back
// to letting the customer type it directly, so nothing to check against.
if (!source) continue;
const same =
key === "etradePhone"
? normalizeE164(String(submitted)) === normalizeE164(source)
: submitted === source;
if (!same) {
throw new BadRequestException(
`${key} doesn't match eTrade's current record for this TIN. Re-verify with eTrade to pick up the latest details.`,
);
}
if (key === "tin" || dto[key] === undefined) continue;
const value = fresh[key];
// eTrade left this field blank — fall back to whatever the client sent
// (the onboarding/settings card lets the customer type it directly then).
if (value) (dto as Record<string, unknown>)[key] = value;
}
}
}

View File

@@ -179,9 +179,12 @@ export class UpdateProfileDto {
@MaxLength(100)
houseNo?: string;
// Not validated as a phone number: eTrade-sourced, so a fresh eTrade lookup
// overwrites whatever the client sends here — see
// CompaniesService.applyEtradeSourcedFields. Presence just flags "this save
// touches an eTrade-owned field."
@IsOptional()
@IsString()
@MaxLength(20)
@IsValidPhone()
etradePhone?: string;
}

View File

@@ -108,7 +108,9 @@ export class ETradeService {
dateRegistered: businessInfo.DateRegistered,
renewedFrom: businessInfo.RenewedFrom,
renewalDate: businessInfo.RenewalDate,
renewedTo: businessInfo.RenewedTo,
// RenewedTo is ISO ("2018-07-07T00:00:00"); RenewedToDateString matches
// RenewedFrom/RenewalDate's "M/D/YYYY" format — use that for consistency.
renewedTo: businessInfo.RenewedToDateString,
// eTrade returns uncoded uppercase text and sometimes a zone name in the
// Region slot. Map it onto the canonical list; an unresolved value yields
// "" so the form asks the user to pick rather than failing validation on