- 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.
134 lines
5.7 KiB
PHP
134 lines
5.7 KiB
PHP
<?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]]);
|