/** * 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>> >(); 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 { 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 { 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>> { 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", }; } }