Implement banking system features and enhancements

- 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.
This commit is contained in:
2026-03-31 11:03:53 -05:00
parent f14de0b7e1
commit 6d10cb4726
21 changed files with 1337 additions and 41 deletions
+88
View File
@@ -0,0 +1,88 @@
<?php
require_once __DIR__ . '/../includes/api_bootstrap.php';
require_once __DIR__ . '/../includes/family_settings.php';
require_once __DIR__ . '/../includes/banking_helpers.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
}
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
$actor = requireActivePerson($people);
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
sendJson(['success' => false, 'error' => 'Only a verified Head of household can adjust checking balances'], 403);
}
$settings = loadFamilySettings();
if (!bankingEnabled($settings)) {
sendJson(['success' => false, 'error' => 'Banking mode is not enabled'], 400);
}
$people = bankingApplySavingsInterestToPeople($people, $settings);
$body = readJsonBody();
$personId = trim((string) ($body['person_id'] ?? ''));
$type = trim((string) ($body['type'] ?? ''));
$note = trim((string) ($body['note'] ?? ''));
$category = bankingCategoryOrDefault((string) ($body['category'] ?? ''), 'manual');
$amountRaw = $body['amount'] ?? null;
if ($personId === '') {
sendJson(['success' => false, 'error' => 'person_id is required'], 400);
}
if (!in_array($type, ['credit', 'debit'], true)) {
sendJson(['success' => false, 'error' => 'type must be credit or debit'], 400);
}
if (!is_numeric($amountRaw)) {
sendJson(['success' => false, 'error' => 'amount must be numeric'], 400);
}
$amount = bankingRoundMoney((float) $amountRaw);
if ($amount <= 0) {
sendJson(['success' => false, 'error' => 'amount must be greater than zero'], 400);
}
$targetIdx = null;
foreach ($people as $i => $p) {
if (($p['id'] ?? '') === $personId) {
$targetIdx = $i;
break;
}
}
if ($targetIdx === null) {
sendJson(['success' => false, 'error' => 'Person not found'], 404);
}
$allocations = ['checking' => 0.0, 'savings' => 0.0, 'charity' => 0.0, 'roundup' => 0.0];
if ($type === 'credit') {
$result = bankingCreditCheckingByRule($people[$targetIdx], $amount, $settings);
$people[$targetIdx] = $result['person'];
$allocations = $result['allocations'];
} else {
$checking = (float) ($people[$targetIdx]['checking_balance'] ?? 0);
if ($checking < $amount) {
sendJson(['success' => false, 'error' => 'Insufficient checking balance'], 400);
}
$people[$targetIdx]['checking_balance'] = bankingRoundMoney($checking - $amount);
$people[$targetIdx]['currency_balance'] = $people[$targetIdx]['checking_balance'];
$allocations['checking'] = -$amount;
}
if (!writeJsonFile('people.json', $people)) {
sendJson(['success' => false, 'error' => 'Failed to save people'], 500);
}
$tx = [
'id' => bin2hex(random_bytes(8)),
'type' => 'manual_' . $type,
'person_id' => $personId,
'amount' => $amount,
'allocations' => $allocations,
'category' => $category,
'note' => $note,
'created_at' => gmdate('c'),
'created_by' => (string) ($actor['id'] ?? ''),
];
if (!appendBankTransaction($tx)) {
sendJson(['success' => false, 'error' => 'Saved balances but failed to write transaction log'], 500);
}
sendJson(['success' => true, 'transaction' => $tx, 'person' => $people[$targetIdx]]);
+74
View File
@@ -0,0 +1,74 @@
<?php
require_once __DIR__ . '/../includes/api_bootstrap.php';
require_once __DIR__ . '/../includes/family_settings.php';
require_once __DIR__ . '/../includes/banking_helpers.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
}
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
$actor = requireActivePerson($people);
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
sendJson(['success' => false, 'error' => 'Only a verified Head of household can log charity outflow'], 403);
}
$settings = loadFamilySettings();
if (!bankingEnabled($settings)) {
sendJson(['success' => false, 'error' => 'Banking mode is not enabled'], 400);
}
$people = bankingApplySavingsInterestToPeople($people, $settings);
$body = readJsonBody();
$personId = trim((string) ($body['person_id'] ?? ''));
$note = trim((string) ($body['note'] ?? ''));
$category = bankingCategoryOrDefault((string) ($body['category'] ?? ''), 'donation');
$amountRaw = $body['amount'] ?? null;
if ($personId === '') {
sendJson(['success' => false, 'error' => 'person_id is required'], 400);
}
if (!is_numeric($amountRaw)) {
sendJson(['success' => false, 'error' => 'amount must be numeric'], 400);
}
$amount = bankingRoundMoney((float) $amountRaw);
if ($amount <= 0) {
sendJson(['success' => false, 'error' => 'amount must be greater than zero'], 400);
}
$idx = null;
foreach ($people as $i => $p) {
if (($p['id'] ?? '') === $personId) {
$idx = $i;
break;
}
}
if ($idx === null) {
sendJson(['success' => false, 'error' => 'Person not found'], 404);
}
$pending = (float) ($people[$idx]['charity_pending_balance'] ?? 0);
if ($pending < $amount) {
sendJson(['success' => false, 'error' => 'Insufficient charity pending balance'], 400);
}
$people[$idx]['charity_pending_balance'] = bankingRoundMoney($pending - $amount);
$people[$idx]['charity_donated_total'] = bankingRoundMoney((float) ($people[$idx]['charity_donated_total'] ?? 0) + $amount);
if (!writeJsonFile('people.json', $people)) {
sendJson(['success' => false, 'error' => 'Failed to save people'], 500);
}
$tx = [
'id' => bin2hex(random_bytes(8)),
'type' => 'charity_outflow',
'person_id' => $personId,
'amount' => $amount,
'category' => $category,
'note' => $note,
'created_at' => gmdate('c'),
'created_by' => (string) ($actor['id'] ?? ''),
];
if (!appendBankTransaction($tx)) {
sendJson(['success' => false, 'error' => 'Saved outflow but failed to write transaction log'], 500);
}
sendJson(['success' => true, 'transaction' => $tx, 'person' => $people[$idx]]);
+133
View File
@@ -0,0 +1,133 @@
<?php
require_once __DIR__ . '/../includes/api_bootstrap.php';
require_once __DIR__ . '/../includes/family_settings.php';
require_once __DIR__ . '/../includes/banking_helpers.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
}
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
$actor = requireActivePerson($people);
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
sendJson(['success' => false, 'error' => 'Only a verified Head of household can reverse transactions'], 403);
}
$settings = loadFamilySettings();
if (!bankingEnabled($settings)) {
sendJson(['success' => false, 'error' => 'Banking mode is not enabled'], 400);
}
$people = bankingApplySavingsInterestToPeople($people, $settings);
$body = readJsonBody();
$transactionId = trim((string) ($body['transaction_id'] ?? ''));
$note = trim((string) ($body['note'] ?? ''));
if ($transactionId === '') {
sendJson(['success' => false, 'error' => 'transaction_id is required'], 400);
}
$rows = readJsonFile('bank_transactions.json');
if (!is_array($rows)) {
$rows = [];
}
$original = null;
foreach ($rows as $row) {
if (is_array($row) && (($row['id'] ?? '') === $transactionId)) {
$original = $row;
break;
}
}
if ($original === null) {
sendJson(['success' => false, 'error' => 'Transaction not found'], 404);
}
if (!empty($original['reversed_by_transaction_id'])) {
sendJson(['success' => false, 'error' => 'Transaction already reversed'], 400);
}
if (($original['type'] ?? '') === 'reversal') {
sendJson(['success' => false, 'error' => 'Cannot reverse a reversal transaction'], 400);
}
$personId = (string) ($original['person_id'] ?? '');
$idx = null;
foreach ($people as $i => $p) {
if (($p['id'] ?? '') === $personId) {
$idx = $i;
break;
}
}
if ($idx === null) {
sendJson(['success' => false, 'error' => 'Person for original transaction not found'], 404);
}
$type = (string) ($original['type'] ?? '');
$amount = bankingRoundMoney((float) ($original['amount'] ?? 0));
if ($amount <= 0) {
sendJson(['success' => false, 'error' => 'Original transaction amount is invalid'], 400);
}
if ($type === 'manual_credit' || $type === 'income_chore') {
$alloc = is_array($original['allocations'] ?? null) ? $original['allocations'] : [];
$ck = (float) ($alloc['checking'] ?? 0);
$sv = (float) ($alloc['savings'] ?? 0);
$ch = (float) ($alloc['charity'] ?? 0);
if ((float) $people[$idx]['checking_balance'] < $ck || (float) $people[$idx]['savings_balance'] < $sv || (float) $people[$idx]['charity_pending_balance'] < $ch) {
sendJson(['success' => false, 'error' => 'Insufficient balances to reverse this credit'], 400);
}
$people[$idx]['checking_balance'] = bankingRoundMoney((float) $people[$idx]['checking_balance'] - $ck);
$people[$idx]['savings_balance'] = bankingRoundMoney((float) $people[$idx]['savings_balance'] - $sv);
$people[$idx]['charity_pending_balance'] = bankingRoundMoney((float) $people[$idx]['charity_pending_balance'] - $ch);
} elseif ($type === 'manual_debit') {
$people[$idx]['checking_balance'] = bankingRoundMoney((float) $people[$idx]['checking_balance'] + $amount);
} elseif ($type === 'charity_outflow') {
if ((float) $people[$idx]['charity_donated_total'] < $amount) {
sendJson(['success' => false, 'error' => 'Insufficient donated total to reverse outflow'], 400);
}
$people[$idx]['charity_donated_total'] = bankingRoundMoney((float) $people[$idx]['charity_donated_total'] - $amount);
$people[$idx]['charity_pending_balance'] = bankingRoundMoney((float) $people[$idx]['charity_pending_balance'] + $amount);
} elseif ($type === 'transfer') {
$from = (string) ($original['from_account'] ?? '');
$to = (string) ($original['to_account'] ?? '');
$fieldMap = ['checking' => 'checking_balance', 'savings' => 'savings_balance', 'charity' => 'charity_pending_balance'];
if (!isset($fieldMap[$from], $fieldMap[$to])) {
sendJson(['success' => false, 'error' => 'Original transfer accounts are invalid'], 400);
}
$toField = $fieldMap[$to];
$fromField = $fieldMap[$from];
if ((float) $people[$idx][$toField] < $amount) {
sendJson(['success' => false, 'error' => 'Insufficient balances to reverse this transfer'], 400);
}
$people[$idx][$toField] = bankingRoundMoney((float) $people[$idx][$toField] - $amount);
$people[$idx][$fromField] = bankingRoundMoney((float) $people[$idx][$fromField] + $amount);
} else {
sendJson(['success' => false, 'error' => 'This transaction type cannot be reversed'], 400);
}
$people[$idx]['currency_balance'] = (float) ($people[$idx]['checking_balance'] ?? 0);
if (!writeJsonFile('people.json', $people)) {
sendJson(['success' => false, 'error' => 'Failed to save people balances'], 500);
}
$reversalId = bin2hex(random_bytes(8));
for ($i = 0; $i < count($rows); $i++) {
if (($rows[$i]['id'] ?? '') === $transactionId) {
$rows[$i]['reversed_by_transaction_id'] = $reversalId;
$rows[$i]['reversed_at'] = gmdate('c');
$rows[$i]['reversed_by'] = (string) ($actor['id'] ?? '');
}
}
$reversal = [
'id' => $reversalId,
'type' => 'reversal',
'person_id' => $personId,
'amount' => $amount,
'category' => 'reversal',
'note' => $note,
'reversal_of_transaction_id' => $transactionId,
'created_at' => gmdate('c'),
'created_by' => (string) ($actor['id'] ?? ''),
];
$rows[] = $reversal;
if (!writeJsonFile('bank_transactions.json', $rows)) {
sendJson(['success' => false, 'error' => 'Failed to save transaction log'], 500);
}
sendJson(['success' => true, 'transaction' => $reversal, 'person' => $people[$idx]]);
+50
View File
@@ -0,0 +1,50 @@
<?php
require_once __DIR__ . '/../includes/api_bootstrap.php';
require_once __DIR__ . '/../includes/family_settings.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
}
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
$actor = requireActivePerson($people);
$settings = loadFamilySettings();
if (empty($settings['banking_enabled'])) {
sendJson(['success' => false, 'error' => 'Banking mode is not enabled'], 400);
}
$body = readJsonBody();
$goalRaw = $body['goal_monthly'] ?? null;
if (!is_numeric($goalRaw)) {
sendJson(['success' => false, 'error' => 'goal_monthly must be numeric'], 400);
}
$goal = round((float) $goalRaw, 2);
if ($goal < 0) {
sendJson(['success' => false, 'error' => 'goal_monthly must be >= 0'], 400);
}
$targetPersonId = trim((string) ($body['person_id'] ?? ''));
$actorId = (string) ($actor['id'] ?? '');
$isHoh = (($actor['role'] ?? '') === ROLE_HEAD) && isHohVerified();
if ($targetPersonId === '') {
$targetPersonId = $actorId;
}
if (!$isHoh && $targetPersonId !== $actorId) {
sendJson(['success' => false, 'error' => 'Only verified Head of household can set goals for others'], 403);
}
$idx = null;
foreach ($people as $i => $p) {
if (($p['id'] ?? '') === $targetPersonId) {
$idx = $i;
break;
}
}
if ($idx === null) {
sendJson(['success' => false, 'error' => 'Person not found'], 404);
}
$people[$idx]['donation_goal_monthly'] = $goal;
if (!writeJsonFile('people.json', $people)) {
sendJson(['success' => false, 'error' => 'Failed to save goal'], 500);
}
sendJson(['success' => true, 'person' => $people[$idx]]);
+107
View File
@@ -0,0 +1,107 @@
<?php
require_once __DIR__ . '/../includes/api_bootstrap.php';
require_once __DIR__ . '/../includes/family_settings.php';
require_once __DIR__ . '/../includes/banking_helpers.php';
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
}
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
$actor = requireActivePerson($people);
$settings = loadFamilySettings();
if (!bankingEnabled($settings)) {
sendJson(['success' => false, 'error' => 'Banking mode is not enabled'], 400);
}
$month = trim((string) ($_GET['month'] ?? gmdate('Y-m')));
if (!preg_match('/^\d{4}-\d{2}$/', $month)) {
sendJson(['success' => false, 'error' => 'month must be YYYY-MM'], 400);
}
$format = trim((string) ($_GET['format'] ?? 'json'));
if (!in_array($format, ['json', 'csv'], true)) {
sendJson(['success' => false, 'error' => 'format must be json or csv'], 400);
}
$personId = trim((string) ($_GET['person_id'] ?? ''));
if ($personId === '') {
$personId = (string) ($actor['id'] ?? '');
}
$isHoh = (($actor['role'] ?? '') === ROLE_HEAD) && isHohVerified();
if (!$isHoh && $personId !== (string) ($actor['id'] ?? '')) {
sendJson(['success' => false, 'error' => 'Only verified Head of household can view statements for others'], 403);
}
$person = null;
foreach ($people as $p) {
if (($p['id'] ?? '') === $personId) {
$person = $p;
break;
}
}
if ($person === null) {
sendJson(['success' => false, 'error' => 'Person not found'], 404);
}
$rows = readJsonFile('bank_transactions.json');
if (!is_array($rows)) {
$rows = [];
}
$prefix = $month . '-';
$filtered = [];
foreach ($rows as $row) {
if (!is_array($row)) {
continue;
}
if ((string) ($row['person_id'] ?? '') !== $personId) {
continue;
}
$createdAt = (string) ($row['created_at'] ?? '');
$ymd = bankingYmd($createdAt);
if (strpos($ymd, $prefix) !== 0) {
continue;
}
$filtered[] = $row;
}
if ($format === 'csv') {
header('Content-Type: text/csv; charset=utf-8');
header('Content-Disposition: attachment; filename="bank_statement_' . $personId . '_' . $month . '.csv"');
$out = fopen('php://output', 'w');
if ($out === false) {
exit;
}
fputcsv($out, ['id', 'created_at', 'type', 'amount', 'category', 'note', 'from_account', 'to_account', 'reversal_of_transaction_id']);
foreach ($filtered as $row) {
fputcsv($out, [
(string) ($row['id'] ?? ''),
(string) ($row['created_at'] ?? ''),
(string) ($row['type'] ?? ''),
(string) ($row['amount'] ?? ''),
(string) ($row['category'] ?? ''),
(string) ($row['note'] ?? ''),
(string) ($row['from_account'] ?? ''),
(string) ($row['to_account'] ?? ''),
(string) ($row['reversal_of_transaction_id'] ?? ''),
]);
}
fclose($out);
exit;
}
sendJson([
'success' => true,
'month' => $month,
'person' => [
'id' => (string) ($person['id'] ?? ''),
'name' => (string) ($person['name'] ?? ''),
],
'balances' => [
'checking' => (float) ($person['checking_balance'] ?? 0),
'savings' => (float) ($person['savings_balance'] ?? 0),
'charity_pending' => (float) ($person['charity_pending_balance'] ?? 0),
'charity_donated_total' => (float) ($person['charity_donated_total'] ?? 0),
],
'transactions' => $filtered,
]);
+87
View File
@@ -0,0 +1,87 @@
<?php
require_once __DIR__ . '/../includes/api_bootstrap.php';
require_once __DIR__ . '/../includes/family_settings.php';
require_once __DIR__ . '/../includes/banking_helpers.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
}
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
$actor = requireActivePerson($people);
$settings = loadFamilySettings();
if (!bankingEnabled($settings)) {
sendJson(['success' => false, 'error' => 'Banking mode is not enabled'], 400);
}
$people = bankingApplySavingsInterestToPeople($people, $settings);
$body = readJsonBody();
$from = trim((string) ($body['from_account'] ?? ''));
$to = trim((string) ($body['to_account'] ?? ''));
$note = trim((string) ($body['note'] ?? ''));
$category = bankingCategoryOrDefault((string) ($body['category'] ?? ''), 'transfer');
$amountRaw = $body['amount'] ?? null;
if (!is_numeric($amountRaw)) {
sendJson(['success' => false, 'error' => 'amount must be numeric'], 400);
}
$amount = bankingRoundMoney((float) $amountRaw);
if ($amount <= 0) {
sendJson(['success' => false, 'error' => 'amount must be greater than zero'], 400);
}
$allowedAccounts = ['checking', 'savings', 'charity'];
if (!in_array($from, $allowedAccounts, true) || !in_array($to, $allowedAccounts, true) || $from === $to) {
sendJson(['success' => false, 'error' => 'Invalid transfer accounts'], 400);
}
if ($from === 'charity') {
sendJson(['success' => false, 'error' => 'Funds in charity cannot be transferred out'], 400);
}
$actorId = (string) ($actor['id'] ?? '');
$idx = null;
foreach ($people as $i => $p) {
if (($p['id'] ?? '') === $actorId) {
$idx = $i;
break;
}
}
if ($idx === null) {
sendJson(['success' => false, 'error' => 'Active person not found'], 404);
}
$fieldMap = [
'checking' => 'checking_balance',
'savings' => 'savings_balance',
'charity' => 'charity_pending_balance',
];
$fromField = $fieldMap[$from];
$toField = $fieldMap[$to];
$fromBalance = (float) ($people[$idx][$fromField] ?? 0);
if ($fromBalance < $amount) {
sendJson(['success' => false, 'error' => 'Insufficient source balance'], 400);
}
$people[$idx][$fromField] = bankingRoundMoney($fromBalance - $amount);
$people[$idx][$toField] = bankingRoundMoney((float) ($people[$idx][$toField] ?? 0) + $amount);
$people[$idx]['currency_balance'] = (float) ($people[$idx]['checking_balance'] ?? 0);
if (!writeJsonFile('people.json', $people)) {
sendJson(['success' => false, 'error' => 'Failed to save people'], 500);
}
$tx = [
'id' => bin2hex(random_bytes(8)),
'type' => 'transfer',
'person_id' => $actorId,
'amount' => $amount,
'from_account' => $from,
'to_account' => $to,
'category' => $category,
'note' => $note,
'created_at' => gmdate('c'),
'created_by' => $actorId,
];
if (!appendBankTransaction($tx)) {
sendJson(['success' => false, 'error' => 'Saved transfer but failed to write transaction log'], 500);
}
sendJson(['success' => true, 'transaction' => $tx, 'person' => $people[$idx]]);
+23 -2
View File
@@ -2,12 +2,16 @@
require_once __DIR__ . '/../includes/api_bootstrap.php';
require_once __DIR__ . '/../includes/chore_helpers.php';
require_once __DIR__ . '/../includes/family_settings.php';
require_once __DIR__ . '/../includes/banking_helpers.php';
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
sendJson(['success' => false, 'error' => 'Method not allowed'], 405);
}
$people = migrateAllPeople(normalizePeopleList(readJsonFile('people.json')));
$familySettings = loadFamilySettings();
$people = bankingApplySavingsInterestToPeople($people, $familySettings);
$actor = requireActivePerson($people);
if (($actor['role'] ?? '') !== ROLE_HEAD || !isHohVerified()) {
sendJson(['success' => false, 'error' => 'Only a verified Head of household can review chores'], 403);
@@ -86,8 +90,25 @@ foreach ($people as $pi => $p) {
$n = count($assignees);
$creditedEach = $n > 0 ? round($value / $n, 2) : 0.0;
}
$bal = $p['currency_balance'] ?? 0;
$people[$pi]['currency_balance'] = (is_numeric($bal) ? (float) $bal : 0.0) + $creditedEach;
if (bankingEnabled($familySettings)) {
$result = bankingCreditCheckingByRule($people[$pi], $creditedEach, $familySettings);
$people[$pi] = $result['person'];
appendBankTransaction([
'id' => bin2hex(random_bytes(8)),
'type' => 'income_chore',
'person_id' => $pid,
'amount' => bankingRoundMoney($creditedEach),
'allocations' => $result['allocations'],
'category' => 'chore',
'note' => (string) ($row['title'] ?? ''),
'created_at' => gmdate('c'),
'created_by' => (string) ($actor['id'] ?? ''),
]);
} else {
$bal = $p['currency_balance'] ?? 0;
$people[$pi]['currency_balance'] = (is_numeric($bal) ? (float) $bal : 0.0) + $creditedEach;
$people[$pi]['checking_balance'] = $people[$pi]['currency_balance'];
}
$creditedRecipients++;
}
+1
View File
@@ -19,6 +19,7 @@ $allowed = [
'meal_plans' => 'meal_plans.json',
'family_settings' => 'family_settings.json',
'expenses' => 'expenses.json',
'bank_transactions' => 'bank_transactions.json',
];
$type = isset($_GET['type']) ? trim((string) $_GET['type']) : '';
+48
View File
@@ -30,6 +30,49 @@ if (isset($body['currency_permanence'])) {
}
$merged['currency_permanence'] = $p;
}
if (array_key_exists('banking_enabled', $body)) {
$merged['banking_enabled'] = !empty($body['banking_enabled']);
}
if (array_key_exists('banking_auto_split_enabled', $body)) {
$merged['banking_auto_split_enabled'] = !empty($body['banking_auto_split_enabled']);
}
if (array_key_exists('banking_auto_split_savings_pct', $body)) {
if (!is_numeric($body['banking_auto_split_savings_pct'])) {
sendJson(['success' => false, 'error' => 'banking_auto_split_savings_pct must be numeric'], 400);
}
$pct = round((float) $body['banking_auto_split_savings_pct'], 2);
if ($pct < 0 || $pct > 100) {
sendJson(['success' => false, 'error' => 'banking_auto_split_savings_pct must be 0-100'], 400);
}
$merged['banking_auto_split_savings_pct'] = $pct;
}
if (array_key_exists('banking_auto_split_charity_pct', $body)) {
if (!is_numeric($body['banking_auto_split_charity_pct'])) {
sendJson(['success' => false, 'error' => 'banking_auto_split_charity_pct must be numeric'], 400);
}
$pct = round((float) $body['banking_auto_split_charity_pct'], 2);
if ($pct < 0 || $pct > 100) {
sendJson(['success' => false, 'error' => 'banking_auto_split_charity_pct must be 0-100'], 400);
}
$merged['banking_auto_split_charity_pct'] = $pct;
}
if (array_key_exists('banking_savings_monthly_interest_rate', $body)) {
if (!is_numeric($body['banking_savings_monthly_interest_rate'])) {
sendJson(['success' => false, 'error' => 'banking_savings_monthly_interest_rate must be numeric'], 400);
}
$rate = round((float) $body['banking_savings_monthly_interest_rate'], 6);
if ($rate < 0) {
sendJson(['success' => false, 'error' => 'banking_savings_monthly_interest_rate must be >= 0'], 400);
}
$merged['banking_savings_monthly_interest_rate'] = $rate;
}
if (array_key_exists('banking_roundup_destination', $body)) {
$dest = trim((string) $body['banking_roundup_destination']);
if (!in_array($dest, ['off', 'savings', 'charity'], true)) {
sendJson(['success' => false, 'error' => 'banking_roundup_destination must be off, savings, or charity'], 400);
}
$merged['banking_roundup_destination'] = $dest;
}
if (isset($body['timezone'])) {
$tz = trim((string) $body['timezone']);
if (!in_array($tz, familyHubUsTimezoneIdentifiers(), true)) {
@@ -67,6 +110,11 @@ if (array_key_exists('nfc_scan_cooldown_seconds', $body)) {
}
$merged['nfc_scan_cooldown_seconds'] = $cooldown;
}
if (
((float) ($merged['banking_auto_split_savings_pct'] ?? 0) + (float) ($merged['banking_auto_split_charity_pct'] ?? 0)) > 100
) {
sendJson(['success' => false, 'error' => 'banking auto split percentages cannot exceed 100 total'], 400);
}
$merged = normalizeLoadedFamilySettings($merged);
+6
View File
@@ -58,6 +58,12 @@ $newPerson = [
'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'),
+6
View File
@@ -46,6 +46,12 @@ $newPerson = [
'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'),