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:
@@ -0,0 +1,168 @@
|
||||
<?php
|
||||
|
||||
require_once __DIR__ . '/family_settings.php';
|
||||
require_once __DIR__ . '/persona.php';
|
||||
|
||||
function bankingEnabled(array $familySettings): bool {
|
||||
return !empty($familySettings['banking_enabled']);
|
||||
}
|
||||
|
||||
function bankingRoundMoney(float $value): float {
|
||||
return round($value, 2);
|
||||
}
|
||||
|
||||
function bankingYmd(string $isoDate): string {
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $isoDate)) {
|
||||
return $isoDate;
|
||||
}
|
||||
if ($isoDate !== '' && strlen($isoDate) >= 10) {
|
||||
$candidate = substr($isoDate, 0, 10);
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
}
|
||||
return gmdate('Y-m-d');
|
||||
}
|
||||
|
||||
function bankingDaysInMonth(string $ymd): int {
|
||||
$parts = explode('-', $ymd);
|
||||
if (count($parts) !== 3) {
|
||||
return 30;
|
||||
}
|
||||
$year = (int) $parts[0];
|
||||
$month = (int) $parts[1];
|
||||
if ($year < 1970 || $month < 1 || $month > 12) {
|
||||
return 30;
|
||||
}
|
||||
return cal_days_in_month(CAL_GREGORIAN, $month, $year);
|
||||
}
|
||||
|
||||
function bankingDailyRate(float $monthlyRatePercent, string $ymd): float {
|
||||
if ($monthlyRatePercent <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
$days = bankingDaysInMonth($ymd);
|
||||
if ($days <= 0) {
|
||||
return 0.0;
|
||||
}
|
||||
$monthlyRate = $monthlyRatePercent / 100.0;
|
||||
return pow(1 + $monthlyRate, 1.0 / $days) - 1.0;
|
||||
}
|
||||
|
||||
function bankingApplySavingsInterestToPerson(array $person, array $familySettings, ?string $asOfYmd = null): array {
|
||||
if (!bankingEnabled($familySettings)) {
|
||||
return $person;
|
||||
}
|
||||
$asOf = $asOfYmd !== null ? bankingYmd($asOfYmd) : gmdate('Y-m-d');
|
||||
$last = bankingYmd((string) ($person['banking_interest_last_applied_at'] ?? ''));
|
||||
if (($person['banking_interest_last_applied_at'] ?? '') === '') {
|
||||
$person['banking_interest_last_applied_at'] = $asOf;
|
||||
return $person;
|
||||
}
|
||||
if ($last >= $asOf) {
|
||||
return $person;
|
||||
}
|
||||
$monthlyRate = (float) ($familySettings['banking_savings_monthly_interest_rate'] ?? 0);
|
||||
if ($monthlyRate <= 0) {
|
||||
$person['banking_interest_last_applied_at'] = $asOf;
|
||||
return $person;
|
||||
}
|
||||
$savings = is_numeric($person['savings_balance'] ?? null) ? (float) $person['savings_balance'] : 0.0;
|
||||
if ($savings <= 0) {
|
||||
$person['banking_interest_last_applied_at'] = $asOf;
|
||||
return $person;
|
||||
}
|
||||
|
||||
$cursor = $last;
|
||||
while ($cursor < $asOf) {
|
||||
$next = gmdate('Y-m-d', strtotime($cursor . ' +1 day'));
|
||||
$dailyRate = bankingDailyRate($monthlyRate, $next);
|
||||
if ($dailyRate > 0 && $savings > 0) {
|
||||
$savings = $savings * (1 + $dailyRate);
|
||||
}
|
||||
$cursor = $next;
|
||||
}
|
||||
$person['savings_balance'] = bankingRoundMoney($savings);
|
||||
$person['banking_interest_last_applied_at'] = $asOf;
|
||||
return $person;
|
||||
}
|
||||
|
||||
function bankingApplySavingsInterestToPeople(array $people, array $familySettings, ?string $asOfYmd = null): array {
|
||||
$out = [];
|
||||
foreach ($people as $person) {
|
||||
if (!is_array($person)) {
|
||||
continue;
|
||||
}
|
||||
$out[] = bankingApplySavingsInterestToPerson($person, $familySettings, $asOfYmd);
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
function bankingCreditCheckingByRule(array $person, float $amount, array $familySettings): array {
|
||||
$amount = bankingRoundMoney($amount);
|
||||
if ($amount <= 0) {
|
||||
return ['person' => $person, 'allocations' => ['checking' => 0.0, 'savings' => 0.0, 'charity' => 0.0, 'roundup' => 0.0]];
|
||||
}
|
||||
$checking = $amount;
|
||||
$savings = 0.0;
|
||||
$charity = 0.0;
|
||||
$roundup = 0.0;
|
||||
|
||||
if (!empty($familySettings['banking_auto_split_enabled'])) {
|
||||
$sPct = (float) ($familySettings['banking_auto_split_savings_pct'] ?? 0);
|
||||
$cPct = (float) ($familySettings['banking_auto_split_charity_pct'] ?? 0);
|
||||
if (($sPct + $cPct) > 100) {
|
||||
$cPct = max(0, 100 - $sPct);
|
||||
}
|
||||
$savings = bankingRoundMoney($amount * ($sPct / 100.0));
|
||||
$charity = bankingRoundMoney($amount * ($cPct / 100.0));
|
||||
$checking = bankingRoundMoney($amount - $savings - $charity);
|
||||
}
|
||||
|
||||
$roundupDest = (string) ($familySettings['banking_roundup_destination'] ?? 'off');
|
||||
if ($roundupDest !== 'off' && $checking > 0) {
|
||||
$fraction = $checking - floor($checking);
|
||||
$needed = $fraction > 0 ? bankingRoundMoney(1 - $fraction) : 0.0;
|
||||
if ($needed > 0) {
|
||||
$roundup = min($needed, $checking);
|
||||
$checking = bankingRoundMoney($checking - $roundup);
|
||||
if ($roundupDest === 'savings') {
|
||||
$savings = bankingRoundMoney($savings + $roundup);
|
||||
} elseif ($roundupDest === 'charity') {
|
||||
$charity = bankingRoundMoney($charity + $roundup);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$person['checking_balance'] = bankingRoundMoney((float) ($person['checking_balance'] ?? 0) + $checking);
|
||||
$person['savings_balance'] = bankingRoundMoney((float) ($person['savings_balance'] ?? 0) + $savings);
|
||||
$person['charity_pending_balance'] = bankingRoundMoney((float) ($person['charity_pending_balance'] ?? 0) + $charity);
|
||||
$person['currency_balance'] = $person['checking_balance'];
|
||||
|
||||
return [
|
||||
'person' => $person,
|
||||
'allocations' => [
|
||||
'checking' => $checking,
|
||||
'savings' => $savings,
|
||||
'charity' => $charity,
|
||||
'roundup' => $roundup,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
function appendBankTransaction(array $tx): bool {
|
||||
$rows = readJsonFile('bank_transactions.json');
|
||||
if (!is_array($rows)) {
|
||||
$rows = [];
|
||||
}
|
||||
$rows[] = $tx;
|
||||
return writeJsonFile('bank_transactions.json', $rows);
|
||||
}
|
||||
|
||||
function bankingCategoryOrDefault(string $category, string $default): string {
|
||||
$category = trim($category);
|
||||
if ($category === '') {
|
||||
return $default;
|
||||
}
|
||||
return substr($category, 0, 50);
|
||||
}
|
||||
Reference in New Issue
Block a user