Assist_Design/docs/plans/2026-03-03-bilingual-address-handler-plan.md
barsa 6299fbabdc 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.
2026-03-03 16:33:40 +09:00

30 KiB

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:

// 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:

address: bilingualEligibilityAddressSchema,

to:

address: bilingualAddressSchema,

Step 4: Update guestHandoffTokenSchema (line 181-202)

Change line 195 from:

address: addressFormSchema.partial().optional(),

to:

address: bilingualAddressSchema.partial().optional(),

Step 5: Update completeAccountRequestSchema (line 215-238)

Change line 223 from:

address: addressFormSchema.optional(),

to:

address: bilingualAddressSchema.optional(),

Step 6: Update signupWithEligibilityRequestSchema (line 249-272)

Change line 257 from:

address: addressFormSchema,

to:

address: bilingualAddressSchema,

Step 7: Update verifyCodeResponseSchema prefill (line 80-102)

Change line 98 from:

address: addressFormSchema.partial().optional(),

to:

address: bilingualAddressSchema.partial().optional(),

Step 8: Update getStartedSessionSchema (line 320-345)

Change line 330 from:

address: addressFormSchema.partial().optional(),

to:

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:

// 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:

// 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

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

/**
 * 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

/**
 * 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:

# Check the actual module file path
ls apps/bff/src/integrations/salesforce/salesforce.module.ts

Step 4: Commit

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:

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:

// 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:

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:

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:

# 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

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

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:

// 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:

// 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):

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

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

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:

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():

// 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:

// 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

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:

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:

import type { BilingualAddress } from "@customer-portal/domain/address";

export type GetStartedAddress = Partial<BilingualAddress>;

Step 2: Update handleAddressChange in useCompleteAccountForm.ts (lines 63-79)

Replace:

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:

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:

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:

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:

// Remove the entire buildEligibilityAddress function and EligibilityCheckAddress interface (lines 157-192)

Update submitOnlyAction (line 216):

// OLD:
address: buildEligibilityAddress(formData.address),
// NEW:
address: formData.address,

Update completeAccountAction (lines 365-379): Replace:

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:

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

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:

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

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

# Find the file
ls apps/bff/src/modules/auth/auth.module.ts

Step 2: Add AddressModule import

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

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)
# 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

git add -A
git commit -m "chore: cleanup remaining references to old address schemas"