Files
real-random-taxfree-address/src/js/address-generator.js
2026-03-11 01:15:49 +08:00

1541 lines
62 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Address Generator
import { randomElement, randomInt, generatePhoneNumber, generateEmail, formatAddress } from './utils.js';
import { getConfig } from './config.js';
function ensureNameArray(maybeList) {
if (Array.isArray(maybeList)) return maybeList;
if (!maybeList) return [];
// Support { male: [...], female: [...] } shape by flattening
if (typeof maybeList === 'object') {
const out = [];
if (Array.isArray(maybeList.male)) out.push(...maybeList.male);
if (Array.isArray(maybeList.female)) out.push(...maybeList.female);
return out;
}
return [];
}
// Data cache to reduce server requests
const dataCache = new Map();
const CACHE_PREFIX = 'address_data_cache_';
const CACHE_VERSION = 'v1'; // Increment this to invalidate old cache
const CACHE_EXPIRY = 24 * 60 * 60 * 1000; // 24 hours in milliseconds
// Load data from JSON file with caching (memory + localStorage)
async function loadData(filePath) {
try {
// Check memory cache first (fastest)
if (dataCache.has(filePath)) {
return dataCache.get(filePath);
}
// Check localStorage cache (survives page refresh)
const cacheKey = CACHE_PREFIX + filePath;
try {
const cachedData = localStorage.getItem(cacheKey);
if (cachedData) {
const parsed = JSON.parse(cachedData);
// Check if cache is still valid (not expired)
if (parsed.timestamp && (Date.now() - parsed.timestamp) < CACHE_EXPIRY) {
// Restore to memory cache for faster access
dataCache.set(filePath, parsed.data);
return parsed.data;
} else {
// Cache expired, remove it
localStorage.removeItem(cacheKey);
}
}
} catch (e) {
// If localStorage fails (e.g., private mode), continue to fetch
console.warn('localStorage cache read failed:', e);
}
// Get user configuration
const config = getConfig();
const fileName = filePath.split('/').pop();
// Build paths array based on configuration
const paths = [];
// If user has configured a custom dataBasePath, use it first
if (config.dataBasePath) {
// Ensure trailing slash
const basePath = config.dataBasePath.endsWith('/') ? config.dataBasePath : config.dataBasePath + '/';
paths.push(basePath + fileName);
}
// If autoDetectPaths is enabled (default), add automatic path detection
// This preserves the original behavior for mockaddress.com
if (config.autoDetectPaths !== false) {
const currentPath = window.location.pathname;
// Try multiple possible paths
// Priority: relative path (../data/) first to avoid cross-language references
paths.push(
`../data/${fileName}`, // Relative: go up one level, then into data (works for all language versions)
`data/${fileName}`, // Relative to current directory (fallback)
filePath // Original path (fallback)
);
// Add language-specific absolute paths if we're in a language subdirectory
const pathParts = currentPath.split('/').filter(p => p && p !== 'index.html' && p !== '');
if (pathParts.length >= 1 && ['en', 'ru', 'es', 'pt'].includes(pathParts[0])) {
// We're in a language subdirectory, add language-specific absolute path
const lang = pathParts[0];
paths.splice(paths.length - 2, 0, `/${lang}/data/${fileName}`); // Insert before fallback paths
} else {
// We're in root (Chinese version), add root absolute path
paths.splice(paths.length - 2, 0, `/data/${fileName}`); // Insert before fallback paths
}
}
let lastError = null;
for (const path of paths) {
try {
const response = await fetch(path, {
// Add cache control to help browser cache
cache: 'default'
});
if (response.ok) {
const data = await response.json();
// Store in memory cache for current session
dataCache.set(filePath, data);
// Also store in localStorage for persistence across page refreshes
try {
const cacheKey = CACHE_PREFIX + filePath;
const cacheData = {
data: data,
timestamp: Date.now(),
version: CACHE_VERSION
};
localStorage.setItem(cacheKey, JSON.stringify(cacheData));
} catch (e) {
// If localStorage fails (e.g., quota exceeded, private mode), continue
console.warn('localStorage cache write failed:', e);
}
return data;
} else {
// Store the error but continue trying
lastError = `HTTP ${response.status} for ${path}`;
}
} catch (e) {
// Store the error but continue trying
lastError = e.message;
continue;
}
}
// If all paths failed, log error but still throw to allow error handling
console.error(`Failed to load ${filePath}. Tried paths:`, paths);
console.error(`Last error:`, lastError);
throw new Error(`Failed to load ${filePath} from any path`);
} catch (error) {
console.error(`Error loading data from ${filePath}:`, error);
throw error;
}
}
// Generate US address
export async function generateUSAddress(selectedState = 'RANDOM') {
try {
const usData = await loadData('data/usData.json');
const namesData = await loadData('data/namesData.json');
// Select state
let stateCode = selectedState;
if (selectedState === 'RANDOM') {
const states = Object.keys(usData.states);
stateCode = randomElement(states);
}
const state = usData.states[stateCode];
if (!state) {
throw new Error(`State ${stateCode} not found`);
}
// Generate name - decide gender first, then select name
const nameGroup = namesData.nameGroups.western;
const gender = Math.random() > 0.5 ? 'Male' : 'Female'; // English: Male/Female
// Select name based on gender - use gender-specific name lists if available
let firstName;
if (nameGroup.first.male && nameGroup.first.female) {
// Use gender-specific name lists
firstName = randomElement(gender === 'Male' ? nameGroup.first.male : nameGroup.first.female);
} else {
// Fallback: use all names if gender classification not available
firstName = randomElement(nameGroup.first);
}
const lastName = randomElement(nameGroup.last);
// Generate phone
const areaCode = randomElement(state.area_codes);
const phone = generatePhoneNumber(areaCode);
// Generate email
const email = generateEmail(firstName, lastName);
// Generate address components - use common US street names (these exist in most US cities)
const streetNumber = randomInt(100, 9999);
const streetNames = [
'Main Street', 'Oak Avenue', 'Park Road', 'Maple Drive', 'Elm Street',
'Washington Avenue', 'Lincoln Street', 'Jefferson Drive', 'Madison Road',
'Franklin Avenue', 'Church Street', 'Market Street', 'Broadway',
'First Street', 'Second Street', 'Third Avenue', 'Fourth Street',
'Fifth Street', 'Sixth Street', 'Seventh Street', 'Eighth Street',
'Ninth Street', 'Tenth Street', 'Pine Street', 'Cedar Avenue',
'Spring Street', 'Summer Street', 'Winter Street', 'Lake Avenue',
'River Road', 'Hill Street', 'Valley Drive', 'Forest Avenue',
'Garden Street', 'Rose Avenue', 'Sunset Boulevard', 'Sunrise Drive',
'College Avenue', 'University Drive', 'School Street', 'Library Lane'
];
const streetName = randomElement(streetNames);
const street = `${streetNumber} ${streetName}`;
// Generate city - use state-specific cities if available, otherwise use fallback
let city;
if (state.cities && state.cities.length > 0) {
// Use cities from the state's city list
city = randomElement(state.cities);
} else {
// Fallback: use common US city names (should not happen if data is complete)
const fallbackCities = [
'New York', 'Los Angeles', 'Chicago', 'Houston', 'Phoenix',
'Philadelphia', 'San Antonio', 'San Diego', 'Dallas', 'San Jose',
'Austin', 'Jacksonville', 'Fort Worth', 'Columbus', 'Charlotte',
'San Francisco', 'Indianapolis', 'Seattle', 'Denver', 'Washington'
];
city = randomElement(fallbackCities);
}
// Generate zip code - use state-specific zip range if available, otherwise use fallback
let zip;
if (state.zip_range && state.zip_range.min && state.zip_range.max) {
// Use zip code range from the state's zip_range
zip = randomInt(state.zip_range.min, state.zip_range.max).toString();
} else {
// Fallback: use random 5-digit zip code (should not happen if data is complete)
zip = randomInt(10000, 99999).toString();
}
// Full address should be in English only
const stateNameEn = state.name.en || state.name.zh;
const fullAddress = `${street}, ${city}, ${stateNameEn} ${zip}`;
return {
firstName,
lastName,
gender,
phone,
email,
street,
city,
state: stateNameEn, // 显示英文州名
stateCode,
zip,
fullAddress,
country: 'US'
};
} catch (error) {
console.error('Error generating US address:', error);
throw error;
}
}
// Generate Hong Kong address
// selectedRegion: 'RANDOM' | 'HK' | 'KL' | 'NT'
// isEnglish: true -> 生成英文姓名和英文地址false -> 保持中文
export async function generateHKAddress(selectedRegion = 'RANDOM', isEnglish = false) {
try {
const hkData = await loadData('data/hkData.json');
const namesData = await loadData('data/namesData.json');
// Filter districts based on selected region
let availableDistricts = {};
if (selectedRegion === 'RANDOM') {
// Use all districts
availableDistricts = hkData.districts;
} else if (hkData.districts[selectedRegion]) {
// Use only the selected region
availableDistricts[selectedRegion] = hkData.districts[selectedRegion];
} else {
// Fallback to all districts if invalid selection
availableDistricts = hkData.districts;
}
// Select random district and area from filtered districts
const districts = Object.keys(availableDistricts);
if (districts.length === 0) {
throw new Error('No districts available for selected region');
}
const districtKey = randomElement(districts);
const district = availableDistricts[districtKey];
const area = randomElement(district.areas);
// Generate name
let firstName;
let lastName;
let gender;
if (isEnglish && hkData.names && hkData.names.en) {
// 英文模式:使用 hkData 内置的英文化姓名(香港拼音格式)
// 为了符合Apple ID等注册要求名字组合成2-3个字更符合香港人传统姓名习惯
const nameGroupEn = hkData.names.en;
const isMale = Math.random() > 0.5;
gender = isMale ? 'Male' : 'Female'; // 修复:性别逻辑正确对应
const firstPool = isMale ? nameGroupEn.first.male : nameGroupEn.first.female;
// 组合2-3个字的名字70%概率双字30%概率三字)
if (firstPool && firstPool.length > 0) {
const nameCount = Math.random() < 0.7 ? 2 : 3; // 70%双字30%三字
const selectedNames = [];
const availableNames = [...firstPool]; // 复制数组避免修改原数组
for (let i = 0; i < nameCount && availableNames.length > 0; i++) {
const selected = randomElement(availableNames);
selectedNames.push(selected);
// 移除已选的名字,避免重复
const index = availableNames.indexOf(selected);
if (index > -1) availableNames.splice(index, 1);
}
firstName = selectedNames.join(' '); // 用空格连接,如 "Wing Man" 或 "Wing Man Kai"
} else {
firstName = randomElement(firstPool || []);
}
lastName = randomElement(nameGroupEn.last || []);
// 如果数据异常,兜底
if (!firstName || !lastName) {
const fallback = namesData.nameGroups.western;
gender = Math.random() > 0.5 ? 'Male' : 'Female';
firstName = randomElement(
gender === 'Male' ? fallback.first.male : fallback.first.female
);
lastName = randomElement(fallback.last);
}
} else {
// 中文模式:沿用原有中文姓名逻辑
const nameGroup = namesData.nameGroups.chinese;
gender = Math.random() > 0.5 ? '男' : '女'; // 中文:男/女
firstName = randomElement(gender === '男' ? nameGroup.first.male : nameGroup.first.female);
lastName = randomElement(nameGroup.last);
}
// Generate phone (Hong Kong format: +852 XXXX XXXX)
const phone = `+852 ${randomInt(2000, 9999)} ${randomInt(1000, 9999)}`;
// Hong Kong has no official postal code; use fixed placeholder for forms
const zip = '000000';
// Generate email - use English names for email to avoid Chinese characters
const englishNameGroup = namesData.nameGroups.western || namesData.nameGroups.asian;
let emailFirstName, emailLastName;
if (englishNameGroup && englishNameGroup.first && englishNameGroup.last) {
const firstList = ensureNameArray(englishNameGroup.first);
const lastList = ensureNameArray(englishNameGroup.last);
emailFirstName = randomElement(firstList);
emailLastName = randomElement(lastList);
} else {
// Fallback: generate random English username
const randomNames = ['john', 'mary', 'david', 'sarah', 'michael', 'emily', 'james', 'lisa', 'robert', 'anna'];
emailFirstName = randomElement(randomNames);
emailLastName = randomElement(randomNames);
}
const email = generateEmail(emailFirstName, emailLastName);
// Generate address
const floor = randomInt(1, 50);
const unit = randomInt(1, 20);
let street;
let building;
let address;
let city;
let districtName;
let fullAddress;
if (isEnglish) {
street = randomElement(area.streets.en);
building = randomElement(area.buildings.en);
// 英文地址格式Flat 12, 32/F, Building Name, Street Name
address = `Flat ${unit}, ${floor}/F, ${building}, ${street}`;
city = area.name_en;
districtName = district.name.en;
fullAddress = `${address}, ${city}, ${districtName}, Hong Kong`;
// 英文模式下,性别字段统一英文化
gender = gender === '男' ? 'Male' : gender === '女' ? 'Female' : gender;
} else {
street = randomElement(area.streets.zh);
building = randomElement(area.buildings.zh);
address = `${street} ${building} ${floor}${unit}`;
city = area.name_zh;
districtName = district.name.zh;
fullAddress = `${address}, ${city}, ${districtName}`;
}
return {
firstName,
lastName,
gender,
phone,
email,
street: address,
city, // 区域作为城市(中英文根据模式切换)
county: districtName, // 区作为区县
district: districtName, // 保留原字段以兼容
area: city, // 保留原字段以兼容
fullAddress,
zip,
country: 'HK'
};
} catch (error) {
console.error('Error generating HK address:', error);
throw error;
}
}
// Generate UK address
export async function generateUKAddress(selectedRegion = 'RANDOM') {
try {
const ukData = await loadData('data/ukData.json');
const namesData = await loadData('data/namesData.json');
// Filter regions based on selected region
let availableRegions = {};
if (selectedRegion === 'RANDOM') {
// Use all regions
availableRegions = ukData.regions;
} else if (ukData.regions[selectedRegion]) {
// Use only the selected region
availableRegions[selectedRegion] = ukData.regions[selectedRegion];
} else {
// Fallback to all regions if invalid selection
availableRegions = ukData.regions;
}
// Select random region from filtered regions
const regions = Object.keys(availableRegions);
if (regions.length === 0) {
throw new Error('No regions available for selected region');
}
const regionKey = randomElement(regions);
const region = availableRegions[regionKey];
// Generate name - decide gender first, then select name
const nameGroup = namesData.nameGroups.western;
const gender = Math.random() > 0.5 ? 'Male' : 'Female'; // English: Male/Female
// Select name based on gender - use gender-specific name lists if available
let firstName;
if (nameGroup.first.male && nameGroup.first.female) {
// Use gender-specific name lists
firstName = randomElement(gender === 'Male' ? nameGroup.first.male : nameGroup.first.female);
} else {
// Fallback: use all names if gender classification not available
firstName = randomElement(nameGroup.first);
}
const lastName = randomElement(nameGroup.last);
// Generate phone
const phoneCode = randomElement(region.phone_codes);
const phone = `0${phoneCode} ${randomInt(1000, 9999)} ${randomInt(100000, 999999)}`;
// Generate email
const email = generateEmail(firstName, lastName);
// Generate address
const streetNumbers = [randomInt(1, 999), randomInt(1, 999)];
const streetName = randomElement([
'High Street', 'Church Road', 'Park Avenue', 'Main Road', 'London Road',
'Victoria Street', 'King Street', 'Queen Street', 'Market Street',
'Station Road', 'Mill Lane', 'Bridge Street', 'New Street', 'Old Street',
'Castle Street', 'Church Street', 'School Lane', 'Garden Street', 'Hill Road',
'Oak Avenue', 'Elm Street', 'Maple Drive', 'Cedar Road', 'Pine Street',
'Rose Lane', 'Lily Street', 'Orchard Road', 'Meadow Way', 'River Street'
]);
const street = `${streetNumbers[0]}${streetNumbers[1] > 0 ? '-' + streetNumbers[1] : ''} ${streetName}`;
// Generate city - use region-specific cities if available, otherwise use fallback
let city;
if (region.cities && region.cities.length > 0) {
// Use cities from the region's city list
city = randomElement(region.cities);
} else {
// Fallback: use common UK city names (should not happen if data is complete)
const fallbackCities = [
'London', 'Manchester', 'Birmingham', 'Liverpool', 'Leeds',
'Glasgow', 'Edinburgh', 'Bristol', 'Cardiff', 'Belfast'
];
city = randomElement(fallbackCities);
}
// UK postcode format: SW1A 1AA - use region-specific postcode areas if available
let postcodeArea;
if (region.postcode_areas && region.postcode_areas.length > 0) {
// Use postcode areas from the region's list
postcodeArea = randomElement(region.postcode_areas);
} else {
// Fallback: use common UK postcode areas (should not happen if data is complete)
postcodeArea = randomElement(['SW', 'NW', 'SE', 'NE', 'W', 'E', 'N', 'S']);
}
const postcode = `${postcodeArea}${randomInt(1, 9)}${randomElement(['A', 'B', 'C'])} ${randomInt(1, 9)}${randomElement(['A', 'B', 'C'])}${randomElement(['A', 'B', 'C'])}`;
// Use English region name for full address and region field
const regionNameEn = region.name.en || region.name.zh;
const fullAddress = `${street}, ${city}, ${postcode}, ${regionNameEn}`;
return {
firstName,
lastName,
gender,
phone,
email,
street,
city,
postcode,
region: regionNameEn, // 显示英文地区名
fullAddress,
country: 'UK'
};
} catch (error) {
console.error('Error generating UK address:', error);
throw error;
}
}
// Generate Canada address
export async function generateCAAddress(selectedProvince = 'RANDOM') {
try {
const caData = await loadData('data/caData.json');
const namesData = await loadData('data/namesData.json');
// Filter provinces based on selected province
let availableProvinces = {};
if (selectedProvince === 'RANDOM') {
// Use all provinces
availableProvinces = caData.provinces;
} else if (caData.provinces[selectedProvince]) {
// Use only the selected province
availableProvinces[selectedProvince] = caData.provinces[selectedProvince];
} else {
// Fallback to all provinces if invalid selection
availableProvinces = caData.provinces;
}
// Select random province from filtered provinces
const provinces = Object.keys(availableProvinces);
if (provinces.length === 0) {
throw new Error('No provinces available for selected province');
}
const provinceKey = randomElement(provinces);
const province = availableProvinces[provinceKey];
// Generate name - decide gender first, then select name
const nameGroup = namesData.nameGroups.western;
const gender = Math.random() > 0.5 ? 'Male' : 'Female'; // English: Male/Female
// Select name based on gender - use gender-specific name lists if available
let firstName;
if (nameGroup.first.male && nameGroup.first.female) {
// Use gender-specific name lists
firstName = randomElement(gender === 'Male' ? nameGroup.first.male : nameGroup.first.female);
} else {
// Fallback: use all names if gender classification not available
firstName = randomElement(nameGroup.first);
}
const lastName = randomElement(nameGroup.last);
// Generate phone
const areaCode = randomElement(province.area_codes);
const phone = `(${areaCode}) ${randomInt(200, 999)}-${randomInt(1000, 9999)}`;
// Generate email
const email = generateEmail(firstName, lastName);
// Generate address
const streetNumber = randomInt(100, 9999);
const streetName = randomElement([
'Main Street', 'Oak Avenue', 'Park Road', 'Maple Drive', 'Elm Street',
'King Street', 'Queen Street', 'Church Street', 'Market Street',
'First Street', 'Second Street', 'Third Avenue', 'Fourth Street',
'Bay Street', 'Yonge Street', 'University Avenue', 'College Street',
'Dundas Street', 'Bloor Street', 'Queen Street', 'King Street',
'River Road', 'Lake Avenue', 'Hill Street', 'Valley Drive',
'Forest Avenue', 'Garden Street', 'Rose Lane', 'Pine Street'
]);
const street = `${streetNumber} ${streetName}`;
// Generate city - use province-specific cities if available, otherwise use fallback
let city;
if (province.cities && province.cities.length > 0) {
// Use cities from the province's city list
city = randomElement(province.cities);
} else {
// Fallback: use common Canadian city names (should not happen if data is complete)
const fallbackCities = [
'Toronto', 'Vancouver', 'Montreal', 'Calgary', 'Ottawa',
'Edmonton', 'Winnipeg', 'Quebec City', 'Hamilton', 'Halifax'
];
city = randomElement(fallbackCities);
}
// Canadian postal code format: A1A 1A1 - use province-specific prefixes if available
let postcodePrefix;
if (province.postcode_prefixes && province.postcode_prefixes.length > 0) {
// Use postcode prefixes from the province's list
postcodePrefix = randomElement(province.postcode_prefixes);
} else {
// Fallback: use common Canadian postal code prefixes (should not happen if data is complete)
postcodePrefix = randomElement(['A', 'B', 'C', 'E', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'V', 'X', 'Y']);
}
const postcode = `${postcodePrefix}${randomInt(0, 9)}${randomElement(['A', 'B', 'C', 'E', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'])} ${randomInt(0, 9)}${randomElement(['A', 'B', 'C', 'E', 'G', 'H', 'J', 'K', 'L', 'M', 'N', 'P', 'R', 'S', 'T', 'V', 'W', 'X', 'Y', 'Z'])}${randomInt(0, 9)}`;
// Use English province name for full address and province field
const provinceNameEn = province.name.en || province.name.zh;
const fullAddress = `${street}, ${city}, ${provinceNameEn} ${postcode}`;
return {
firstName,
lastName,
gender,
phone,
email,
street,
city,
postcode,
province: provinceNameEn, // 显示英文省份名
fullAddress,
country: 'CA'
};
} catch (error) {
console.error('Error generating CA address:', error);
throw error;
}
}
// Generate Japan address
export async function generateJPAddress(selectedPrefecture = 'RANDOM') {
try {
const jpData = await loadData('data/jpData.json');
const jpNamesData = await loadData('data/jpNamesData.json');
const namesData = await loadData('data/namesData.json');
// Filter prefectures based on selected prefecture
let availablePrefectures = {};
if (selectedPrefecture === 'RANDOM') {
// Use all prefectures
availablePrefectures = jpData.prefectures || {};
} else if (jpData.prefectures && jpData.prefectures[selectedPrefecture]) {
// Use only the selected prefecture
availablePrefectures[selectedPrefecture] = jpData.prefectures[selectedPrefecture];
} else {
// Fallback to all prefectures if invalid selection
availablePrefectures = jpData.prefectures || {};
}
// Select random prefecture from filtered prefectures
const prefectures = Object.keys(availablePrefectures);
if (prefectures.length === 0) {
throw new Error('No prefectures available for selected prefecture');
}
const prefectureKey = randomElement(prefectures);
const prefecture = availablePrefectures[prefectureKey];
// Generate name using Japanese names database (kanji, hiragana, katakana)
// Japanese surnames (姓) are almost always in kanji, rarely in hiragana/katakana
const surnameScriptTypes = ['kanji', 'kanji', 'kanji', 'kanji', 'kanji', 'kanji', 'kanji', 'kanji', 'kanji', 'hiragana']; // 90% kanji, 10% hiragana
const surnameScriptType = randomElement(surnameScriptTypes);
const lastNames = jpNamesData.surnames[surnameScriptType] || jpNamesData.surnames.kanji;
const lastName = randomElement(lastNames);
// Japanese first names (名) can be in kanji, hiragana, or katakana
const firstNameScriptTypes = ['kanji', 'kanji', 'kanji', 'hiragana', 'katakana']; // 60% kanji, 20% hiragana, 20% katakana
const firstNameScriptType = randomElement(firstNameScriptTypes);
const isMale = Math.random() > 0.5;
// Get first name based on gender and script type
let firstName;
if (isMale) {
const maleNames = jpNamesData.firstNames.male[firstNameScriptType] || jpNamesData.firstNames.male.kanji;
firstName = randomElement(maleNames);
} else {
const femaleNames = jpNamesData.firstNames.female[firstNameScriptType] || jpNamesData.firstNames.female.kanji;
firstName = randomElement(femaleNames);
}
// Gender in Japanese
const gender = isMale ? '男性' : '女性';
// Generate phone (Japan format: 0X-XXXX-XXXX) - use prefecture-specific phone codes if available
let phoneCode;
if (prefecture && prefecture.phone_codes && prefecture.phone_codes.length > 0) {
// Use phone codes from the prefecture's list
const selectedCode = randomElement(prefecture.phone_codes);
// Remove leading 0 if present and format
phoneCode = selectedCode.toString().replace(/^0+/, '');
} else {
// Fallback: use random phone code (should not happen if data is complete)
phoneCode = randomInt(3, 9).toString();
}
const phone = `0${phoneCode}-${randomInt(1000, 9999)}-${randomInt(1000, 9999)}`;
// Generate email - use English names for email to avoid non-ASCII characters
const englishNameGroup = namesData.nameGroups.western || namesData.nameGroups.asian;
let emailFirstName, emailLastName;
if (englishNameGroup && englishNameGroup.first && englishNameGroup.last) {
const firstList = ensureNameArray(englishNameGroup.first);
const lastList = ensureNameArray(englishNameGroup.last);
// Use Japanese-style names from asian group if available
const japaneseRomajiNames = firstList.filter(n =>
['Hiroshi', 'Takeshi', 'Akira', 'Satoshi', 'Kenji', 'Taro', 'Jiro', 'Ichiro', 'Yuki', 'Ai', 'Emi', 'Yui', 'Rina', 'Miki', 'Saki', 'Nana', 'Kana', 'Mana', 'Hanako', 'Misaki', 'Sakura', 'Aya', 'Rei', 'Mai', 'Eri', 'Yuka'].includes(n)
);
const japaneseRomajiLastNames = lastList.filter(n =>
['Sato', 'Suzuki', 'Takahashi', 'Tanaka', 'Watanabe', 'Ito', 'Yamamoto', 'Nakamura', 'Kobayashi', 'Kato', 'Yoshida', 'Yamada', 'Sasaki', 'Yamaguchi', 'Matsumoto', 'Inoue', 'Kimura', 'Hayashi', 'Shimizu', 'Yamazaki', 'Mori', 'Abe', 'Ikeda', 'Hashimoto', 'Ishikawa', 'Maeda', 'Fujita', 'Ogawa', 'Goto', 'Okada'].includes(n)
);
if (japaneseRomajiNames.length > 0 && japaneseRomajiLastNames.length > 0) {
emailFirstName = randomElement(japaneseRomajiNames);
emailLastName = randomElement(japaneseRomajiLastNames);
} else {
emailFirstName = randomElement(firstList);
emailLastName = randomElement(lastList);
}
} else {
// Fallback: generate random English username
const randomNames = ['john', 'mary', 'david', 'sarah', 'michael', 'emily', 'james', 'lisa', 'robert', 'anna'];
emailFirstName = randomElement(randomNames);
emailLastName = randomElement(randomNames);
}
const email = generateEmail(emailFirstName, emailLastName);
// Generate address using real Japanese address data
// Use kanji for addresses (most common in real addresses)
const addressData = jpData.address_data || {};
// City - use prefecture-specific cities if available, otherwise use fallback
let city;
if (prefecture && prefecture.cities && prefecture.cities.length > 0) {
// Use cities from the prefecture's city list
city = randomElement(prefecture.cities);
} else {
// Fallback: use common Japanese city names (should not happen if data is complete)
const fallbackCities = addressData.cities?.kanji || ['東京', '大阪', '京都', '横浜', '名古屋', '札幌', '福岡', '神戸', '仙台', '広島', '千葉', '埼玉', '新潟', '静岡', '浜松', '岡山', '熊本', '鹿児島', '長崎', '大分'];
city = randomElement(fallbackCities);
}
// Ward/丁目 (use kanji)
const wards = addressData.wards?.kanji || ['1丁目', '2丁目', '3丁目', '4丁目', '5丁目', '6丁目', '7丁目', '8丁目', '9丁目', '10丁目'];
const ward = randomElement(wards);
// Street name (use kanji from real street data)
const streets = addressData.streets?.kanji || ['中央', '本町', '新町', '大通', '駅前', '公園', '桜', '松', '竹', '梅', '富士', '山', '川', '海', '森', '田', '橋', '坂', '谷', '原'];
const streetName = randomElement(streets);
// District (区/市) - optional, sometimes included
const includeDistrict = Math.random() > 0.7; // 30% chance to include district
let district = '';
if (includeDistrict && addressData.districts?.kanji) {
district = randomElement(addressData.districts.kanji) + '';
}
const streetNumber = randomInt(1, 50);
const buildingNumber = randomInt(1, 20);
// Build address: [district] [street] [ward] [number]番[building]号
let address;
if (district) {
address = `${district}${streetName}${ward}${streetNumber}${buildingNumber}`;
} else {
address = `${streetName}${ward}${streetNumber}${buildingNumber}`;
}
// Japanese postal code format: 123-4567 (使用都道府県的邮编前缀)
let postcodePrefix = '100';
if (prefecture && prefecture.postal_prefix && prefecture.postal_prefix.length > 0) {
postcodePrefix = randomElement(prefecture.postal_prefix);
}
const postcode = `${postcodePrefix}-${randomInt(1000, 9999)}`;
// 使用日文都道府県名称
const prefectureName = prefecture ? (prefecture.name.ja || prefecture.name.zh) : '東京都';
const fullAddress = `${postcode} ${prefectureName}${city}${address}`;
return {
firstName,
lastName,
gender,
phone,
email,
street: address,
city,
prefecture: prefectureName,
postcode,
fullAddress,
country: 'JP'
};
} catch (error) {
console.error('Error generating JP address:', error);
throw error;
}
}
// Generate India address
export async function generateINAddress(selectedState = 'RANDOM') {
try {
const inData = await loadData('data/inData.json');
const namesData = await loadData('data/namesData.json');
// Filter states based on selected state
let availableStates = {};
if (selectedState === 'RANDOM') {
// Use all states
availableStates = inData.states;
} else if (inData.states[selectedState]) {
// Use only the selected state
availableStates[selectedState] = inData.states[selectedState];
} else {
// Fallback to all states if invalid selection
availableStates = inData.states;
}
// Select random state from filtered states
const states = Object.keys(availableStates);
if (states.length === 0) {
throw new Error('No states available for selected state');
}
const stateKey = randomElement(states);
const state = availableStates[stateKey];
// Generate name (Indian) - decide gender first, then select name
const nameGroup = namesData.nameGroups.indian;
const gender = Math.random() > 0.5 ? 'Male' : 'Female'; // English: Male/Female
// Select name based on gender - use gender-specific name lists if available
let firstName;
if (nameGroup.first.male && nameGroup.first.female) {
// Use gender-specific name lists
firstName = randomElement(gender === 'Male' ? nameGroup.first.male : nameGroup.first.female);
} else {
// Fallback: use all names if gender classification not available
firstName = randomElement(nameGroup.first);
}
const lastName = randomElement(nameGroup.last);
// Generate phone (India format: +91 XXXXXXXXXX) - use state-specific area codes if available
let phoneNumber;
if (state.area_codes && state.area_codes.length > 0) {
// Use area codes from the state's list (format: XXXX, convert to phone number)
const areaCode = randomElement(state.area_codes);
// Indian mobile numbers start with 6-9, generate remaining digits
const mobilePrefix = randomElement([6, 7, 8, 9]);
const remainingDigits = randomInt(10000000, 99999999);
phoneNumber = `${mobilePrefix}${remainingDigits}`;
} else {
// Fallback: use random phone number (should not happen if data is complete)
phoneNumber = randomInt(6000000000, 9999999999).toString();
}
const phone = `+91 ${phoneNumber}`;
// Generate email
const email = generateEmail(firstName, lastName);
// Generate address
const streetNumber = randomInt(1, 999);
const streetName = randomElement([
'Main Road', 'Gandhi Street', 'Nehru Road', 'Park Street', 'Market Road',
'Church Street', 'Temple Street', 'School Road', 'Hospital Road',
'MG Road', 'Station Road', 'Airport Road', 'Highway Road', 'Ring Road',
'First Street', 'Second Street', 'Third Street', 'Fourth Street',
'Gandhi Nagar', 'Nehru Nagar', 'Rajiv Nagar', 'Indira Nagar',
'College Road', 'University Road', 'Library Road', 'Museum Road'
]);
const street = `${streetNumber}, ${streetName}`;
// Generate city - use state-specific cities if available, otherwise use fallback
let city;
if (state.cities && state.cities.length > 0) {
// Use cities from the state's city list
city = randomElement(state.cities);
} else {
// Fallback: use common Indian city names (should not happen if data is complete)
const fallbackCities = [
'Mumbai', 'Delhi', 'Bangalore', 'Hyderabad', 'Chennai',
'Kolkata', 'Pune', 'Ahmedabad', 'Jaipur', 'Surat'
];
city = randomElement(fallbackCities);
}
// Indian PIN code (6 digits) - use state-specific PIN range if available
let pin;
if (state.pin_range && state.pin_range.min && state.pin_range.max) {
// Use PIN code range from the state's pin_range
pin = randomInt(state.pin_range.min, state.pin_range.max).toString();
} else {
// Fallback: use random 6-digit PIN code (should not happen if data is complete)
pin = randomInt(100000, 999999).toString();
}
// Use English state name for full address and state field
const stateNameEn = state.name.en || state.name.zh;
const fullAddress = `${street}, ${city}, ${stateNameEn} ${pin}`;
return {
firstName,
lastName,
gender,
phone,
email,
street,
city,
state: stateNameEn, // 显示英文邦名
pin,
fullAddress,
country: 'IN'
};
} catch (error) {
console.error('Error generating IN address:', error);
throw error;
}
}
// Generate Taiwan address
export async function generateTWAddress(selectedCounty = 'RANDOM') {
try {
const namesData = await loadData('data/namesData.json');
// Try to load Taiwan data
let twData = null;
let selectedCountyData = null;
try {
twData = await loadData('data/twData.json');
if (twData && twData.counties) {
// Filter counties based on selected county
let availableCounties = {};
if (selectedCounty === 'RANDOM') {
// Use all counties
availableCounties = twData.counties;
} else if (twData.counties[selectedCounty]) {
// Use only the selected county
availableCounties[selectedCounty] = twData.counties[selectedCounty];
} else {
// Fallback to all counties if invalid selection
availableCounties = twData.counties;
}
// Select random county from filtered counties
const counties = Object.keys(availableCounties);
if (counties.length > 0) {
const countyKey = randomElement(counties);
selectedCountyData = availableCounties[countyKey];
}
}
} catch (e) {
// If data file doesn't exist, use fallback
console.warn('Taiwan data file not found, using fallback');
}
// Generate name (Chinese) - decide gender first, then select name
const nameGroup = namesData.nameGroups.chinese;
const gender = Math.random() > 0.5 ? '男' : '女'; // 中文:男/女
const firstName = randomElement(gender === '男' ? nameGroup.first.male : nameGroup.first.female);
const lastName = randomElement(nameGroup.last);
// Generate phone (Taiwan format: 09XX-XXX-XXX) - use county-specific area codes if available
let phoneAreaCode;
if (selectedCountyData && selectedCountyData.phone_area_codes && selectedCountyData.phone_area_codes.length > 0) {
// Use phone area codes from the county's list
phoneAreaCode = randomElement(selectedCountyData.phone_area_codes);
} else {
// Fallback: use random area code (should not happen if data is complete)
phoneAreaCode = randomInt(2, 9);
}
const phone = `09${phoneAreaCode}${randomInt(10, 99)}-${randomInt(100, 999)}-${randomInt(100, 999)}`;
// Generate email - use English names for email to avoid Chinese characters
const englishNameGroup = namesData.nameGroups.western || namesData.nameGroups.asian;
let emailFirstName, emailLastName;
if (englishNameGroup && englishNameGroup.first && englishNameGroup.last) {
const firstList = ensureNameArray(englishNameGroup.first);
const lastList = ensureNameArray(englishNameGroup.last);
emailFirstName = randomElement(firstList);
emailLastName = randomElement(lastList);
} else {
// Fallback: generate random English username
const randomNames = ['john', 'mary', 'david', 'sarah', 'michael', 'emily', 'james', 'lisa', 'robert', 'anna'];
emailFirstName = randomElement(randomNames);
emailLastName = randomElement(randomNames);
}
const email = generateEmail(emailFirstName, emailLastName);
// Generate address
// Use county data if available, otherwise use fallback cities
let city, district;
if (selectedCountyData && selectedCountyData.name) {
city = selectedCountyData.name.zh;
// Use common districts for the selected city
district = randomElement(['中正區', '大同區', '中山區', '松山區', '大安區', '萬華區', '信義區', '士林區', '北投區', '內湖區', '南港區', '文山區']);
} else {
// Fallback cities
const cities = ['台北市', '新北市', '台中市', '台南市', '高雄市', '桃園市'];
city = randomElement(cities);
district = randomElement(['中正區', '大同區', '中山區', '松山區', '大安區', '萬華區']);
}
const street = randomElement([
'中山路', '中正路', '民生路', '民權路', '民族路',
'建國路', '復興路', '和平路', '自由路', '成功路',
'忠孝路', '仁愛路', '信義路', '和平路', '光復路',
'中華路', '文化路', '大學路', '公園路', '車站路',
'中央路', '大同路', '大安路', '大業路', '大興路',
'新生路', '新興路', '新市路', '新莊路', '新店路'
]);
const streetNumber = randomInt(1, 999);
const address = `${city}${district}${street}${streetNumber}`;
// Taiwan postal code (5 digits) - use county-specific postcode range if available
let postcode;
if (selectedCountyData && selectedCountyData.postcode_range && selectedCountyData.postcode_range.min && selectedCountyData.postcode_range.max) {
// Use postcode range from the county's postcode_range
postcode = randomInt(selectedCountyData.postcode_range.min, selectedCountyData.postcode_range.max).toString();
} else {
// Fallback: use random 5-digit postcode (should not happen if data is complete)
postcode = randomInt(10000, 99999).toString();
}
const fullAddress = `${address}, 郵遞區號: ${postcode}`;
return {
firstName,
lastName,
gender,
phone,
email,
street: address,
city,
district,
postcode,
fullAddress,
country: 'TW'
};
} catch (error) {
console.error('Error generating TW address:', error);
throw error;
}
}
// Generate tax-free US address (only from tax-free states)
export async function generateTaxFreeAddress(selectedState = 'DE') {
const taxFreeStates = ['AK', 'DE', 'MT', 'NH', 'OR'];
if (!taxFreeStates.includes(selectedState)) {
selectedState = randomElement(taxFreeStates);
}
return await generateUSAddress(selectedState);
}
// Generate identity information
export async function generateIdentityInfo(address) {
try {
// Load names data - use the same loadData function which handles paths correctly
const namesData = await loadData('data/namesData.json');
if (!namesData || !namesData.nameGroups) {
throw new Error('Names data not available');
}
// Use the same name group as the address based on country
const c = address.country || '';
let nameGroup;
if (c === '香港' || c === '台灣' || c === 'HK' || c === 'TW') {
nameGroup = namesData.nameGroups.chinese;
} else if (c === '印度' || c === 'IN') {
nameGroup = namesData.nameGroups.indian;
} else if (c === '日本' || c === 'JP') {
nameGroup = namesData.nameGroups.asian || namesData.nameGroups.western;
} else {
// Default to western names for US, UK, Canada, etc.
nameGroup = namesData.nameGroups.western || namesData.nameGroups.asian;
}
if (!nameGroup) {
throw new Error('Name group not found');
}
// Resolve gender for identity (name selection + Taiwan ID gender digit)
let isMaleForIdentity;
if (address.gender) {
const g = address.gender;
const isMale = g === 'Male' || g === 'male' || g === 'm' || g === '男' || g === '男性' ||
(typeof g === 'string' && (g.includes('Männlich') || g.includes('男')));
const isFemale = g === 'Female' || g === 'female' || g === 'f' || g === '女' || g === '女性' ||
(typeof g === 'string' && (g.includes('Weiblich') || g.includes('女')));
isMaleForIdentity = isMale ? true : (isFemale ? false : Math.random() > 0.5);
} else {
isMaleForIdentity = Math.random() > 0.5;
}
// Generate name based on address gender if available
let firstName, lastName;
if (address.gender) {
const genderKey = address.gender.toLowerCase();
const isMale = genderKey === 'male' || genderKey === 'm' || address.gender === '男' || address.gender === '男性' ||
address.gender.includes('Männlich') || address.gender.includes('男');
const isFemale = genderKey === 'female' || genderKey === 'f' || address.gender === '女' || address.gender === '女性' ||
address.gender.includes('Weiblich') || address.gender.includes('女');
// Use gender-specific name lists if available
if (nameGroup.first.male && nameGroup.first.female) {
if (isMale) {
firstName = randomElement(nameGroup.first.male);
} else if (isFemale) {
firstName = randomElement(nameGroup.first.female);
} else {
// Fallback: randomly choose from either gender
firstName = randomElement(Math.random() > 0.5 ? nameGroup.first.male : nameGroup.first.female);
}
} else {
// Fallback: use all names if gender classification not available
firstName = randomElement(Array.isArray(nameGroup.first) ? nameGroup.first : []);
}
} else {
// If no gender specified, randomly choose gender and corresponding name
const randomGender = Math.random() > 0.5;
if (nameGroup.first.male && nameGroup.first.female) {
firstName = randomElement(randomGender ? nameGroup.first.male : nameGroup.first.female);
} else {
firstName = randomElement(Array.isArray(nameGroup.first) ? nameGroup.first : []);
}
}
lastName = randomElement(nameGroup.last);
// Generate date of birth (age between 20 and 50)
const age = randomInt(20, 50);
const birthYear = new Date().getFullYear() - age;
const birthMonth = randomInt(1, 12);
const daysInMonth = new Date(birthYear, birthMonth, 0).getDate();
const birthDay = randomInt(1, daysInMonth);
const dateOfBirth = `${birthMonth.toString().padStart(2, '0')}/${birthDay.toString().padStart(2, '0')}/${birthYear}`;
// Generate identity ID based on country
let ssn;
const country = address.country || '';
if (country.includes('德国') || country.includes('Germany') || country === 'DE') {
// Generate German Steuer-ID (Tax ID): 11 digits, format: XX XXX XXX XXX
const part1 = randomInt(10, 99);
const part2 = randomInt(100, 999);
const part3 = randomInt(100, 999);
const part4 = randomInt(100, 999);
ssn = `${part1} ${part2} ${part3} ${part4}`;
} else if (country.includes('英国') || country.includes('UK') || country.includes('United Kingdom') || country === 'UK') {
// Generate UK NINO: Format: AA 12 34 56 A
const letters1 = 'ABCDEFGHJKLMNOPRSTUVWXYZ';
const letters2 = 'ABCDEFGHJKLMNOPRSTUVWXYZ';
const letter1 = randomElement(letters1.split(''));
const letter2 = randomElement(letters2.split(''));
const digits = randomInt(100000, 999999);
const letter3 = randomElement(letters2.split(''));
ssn = `${letter1}${letter2} ${digits.toString().slice(0, 2)} ${digits.toString().slice(2, 4)} ${digits.toString().slice(4, 6)} ${letter3}`;
} else if (country.includes('加拿大') || country.includes('Canada') || country === 'CA') {
// Generate Canadian SIN: Format: XXX XXX XXX
const sin1 = randomInt(100, 999);
const sin2 = randomInt(100, 999);
const sin3 = randomInt(100, 999);
ssn = `${sin1} ${sin2} ${sin3}`;
} else if (country.includes('日本') || country.includes('Japan') || country === 'JP') {
// Generate Japanese My Number: Format: XXXX-XXXX-XXXX
const myNum1 = randomInt(1000, 9999);
const myNum2 = randomInt(1000, 9999);
const myNum3 = randomInt(1000, 9999);
ssn = `${myNum1}-${myNum2}-${myNum3}`;
} else if (country.includes('印度') || country.includes('India') || country === 'IN') {
// Generate Indian Aadhaar: Format: XXXX XXXX XXXX
const aadhaar1 = randomInt(1000, 9999);
const aadhaar2 = randomInt(1000, 9999);
const aadhaar3 = randomInt(1000, 9999);
ssn = `${aadhaar1} ${aadhaar2} ${aadhaar3}`;
} else if (country.includes('香港') || country.includes('Hong Kong') || country === 'HK') {
// Generate Hong Kong ID Card: Format: A123456(7) or AB123456(7)
// 70%概率单字母30%概率双字母
const letters = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
const isDoubleLetter = Math.random() < 0.3;
let prefix;
if (isDoubleLetter) {
const letter1 = randomElement(letters.split(''));
const letter2 = randomElement(letters.split(''));
prefix = `${letter1}${letter2}`;
} else {
prefix = randomElement(letters.split(''));
}
const digits = randomInt(100000, 999999).toString();
const checkDigit = randomInt(0, 9);
ssn = `${prefix}${digits}(${checkDigit})`;
} else if (country.includes('台灣') || country.includes('台湾') || country.includes('Taiwan') || country === 'TW') {
// Generate Taiwan ID Card: Format: A123456789
// 1st: letter (birthplace), 2nd: gender (1=Male, 2=Female), 3rd-9th: sequence
const letters = 'ABCDEFGHJKLMNPQRSTUVXY';
const firstLetter = randomElement(letters.split(''));
const genderDigit = isMaleForIdentity ? '1' : '2'; // 1=男, 2=女
const sequenceDigits = randomInt(10000000, 99999999).toString();
ssn = `${firstLetter}${genderDigit}${sequenceDigits}`;
} else if (country.includes('新加坡') || country.includes('Singapore') || country === 'SG') {
// Generate Singapore NRIC: Format: S1234567D (prefix + 7 digits + check letter)
// S=citizen pre-2000, T=citizen 2000+, G/F=PR. Prefix should match birth year
let prefix;
if (birthYear < 2000) {
prefix = 'S'; // Born before 2000
} else {
prefix = 'T'; // Born in 2000 or later
}
// Small chance to be PR (G prefix) regardless of year
if (Math.random() < 0.1) {
prefix = 'G'; // Permanent Resident
}
const digits = randomInt(1000000, 9999999).toString();
const checkLetters = 'ABCDEFGHIZJ';
const checkLetter = randomElement(checkLetters.split(''));
ssn = `${prefix}${digits}${checkLetter}`;
} else {
// Default: US SSN format (XXX-XX-XXXX)
// SSN Area Number (first 3 digits) should match the state if available
let ssnAreaNumber;
if (address.stateCode && (address.country === '美国' || address.country === 'US')) {
try {
const usData = await loadData('data/usData.json');
const state = usData.states[address.stateCode];
if (state && state.ssn_area_range && state.ssn_area_range.min && state.ssn_area_range.max) {
// Use state-specific SSN area number range
ssnAreaNumber = randomInt(state.ssn_area_range.min, state.ssn_area_range.max);
} else {
// Fallback: use random area number (avoid 000, 666, and 900-999)
do {
ssnAreaNumber = randomInt(1, 899);
} while (ssnAreaNumber === 666 || ssnAreaNumber < 1);
}
} catch (e) {
// If loading fails, use fallback
do {
ssnAreaNumber = randomInt(1, 899);
} while (ssnAreaNumber === 666 || ssnAreaNumber < 1);
}
} else {
// For non-US addresses or if stateCode is not available, use random area number
do {
ssnAreaNumber = randomInt(1, 899);
} while (ssnAreaNumber === 666 || ssnAreaNumber < 1);
}
// Generate Group Number (middle 2 digits, cannot be 00)
const groupNumber = randomInt(1, 99);
// Generate Serial Number (last 4 digits, cannot be 0000)
const serialNumber = randomInt(1, 9999);
// Format SSN: XXX-XX-XXXX
ssn = `${ssnAreaNumber.toString().padStart(3, '0')}-${groupNumber.toString().padStart(2, '0')}-${serialNumber.toString().padStart(4, '0')}`;
}
// Generate occupation (random job title)
const occupations = [
'Software Engineer', 'Teacher', 'Doctor', 'Nurse', 'Engineer',
'Accountant', 'Lawyer', 'Manager', 'Sales Representative', 'Designer',
'Marketing Specialist', 'Consultant', 'Analyst', 'Administrator', 'Director',
'Developer', 'Architect', 'Coordinator', 'Supervisor', 'Assistant'
];
const occupation = randomElement(occupations);
return {
firstName,
lastName,
dateOfBirth,
age,
ssn,
occupation
};
} catch (error) {
console.error('Error generating identity info:', error);
throw error;
}
}
// Generate credit card information
export async function generateCreditCardInfo() {
// Card types with their prefixes
const cardTypes = [
{ name: 'Visa', prefixes: ['4'] },
{ name: 'MasterCard', prefixes: ['51', '52', '53', '54', '55'] },
{ name: 'American Express', prefixes: ['34', '37'] },
{ name: 'Discover', prefixes: ['6011'] }
];
// Select random card type
const selectedCardType = randomElement(cardTypes);
const prefix = randomElement(selectedCardType.prefixes);
// Generate random credit card number (16 digits, Luhn algorithm)
function generateLuhnNumber(prefix, length = 16) {
let cardNumber = prefix;
// Generate remaining digits (excluding prefix and check digit)
const remainingDigits = length - prefix.length - 1;
for (let i = 0; i < remainingDigits; i++) {
cardNumber += randomInt(0, 9).toString();
}
// Calculate check digit using Luhn algorithm
let sum = 0;
let double = false;
for (let i = cardNumber.length - 1; i >= 0; i--) {
let digit = parseInt(cardNumber[i]);
if (double) {
digit *= 2;
if (digit > 9) digit -= 9;
}
sum += digit;
double = !double;
}
const checkDigit = (10 - (sum % 10)) % 10;
cardNumber += checkDigit.toString();
return cardNumber;
}
// Generate card number (16 digits for most cards, 15 for Amex)
const cardLength = selectedCardType.name === 'American Express' ? 15 : 16;
const cardNumber = generateLuhnNumber(prefix, cardLength);
// Format as XXXX XXXX XXXX XXXX (or XXXX XXXXXX XXXXX for Amex)
const formattedCardNumber = cardNumber.match(/.{1,4}/g).join(' ');
// Generate expiration date (future date, 1-5 years from now)
const currentYear = new Date().getFullYear();
const expYear = currentYear + randomInt(1, 5);
const expMonth = randomInt(1, 12);
const expirationDate = `${expMonth.toString().padStart(2, '0')}/${expYear.toString().slice(-2)}`;
// Generate CVV (3 digits for most cards, 4 for Amex)
const cvvLength = selectedCardType.name === 'American Express' ? 4 : 3;
const cvv = randomInt(Math.pow(10, cvvLength - 1), Math.pow(10, cvvLength) - 1).toString();
// Generate cardholder name (random Western name)
const names = ['John', 'Mary', 'David', 'Sarah', 'Michael', 'Emily', 'James', 'Lisa', 'Robert', 'Anna'];
const surnames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia', 'Miller', 'Davis', 'Rodriguez', 'Martinez'];
const cardholderName = `${randomElement(names)} ${randomElement(surnames)}`;
return {
type: selectedCardType.name,
number: formattedCardNumber,
rawNumber: cardNumber,
expiryDate: expirationDate,
expirationDate: expirationDate,
cvv,
cardholderName
};
}
// Generate Singapore address
export async function generateSGAddress(selectedState = 'RANDOM') {
try {
const sgData = await loadData('data/sgData.json');
const namesData = await loadData('data/namesData.json');
let availableStates = {};
if (selectedState === 'RANDOM') {
availableStates = sgData.states;
} else if (sgData.states[selectedState]) {
availableStates[selectedState] = sgData.states[selectedState];
} else {
availableStates = sgData.states;
}
const states = Object.keys(availableStates);
if (states.length === 0) throw new Error('No regions available');
const stateKey = randomElement(states);
const state = availableStates[stateKey];
const nameGroup = namesData.nameGroups.western;
const genderRaw = Math.random() > 0.5 ? 'Male' : 'Female';
let firstName;
if (nameGroup.first.male && nameGroup.first.female) {
firstName = randomElement(genderRaw === 'Male' ? nameGroup.first.male : nameGroup.first.female);
} else {
firstName = randomElement(nameGroup.first);
}
const lastName = randomElement(nameGroup.last);
const gender = genderRaw === 'Male' ? 'Male' : 'Female';
const mobilePrefix = randomElement(['8', '9']);
const subscriberNumber = randomInt(1000000, 9999999);
const phone = `+65 ${mobilePrefix}${subscriberNumber}`;
const email = generateEmail(firstName, lastName);
const cities = Object.keys(state.cities);
if (cities.length === 0) throw new Error(`No cities for region ${stateKey}`);
const cityName = randomElement(cities);
const cityData = state.cities[cityName];
if (!cityData || !cityData.zip || !cityData.streets) {
throw new Error(`Invalid city data for ${cityName}`);
}
const zip = randomElement(cityData.zip);
const streetName = randomElement(cityData.streets);
const useBlockFormat = Math.random() > 0.3;
let street;
if (useBlockFormat) {
const blockNum = randomInt(1, 999);
const floor = randomInt(1, 25);
const unit = randomInt(1, 12);
street = `Block ${blockNum}, ${streetName}, #${floor.toString().padStart(2, '0')}-${unit.toString().padStart(2, '0')}`;
} else {
const unitNum = randomInt(1, 200);
const floor = randomInt(1, 30);
const unit = randomInt(1, 8);
street = `${unitNum} ${streetName}, #${floor}-${unit}`;
}
const stateNameEn = state.name.en || state.name.zh;
const fullAddress = `${street}, Singapore ${zip}`;
return {
firstName,
lastName,
gender,
phone,
email,
street,
city: cityName,
zip,
postcode: zip,
state: stateNameEn,
stateCode: stateKey,
fullAddress,
country: 'SG'
};
} catch (error) {
console.error('Error generating SG address:', error);
throw error;
}
}
// Generate Germany address
export async function generateDEAddress(selectedState = 'RANDOM') {
try {
const deData = await loadData('data/deData.json');
const namesData = await loadData('data/namesData.json');
// Filter states based on selected state
let availableStates = {};
if (selectedState === 'RANDOM') {
// Use all states
availableStates = deData.states;
} else if (deData.states[selectedState]) {
// Use only the selected state
availableStates[selectedState] = deData.states[selectedState];
} else {
// Fallback to all states if invalid selection
availableStates = deData.states;
}
// Select random state from filtered states
const states = Object.keys(availableStates);
if (states.length === 0) {
throw new Error('No states available for selected state');
}
const stateKey = randomElement(states);
const state = availableStates[stateKey];
// Generate name - decide gender first, then select name
const nameGroup = namesData.nameGroups.western;
const genderRaw = Math.random() > 0.5 ? 'Male' : 'Female';
// Select name based on gender - use gender-specific name lists if available
let firstName;
if (nameGroup.first.male && nameGroup.first.female) {
// Use gender-specific name lists
firstName = randomElement(genderRaw === 'Male' ? nameGroup.first.male : nameGroup.first.female);
} else {
// Fallback: use all names if gender classification not available
firstName = randomElement(nameGroup.first);
}
const lastName = randomElement(nameGroup.last);
// Convert gender to German format for display: Männlich (男) / Weiblich (女)
const gender = genderRaw === 'Male' ? 'Männlich (男)' : 'Weiblich (女)';
// Generate phone - German mobile format: +49 1xx xxxxxxx
// Mobile prefixes: 151, 160, 170, 171, 175 (Telekom); 152, 162, 172, 173, 174 (Vodafone); 157, 163, 176, 177, 178 (O2)
const mobilePrefixes = ['151', '160', '170', '171', '175', '152', '162', '172', '173', '174', '157', '163', '176', '177', '178'];
const prefix = randomElement(mobilePrefixes);
const subscriberNumber = randomInt(1000000, 9999999);
const phone = `+49 ${prefix} ${subscriberNumber}`;
// Generate email - use generateEmail function (ensures no dot before @)
const email = generateEmail(firstName, lastName);
// Generate address - select random city from state
// IMPORTANT: Ensure city is selected from the correct state to avoid cross-state mismatches
const cities = Object.keys(state.cities);
if (cities.length === 0) {
throw new Error(`No cities available for state ${stateKey}`);
}
const cityName = randomElement(cities);
const cityData = state.cities[cityName];
// Validate city data structure
if (!cityData || !cityData.zip || !cityData.streets) {
throw new Error(`Invalid city data structure for ${cityName} in state ${stateKey}`);
}
// Get zip code from city data (ensures zip matches the city)
if (cityData.zip.length === 0) {
throw new Error(`No zip codes available for city ${cityName} in state ${stateKey}`);
}
const zip = randomElement(cityData.zip);
// Get street name from city data (ensures street matches the city)
if (cityData.streets.length === 0) {
throw new Error(`No streets available for city ${cityName} in state ${stateKey}`);
}
const streetName = randomElement(cityData.streets);
// Generate house number (1-150, occasionally with letter suffix like 12a)
const houseNumber = randomInt(1, 150);
const suffix = Math.random() < 0.2 ? String.fromCharCode(97 + randomInt(0, 2)) : '';
const street = `${streetName} ${houseNumber}${suffix}`;
// Use English state name for full address and state field
const stateNameEn = state.name.en || state.name.zh;
const fullAddress = `${street}, ${zip} ${cityName}, ${stateNameEn}, Germany`;
return {
firstName,
lastName,
gender,
phone,
email,
street,
city: cityName,
zip,
postcode: zip,
state: stateNameEn,
stateCode: stateKey,
fullAddress,
country: 'DE'
};
} catch (error) {
console.error('Error generating DE address:', error);
throw error;
}
}