From 6299fbabdcdbd53b6345c84791d0cea20b257d93 Mon Sep 17 00:00:00 2001 From: barsa Date: Tue, 3 Mar 2026 16:33:40 +0900 Subject: [PATCH] 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. --- .../modules/address/address-writer.service.ts | 129 +++ .../bff/src/modules/address/address.module.ts | 8 +- .../auth/get-started/get-started.module.ts | 2 + .../guest-eligibility-workflow.service.ts | 32 +- .../new-customer-signup-workflow.service.ts | 35 +- .../sf-completion-workflow.service.ts | 47 +- .../verification-workflow.service.ts | 20 +- .../useCompleteAccountForm.ts | 13 +- .../machines/get-started.actors.ts | 14 +- .../machines/get-started.machine.ts | 37 +- .../get-started/machines/get-started.types.ts | 13 +- .../stores/eligibility-check.store.ts | 55 +- ...-03-03-bilingual-address-handler-design.md | 152 +++ ...26-03-03-bilingual-address-handler-plan.md | 951 ++++++++++++++++++ packages/domain/get-started/contract.ts | 2 - packages/domain/get-started/index.ts | 2 - packages/domain/get-started/schema.ts | 40 +- 17 files changed, 1356 insertions(+), 196 deletions(-) create mode 100644 apps/bff/src/modules/address/address-writer.service.ts create mode 100644 docs/plans/2026-03-03-bilingual-address-handler-design.md create mode 100644 docs/plans/2026-03-03-bilingual-address-handler-plan.md diff --git a/apps/bff/src/modules/address/address-writer.service.ts b/apps/bff/src/modules/address/address-writer.service.ts new file mode 100644 index 00000000..b816bd92 --- /dev/null +++ b/apps/bff/src/modules/address/address-writer.service.ts @@ -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 { + 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 { + 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 { + return this.resolveWhmcsAddress({ + postcode: address.postcode, + townJa: address.townJa, + streetAddress: address.streetAddress, + buildingName: address.buildingName, + roomNumber: address.roomNumber, + residenceType: address.residenceType, + }); + } +} diff --git a/apps/bff/src/modules/address/address.module.ts b/apps/bff/src/modules/address/address.module.ts index 6d6d6844..04e1a5aa 100644 --- a/apps/bff/src/modules/address/address.module.ts +++ b/apps/bff/src/modules/address/address.module.ts @@ -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 {} diff --git a/apps/bff/src/modules/auth/get-started/get-started.module.ts b/apps/bff/src/modules/auth/get-started/get-started.module.ts index 67cc3ba9..ae6c2722 100644 --- a/apps/bff/src/modules/auth/get-started/get-started.module.ts +++ b/apps/bff/src/modules/auth/get-started/get-started.module.ts @@ -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: [ diff --git a/apps/bff/src/modules/auth/infra/workflows/guest-eligibility-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/guest-eligibility-workflow.service.ts index b117a488..c53e9996 100644 --- a/apps/bff/src/modules/auth/infra/workflows/guest-eligibility-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/guest-eligibility-workflow.service.ts @@ -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", }, }); diff --git a/apps/bff/src/modules/auth/infra/workflows/new-customer-signup-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/new-customer-signup-workflow.service.ts index af50b051..2454e232 100644 --- a/apps/bff/src/modules/auth/infra/workflows/new-customer-signup-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/new-customer-signup-workflow.service.ts @@ -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, diff --git a/apps/bff/src/modules/auth/infra/workflows/sf-completion-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/sf-completion-workflow.service.ts index 2c912e2f..5cdfcd62 100644 --- a/apps/bff/src/modules/auth/infra/workflows/sf-completion-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/sf-completion-workflow.service.ts @@ -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 | undefined; + address?: Record | undefined; } ): Promise { 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 | undefined + sessionAddress: Record | undefined ): NonNullable { 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." ); } diff --git a/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts b/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts index b5dc76f4..416cc337 100644 --- a/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts +++ b/apps/bff/src/modules/auth/infra/workflows/verification-workflow.service.ts @@ -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, + }, + }), }; } diff --git a/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/useCompleteAccountForm.ts b/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/useCompleteAccountForm.ts index 3adb139d..7c357ac0 100644 --- a/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/useCompleteAccountForm.ts +++ b/apps/portal/src/features/get-started/components/GetStartedForm/steps/complete-account/useCompleteAccountForm.ts @@ -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] ); diff --git a/apps/portal/src/features/get-started/machines/get-started.actors.ts b/apps/portal/src/features/get-started/machines/get-started.actors.ts index ceddada0..a13bd1bf 100644 --- a/apps/portal/src/features/get-started/machines/get-started.actors.ts +++ b/apps/portal/src/features/get-started/machines/get-started.actors.ts @@ -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 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 **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; +``` + +**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 { + 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 { + 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 { + 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 | undefined +): NonNullable { + const address = requestAddress ?? sessionAddress; + + if (!address || !address.postcode) { + throw new BadRequestException( + "Address information is incomplete. Please ensure postcode is provided." + ); + } + + return address as NonNullable; +} +``` + +**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; +``` + +**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( + 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" +``` diff --git a/packages/domain/get-started/contract.ts b/packages/domain/get-started/contract.ts index af220144..61c9054a 100644 --- a/packages/domain/get-started/contract.ts +++ b/packages/domain/get-started/contract.ts @@ -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; export type VerifyCodeRequest = z.infer; -export type BilingualEligibilityAddress = z.infer; export type GuestEligibilityRequest = z.infer; export type CompleteAccountRequest = z.infer; export type SignupWithEligibilityRequest = z.infer; diff --git a/packages/domain/get-started/index.ts b/packages/domain/get-started/index.ts index 6ea68ab7..df8e05d8 100644 --- a/packages/domain/get-started/index.ts +++ b/packages/domain/get-started/index.ts @@ -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, diff --git a/packages/domain/get-started/schema.ts b/packages/domain/get-started/schema.ts index 4af4c6ab..4a68f306 100644 --- a/packages/domain/get-started/schema.ts +++ b/packages/domain/get-started/schema.ts @@ -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 */