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

130 lines
4.4 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 {
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.
*
* Flow:
* 1. Call Japan Post API with postal code
* 2. Match result using townJa (if multiple matches)
* 3. Build BilingualAddress with resolved English fields
* 4. Return WhmcsAddressFields via prepareWhmcsAddressFields()
*/
async resolveWhmcsAddress(params: ResolveWhmcsAddressParams): Promise<WhmcsAddressFields> {
const { postcode, townJa, streetAddress, buildingName, roomNumber, residenceType } = params;
const lookupResult = await this.japanPost.lookupByZipCode(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);
}
/**
* Convenience: resolve WHMCS address from a full BilingualAddress.
* Re-derives English fields from postal code via Japan Post API.
*/
async resolveWhmcsAddressFromBilingual(address: BilingualAddress): Promise<WhmcsAddressFields> {
return this.resolveWhmcsAddress({
postcode: address.postcode,
townJa: address.townJa,
streetAddress: address.streetAddress,
buildingName: address.buildingName,
roomNumber: address.roomNumber,
residenceType: address.residenceType,
});
}
}