952 lines
30 KiB
Markdown
952 lines
30 KiB
Markdown
|
|
# 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"
|
||
|
|
```
|