barsa bb4be98444 feat: Enhance About Us page with animations and updated styles
- Updated typography for headings and paragraphs in AboutUsView.
- Added animation effects for header and sections to improve user experience.
- Refactored section headers to use new display styles.

feat: Implement bilingual address handling in AddressConfirmation

- Integrated JapanAddressForm for ZIP code lookup and bilingual address input.
- Updated state management to handle bilingual addresses and validation.
- Enhanced save functionality to support dual-write to WHMCS and Salesforce.

fix: Adjust Japan Post address mapping to handle nullish values

- Updated address mapping to use nullish coalescing for optional fields.
- Ensured compatibility with API responses that may return null for certain fields.

feat: Add ServiceCard component for displaying services

- Created a flexible ServiceCard component with multiple variants (default, featured, minimal, bento).
- Implemented accent color options and responsive design for better UI.
- Added detailed props documentation and usage examples.

chore: Clean up development scripts

- Removed unnecessary build steps for validation package in manage.sh.
2026-01-14 16:25:06 +09:00

238 lines
7.5 KiB
TypeScript

/**
* 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(),
});
// ============================================================================
// 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)"
);
// ============================================================================
// 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(),
// Street address detail (chome/banchi/go) - e.g., "1-5-3" or "1丁目5番3号"
streetAddress: streetAddressDetailSchema,
// 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
*
* 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)
*/
export function prepareWhmcsAddressFields(address: BilingualAddress): WhmcsAddressFields {
const buildingPart = address.buildingName || "";
const roomPart = address.roomNumber || "";
// address1: Building + Room (for apartments) or just Building (for houses)
const address1 =
address.residenceType === "apartment" ? `${buildingPart} ${roomPart}`.trim() : buildingPart;
// address2: Town + Street address (romanized)
const address2 = `${address.town} ${address.streetAddress}`.trim();
return {
address1: address1 || undefined,
address2: address2 || undefined,
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
*
* Salesforce field mapping:
* - MailingStreet: Town + Street address (Japanese)
* - MailingCity: City (Japanese)
* - MailingState: Prefecture (Japanese)
*/
export function prepareSalesforceContactAddressFields(
address: BilingualAddress
): SalesforceContactAddressFields {
// Combine town and street address for MailingStreet
// Example: "東麻布1-5-3" or "東麻布1丁目5番3号"
const mailingStreet = `${address.townJa}${address.streetAddress}`;
return {
MailingStreet: mailingStreet,
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>;
export type StreetAddressDetail = z.infer<typeof streetAddressDetailSchema>;
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;
}