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