Assist_Design/apps/bff/src/modules/address/address-writer.service.ts

189 lines
5.8 KiB
TypeScript
Raw Normal View History

/**
* Address Writer Service
*
* Centralizes bilingual address handling for registration workflows.
* Writes Japanese address to Salesforce and English address to WHMCS.
*
* - SF: Uses Japanese fields (prefectureJa, cityJa, townJa) directly from bilingual address
* - WHMCS: Resolves English fields by looking up postal code via Japan Post API
*/
import { Injectable, Inject } from "@nestjs/common";
import { Logger } from "nestjs-pino";
import {
type BilingualAddress,
type WhmcsAddressFields,
prepareSalesforceContactAddressFields,
prepareWhmcsAddressFields,
} from "@customer-portal/domain/address";
import { JapanPostFacade } from "@bff/integrations/japanpost/index.js";
import { SalesforceFacade } from "@bff/integrations/salesforce/facades/salesforce.facade.js";
export interface ResolveWhmcsAddressParams {
postcode: string;
/** Japanese town name for disambiguation when postal code returns multiple results */
townJa?: string | undefined;
streetAddress: string;
buildingName?: string | null | undefined;
roomNumber?: string | null | undefined;
residenceType: "house" | "apartment";
}
@Injectable()
export class AddressWriterService {
/** In-flight Japan Post lookups keyed by postcode — deduplicates concurrent calls */
private readonly inflightLookups = new Map<
string,
Promise<Awaited<ReturnType<JapanPostFacade["lookupByZipCode"]>>>
>();
constructor(
private readonly japanPost: JapanPostFacade,
private readonly salesforceFacade: SalesforceFacade,
@Inject(Logger) private readonly logger: Logger
) {}
/**
* Write Japanese address to Salesforce Contact.
* Uses prepareSalesforceContactAddressFields() for field mapping.
*/
async writeToSalesforce(sfAccountId: string, address: BilingualAddress): Promise<void> {
const sfFields = prepareSalesforceContactAddressFields(address);
await this.salesforceFacade.updateContactAddress(sfAccountId, {
mailingStreet: sfFields.MailingStreet,
mailingCity: sfFields.MailingCity,
mailingState: sfFields.MailingState,
mailingPostalCode: sfFields.MailingPostalCode,
mailingCountry: sfFields.MailingCountry,
buildingName: sfFields.BuildingName__c,
roomNumber: sfFields.RoomNumber__c,
});
this.logger.debug({ sfAccountId }, "Wrote Japanese address to Salesforce");
}
/**
* Resolve English address from postal code via Japan Post API
* and prepare WHMCS address fields.
*
* Uses in-flight deduplication so concurrent lookups for the same
* postcode share a single API call.
*/
async resolveWhmcsAddress(params: ResolveWhmcsAddressParams): Promise<WhmcsAddressFields> {
const { postcode, townJa, streetAddress, buildingName, roomNumber, residenceType } = params;
const lookupResult = await this.lookupPostalCode(postcode);
if (lookupResult.addresses.length === 0) {
this.logger.warn({ postcode }, "Japan Post API returned no results for postal code");
throw new Error(`No address found for postal code ${postcode}`);
}
// Find the matching address entry
let matched = lookupResult.addresses[0]!;
if (townJa && lookupResult.addresses.length > 1) {
const townMatch = lookupResult.addresses.find(a => a.town === townJa);
if (townMatch) {
matched = townMatch;
} else {
this.logger.warn(
{ postcode, townJa, availableTowns: lookupResult.addresses.map(a => a.town) },
"Could not match townJa in Japan Post results, using first result"
);
}
}
// Build a BilingualAddress from resolved data
const bilingualAddress: BilingualAddress = {
postcode,
prefecture: matched.prefectureRoma,
city: matched.cityRoma,
town: matched.townRoma,
prefectureJa: matched.prefecture,
cityJa: matched.city,
townJa: matched.town,
streetAddress,
buildingName: buildingName ?? null,
roomNumber: roomNumber ?? null,
residenceType,
};
return prepareWhmcsAddressFields(bilingualAddress);
}
/**
* Deduplicated Japan Post lookup concurrent calls for the same postcode
* share one API request.
*/
private async lookupPostalCode(
postcode: string
): Promise<Awaited<ReturnType<JapanPostFacade["lookupByZipCode"]>>> {
const inflight = this.inflightLookups.get(postcode);
if (inflight) {
return inflight;
}
const promise = this.japanPost.lookupByZipCode(postcode);
this.inflightLookups.set(postcode, promise);
try {
return await promise;
} finally {
this.inflightLookups.delete(postcode);
}
}
/**
* Prepare WHMCS address fields from a full BilingualAddress.
* Uses the already-populated English fields no API call needed.
*/
resolveWhmcsAddressFromBilingual(address: BilingualAddress): WhmcsAddressFields {
return prepareWhmcsAddressFields(address);
}
/**
* Convert WhmcsAddressFields to the shape expected by CreateWhmcsClientStep.
*/
toWhmcsStepAddress(fields: WhmcsAddressFields): {
address1: string;
address2?: string;
city: string;
state: string;
postcode: string;
country: string;
} {
return {
address1: fields.address1 || "",
...(fields.address2 && { address2: fields.address2 }),
city: fields.city,
state: fields.state,
postcode: fields.postcode,
country: fields.country,
};
}
/**
* Build case address from bilingual address.
* Maps Japanese fields to the eligibility case step's expected format.
*/
toCaseAddress(address: BilingualAddress): {
address1: string;
city: string;
state: string;
postcode: string;
country: string;
} {
return {
address1: `${address.townJa}${address.streetAddress}`,
city: address.cityJa,
state: address.prefectureJa,
postcode: address.postcode,
country: "Japan",
};
}
}