2026-01-13 18:41:17 +09:00
|
|
|
/**
|
|
|
|
|
* Address Domain - Schemas
|
|
|
|
|
*
|
|
|
|
|
* Zod validation schemas for address lookup and bilingual address data.
|
|
|
|
|
* Types are derived from schemas (Schema-First Approach).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { z } from "zod";
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// ZIP Code Schemas
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Japanese ZIP code schema
|
|
|
|
|
* Accepts: "1000001", "100-0001" -> normalizes to "1000001"
|
|
|
|
|
*/
|
|
|
|
|
export const zipCodeSchema = z
|
|
|
|
|
.string()
|
|
|
|
|
.regex(/^\d{3}-?\d{4}$/, "ZIP code must be 7 digits (e.g., 100-0001)")
|
|
|
|
|
.transform(val => val.replace(/-/g, ""));
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* ZIP code lookup request
|
|
|
|
|
*/
|
|
|
|
|
export const zipCodeLookupRequestSchema = z.object({
|
|
|
|
|
zipCode: zipCodeSchema,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Japan Post Address Schemas
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Address from Japan Post API lookup
|
|
|
|
|
* Contains both Japanese and romanized versions
|
|
|
|
|
*/
|
|
|
|
|
export const japanPostAddressSchema = z.object({
|
|
|
|
|
zipCode: z.string(),
|
|
|
|
|
// Japanese (for Salesforce)
|
|
|
|
|
prefecture: z.string(),
|
|
|
|
|
prefectureKana: z.string().optional(),
|
|
|
|
|
city: z.string(),
|
|
|
|
|
cityKana: z.string().optional(),
|
|
|
|
|
town: z.string(),
|
|
|
|
|
townKana: z.string().optional(),
|
|
|
|
|
// Romanized (for WHMCS)
|
|
|
|
|
prefectureRoma: z.string(),
|
|
|
|
|
cityRoma: z.string(),
|
|
|
|
|
townRoma: z.string(),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Address lookup result containing multiple potential matches
|
|
|
|
|
*/
|
|
|
|
|
export const addressLookupResultSchema = z.object({
|
|
|
|
|
zipCode: z.string(),
|
|
|
|
|
addresses: z.array(japanPostAddressSchema),
|
|
|
|
|
count: z.number(),
|
|
|
|
|
});
|
|
|
|
|
|
2026-01-14 16:25:06 +09:00
|
|
|
// ============================================================================
|
|
|
|
|
// Street Address Detail (Chome/Banchi/Go)
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Street address detail schema (chome/banchi/go)
|
|
|
|
|
* Only accepts hyphenated number format: "1-5-3", "1-5", "15-3"
|
|
|
|
|
*
|
|
|
|
|
* Format: {chome}-{banchi}-{go} or {chome}-{banchi}
|
|
|
|
|
* - chome: Block district number (1-99)
|
|
|
|
|
* - banchi: Block number (1-999)
|
|
|
|
|
* - go: Building/house number (1-999, optional)
|
|
|
|
|
*/
|
|
|
|
|
export const streetAddressDetailSchema = z
|
|
|
|
|
.string()
|
|
|
|
|
.min(1, "Street address is required")
|
|
|
|
|
.max(20, "Street address is too long")
|
|
|
|
|
.regex(
|
|
|
|
|
/^\d{1,2}-\d{1,3}(-\d{1,3})?$/,
|
|
|
|
|
"Use format like 1-5-3 (chome-banchi-go) or 1-5 (chome-banchi)"
|
|
|
|
|
);
|
|
|
|
|
|
2026-01-13 18:41:17 +09:00
|
|
|
// ============================================================================
|
|
|
|
|
// Bilingual Address Schemas (Extended from customer/addressSchema)
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Building information for Japanese addresses
|
|
|
|
|
*/
|
|
|
|
|
export const buildingInfoSchema = z.object({
|
|
|
|
|
buildingName: z.string().max(200).optional().nullable(),
|
|
|
|
|
roomNumber: z.string().max(50).optional().nullable(),
|
|
|
|
|
residenceType: z.enum(["house", "apartment"]),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extended address data with bilingual fields
|
|
|
|
|
* Used when updating address in both WHMCS (English) and Salesforce (Japanese)
|
|
|
|
|
*/
|
|
|
|
|
export const bilingualAddressSchema = z.object({
|
|
|
|
|
// ZIP code
|
|
|
|
|
postcode: z.string(),
|
|
|
|
|
|
|
|
|
|
// English/Romanized (for WHMCS)
|
|
|
|
|
prefecture: z.string(), // romanized
|
|
|
|
|
city: z.string(), // romanized
|
|
|
|
|
town: z.string(), // romanized
|
|
|
|
|
|
|
|
|
|
// Japanese (for Salesforce)
|
|
|
|
|
prefectureJa: z.string(),
|
|
|
|
|
cityJa: z.string(),
|
|
|
|
|
townJa: z.string(),
|
|
|
|
|
|
2026-01-14 16:25:06 +09:00
|
|
|
// Street address detail (chome/banchi/go) - e.g., "1-5-3" or "1丁目5番3号"
|
|
|
|
|
streetAddress: streetAddressDetailSchema,
|
|
|
|
|
|
2026-01-13 18:41:17 +09:00
|
|
|
// Building info (same for both systems)
|
|
|
|
|
buildingName: z.string().max(200).optional().nullable(),
|
|
|
|
|
roomNumber: z.string().max(50).optional().nullable(),
|
|
|
|
|
residenceType: z.enum(["house", "apartment"]),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Address update request for profile/signup
|
|
|
|
|
* Combines bilingual address data for dual-write to WHMCS + Salesforce
|
|
|
|
|
*/
|
|
|
|
|
export const addressUpdateRequestSchema = bilingualAddressSchema.extend({
|
|
|
|
|
country: z.literal("JP").default("JP"),
|
|
|
|
|
countryCode: z.literal("JP").default("JP"),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// WHMCS Address Mapping
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prepare address fields for WHMCS update
|
|
|
|
|
* Maps bilingual address to WHMCS field format
|
2026-01-14 16:25:06 +09:00
|
|
|
*
|
|
|
|
|
* WHMCS field mapping:
|
|
|
|
|
* - address1: Building name + Room number (e.g., "Sunshine Mansion 101")
|
|
|
|
|
* - address2: Town + Street address (e.g., "Higashiazabu 1-5-3")
|
|
|
|
|
* - city: City (romanized)
|
|
|
|
|
* - state: Prefecture (romanized)
|
2026-01-13 18:41:17 +09:00
|
|
|
*/
|
|
|
|
|
export function prepareWhmcsAddressFields(address: BilingualAddress): WhmcsAddressFields {
|
|
|
|
|
const buildingPart = address.buildingName || "";
|
|
|
|
|
const roomPart = address.roomNumber || "";
|
|
|
|
|
|
2026-01-14 16:25:06 +09:00
|
|
|
// address1: Building + Room (for apartments) or just Building (for houses)
|
2026-01-13 18:41:17 +09:00
|
|
|
const address1 =
|
|
|
|
|
address.residenceType === "apartment" ? `${buildingPart} ${roomPart}`.trim() : buildingPart;
|
|
|
|
|
|
2026-01-14 16:25:06 +09:00
|
|
|
// address2: Town + Street address (romanized)
|
|
|
|
|
const address2 = `${address.town} ${address.streetAddress}`.trim();
|
|
|
|
|
|
2026-01-13 18:41:17 +09:00
|
|
|
return {
|
|
|
|
|
address1: address1 || undefined,
|
2026-01-14 16:25:06 +09:00
|
|
|
address2: address2 || undefined,
|
2026-01-13 18:41:17 +09:00
|
|
|
city: address.city, // romanized city
|
|
|
|
|
state: address.prefecture, // romanized prefecture
|
|
|
|
|
postcode: address.postcode,
|
|
|
|
|
country: "JP",
|
|
|
|
|
countrycode: "JP",
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Salesforce Contact Address Mapping
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Prepare address fields for Salesforce Contact update
|
|
|
|
|
* Maps bilingual address to Salesforce field format
|
2026-01-14 16:25:06 +09:00
|
|
|
*
|
|
|
|
|
* Salesforce field mapping:
|
|
|
|
|
* - MailingStreet: Town + Street address (Japanese)
|
|
|
|
|
* - MailingCity: City (Japanese)
|
|
|
|
|
* - MailingState: Prefecture (Japanese)
|
2026-01-13 18:41:17 +09:00
|
|
|
*/
|
|
|
|
|
export function prepareSalesforceContactAddressFields(
|
|
|
|
|
address: BilingualAddress
|
|
|
|
|
): SalesforceContactAddressFields {
|
2026-01-14 16:25:06 +09:00
|
|
|
// Combine town and street address for MailingStreet
|
|
|
|
|
// Example: "東麻布1-5-3" or "東麻布1丁目5番3号"
|
|
|
|
|
const mailingStreet = `${address.townJa}${address.streetAddress}`;
|
|
|
|
|
|
2026-01-13 18:41:17 +09:00
|
|
|
return {
|
2026-01-14 16:25:06 +09:00
|
|
|
MailingStreet: mailingStreet,
|
2026-01-13 18:41:17 +09:00
|
|
|
MailingCity: address.cityJa, // Japanese city
|
|
|
|
|
MailingState: address.prefectureJa, // Japanese prefecture
|
|
|
|
|
MailingPostalCode: address.postcode,
|
|
|
|
|
MailingCountry: "Japan",
|
|
|
|
|
BuildingName__c: address.buildingName || null,
|
|
|
|
|
RoomNumber__c: address.roomNumber || null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
|
// Exported Types
|
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
|
|
export type ZipCode = z.input<typeof zipCodeSchema>;
|
|
|
|
|
export type ZipCodeLookupRequest = z.infer<typeof zipCodeLookupRequestSchema>;
|
|
|
|
|
export type JapanPostAddress = z.infer<typeof japanPostAddressSchema>;
|
|
|
|
|
export type AddressLookupResult = z.infer<typeof addressLookupResultSchema>;
|
2026-01-14 16:25:06 +09:00
|
|
|
export type StreetAddressDetail = z.infer<typeof streetAddressDetailSchema>;
|
2026-01-13 18:41:17 +09:00
|
|
|
export type BuildingInfo = z.infer<typeof buildingInfoSchema>;
|
|
|
|
|
export type BilingualAddress = z.infer<typeof bilingualAddressSchema>;
|
|
|
|
|
export type AddressUpdateRequest = z.infer<typeof addressUpdateRequestSchema>;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* WHMCS address field structure
|
|
|
|
|
*/
|
|
|
|
|
export interface WhmcsAddressFields {
|
|
|
|
|
address1?: string;
|
|
|
|
|
address2?: string;
|
|
|
|
|
city?: string;
|
|
|
|
|
state?: string;
|
|
|
|
|
postcode?: string;
|
|
|
|
|
country?: string;
|
|
|
|
|
countrycode?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Salesforce Contact address field structure
|
|
|
|
|
*/
|
|
|
|
|
export interface SalesforceContactAddressFields {
|
|
|
|
|
MailingStreet: string;
|
|
|
|
|
MailingCity: string;
|
|
|
|
|
MailingState: string;
|
|
|
|
|
MailingPostalCode: string;
|
|
|
|
|
MailingCountry: string;
|
|
|
|
|
BuildingName__c: string | null;
|
|
|
|
|
RoomNumber__c: string | null;
|
|
|
|
|
}
|