refactor: enhance address handling in BFF workflows
- Integrated AddressWriterService into GuestEligibilityWorkflowService and NewCustomerSignupWorkflowService for improved address writing to Salesforce. - Updated AddressModule to include SalesforceModule and export AddressWriterService. - Refactored address handling in various workflows to utilize the new address structure, ensuring consistency and reliability in address processing. - Removed deprecated address building logic from eligibility check store, streamlining address management across components.
This commit is contained in:
parent
26a1419189
commit
6299fbabdc
129
apps/bff/src/modules/address/address-writer.service.ts
Normal file
129
apps/bff/src/modules/address/address-writer.service.ts
Normal file
@ -0,0 +1,129 @@
|
||||
/**
|
||||
* 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,19 @@
|
||||
/**
|
||||
* Address Module
|
||||
*
|
||||
* NestJS module for address lookup functionality.
|
||||
* NestJS module for address lookup and writing functionality.
|
||||
*/
|
||||
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AddressController } from "./address.controller.js";
|
||||
import { AddressWriterService } from "./address-writer.service.js";
|
||||
import { JapanPostModule } from "@bff/integrations/japanpost/japanpost.module.js";
|
||||
import { SalesforceModule } from "@bff/integrations/salesforce/salesforce.module.js";
|
||||
|
||||
@Module({
|
||||
imports: [JapanPostModule],
|
||||
imports: [JapanPostModule, SalesforceModule],
|
||||
controllers: [AddressController],
|
||||
providers: [AddressWriterService],
|
||||
exports: [AddressWriterService],
|
||||
})
|
||||
export class AddressModule {}
|
||||
|
||||
@ -3,6 +3,7 @@ import { UsersModule } from "@bff/modules/users/users.module.js";
|
||||
import { MappingsModule } from "@bff/modules/id-mappings/mappings.module.js";
|
||||
import { IntegrationsModule } from "@bff/integrations/integrations.module.js";
|
||||
import { WorkflowModule } from "@bff/modules/shared/workflow/index.js";
|
||||
import { AddressModule } from "@bff/modules/address/address.module.js";
|
||||
import { TokensModule } from "../tokens/tokens.module.js";
|
||||
import { OtpModule } from "../otp/otp.module.js";
|
||||
// Coordinator
|
||||
@ -38,6 +39,7 @@ import { GetStartedController } from "../presentation/http/get-started.controlle
|
||||
MappingsModule,
|
||||
IntegrationsModule,
|
||||
WorkflowModule,
|
||||
AddressModule,
|
||||
],
|
||||
controllers: [GetStartedController],
|
||||
providers: [
|
||||
|
||||
@ -14,6 +14,7 @@ import {
|
||||
PORTAL_SOURCE_INTERNET_ELIGIBILITY,
|
||||
PORTAL_STATUS_NOT_YET,
|
||||
} from "@bff/modules/auth/constants/portal.constants.js";
|
||||
import { AddressWriterService } from "@bff/modules/address/address-writer.service.js";
|
||||
|
||||
import { GetStartedSessionService } from "../otp/get-started-session.service.js";
|
||||
import { CreateEligibilityCaseStep } from "./steps/index.js";
|
||||
@ -41,6 +42,7 @@ export class GuestEligibilityWorkflowService {
|
||||
private readonly salesforceFacade: SalesforceFacade,
|
||||
private readonly lockService: DistributedLockService,
|
||||
private readonly eligibilityCaseStep: CreateEligibilityCaseStep,
|
||||
private readonly addressWriter: AddressWriterService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
@ -99,31 +101,25 @@ export class GuestEligibilityWorkflowService {
|
||||
);
|
||||
}
|
||||
|
||||
// Save Japanese address to SF Contact (if Japanese address fields provided)
|
||||
if (address.prefectureJa || address.cityJa || address.townJa) {
|
||||
await this.salesforceAccountService.updateContactAddress(sfAccountId, {
|
||||
mailingStreet: `${address.townJa || ""}${address.streetAddress || ""}`.trim(),
|
||||
mailingCity: address.cityJa || address.city,
|
||||
mailingState: address.prefectureJa || address.state,
|
||||
mailingPostalCode: address.postcode,
|
||||
mailingCountry: "Japan",
|
||||
buildingName: address.buildingName ?? null,
|
||||
roomNumber: address.roomNumber ?? null,
|
||||
});
|
||||
this.logger.debug({ sfAccountId }, "Updated SF Contact with Japanese address");
|
||||
// Write Japanese address to Salesforce
|
||||
try {
|
||||
await this.addressWriter.writeToSalesforce(sfAccountId, address);
|
||||
} catch (addressError) {
|
||||
this.logger.warn(
|
||||
{ error: extractErrorMessage(addressError), email: normalizedEmail },
|
||||
"SF address write failed (non-critical, continuing)"
|
||||
);
|
||||
}
|
||||
|
||||
// Create eligibility case via shared step
|
||||
const { caseId } = await this.eligibilityCaseStep.execute({
|
||||
sfAccountId,
|
||||
address: {
|
||||
address1: address.address1,
|
||||
...(address.address2 ? { address2: address.address2 } : {}),
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
address1: `${address.townJa}${address.streetAddress}`,
|
||||
city: address.cityJa,
|
||||
state: address.prefectureJa,
|
||||
postcode: address.postcode,
|
||||
...(address.country ? { country: address.country } : {}),
|
||||
...(address.streetAddress ? { streetAddress: address.streetAddress } : {}),
|
||||
country: "Japan",
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import { WhmcsAccountDiscoveryService } from "@bff/integrations/whmcs/services/w
|
||||
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
||||
import { PORTAL_SOURCE_INTERNET_ELIGIBILITY } from "@bff/modules/auth/constants/portal.constants.js";
|
||||
import type { AuthResultInternal } from "@bff/modules/auth/auth.types.js";
|
||||
import { AddressWriterService } from "@bff/modules/address/address-writer.service.js";
|
||||
|
||||
import { GetStartedSessionService } from "../otp/get-started-session.service.js";
|
||||
import {
|
||||
@ -46,6 +47,7 @@ export class NewCustomerSignupWorkflowService {
|
||||
private readonly portalUserStep: CreatePortalUserStep,
|
||||
private readonly sfFlagsStep: UpdateSalesforceFlagsStep,
|
||||
private readonly authResultStep: GenerateAuthResultStep,
|
||||
private readonly addressWriter: AddressWriterService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
@ -146,18 +148,27 @@ export class NewCustomerSignupWorkflowService {
|
||||
updateSourceIfExists: true,
|
||||
});
|
||||
|
||||
// Step 1.5: Write Japanese address to Salesforce (DEGRADABLE)
|
||||
try {
|
||||
await this.addressWriter.writeToSalesforce(sfResult.sfAccountId, address);
|
||||
} catch (addressError) {
|
||||
this.logger.warn(
|
||||
{ error: extractErrorMessage(addressError), email },
|
||||
"SF address write failed (non-critical, continuing)"
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Create eligibility case (DEGRADABLE)
|
||||
let eligibilityRequestId: string | undefined;
|
||||
try {
|
||||
const caseResult = await this.caseStep.execute({
|
||||
sfAccountId: sfResult.sfAccountId,
|
||||
address: {
|
||||
address1: address.address1,
|
||||
...(address.address2 && { address2: address.address2 }),
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
address1: `${address.townJa}${address.streetAddress}`,
|
||||
city: address.cityJa,
|
||||
state: address.prefectureJa,
|
||||
postcode: address.postcode,
|
||||
...(address.country ? { country: address.country } : {}),
|
||||
country: "Japan",
|
||||
},
|
||||
});
|
||||
eligibilityRequestId = caseResult.caseId;
|
||||
@ -169,6 +180,8 @@ export class NewCustomerSignupWorkflowService {
|
||||
}
|
||||
|
||||
// Step 3: Create WHMCS client (CRITICAL, has rollback)
|
||||
const whmcsAddress = await this.addressWriter.resolveWhmcsAddressFromBilingual(address);
|
||||
|
||||
const whmcsResult = await this.whmcsStep.execute({
|
||||
email,
|
||||
password,
|
||||
@ -176,12 +189,12 @@ export class NewCustomerSignupWorkflowService {
|
||||
lastName,
|
||||
phone: phone ?? "",
|
||||
address: {
|
||||
address1: address.address1,
|
||||
...(address.address2 && { address2: address.address2 }),
|
||||
city: address.city,
|
||||
state: address.state ?? "",
|
||||
postcode: address.postcode,
|
||||
country: address.country ?? "Japan",
|
||||
address1: whmcsAddress.address1 || "",
|
||||
...(whmcsAddress.address2 && { address2: whmcsAddress.address2 }),
|
||||
city: whmcsAddress.city,
|
||||
state: whmcsAddress.state,
|
||||
postcode: whmcsAddress.postcode,
|
||||
country: whmcsAddress.country,
|
||||
},
|
||||
customerNumber: sfResult.customerNumber ?? null,
|
||||
dateOfBirth,
|
||||
|
||||
@ -10,6 +10,8 @@ import { WhmcsAccountDiscoveryService } from "@bff/integrations/whmcs/services/w
|
||||
import { extractErrorMessage } from "@bff/core/utils/error.util.js";
|
||||
import { PORTAL_SOURCE_NEW_SIGNUP } from "@bff/modules/auth/constants/portal.constants.js";
|
||||
import type { AuthResultInternal } from "@bff/modules/auth/auth.types.js";
|
||||
import { AddressWriterService } from "@bff/modules/address/address-writer.service.js";
|
||||
import type { BilingualAddress } from "@customer-portal/domain/address";
|
||||
|
||||
import { GetStartedSessionService } from "../otp/get-started-session.service.js";
|
||||
import {
|
||||
@ -39,6 +41,7 @@ export class SfCompletionWorkflowService {
|
||||
private readonly portalUserStep: CreatePortalUserStep,
|
||||
private readonly sfFlagsStep: UpdateSalesforceFlagsStep,
|
||||
private readonly authResultStep: GenerateAuthResultStep,
|
||||
private readonly addressWriter: AddressWriterService,
|
||||
@Inject(Logger) private readonly logger: Logger
|
||||
) {}
|
||||
|
||||
@ -101,7 +104,7 @@ export class SfCompletionWorkflowService {
|
||||
sfAccountId?: string | undefined;
|
||||
firstName?: string | undefined;
|
||||
lastName?: string | undefined;
|
||||
address?: Record<string, string | undefined> | undefined;
|
||||
address?: Record<string, unknown> | undefined;
|
||||
}
|
||||
): Promise<AuthResultInternal> {
|
||||
const {
|
||||
@ -132,7 +135,31 @@ export class SfCompletionWorkflowService {
|
||||
source: PORTAL_SOURCE_NEW_SIGNUP,
|
||||
});
|
||||
|
||||
// Step 1.5: Write Japanese address to Salesforce for new customers (DEGRADABLE)
|
||||
if (isNewCustomer && address.prefectureJa) {
|
||||
try {
|
||||
await this.addressWriter.writeToSalesforce(
|
||||
sfResult.sfAccountId,
|
||||
address as BilingualAddress
|
||||
);
|
||||
} catch (addressError) {
|
||||
this.logger.warn(
|
||||
{ error: extractErrorMessage(addressError), email: session.email },
|
||||
"SF address write failed (non-critical, continuing)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Create WHMCS client (CRITICAL, has rollback)
|
||||
const whmcsAddress = await this.addressWriter.resolveWhmcsAddress({
|
||||
postcode: address.postcode,
|
||||
townJa: address.townJa,
|
||||
streetAddress: address.streetAddress || "",
|
||||
buildingName: address.buildingName,
|
||||
roomNumber: address.roomNumber,
|
||||
residenceType: address.residenceType || "house",
|
||||
});
|
||||
|
||||
const whmcsResult = await this.whmcsStep.execute({
|
||||
firstName: finalFirstName,
|
||||
lastName: finalLastName,
|
||||
@ -140,12 +167,12 @@ export class SfCompletionWorkflowService {
|
||||
password,
|
||||
phone: phone ?? "",
|
||||
address: {
|
||||
address1: address.address1,
|
||||
...(address.address2 && { address2: address.address2 }),
|
||||
city: address.city,
|
||||
state: address.state,
|
||||
postcode: address.postcode,
|
||||
country: address.country ?? "Japan",
|
||||
address1: whmcsAddress.address1 || "",
|
||||
...(whmcsAddress.address2 && { address2: whmcsAddress.address2 }),
|
||||
city: whmcsAddress.city,
|
||||
state: whmcsAddress.state,
|
||||
postcode: whmcsAddress.postcode,
|
||||
country: whmcsAddress.country,
|
||||
},
|
||||
customerNumber: sfResult.customerNumber ?? null,
|
||||
dateOfBirth,
|
||||
@ -216,13 +243,13 @@ export class SfCompletionWorkflowService {
|
||||
|
||||
private resolveAddress(
|
||||
requestAddress: CompleteAccountRequest["address"] | undefined,
|
||||
sessionAddress: Record<string, string | undefined> | undefined
|
||||
sessionAddress: Record<string, unknown> | undefined
|
||||
): NonNullable<CompleteAccountRequest["address"]> {
|
||||
const address = requestAddress ?? sessionAddress;
|
||||
|
||||
if (!address || !address.address1 || !address.city || !address.state || !address.postcode) {
|
||||
if (!address || !address.postcode) {
|
||||
throw new BadRequestException(
|
||||
"Address information is incomplete. Please ensure all required fields (address, city, prefecture, postcode) are provided."
|
||||
"Address information is incomplete. Please ensure postcode is provided."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -33,11 +33,11 @@ interface AccountStatusResult {
|
||||
sfFirstName?: string;
|
||||
sfLastName?: string;
|
||||
sfAddress?: {
|
||||
address1: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postcode: string;
|
||||
country: string;
|
||||
prefectureJa?: string;
|
||||
cityJa?: string;
|
||||
townJa?: string;
|
||||
streetAddress?: string;
|
||||
postcode?: string;
|
||||
};
|
||||
whmcsClientId?: number;
|
||||
whmcsFirstName?: string;
|
||||
@ -247,7 +247,15 @@ export class VerificationWorkflowService {
|
||||
sfAccountId: sfAccount.id,
|
||||
...(sfAccount.firstName && { sfFirstName: sfAccount.firstName }),
|
||||
...(sfAccount.lastName && { sfLastName: sfAccount.lastName }),
|
||||
...(sfAccount.address && { sfAddress: sfAccount.address }),
|
||||
...(sfAccount.address && {
|
||||
sfAddress: {
|
||||
prefectureJa: sfAccount.address.state,
|
||||
cityJa: sfAccount.address.city,
|
||||
// MailingStreet contains townJa + streetAddress concatenated — store as-is
|
||||
streetAddress: sfAccount.address.address1,
|
||||
postcode: sfAccount.address.postcode,
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { type JapanAddressFormData } from "@/features/address/components/JapanAddressForm";
|
||||
import { prepareWhmcsAddressFields } from "@customer-portal/domain/address";
|
||||
import { usePasswordValidation } from "@/features/auth/hooks/usePasswordValidation";
|
||||
import { phoneSchema } from "@customer-portal/domain/common";
|
||||
import type { AccountFormErrors } from "./types";
|
||||
@ -63,17 +62,7 @@ export function useCompleteAccountForm({
|
||||
const handleAddressChange = useCallback(
|
||||
(data: JapanAddressFormData, isComplete: boolean) => {
|
||||
setIsAddressComplete(isComplete);
|
||||
const whmcsFields = prepareWhmcsAddressFields(data);
|
||||
updateFormData({
|
||||
address: {
|
||||
address1: whmcsFields.address1 || "",
|
||||
address2: whmcsFields.address2 || "",
|
||||
city: whmcsFields.city || "",
|
||||
state: whmcsFields.state || "",
|
||||
postcode: whmcsFields.postcode || "",
|
||||
country: "JP",
|
||||
},
|
||||
});
|
||||
updateFormData({ address: data });
|
||||
},
|
||||
[updateFormData]
|
||||
);
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
*/
|
||||
|
||||
import { fromPromise } from "xstate";
|
||||
import type { BilingualAddress } from "@customer-portal/domain/address";
|
||||
import * as getStartedApi from "../api/get-started.api";
|
||||
import type {
|
||||
SendCodeInput,
|
||||
@ -54,18 +55,7 @@ export const completeAccountActor = fromPromise<CompleteAccountOutput, CompleteA
|
||||
marketingConsent: formData.marketingConsent,
|
||||
...(isNewCustomer || formData.firstName ? { firstName: formData.firstName } : {}),
|
||||
...(isNewCustomer || formData.lastName ? { lastName: formData.lastName } : {}),
|
||||
...(isNewCustomer || formData.address?.address1
|
||||
? {
|
||||
address: {
|
||||
address1: formData.address.address1 || "",
|
||||
address2: formData.address.address2,
|
||||
city: formData.address.city || "",
|
||||
state: formData.address.state || "",
|
||||
postcode: formData.address.postcode || "",
|
||||
country: formData.address.country || "JP",
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
...(formData.address?.postcode ? { address: formData.address as BilingualAddress } : {}),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@ -10,18 +10,19 @@
|
||||
|
||||
import { setup, assign } from "xstate";
|
||||
import { getErrorMessage } from "@/shared/utils";
|
||||
import type {
|
||||
GetStartedAddress,
|
||||
GetStartedContext,
|
||||
GetStartedEvent,
|
||||
GetStartedFormData,
|
||||
GetStartedMachineInput,
|
||||
} from "./get-started.types";
|
||||
import {
|
||||
sendCodeActor,
|
||||
verifyCodeActor,
|
||||
completeAccountActor,
|
||||
migrateAccountActor,
|
||||
} from "./get-started.actors";
|
||||
import type {
|
||||
GetStartedContext,
|
||||
GetStartedEvent,
|
||||
GetStartedFormData,
|
||||
GetStartedMachineInput,
|
||||
} from "./get-started.types";
|
||||
|
||||
// ============================================================================
|
||||
// Initial form data (matches store defaults)
|
||||
@ -153,17 +154,7 @@ export const getStartedMachine = setup({
|
||||
firstName: prefill.firstName ?? context.formData.firstName,
|
||||
lastName: prefill.lastName ?? context.formData.lastName,
|
||||
phone: prefill.phone ?? context.formData.phone,
|
||||
address: prefill.address
|
||||
? {
|
||||
address1: prefill.address.address1,
|
||||
address2: prefill.address.address2,
|
||||
city: prefill.address.city,
|
||||
state: prefill.address.state,
|
||||
postcode: prefill.address.postcode,
|
||||
country: prefill.address.country,
|
||||
countryCode: prefill.address.countryCode,
|
||||
}
|
||||
: context.formData.address,
|
||||
address: (prefill.address as GetStartedAddress) ?? context.formData.address,
|
||||
};
|
||||
},
|
||||
}),
|
||||
@ -271,17 +262,7 @@ export const getStartedMachine = setup({
|
||||
firstName: prefill.firstName ?? context.formData.firstName,
|
||||
lastName: prefill.lastName ?? context.formData.lastName,
|
||||
phone: prefill.phone ?? context.formData.phone,
|
||||
address: prefill.address
|
||||
? {
|
||||
address1: prefill.address.address1,
|
||||
address2: prefill.address.address2,
|
||||
city: prefill.address.city,
|
||||
state: prefill.address.state,
|
||||
postcode: prefill.address.postcode,
|
||||
country: prefill.address.country,
|
||||
countryCode: prefill.address.countryCode,
|
||||
}
|
||||
: context.formData.address,
|
||||
address: (prefill.address as GetStartedAddress) ?? context.formData.address,
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
||||
@ -10,6 +10,7 @@ import type {
|
||||
SendVerificationCodeResponse,
|
||||
} from "@customer-portal/domain/get-started";
|
||||
import type { AuthResponse } from "@customer-portal/domain/auth";
|
||||
import type { BilingualAddress } from "@customer-portal/domain/address";
|
||||
|
||||
// ============================================================================
|
||||
// Form & Service Types (mirrored from store for compatibility)
|
||||
@ -21,15 +22,9 @@ export interface ServiceContext {
|
||||
redirectTo?: string | undefined;
|
||||
}
|
||||
|
||||
export interface GetStartedAddress {
|
||||
address1?: string | undefined;
|
||||
address2?: string | undefined;
|
||||
city?: string | undefined;
|
||||
state?: string | undefined;
|
||||
postcode?: string | undefined;
|
||||
country?: string | undefined;
|
||||
countryCode?: string | undefined;
|
||||
}
|
||||
export type GetStartedAddress = {
|
||||
[K in keyof BilingualAddress]?: BilingualAddress[K] | undefined;
|
||||
};
|
||||
|
||||
export interface GetStartedFormData {
|
||||
email: string;
|
||||
|
||||
@ -17,7 +17,6 @@ import {
|
||||
verifyCode,
|
||||
signupWithEligibility,
|
||||
} from "@/features/get-started/api/get-started.api";
|
||||
import { prepareWhmcsAddressFields } from "@customer-portal/domain/address";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
@ -150,47 +149,6 @@ const initialState = {
|
||||
_resendTimerId: null,
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Address Building Helper
|
||||
// ============================================================================
|
||||
|
||||
interface EligibilityCheckAddress {
|
||||
address1: string;
|
||||
address2?: string;
|
||||
city: string;
|
||||
state: string;
|
||||
postcode: string;
|
||||
country: "JP";
|
||||
prefectureJa?: string;
|
||||
cityJa?: string;
|
||||
townJa?: string;
|
||||
streetAddress?: string;
|
||||
buildingName?: string;
|
||||
roomNumber?: string;
|
||||
}
|
||||
|
||||
function buildEligibilityAddress(addressData: JapanAddressFormData): EligibilityCheckAddress {
|
||||
const whmcsAddress = prepareWhmcsAddressFields(addressData);
|
||||
|
||||
const address: EligibilityCheckAddress = {
|
||||
address1: whmcsAddress.address1 || "",
|
||||
city: whmcsAddress.city || "",
|
||||
state: whmcsAddress.state || "",
|
||||
postcode: whmcsAddress.postcode || "",
|
||||
country: "JP",
|
||||
};
|
||||
|
||||
if (whmcsAddress.address2) address.address2 = whmcsAddress.address2;
|
||||
if (addressData.prefectureJa) address.prefectureJa = addressData.prefectureJa;
|
||||
if (addressData.cityJa) address.cityJa = addressData.cityJa;
|
||||
if (addressData.townJa) address.townJa = addressData.townJa;
|
||||
if (addressData.streetAddress) address.streetAddress = addressData.streetAddress;
|
||||
if (addressData.buildingName) address.buildingName = addressData.buildingName;
|
||||
if (addressData.roomNumber) address.roomNumber = addressData.roomNumber;
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Action Implementations (extracted to reduce store function length)
|
||||
// ============================================================================
|
||||
@ -213,7 +171,7 @@ async function submitOnlyAction(get: StoreGet, set: StoreSet): Promise<boolean>
|
||||
email: formData.email.trim(),
|
||||
firstName: formData.firstName.trim(),
|
||||
lastName: formData.lastName.trim(),
|
||||
address: buildEligibilityAddress(formData.address),
|
||||
address: formData.address,
|
||||
continueToAccount: false,
|
||||
});
|
||||
|
||||
@ -363,20 +321,11 @@ async function completeAccountAction(get: StoreGet, set: StoreSet): Promise<bool
|
||||
set({ loading: true, error: null });
|
||||
|
||||
try {
|
||||
const whmcsAddress = prepareWhmcsAddressFields(formData.address);
|
||||
|
||||
const result = await signupWithEligibility({
|
||||
sessionToken,
|
||||
firstName: formData.firstName.trim(),
|
||||
lastName: formData.lastName.trim(),
|
||||
address: {
|
||||
address1: whmcsAddress.address1 || "",
|
||||
...(whmcsAddress.address2 && { address2: whmcsAddress.address2 }),
|
||||
city: whmcsAddress.city || "",
|
||||
state: whmcsAddress.state || "",
|
||||
postcode: whmcsAddress.postcode || "",
|
||||
country: "JP",
|
||||
},
|
||||
address: formData.address,
|
||||
phone: accountData.phone.trim(),
|
||||
password: accountData.password,
|
||||
dateOfBirth: accountData.dateOfBirth,
|
||||
|
||||
152
docs/plans/2026-03-03-bilingual-address-handler-design.md
Normal file
152
docs/plans/2026-03-03-bilingual-address-handler-design.md
Normal file
@ -0,0 +1,152 @@
|
||||
# Bilingual Address Handler for Registration
|
||||
|
||||
**Date**: 2026-03-03
|
||||
**Status**: Design
|
||||
|
||||
## Problem
|
||||
|
||||
Registration flows use `addressFormSchema` (simple English-only: address1, address2, city, state, postcode). The bilingual address infrastructure exists (`bilingualAddressSchema`, `prepareWhmcsAddressFields`, `prepareSalesforceContactAddressFields`, `JapanAddressForm`, Japan Post API integration) but isn't wired into signup workflows.
|
||||
|
||||
Salesforce receives English address data when it should receive Japanese. Address transformation logic is scattered inline across workflows.
|
||||
|
||||
## Requirements
|
||||
|
||||
| Outcome | SF Write | WHMCS Write | Address Input |
|
||||
| ------------------- | ------------------------------- | ------------------------------- | ------------------------------- |
|
||||
| A (New Customer) | Japanese (at signup) | English (at signup) | JapanAddressForm |
|
||||
| B (SF-Only) | Japanese (at eligibility check) | English (at account completion) | JapanAddressForm at eligibility |
|
||||
| C (WHMCS Migration) | None | None | None |
|
||||
| D (Portal Exists) | None | None | None |
|
||||
|
||||
## Key Decisions
|
||||
|
||||
1. **Japan Post API postal code lookup** auto-populates both JA and EN fields on the frontend.
|
||||
2. **Always re-derive EN fields from postal code** via Japan Post API on the backend when writing to WHMCS. One code path regardless of whether data is in Redis (fresh) or Salesforce (days later). Avoids Redis TTL dependency.
|
||||
3. **Centralized `AddressWriterService`** in BFF orchestrates all address writes.
|
||||
4. **`bilingualAddressSchema` replaces `addressFormSchema`** in all signup request/session/handoff schemas.
|
||||
5. **`JapanAddressForm` replaces simple address fields** in all signup frontend forms.
|
||||
|
||||
## Architecture
|
||||
|
||||
### AddressWriterService (BFF)
|
||||
|
||||
Location: `apps/bff/src/modules/address/address-writer.service.ts`
|
||||
|
||||
```
|
||||
AddressWriterService
|
||||
Dependencies: JapanPostFacade, SalesforceAccountService
|
||||
|
||||
writeToSalesforce(sfAccountId, bilingualAddress)
|
||||
→ prepareSalesforceContactAddressFields(address) → SF Contact update
|
||||
→ Writes: MailingStreet (JA town+street), MailingCity (JA), MailingState (JA), BuildingName__c, RoomNumber__c
|
||||
|
||||
resolveAndPrepareWhmcsAddress(postalCode, townJa?, streetAddress, buildingInfo)
|
||||
→ Japan Post API lookup by postal code
|
||||
→ Match correct result using townJa (for multi-match postal codes)
|
||||
→ prepareWhmcsAddressFields() with resolved EN fields
|
||||
→ Returns: WhmcsAddressFields (address1, address2, city, state, postcode, country)
|
||||
```
|
||||
|
||||
### Schema Changes
|
||||
|
||||
Replace `addressFormSchema` with `bilingualAddressSchema` in:
|
||||
|
||||
| Schema | File | Change |
|
||||
| ------------------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------------- |
|
||||
| `signupWithEligibilityRequestSchema` | `domain/get-started/schema.ts` | `address: addressFormSchema` → `address: bilingualAddressSchema` |
|
||||
| `completeAccountRequestSchema` | `domain/get-started/schema.ts` | `address: addressFormSchema.optional()` → `address: bilingualAddressSchema.optional()` |
|
||||
| `getStartedSessionSchema` | `domain/get-started/schema.ts` | `address: addressFormSchema.partial().optional()` → `address: bilingualAddressSchema.partial().optional()` |
|
||||
| `guestHandoffTokenSchema` | `domain/get-started/schema.ts` | `address: addressFormSchema.partial().optional()` → `address: bilingualAddressSchema.partial().optional()` |
|
||||
| `verifyCodeResponseSchema` prefill | `domain/get-started/schema.ts` | `address: addressFormSchema.partial().optional()` → `address: bilingualAddressSchema.partial().optional()` |
|
||||
| `guestEligibilityRequestSchema` | `domain/get-started/schema.ts` | Remove `bilingualEligibilityAddressSchema`, use `bilingualAddressSchema` |
|
||||
|
||||
### Workflow Changes
|
||||
|
||||
**NewCustomerSignupWorkflowService (Outcome A)**:
|
||||
|
||||
- Accept `bilingualAddressSchema` from request
|
||||
- After SF account creation: `addressWriter.writeToSalesforce(sfAccountId, address)` → JA to SF
|
||||
- For WHMCS client: `addressWriter.resolveAndPrepareWhmcsAddress(postcode, townJa, streetAddress, building)` → EN to WHMCS
|
||||
|
||||
**GuestEligibilityWorkflowService (Outcome B - eligibility check)**:
|
||||
|
||||
- Refactor inline SF address write (lines 103-113) to use `addressWriter.writeToSalesforce()`
|
||||
- Store bilingual address in handoff token (for B1 immediate flow)
|
||||
|
||||
**SfCompletionWorkflowService (Outcome B - account completion)**:
|
||||
|
||||
- Resolve address from session (may have bilingual data if B1, or JA-only + postcode if B2)
|
||||
- `addressWriter.resolveAndPrepareWhmcsAddress(postcode, townJa, streetAddress, building)` → EN to WHMCS
|
||||
|
||||
### Frontend Changes
|
||||
|
||||
Replace simple address fields with `JapanAddressForm` component in:
|
||||
|
||||
- Signup with eligibility form (Outcome A)
|
||||
- Guest eligibility form (Outcome B)
|
||||
- Forms submit `bilingualAddressSchema` shape
|
||||
|
||||
No address form needed for:
|
||||
|
||||
- Complete account (Outcome B) — address from session
|
||||
- WHMCS migration (Outcome C) — no address needed
|
||||
- Portal exists (Outcome D) — redirect to login
|
||||
|
||||
### Data Flow Diagrams
|
||||
|
||||
**Outcome A (New Customer)**:
|
||||
|
||||
```
|
||||
User → JapanAddressForm → postal code lookup → auto-fill JA+EN
|
||||
→ Submit (bilingualAddress) → BFF
|
||||
→ addressWriter.writeToSalesforce(sfAccountId, address) → SF gets JA
|
||||
→ addressWriter.resolveAndPrepareWhmcsAddress(postcode, townJa, ...) → Japan Post API → WHMCS gets EN
|
||||
```
|
||||
|
||||
**Outcome B1 (SF-Only, immediate)**:
|
||||
|
||||
```
|
||||
User → JapanAddressForm (guest eligibility) → Submit (bilingualAddress) → BFF
|
||||
→ addressWriter.writeToSalesforce(sfAccountId, address) → SF gets JA
|
||||
→ Store bilingual in handoff token → Redis (30 min TTL)
|
||||
→ User verifies email → complete account
|
||||
→ addressWriter.resolveAndPrepareWhmcsAddress(postcode, townJa, ...) → Japan Post API → WHMCS gets EN
|
||||
```
|
||||
|
||||
**Outcome B2 (SF-Only, returns days later)**:
|
||||
|
||||
```
|
||||
[Days ago] Guest eligibility → SF got JA address + postal code
|
||||
[Now] User verifies email → system detects SF_UNMAPPED
|
||||
→ Prefill from SF: postal code, JA address fields
|
||||
→ addressWriter.resolveAndPrepareWhmcsAddress(postcode, townJa, ...) → Japan Post API → WHMCS gets EN
|
||||
```
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### Domain (packages/domain/)
|
||||
|
||||
- `get-started/schema.ts` — Replace addressFormSchema references with bilingualAddressSchema
|
||||
- `address/schema.ts` — May need minor additions for the resolve flow
|
||||
|
||||
### BFF (apps/bff/)
|
||||
|
||||
- **New**: `modules/address/address-writer.service.ts` — Centralized address write service
|
||||
- `modules/address/address.module.ts` — Export AddressWriterService
|
||||
- `modules/auth/infra/workflows/new-customer-signup-workflow.service.ts` — Use AddressWriterService
|
||||
- `modules/auth/infra/workflows/guest-eligibility-workflow.service.ts` — Use AddressWriterService
|
||||
- `modules/auth/infra/workflows/sf-completion-workflow.service.ts` — Use AddressWriterService
|
||||
- `modules/auth/infra/workflows/steps/create-whmcs-client.step.ts` — Accept WhmcsAddressFields
|
||||
- `modules/auth/infra/workflows/verification-workflow.service.ts` — Prefill bilingual address from SF
|
||||
|
||||
### Portal (apps/portal/)
|
||||
|
||||
- Signup with eligibility form — Replace address fields with JapanAddressForm
|
||||
- Guest eligibility form — Ensure JapanAddressForm is used, submits bilingualAddressSchema
|
||||
- API layer — Update request types to match new schemas
|
||||
|
||||
## Risks
|
||||
|
||||
- **Japan Post API availability**: If API is down during WHMCS write, registration fails. Mitigation: retry with backoff (existing retry.util), graceful error message.
|
||||
- **Multi-match postal codes**: Some postal codes return multiple town entries. Mitigation: match using `townJa` from the stored/submitted address.
|
||||
- **Schema migration**: Changing session/handoff schemas could affect in-flight registrations. Mitigation: make new fields optional, handle old format gracefully during rollout.
|
||||
951
docs/plans/2026-03-03-bilingual-address-handler-plan.md
Normal file
951
docs/plans/2026-03-03-bilingual-address-handler-plan.md
Normal file
@ -0,0 +1,951 @@
|
||||
# Bilingual Address Handler Implementation Plan
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** Centralize address handling so registration writes Japanese address to Salesforce and English address to WHMCS, using the existing bilingual address infrastructure.
|
||||
|
||||
**Architecture:** Replace `addressFormSchema` (simple English) with `bilingualAddressSchema` in all signup request/session/handoff schemas. Create `AddressWriterService` in BFF that centralizes SF (Japanese) and WHMCS (English via Japan Post API postal code lookup) address writes. Update all signup workflows to use it. Frontend already uses `JapanAddressForm` — just stop converting to WHMCS format before sending.
|
||||
|
||||
**Tech Stack:** Zod schemas (domain), NestJS services (BFF), React/XState/Zustand (Portal), Japan Post API
|
||||
|
||||
---
|
||||
|
||||
### Task 1: Update Domain Schemas — Replace addressFormSchema with bilingualAddressSchema in get-started
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `packages/domain/get-started/schema.ts`
|
||||
- Modify: `packages/domain/get-started/contract.ts`
|
||||
- Modify: `packages/domain/get-started/index.ts`
|
||||
|
||||
**Context:** The get-started schemas currently reference `addressFormSchema` (simple: address1, address2, city, state, postcode, country) from `customer/schema.ts`. We need to replace all these references with `bilingualAddressSchema` from `address/schema.ts`, which has both Japanese and English fields. We also remove the duplicate `bilingualEligibilityAddressSchema` that exists in this file.
|
||||
|
||||
**Step 1: Update imports in `packages/domain/get-started/schema.ts`**
|
||||
|
||||
Replace the import on line 16-17:
|
||||
|
||||
```typescript
|
||||
// OLD (line 16-17):
|
||||
import { addressFormSchema } from "../customer/schema.js";
|
||||
|
||||
// NEW:
|
||||
import { bilingualAddressSchema } from "../address/schema.js";
|
||||
```
|
||||
|
||||
**Step 2: Remove `bilingualEligibilityAddressSchema` (lines 124-140)**
|
||||
|
||||
Delete the entire `bilingualEligibilityAddressSchema` definition. It's a duplicate of `bilingualAddressSchema` with slightly different field definitions. We'll use `bilingualAddressSchema` directly.
|
||||
|
||||
**Step 3: Update `guestEligibilityRequestSchema` (line 146-159)**
|
||||
|
||||
Change line 154 from:
|
||||
|
||||
```typescript
|
||||
address: bilingualEligibilityAddressSchema,
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```typescript
|
||||
address: bilingualAddressSchema,
|
||||
```
|
||||
|
||||
**Step 4: Update `guestHandoffTokenSchema` (line 181-202)**
|
||||
|
||||
Change line 195 from:
|
||||
|
||||
```typescript
|
||||
address: addressFormSchema.partial().optional(),
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```typescript
|
||||
address: bilingualAddressSchema.partial().optional(),
|
||||
```
|
||||
|
||||
**Step 5: Update `completeAccountRequestSchema` (line 215-238)**
|
||||
|
||||
Change line 223 from:
|
||||
|
||||
```typescript
|
||||
address: addressFormSchema.optional(),
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```typescript
|
||||
address: bilingualAddressSchema.optional(),
|
||||
```
|
||||
|
||||
**Step 6: Update `signupWithEligibilityRequestSchema` (line 249-272)**
|
||||
|
||||
Change line 257 from:
|
||||
|
||||
```typescript
|
||||
address: addressFormSchema,
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```typescript
|
||||
address: bilingualAddressSchema,
|
||||
```
|
||||
|
||||
**Step 7: Update `verifyCodeResponseSchema` prefill (line 80-102)**
|
||||
|
||||
Change line 98 from:
|
||||
|
||||
```typescript
|
||||
address: addressFormSchema.partial().optional(),
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```typescript
|
||||
address: bilingualAddressSchema.partial().optional(),
|
||||
```
|
||||
|
||||
**Step 8: Update `getStartedSessionSchema` (line 320-345)**
|
||||
|
||||
Change line 330 from:
|
||||
|
||||
```typescript
|
||||
address: addressFormSchema.partial().optional(),
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```typescript
|
||||
address: bilingualAddressSchema.partial().optional(),
|
||||
```
|
||||
|
||||
**Step 9: Update `packages/domain/get-started/contract.ts`**
|
||||
|
||||
Remove the import of `bilingualEligibilityAddressSchema` on line 19 and the type export on line 90:
|
||||
|
||||
```typescript
|
||||
// Remove from imports (line 19):
|
||||
bilingualEligibilityAddressSchema,
|
||||
|
||||
// Remove from types (line 90):
|
||||
export type BilingualEligibilityAddress = z.infer<typeof bilingualEligibilityAddressSchema>;
|
||||
```
|
||||
|
||||
**Step 10: Update `packages/domain/get-started/index.ts`**
|
||||
|
||||
Remove the export of `bilingualEligibilityAddressSchema` on line 52 and `BilingualEligibilityAddress` on line 28:
|
||||
|
||||
```typescript
|
||||
// Remove from schema exports (line 52):
|
||||
bilingualEligibilityAddressSchema,
|
||||
|
||||
// Remove from type exports (line 28):
|
||||
type BilingualEligibilityAddress,
|
||||
```
|
||||
|
||||
**Step 11: Build domain package and verify**
|
||||
|
||||
Run: `pnpm domain:build`
|
||||
Expected: Build succeeds
|
||||
|
||||
Run: `pnpm type-check`
|
||||
Expected: May show errors in BFF/Portal files that reference the old schemas — those will be fixed in subsequent tasks.
|
||||
|
||||
**Step 12: Commit**
|
||||
|
||||
```bash
|
||||
git add packages/domain/get-started/
|
||||
git commit -m "refactor: replace addressFormSchema with bilingualAddressSchema in get-started schemas"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Create AddressWriterService in BFF
|
||||
|
||||
**Files:**
|
||||
|
||||
- Create: `apps/bff/src/modules/address/address-writer.service.ts`
|
||||
- Modify: `apps/bff/src/modules/address/address.module.ts`
|
||||
|
||||
**Context:** This service centralizes all address transformation and writing logic. It uses `prepareSalesforceContactAddressFields()` from the domain for SF writes, and calls the Japan Post API to resolve English fields for WHMCS writes. It depends on `JapanPostFacade` (for postal code lookup) and `SalesforceFacade` (for SF contact updates).
|
||||
|
||||
**Step 1: Create `apps/bff/src/modules/address/address-writer.service.ts`**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* 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";
|
||||
import { extractErrorMessage } from "@bff/core/utils/error.util.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,
|
||||
});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Update `apps/bff/src/modules/address/address.module.ts`**
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* Address Module
|
||||
*
|
||||
* NestJS module for address lookup and writing functionality.
|
||||
*/
|
||||
|
||||
import { Module } from "@nestjs/common";
|
||||
import { AddressController } from "./address.controller.js";
|
||||
import { AddressWriterService } from "./address-writer.service.js";
|
||||
import { JapanPostModule } from "@bff/integrations/japanpost/japanpost.module.js";
|
||||
import { SalesforceModule } from "@bff/integrations/salesforce/salesforce.module.js";
|
||||
|
||||
@Module({
|
||||
imports: [JapanPostModule, SalesforceModule],
|
||||
controllers: [AddressController],
|
||||
providers: [AddressWriterService],
|
||||
exports: [AddressWriterService],
|
||||
})
|
||||
export class AddressModule {}
|
||||
```
|
||||
|
||||
**Step 3: Verify the SalesforceModule exists and exports SalesforceFacade**
|
||||
|
||||
Run: `pnpm type-check` from the BFF
|
||||
Expected: May still have downstream errors, but AddressWriterService itself should compile. Check that `SalesforceModule` is importable. If not, check the path — it may be `@bff/integrations/salesforce/salesforce.module.js`. Verify with:
|
||||
|
||||
```bash
|
||||
# Check the actual module file path
|
||||
ls apps/bff/src/integrations/salesforce/salesforce.module.ts
|
||||
```
|
||||
|
||||
**Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/bff/src/modules/address/
|
||||
git commit -m "feat: create AddressWriterService for centralized bilingual address handling"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Update NewCustomerSignupWorkflow (Outcome A) to use AddressWriterService
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/bff/src/modules/auth/infra/workflows/new-customer-signup-workflow.service.ts`
|
||||
- Modify: `apps/bff/src/modules/auth/auth.module.ts` (or wherever the auth module imports are — ensure AddressModule is imported)
|
||||
|
||||
**Context:** Currently this workflow (lines 137-189) destructures `address` from the request as `addressFormSchema` shape and manually composes address fields for WHMCS and eligibility case steps. Now `address` will be a `BilingualAddress`. We need to: (1) write Japanese to SF after account creation, (2) resolve English from postal code for WHMCS.
|
||||
|
||||
**Step 1: Add AddressWriterService dependency**
|
||||
|
||||
Add to constructor:
|
||||
|
||||
```typescript
|
||||
import { AddressWriterService } from "@bff/modules/address/address-writer.service.js";
|
||||
|
||||
// In constructor:
|
||||
private readonly addressWriter: AddressWriterService,
|
||||
```
|
||||
|
||||
**Step 2: Update `executeWithSteps()` method (lines 126-240)**
|
||||
|
||||
The `address` variable (line 137) is now `BilingualAddress`. Update the method:
|
||||
|
||||
After Step 1 (SF account creation, ~line 148), add SF address write:
|
||||
|
||||
```typescript
|
||||
// Step 1.5: Write Japanese address to Salesforce (DEGRADABLE)
|
||||
try {
|
||||
await this.addressWriter.writeToSalesforce(sfResult.sfAccountId, address);
|
||||
} catch (addressError) {
|
||||
this.logger.warn(
|
||||
{ error: extractErrorMessage(addressError), email },
|
||||
"SF address write failed (non-critical, continuing)"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
Update Step 2 (eligibility case, ~lines 151-169) — the case step likely accepts simple address. Keep passing the basic fields:
|
||||
|
||||
```typescript
|
||||
const caseResult = await this.caseStep.execute({
|
||||
sfAccountId: sfResult.sfAccountId,
|
||||
address: {
|
||||
address1: `${address.townJa}${address.streetAddress}`,
|
||||
city: address.cityJa,
|
||||
state: address.prefectureJa,
|
||||
postcode: address.postcode,
|
||||
country: "Japan",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Update Step 3 (WHMCS client, ~lines 172-189) — resolve English via postal code:
|
||||
|
||||
```typescript
|
||||
const whmcsAddress = await this.addressWriter.resolveWhmcsAddressFromBilingual(address);
|
||||
|
||||
const whmcsResult = await this.whmcsStep.execute({
|
||||
email,
|
||||
password,
|
||||
firstName,
|
||||
lastName,
|
||||
phone: phone ?? "",
|
||||
address: {
|
||||
address1: whmcsAddress.address1 || "",
|
||||
...(whmcsAddress.address2 && { address2: whmcsAddress.address2 }),
|
||||
city: whmcsAddress.city,
|
||||
state: whmcsAddress.state,
|
||||
postcode: whmcsAddress.postcode,
|
||||
country: whmcsAddress.country,
|
||||
},
|
||||
customerNumber: sfResult.customerNumber ?? null,
|
||||
dateOfBirth,
|
||||
gender,
|
||||
});
|
||||
```
|
||||
|
||||
**Step 3: Ensure AddressModule is imported in the auth module**
|
||||
|
||||
Find the auth module file and add `AddressModule` to imports. Check:
|
||||
|
||||
```bash
|
||||
# Find the auth module
|
||||
grep -l "AuthModule" apps/bff/src/modules/auth/*.ts
|
||||
```
|
||||
|
||||
Add `AddressModule` to the `imports` array.
|
||||
|
||||
**Step 4: Run type-check**
|
||||
|
||||
Run: `pnpm type-check`
|
||||
Expected: Errors may remain in other files but this workflow should compile.
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/bff/src/modules/auth/
|
||||
git commit -m "refactor: update new-customer-signup workflow to use AddressWriterService"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Update GuestEligibilityWorkflow (Outcome B - eligibility check) to use AddressWriterService
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/bff/src/modules/auth/infra/workflows/guest-eligibility-workflow.service.ts`
|
||||
|
||||
**Context:** This workflow already writes Japanese to SF (lines 102-114) via inline code. The `address` is now `BilingualAddress` (from `guestEligibilityRequestSchema` which was updated in Task 1). We replace the inline SF write with `addressWriter.writeToSalesforce()` and store the full bilingual address in the handoff token.
|
||||
|
||||
**Step 1: Add AddressWriterService dependency**
|
||||
|
||||
```typescript
|
||||
import { AddressWriterService } from "@bff/modules/address/address-writer.service.js";
|
||||
|
||||
// In constructor:
|
||||
private readonly addressWriter: AddressWriterService,
|
||||
```
|
||||
|
||||
**Step 2: Replace inline SF address write (lines 102-114)**
|
||||
|
||||
Replace:
|
||||
|
||||
```typescript
|
||||
// Save Japanese address to SF Contact (if Japanese address fields provided)
|
||||
if (address.prefectureJa || address.cityJa || address.townJa) {
|
||||
await this.salesforceAccountService.updateContactAddress(sfAccountId, {
|
||||
mailingStreet: `${address.townJa || ""}${address.streetAddress || ""}`.trim(),
|
||||
mailingCity: address.cityJa || address.city,
|
||||
mailingState: address.prefectureJa || address.state,
|
||||
mailingPostalCode: address.postcode,
|
||||
mailingCountry: "Japan",
|
||||
buildingName: address.buildingName ?? null,
|
||||
roomNumber: address.roomNumber ?? null,
|
||||
});
|
||||
this.logger.debug({ sfAccountId }, "Updated SF Contact with Japanese address");
|
||||
}
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```typescript
|
||||
// Write Japanese address to Salesforce
|
||||
try {
|
||||
await this.addressWriter.writeToSalesforce(sfAccountId, address);
|
||||
} catch (addressError) {
|
||||
this.logger.warn(
|
||||
{ error: extractErrorMessage(addressError), email: normalizedEmail },
|
||||
"SF address write failed (non-critical, continuing)"
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Update eligibility case address (lines 117-128)**
|
||||
|
||||
Update the case step address to use Japanese fields (since the case is about the Japanese address):
|
||||
|
||||
```typescript
|
||||
const { caseId } = await this.eligibilityCaseStep.execute({
|
||||
sfAccountId,
|
||||
address: {
|
||||
address1: `${address.townJa}${address.streetAddress}`,
|
||||
city: address.cityJa,
|
||||
state: address.prefectureJa,
|
||||
postcode: address.postcode,
|
||||
country: "Japan",
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
**Step 4: Update handoff token storage (lines 131-143)**
|
||||
|
||||
The `removeUndefined(address)` call stores the address. Since `address` is now `BilingualAddress`, this will store all bilingual fields. Verify `removeUndefined` doesn't strip needed fields — it only removes `undefined` values, so `null` values for `buildingName`/`roomNumber` will be preserved. This should work as-is.
|
||||
|
||||
**Step 5: Remove unused imports**
|
||||
|
||||
Remove the `SalesforceAccountService` import if it's no longer directly used (the `addressWriter` handles SF writes). However, this service is still used for `findByEmail()` and `createAccount()` on lines 74 and 88, so keep it.
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/bff/src/modules/auth/infra/workflows/guest-eligibility-workflow.service.ts
|
||||
git commit -m "refactor: update guest-eligibility workflow to use AddressWriterService"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Update SfCompletionWorkflow (Outcome B - account completion) to use AddressWriterService
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/bff/src/modules/auth/infra/workflows/sf-completion-workflow.service.ts`
|
||||
|
||||
**Context:** This workflow handles both NEW_CUSTOMER (complete-account) and SF_UNMAPPED paths. It resolves address from the request or session (line 122). Now the address in session/request is `BilingualAddress` (partial). The WHMCS step needs English fields resolved via postal code. For SF_UNMAPPED users returning days later, we may only have Japanese fields + postcode from the session.
|
||||
|
||||
**Step 1: Add AddressWriterService dependency**
|
||||
|
||||
```typescript
|
||||
import { AddressWriterService } from "@bff/modules/address/address-writer.service.js";
|
||||
|
||||
// In constructor:
|
||||
private readonly addressWriter: AddressWriterService,
|
||||
```
|
||||
|
||||
**Step 2: Update `resolveAddress()` method (lines 217-230)**
|
||||
|
||||
The method currently requires `address1`, `city`, `state`, `postcode`. For bilingual addresses, we need `postcode` and at least `prefectureJa` or `prefecture`. Update:
|
||||
|
||||
```typescript
|
||||
private resolveAddress(
|
||||
requestAddress: CompleteAccountRequest["address"] | undefined,
|
||||
sessionAddress: Record<string, string | undefined> | undefined
|
||||
): NonNullable<CompleteAccountRequest["address"]> {
|
||||
const address = requestAddress ?? sessionAddress;
|
||||
|
||||
if (!address || !address.postcode) {
|
||||
throw new BadRequestException(
|
||||
"Address information is incomplete. Please ensure postcode is provided."
|
||||
);
|
||||
}
|
||||
|
||||
return address as NonNullable<CompleteAccountRequest["address"]>;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 3: Update `executeCompletion()` — WHMCS client creation (lines 136-153)**
|
||||
|
||||
Replace the inline address composition with `addressWriter.resolveWhmcsAddress()`:
|
||||
|
||||
```typescript
|
||||
// Resolve English address from postal code for WHMCS
|
||||
const whmcsAddress = await this.addressWriter.resolveWhmcsAddress({
|
||||
postcode: address.postcode,
|
||||
townJa: address.townJa,
|
||||
streetAddress: address.streetAddress || "",
|
||||
buildingName: address.buildingName,
|
||||
roomNumber: address.roomNumber,
|
||||
residenceType: address.residenceType || "house",
|
||||
});
|
||||
|
||||
// Step 2: Create WHMCS client (CRITICAL, has rollback)
|
||||
const whmcsResult = await this.whmcsStep.execute({
|
||||
firstName: finalFirstName,
|
||||
lastName: finalLastName,
|
||||
email: session.email,
|
||||
password,
|
||||
phone: phone ?? "",
|
||||
address: {
|
||||
address1: whmcsAddress.address1 || "",
|
||||
...(whmcsAddress.address2 && { address2: whmcsAddress.address2 }),
|
||||
city: whmcsAddress.city,
|
||||
state: whmcsAddress.state,
|
||||
postcode: whmcsAddress.postcode,
|
||||
country: whmcsAddress.country,
|
||||
},
|
||||
customerNumber: sfResult.customerNumber ?? null,
|
||||
dateOfBirth,
|
||||
gender,
|
||||
});
|
||||
```
|
||||
|
||||
**Step 4: Add SF address write for NEW_CUSTOMER path**
|
||||
|
||||
After SF account creation (line 126-133), write Japanese address to SF if this is a new customer:
|
||||
|
||||
```typescript
|
||||
// Write Japanese address to Salesforce for new customers
|
||||
if (isNewCustomer && address.prefectureJa) {
|
||||
try {
|
||||
await this.addressWriter.writeToSalesforce(sfResult.sfAccountId, address as BilingualAddress);
|
||||
} catch (addressError) {
|
||||
this.logger.warn(
|
||||
{ error: extractErrorMessage(addressError), email: session.email },
|
||||
"SF address write failed (non-critical, continuing)"
|
||||
);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/bff/src/modules/auth/infra/workflows/sf-completion-workflow.service.ts
|
||||
git commit -m "refactor: update sf-completion workflow to use AddressWriterService"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Update Portal Frontend — Stop converting to WHMCS format, send bilingual data
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/useCompleteAccountForm.ts`
|
||||
- Modify: `apps/portal/src/features/get-started/machines/get-started.types.ts`
|
||||
- Modify: `apps/portal/src/features/get-started/machines/get-started.actors.ts`
|
||||
- Modify: `apps/portal/src/features/services/stores/eligibility-check.store.ts`
|
||||
|
||||
**Context:** The frontend already collects `BilingualAddress` via `JapanAddressForm`. But `useCompleteAccountForm.ts` (lines 63-79) converts it to WHMCS format before storing, and the XState machine types (`GetStartedAddress`) use simple address fields. We need to pass bilingual data through to the API.
|
||||
|
||||
**Step 1: Update `GetStartedAddress` type in `get-started.types.ts` (lines 24-32)**
|
||||
|
||||
Replace:
|
||||
|
||||
```typescript
|
||||
export interface GetStartedAddress {
|
||||
address1?: string | undefined;
|
||||
address2?: string | undefined;
|
||||
city?: string | undefined;
|
||||
state?: string | undefined;
|
||||
postcode?: string | undefined;
|
||||
country?: string | undefined;
|
||||
countryCode?: string | undefined;
|
||||
}
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```typescript
|
||||
import type { BilingualAddress } from "@customer-portal/domain/address";
|
||||
|
||||
export type GetStartedAddress = Partial<BilingualAddress>;
|
||||
```
|
||||
|
||||
**Step 2: Update `handleAddressChange` in `useCompleteAccountForm.ts` (lines 63-79)**
|
||||
|
||||
Replace:
|
||||
|
||||
```typescript
|
||||
const handleAddressChange = useCallback(
|
||||
(data: JapanAddressFormData, isComplete: boolean) => {
|
||||
setIsAddressComplete(isComplete);
|
||||
const whmcsFields = prepareWhmcsAddressFields(data);
|
||||
updateFormData({
|
||||
address: {
|
||||
address1: whmcsFields.address1 || "",
|
||||
address2: whmcsFields.address2 || "",
|
||||
city: whmcsFields.city || "",
|
||||
state: whmcsFields.state || "",
|
||||
postcode: whmcsFields.postcode || "",
|
||||
country: "JP",
|
||||
},
|
||||
});
|
||||
},
|
||||
[updateFormData]
|
||||
);
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```typescript
|
||||
const handleAddressChange = useCallback(
|
||||
(data: JapanAddressFormData, isComplete: boolean) => {
|
||||
setIsAddressComplete(isComplete);
|
||||
updateFormData({ address: data });
|
||||
},
|
||||
[updateFormData]
|
||||
);
|
||||
```
|
||||
|
||||
Remove the unused import of `prepareWhmcsAddressFields` from line 3.
|
||||
|
||||
**Step 3: Update `completeAccountActor` in `get-started.actors.ts` (lines 42-71)**
|
||||
|
||||
The actor currently destructures `formData.address` into simple fields. Update to pass bilingual address:
|
||||
|
||||
```typescript
|
||||
export const completeAccountActor = fromPromise<CompleteAccountOutput, CompleteAccountInput>(
|
||||
async ({ input }) => {
|
||||
const { sessionToken, formData, accountStatus } = input;
|
||||
const isNewCustomer = accountStatus === "new_customer";
|
||||
|
||||
return getStartedApi.completeAccount({
|
||||
sessionToken,
|
||||
password: formData.password,
|
||||
phone: formData.phone,
|
||||
dateOfBirth: formData.dateOfBirth,
|
||||
gender: formData.gender as "male" | "female" | "other",
|
||||
acceptTerms: formData.acceptTerms,
|
||||
marketingConsent: formData.marketingConsent,
|
||||
...(isNewCustomer || formData.firstName ? { firstName: formData.firstName } : {}),
|
||||
...(isNewCustomer || formData.lastName ? { lastName: formData.lastName } : {}),
|
||||
...(formData.address?.postcode ? { address: formData.address as BilingualAddress } : {}),
|
||||
});
|
||||
}
|
||||
);
|
||||
```
|
||||
|
||||
Add the import:
|
||||
|
||||
```typescript
|
||||
import type { BilingualAddress } from "@customer-portal/domain/address";
|
||||
```
|
||||
|
||||
**Step 4: Update eligibility check store — `buildEligibilityAddress` and `completeAccountAction`**
|
||||
|
||||
In `apps/portal/src/features/services/stores/eligibility-check.store.ts`:
|
||||
|
||||
The `buildEligibilityAddress` function (lines 172-192) currently converts to WHMCS format. Since the `guestEligibilityRequestSchema` now expects `bilingualAddressSchema`, we pass the `JapanAddressFormData` directly:
|
||||
|
||||
Replace `buildEligibilityAddress` and its type with:
|
||||
|
||||
```typescript
|
||||
// Remove the entire buildEligibilityAddress function and EligibilityCheckAddress interface (lines 157-192)
|
||||
```
|
||||
|
||||
Update `submitOnlyAction` (line 216):
|
||||
|
||||
```typescript
|
||||
// OLD:
|
||||
address: buildEligibilityAddress(formData.address),
|
||||
// NEW:
|
||||
address: formData.address,
|
||||
```
|
||||
|
||||
Update `completeAccountAction` (lines 365-379):
|
||||
Replace:
|
||||
|
||||
```typescript
|
||||
const whmcsAddress = prepareWhmcsAddressFields(formData.address);
|
||||
|
||||
const result = await signupWithEligibility({
|
||||
sessionToken,
|
||||
firstName: formData.firstName.trim(),
|
||||
lastName: formData.lastName.trim(),
|
||||
address: {
|
||||
address1: whmcsAddress.address1 || "",
|
||||
...(whmcsAddress.address2 && { address2: whmcsAddress.address2 }),
|
||||
city: whmcsAddress.city || "",
|
||||
state: whmcsAddress.state || "",
|
||||
postcode: whmcsAddress.postcode || "",
|
||||
country: "JP",
|
||||
},
|
||||
...
|
||||
});
|
||||
```
|
||||
|
||||
With:
|
||||
|
||||
```typescript
|
||||
const result = await signupWithEligibility({
|
||||
sessionToken,
|
||||
firstName: formData.firstName.trim(),
|
||||
lastName: formData.lastName.trim(),
|
||||
address: formData.address,
|
||||
phone: accountData.phone.trim(),
|
||||
password: accountData.password,
|
||||
dateOfBirth: accountData.dateOfBirth,
|
||||
gender: accountData.gender as "male" | "female" | "other",
|
||||
acceptTerms: accountData.acceptTerms,
|
||||
marketingConsent: accountData.marketingConsent,
|
||||
});
|
||||
```
|
||||
|
||||
Remove unused imports: `prepareWhmcsAddressFields` from line 20.
|
||||
|
||||
**Step 5: Run type-check**
|
||||
|
||||
Run: `pnpm type-check`
|
||||
Expected: Should pass (or show only pre-existing issues).
|
||||
|
||||
**Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/portal/src/features/get-started/ apps/portal/src/features/services/stores/
|
||||
git commit -m "refactor: send bilingual address directly from portal instead of converting to WHMCS format"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Update BFF Verification Workflow — Prefill bilingual address from Salesforce
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts`
|
||||
|
||||
**Context:** When a returning user (SF_UNMAPPED, B2 scenario) verifies their email, the BFF prefills address data from Salesforce. Currently it prefills simple address fields. We need to prefill bilingual fields — at minimum `postcode`, `prefectureJa`, `cityJa`, `townJa`, `streetAddress` from the SF contact's Mailing\* fields.
|
||||
|
||||
**Step 1: Find the prefill logic**
|
||||
|
||||
Search for `prefill` in the verification workflow. The `determineAccountStatus()` method builds the prefill object that gets returned to the frontend.
|
||||
|
||||
**Step 2: Update the SF prefill to include bilingual fields**
|
||||
|
||||
When building the prefill for SF_UNMAPPED users, map Salesforce fields to bilingual schema:
|
||||
|
||||
```typescript
|
||||
prefill: {
|
||||
firstName: sfContact?.FirstName,
|
||||
lastName: sfContact?.LastName,
|
||||
email: session.email,
|
||||
address: {
|
||||
// Japanese fields from SF Contact
|
||||
prefectureJa: sfContact?.MailingState,
|
||||
cityJa: sfContact?.MailingCity,
|
||||
townJa: extractTownFromMailingStreet(sfContact?.MailingStreet),
|
||||
postcode: sfContact?.MailingPostalCode,
|
||||
// streetAddress may be embedded in MailingStreet after town name
|
||||
streetAddress: extractStreetFromMailingStreet(sfContact?.MailingStreet),
|
||||
},
|
||||
eligibilityStatus: sfAccount?.eligibilityStatus,
|
||||
}
|
||||
```
|
||||
|
||||
Note: `MailingStreet` in SF contains `townJa + streetAddress` (e.g., "東麻布1-5-3"). We need to parse this. The town and street address were concatenated without delimiter in `prepareSalesforceContactAddressFields()`. This parsing may be imperfect — consider storing them separately in the session. For now, store `MailingStreet` as-is and let the BFF resolve at WHMCS write time using only the postal code (which is enough for the Japan Post API lookup).
|
||||
|
||||
Actually, the simpler approach: just pass the postcode and whatever SF fields we have. The `AddressWriterService.resolveWhmcsAddress()` only needs `postcode` to derive English fields. The frontend won't show the address form for B2 users (address is stored). So the prefill doesn't need to be perfectly parsed — it just needs the postal code for the WHMCS resolution.
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts
|
||||
git commit -m "refactor: prefill bilingual address fields from Salesforce for SF_UNMAPPED users"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Ensure AuthModule imports AddressModule
|
||||
|
||||
**Files:**
|
||||
|
||||
- Modify: `apps/bff/src/modules/auth/auth.module.ts` (or the module that registers the workflow services)
|
||||
|
||||
**Context:** The workflow services now depend on `AddressWriterService`, which is provided by `AddressModule`. The auth module needs to import it.
|
||||
|
||||
**Step 1: Find the auth module**
|
||||
|
||||
```bash
|
||||
# Find the file
|
||||
ls apps/bff/src/modules/auth/auth.module.ts
|
||||
```
|
||||
|
||||
**Step 2: Add AddressModule import**
|
||||
|
||||
```typescript
|
||||
import { AddressModule } from "@bff/modules/address/address.module.js";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// ... existing imports
|
||||
AddressModule,
|
||||
],
|
||||
// ...
|
||||
})
|
||||
export class AuthModule {}
|
||||
```
|
||||
|
||||
**Step 3: Run type-check and build**
|
||||
|
||||
Run: `pnpm domain:build && pnpm type-check`
|
||||
Expected: All type errors resolved.
|
||||
|
||||
**Step 4: Run lint**
|
||||
|
||||
Run: `pnpm lint`
|
||||
Expected: Passes (may have unused import warnings from removed code — fix those).
|
||||
|
||||
**Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add apps/bff/src/modules/auth/auth.module.ts
|
||||
git commit -m "chore: import AddressModule in AuthModule for AddressWriterService access"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 9: End-to-end verification and cleanup
|
||||
|
||||
**Files:** Various (cleanup pass)
|
||||
|
||||
**Step 1: Search for remaining references to old patterns**
|
||||
|
||||
Search for:
|
||||
|
||||
- `addressFormSchema` usage in get-started contexts (should be replaced)
|
||||
- `bilingualEligibilityAddressSchema` references (should be removed)
|
||||
- `prepareWhmcsAddressFields` in portal files (should be removed from signup flows — still needed in profile/settings)
|
||||
- `buildEligibilityAddress` (should be removed)
|
||||
|
||||
```bash
|
||||
# Check for remaining old references
|
||||
pnpm exec grep -r "addressFormSchema" packages/domain/get-started/
|
||||
pnpm exec grep -r "bilingualEligibilityAddressSchema" packages/domain/ apps/
|
||||
pnpm exec grep -r "buildEligibilityAddress" apps/portal/
|
||||
```
|
||||
|
||||
**Step 2: Full build check**
|
||||
|
||||
Run: `pnpm domain:build && pnpm type-check && pnpm lint`
|
||||
Expected: All pass.
|
||||
|
||||
**Step 3: Verify nothing is broken in the domain package exports**
|
||||
|
||||
Check that `packages/domain/get-started/index.ts` and `packages/domain/address/index.ts` export everything needed.
|
||||
|
||||
**Step 4: Final commit**
|
||||
|
||||
```bash
|
||||
git add -A
|
||||
git commit -m "chore: cleanup remaining references to old address schemas"
|
||||
```
|
||||
@ -16,7 +16,6 @@ import type {
|
||||
sendVerificationCodeResponseSchema,
|
||||
verifyCodeRequestSchema,
|
||||
verifyCodeResponseSchema,
|
||||
bilingualEligibilityAddressSchema,
|
||||
guestEligibilityRequestSchema,
|
||||
guestEligibilityResponseSchema,
|
||||
guestHandoffTokenSchema,
|
||||
@ -87,7 +86,6 @@ export type GetStartedErrorCode =
|
||||
|
||||
export type SendVerificationCodeRequest = z.infer<typeof sendVerificationCodeRequestSchema>;
|
||||
export type VerifyCodeRequest = z.infer<typeof verifyCodeRequestSchema>;
|
||||
export type BilingualEligibilityAddress = z.infer<typeof bilingualEligibilityAddressSchema>;
|
||||
export type GuestEligibilityRequest = z.infer<typeof guestEligibilityRequestSchema>;
|
||||
export type CompleteAccountRequest = z.infer<typeof completeAccountRequestSchema>;
|
||||
export type SignupWithEligibilityRequest = z.infer<typeof signupWithEligibilityRequestSchema>;
|
||||
|
||||
@ -24,7 +24,6 @@ export {
|
||||
type SendVerificationCodeResponse,
|
||||
type VerifyCodeRequest,
|
||||
type VerifyCodeResponse,
|
||||
type BilingualEligibilityAddress,
|
||||
type GuestEligibilityRequest,
|
||||
type GuestEligibilityResponse,
|
||||
type GuestHandoffToken,
|
||||
@ -49,7 +48,6 @@ export {
|
||||
verifyCodeResponseSchema,
|
||||
accountStatusSchema,
|
||||
// Guest eligibility schemas (no OTP required)
|
||||
bilingualEligibilityAddressSchema,
|
||||
guestEligibilityRequestSchema,
|
||||
guestEligibilityResponseSchema,
|
||||
guestHandoffTokenSchema,
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
phoneSchema,
|
||||
genderEnum,
|
||||
} from "../common/schema.js";
|
||||
import { addressFormSchema } from "../customer/schema.js";
|
||||
import { bilingualAddressSchema } from "../address/schema.js";
|
||||
|
||||
// ============================================================================
|
||||
// Validation Message Constants
|
||||
@ -95,7 +95,7 @@ export const verifyCodeResponseSchema = z.object({
|
||||
lastName: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
phone: z.string().optional(),
|
||||
address: addressFormSchema.partial().optional(),
|
||||
address: bilingualAddressSchema.partial().optional(),
|
||||
eligibilityStatus: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
@ -117,28 +117,6 @@ const isoDateOnlySchema = z
|
||||
// Guest Eligibility Check Schemas (No OTP Required)
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Bilingual address schema for eligibility requests
|
||||
* Contains both English (for WHMCS) and Japanese (for Salesforce) address fields
|
||||
*/
|
||||
export const bilingualEligibilityAddressSchema = z.object({
|
||||
// English/Romanized fields (for WHMCS)
|
||||
address1: z.string().min(1, "Address is required").max(200).trim(),
|
||||
address2: z.string().max(200).trim().optional(),
|
||||
city: z.string().min(1, "City is required").max(100).trim(),
|
||||
state: z.string().min(1, "Prefecture is required").max(100).trim(),
|
||||
postcode: z.string().min(1, "Postcode is required").max(20).trim(),
|
||||
country: z.string().max(100).trim().optional(),
|
||||
|
||||
// Japanese fields (for Salesforce)
|
||||
prefectureJa: z.string().max(100).trim().optional(),
|
||||
cityJa: z.string().max(100).trim().optional(),
|
||||
townJa: z.string().max(200).trim().optional(),
|
||||
streetAddress: z.string().max(50).trim().optional(),
|
||||
buildingName: z.string().max(200).trim().nullable().optional(),
|
||||
roomNumber: z.string().max(50).trim().nullable().optional(),
|
||||
});
|
||||
|
||||
/**
|
||||
* Request for guest eligibility check - NO email verification required
|
||||
* Allows users to check availability without verifying email first
|
||||
@ -150,8 +128,8 @@ export const guestEligibilityRequestSchema = z.object({
|
||||
firstName: nameSchema,
|
||||
/** Customer last name */
|
||||
lastName: nameSchema,
|
||||
/** Full address with both English and Japanese fields for eligibility check */
|
||||
address: bilingualEligibilityAddressSchema,
|
||||
/** Full bilingual address for eligibility check */
|
||||
address: bilingualAddressSchema,
|
||||
/** Optional phone number */
|
||||
phone: phoneSchema.optional(),
|
||||
/** Whether user wants to continue to account creation */
|
||||
@ -192,7 +170,7 @@ export const guestHandoffTokenSchema = z.object({
|
||||
/** Last name */
|
||||
lastName: z.string(),
|
||||
/** Address from eligibility check */
|
||||
address: addressFormSchema.partial().optional(),
|
||||
address: bilingualAddressSchema.partial().optional(),
|
||||
/** Phone number if provided */
|
||||
phone: z.string().optional(),
|
||||
/** SF Account ID created during eligibility check */
|
||||
@ -220,7 +198,7 @@ export const completeAccountRequestSchema = z.object({
|
||||
/** Customer last name (required for new customers, optional for SF-only) */
|
||||
lastName: nameSchema.optional(),
|
||||
/** Address (required for new customers, optional for SF-only who have it in session) */
|
||||
address: addressFormSchema.optional(),
|
||||
address: bilingualAddressSchema.optional(),
|
||||
/** Password for the new portal account */
|
||||
password: passwordSchema,
|
||||
/** Phone number (may be pre-filled from SF) */
|
||||
@ -253,8 +231,8 @@ export const signupWithEligibilityRequestSchema = z.object({
|
||||
firstName: nameSchema,
|
||||
/** Customer last name */
|
||||
lastName: nameSchema,
|
||||
/** Full address for eligibility check and WHMCS */
|
||||
address: addressFormSchema,
|
||||
/** Full bilingual address for eligibility check and WHMCS */
|
||||
address: bilingualAddressSchema,
|
||||
/** Phone number */
|
||||
phone: phoneSchema,
|
||||
/** Password for the new portal account */
|
||||
@ -327,7 +305,7 @@ export const getStartedSessionSchema = z.object({
|
||||
/** Last name (if provided during quick check) */
|
||||
lastName: z.string().optional(),
|
||||
/** Address (if provided during quick check) */
|
||||
address: addressFormSchema.partial().optional(),
|
||||
address: bilingualAddressSchema.partial().optional(),
|
||||
/** Phone number (if provided) */
|
||||
phone: z.string().optional(),
|
||||
/** Account status after verification */
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user