familyHub/api/bank_reverse_transaction.php
Louis Whittington 6d10cb4726 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.
2026-03-31 11:03:53 -05:00

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]]);