false, 'error' => 'Method not allowed'], 405); } $people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json'))); if (count($people) === 0) { sendJson(['success' => false, 'error' => 'Use first-time setup to create the initial Head of Household'], 400); } assertHoHCanManagePeople($people); $body = readJsonBody(); $name = isset($body['name']) ? trim((string) $body['name']) : ''; $role = isset($body['role']) ? (string) $body['role'] : ''; $pin = isset($body['pin']) ? (string) $body['pin'] : ''; if ($name === '') { sendJson(['success' => false, 'error' => 'Name is required'], 400); } $allowedRoles = [ROLE_HEAD, ROLE_ADULT, ROLE_CHILD]; if (!in_array($role, $allowedRoles, true)) { sendJson(['success' => false, 'error' => 'Invalid role'], 400); } if ($role === ROLE_HEAD) { if (strlen($pin) < 4) { sendJson(['success' => false, 'error' => 'PIN must be at least 4 characters'], 400); } $pinHash = password_hash($pin, PASSWORD_DEFAULT); } else { $pinHash = null; } $icon = isset($body['icon']) ? trim((string) $body['icon']) : ''; $description = isset($body['description']) ? trim((string) $body['description']) : ''; $birthday = isset($body['birthday']) ? trim((string) $body['birthday']) : ''; $favoriteColor = isset($body['favoriteColor']) ? trim((string) $body['favoriteColor']) : '#4a90e2'; if ($favoriteColor !== '' && !preg_match('/^#[0-9A-Fa-f]{6}$/', $favoriteColor)) { sendJson(['success' => false, 'error' => 'favoriteColor must be a #RRGGBB value'], 400); } if ($birthday !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}$/', $birthday)) { sendJson(['success' => false, 'error' => 'birthday must be YYYY-MM-DD'], 400); } $newPerson = [ 'id' => bin2hex(random_bytes(8)), 'name' => $name, 'role' => $role, 'pin_hash' => $pinHash, 'icon' => $icon, 'description' => $description, 'birthday' => $birthday, 'favoriteColor' => $favoriteColor, 'currency_balance' => 0, 'checking_balance' => 0, 'savings_balance' => 0, 'charity_pending_balance' => 0, 'charity_donated_total' => 0, 'donation_goal_monthly' => 0, 'banking_interest_last_applied_at' => '', 'nfc_submit_token_hash' => '', 'nfc_submit_token_updated_at' => '', 'created_at' => gmdate('c'), ]; $people[] = $newPerson; if (!writeJsonFile('people.json', $people)) { sendJson(['success' => false, 'error' => 'Failed to save people'], 500); } $safe = $newPerson; unset($safe['pin_hash']); sendJson(['success' => true, 'person' => $safe]);