= 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); }