- Added banking mode with checking, savings, and charity accounts, including auto-split options for income. - Introduced banking transaction management, including transfers and charity outflows. - Updated family settings to allow configuration of banking features and interest rates. - Enhanced data export functionality to include bank transactions. - Improved user interface to display banking information and donation goals. - Updated documentation to reflect new banking features and settings.
184 lines
5.8 KiB
PHP
184 lines
5.8 KiB
PHP
<?php
|
|
|
|
require_once __DIR__ . '/db.php';
|
|
|
|
const ROLE_HEAD = 'head_of_household';
|
|
const ROLE_ADULT = 'adult';
|
|
const ROLE_CHILD = 'child';
|
|
|
|
const SESSION_ACTIVE_PERSON = 'active_person_id';
|
|
const SESSION_HOH_VERIFIED = 'hoh_verified';
|
|
|
|
function startFamilyHubSession(): void {
|
|
if (session_status() === PHP_SESSION_NONE) {
|
|
session_start();
|
|
}
|
|
}
|
|
|
|
function getActivePersonId(): ?string {
|
|
$id = $_SESSION[SESSION_ACTIVE_PERSON] ?? null;
|
|
return is_string($id) && $id !== '' ? $id : null;
|
|
}
|
|
|
|
function setSessionPerson(string $personId, bool $hohVerified): void {
|
|
$_SESSION[SESSION_ACTIVE_PERSON] = $personId;
|
|
$_SESSION[SESSION_HOH_VERIFIED] = $hohVerified;
|
|
}
|
|
|
|
function clearPersonaSession(): void {
|
|
unset($_SESSION[SESSION_ACTIVE_PERSON], $_SESSION[SESSION_HOH_VERIFIED]);
|
|
}
|
|
|
|
function isHohVerified(): bool {
|
|
return !empty($_SESSION[SESSION_HOH_VERIFIED]);
|
|
}
|
|
|
|
function findPersonById(array $people, string $id): ?array {
|
|
foreach ($people as $p) {
|
|
if (($p['id'] ?? '') === $id) {
|
|
return $p;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function getActivePerson(array $people): ?array {
|
|
$id = getActivePersonId();
|
|
if ($id === null) {
|
|
return null;
|
|
}
|
|
return findPersonById($people, $id);
|
|
}
|
|
|
|
function personRequiresPinToActivate(?array $person): bool {
|
|
if ($person === null) {
|
|
return false;
|
|
}
|
|
return ($person['role'] ?? '') === ROLE_HEAD && !empty($person['pin_hash']);
|
|
}
|
|
|
|
/**
|
|
* True when at least one person is recorded as Head of household (setup complete).
|
|
*/
|
|
function familyHubHasHeadOfHousehold(array $people): bool {
|
|
foreach ($people as $p) {
|
|
if (($p['role'] ?? '') === ROLE_HEAD) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function assertHoHCanManagePeople(array $people): void {
|
|
if (count($people) === 0) {
|
|
return;
|
|
}
|
|
$active = getActivePerson($people);
|
|
if ($active === null || ($active['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
|
|
http_response_code(403);
|
|
echo json_encode(['success' => false, 'error' => 'Head of household verification required.']);
|
|
exit;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param mixed $raw
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
function normalizePeopleList($raw): array {
|
|
if (!is_array($raw) || !array_is_list($raw)) {
|
|
return [];
|
|
}
|
|
$out = [];
|
|
foreach ($raw as $row) {
|
|
if (is_array($row) && !empty($row['id']) && is_string($row['id'])) {
|
|
$out[] = $row;
|
|
}
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $person
|
|
* @return array<string, mixed>
|
|
*/
|
|
function migrateLegacyPersonRow(array $person): array {
|
|
$currencyBalance = is_numeric($person['currency_balance'] ?? null) ? (float) $person['currency_balance'] : 0.0;
|
|
$checkingBalance = array_key_exists('checking_balance', $person) && is_numeric($person['checking_balance'])
|
|
? (float) $person['checking_balance']
|
|
: $currencyBalance;
|
|
$person['checking_balance'] = round($checkingBalance, 2);
|
|
if (!array_key_exists('currency_balance', $person) || !is_numeric($person['currency_balance'])) {
|
|
$person['currency_balance'] = $person['checking_balance'];
|
|
} else {
|
|
$person['currency_balance'] = round((float) $person['currency_balance'], 2);
|
|
}
|
|
if (!array_key_exists('savings_balance', $person) || !is_numeric($person['savings_balance'])) {
|
|
$person['savings_balance'] = 0.0;
|
|
} else {
|
|
$person['savings_balance'] = round((float) $person['savings_balance'], 2);
|
|
}
|
|
if (!array_key_exists('charity_pending_balance', $person) || !is_numeric($person['charity_pending_balance'])) {
|
|
$person['charity_pending_balance'] = 0.0;
|
|
} else {
|
|
$person['charity_pending_balance'] = round((float) $person['charity_pending_balance'], 2);
|
|
}
|
|
if (!array_key_exists('charity_donated_total', $person) || !is_numeric($person['charity_donated_total'])) {
|
|
$person['charity_donated_total'] = 0.0;
|
|
} else {
|
|
$person['charity_donated_total'] = round((float) $person['charity_donated_total'], 2);
|
|
}
|
|
if (!array_key_exists('donation_goal_monthly', $person) || !is_numeric($person['donation_goal_monthly'])) {
|
|
$person['donation_goal_monthly'] = 0.0;
|
|
} else {
|
|
$person['donation_goal_monthly'] = max(0, round((float) $person['donation_goal_monthly'], 2));
|
|
}
|
|
if (!array_key_exists('banking_interest_last_applied_at', $person) || !is_string($person['banking_interest_last_applied_at'])) {
|
|
$person['banking_interest_last_applied_at'] = '';
|
|
}
|
|
if (!array_key_exists('nfc_submit_token_hash', $person) || !is_string($person['nfc_submit_token_hash'])) {
|
|
$person['nfc_submit_token_hash'] = '';
|
|
}
|
|
if (!array_key_exists('nfc_submit_token_updated_at', $person) || !is_string($person['nfc_submit_token_updated_at'])) {
|
|
$person['nfc_submit_token_updated_at'] = '';
|
|
}
|
|
return $person;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $people
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
function migrateAllPeople(array $people): array {
|
|
$out = [];
|
|
foreach ($people as $person) {
|
|
if (!is_array($person)) {
|
|
continue;
|
|
}
|
|
$out[] = migrateLegacyPersonRow($person);
|
|
}
|
|
return $out;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, array<string, mixed>> $people
|
|
* @return array{index:int, person:array<string, mixed>}|null
|
|
*/
|
|
function findPersonBySubmitToken(array $people, string $token): ?array {
|
|
$token = trim($token);
|
|
if ($token === '') {
|
|
return null;
|
|
}
|
|
$candidateHash = hash('sha256', $token);
|
|
foreach ($people as $index => $person) {
|
|
$storedHash = trim((string) ($person['nfc_submit_token_hash'] ?? ''));
|
|
if ($storedHash === '') {
|
|
continue;
|
|
}
|
|
if (hash_equals($storedHash, $candidateHash)) {
|
|
return ['index' => (int) $index, 'person' => $person];
|
|
}
|
|
}
|
|
return null;
|
|
}
|