'Giro Umum', 'simanja' => 'Tabungan SIMANJA', 'simpeda' => 'Tabungan SIMPEDA']; const SERUBI_GROUPS = [ 'dana' => ['Struktur dana', 'Bagaimana dana nasabah dibagi sebelum menghasilkan pendapatan.'], 'bunga' => ['Suku bunga pendapatan', 'Rate yang menghasilkan pendapatan bank dari dana kelolaan.'], 'biaya' => ['Biaya', 'Biaya yang mengurangi spread sebelum dibagi ke nasabah.'], 'pajak' => ['Pajak', 'Tarif pajak atas bunga dan cashback.'], 'konvensi' => ['Konvensi perhitungan', 'Hari bunga, pinalti, dan batas minimal.'], ]; /** Raw settings rows keyed by skey. */ function settings_rows(): array { $rows = db()->query('SELECT * FROM settings ORDER BY sort')->fetchAll(); return array_column($rows, null, 'skey'); } function to_num($v): ?float { return $v === null ? null : (float)$v; } const PARAMS_CACHE_TTL = 300; // seconds; the cache is also cleared whenever an admin saves function params_cache_file(): string { $dir = (string)(app_config()['app']['cache_dir'] ?? ''); if ($dir === '') { $dir = sys_get_temp_dir() . '/serubi-' . substr(md5(__DIR__), 0, 12); } return rtrim($dir, '/\\') . '/params.json'; } /** Parameters in the shape assets/js/engine.js expects (cached to spare the database). */ function load_params(): array { $file = params_cache_file(); if (is_file($file) && time() - (int)@filemtime($file) < PARAMS_CACHE_TTL) { $cached = json_decode((string)@file_get_contents($file), true); if (is_array($cached) && isset($cached['settings'])) { return $cached; } } $params = load_params_db(); $dir = dirname($file); if (is_dir($dir) || @mkdir($dir, 0700, true)) { $tmp = $file . '.' . bin2hex(random_bytes(4)); if (@file_put_contents($tmp, json_encode($params, JSON_PRESERVE_ZERO_FRACTION)) !== false) { @rename($tmp, $file); } @unlink($tmp); } return $params; } function params_cache_clear(): void { @unlink(params_cache_file()); } function load_params_db(): array { $pdo = db(); $settings = []; $updated = null; foreach (settings_rows() as $key => $row) { $settings[$key] = (float)$row['value']; $updated = max($updated ?? '', $row['updated_at']); } $tenors = $pdo->query('SELECT months, cashback FROM tenors ORDER BY months')->fetchAll(); $productRates = array_fill_keys(array_keys(SERUBI_PRODUCTS), []); foreach ($pdo->query('SELECT product, max_nominal, rate FROM product_rates ORDER BY product, sort, id') as $r) { $productRates[$r['product']][] = ['max' => to_num($r['max_nominal']), 'rate' => (float)$r['rate']]; } $bands = $pdo->query('SELECT id, max_nominal FROM deposito_bands ORDER BY sort, id')->fetchAll(); $rates = []; foreach ($pdo->query('SELECT band_id, tenor, rate FROM deposito_rates') as $r) { $rates[$r['band_id']][$r['tenor']] = (float)$r['rate']; } $depositoRates = []; foreach ($tenors as $t) { $m = (int)$t['months']; $depositoRates[$m] = array_map(fn($b) => [ 'max' => to_num($b['max_nominal']), 'rate' => $rates[$b['id']][$m] ?? 0.0, ], $bands); } $tiers = array_map(fn($r) => ['min' => (float)$r['min_nominal'], 'nasabah' => (float)$r['share_nasabah']], $pdo->query('SELECT min_nominal, share_nasabah FROM cashback_tiers ORDER BY min_nominal')->fetchAll()); $last = $pdo->query("SELECT username, created_at FROM audit_log WHERE action LIKE 'param%' ORDER BY id DESC LIMIT 1")->fetch(); return [ 'settings' => $settings, 'tenors' => array_map(fn($t) => (int)$t['months'], $tenors), 'cashbackTenors' => array_values(array_map(fn($t) => (int)$t['months'], array_filter($tenors, fn($t) => (int)$t['cashback'] === 1))), 'productRates' => $productRates, 'depositoBands' => array_map(fn($b) => to_num($b['max_nominal']), $bands), 'depositoRates' => $depositoRates, 'cashbackTiers' => $tiers, 'updatedAt' => $last['created_at'] ?? $updated, 'updatedBy' => $last['username'] ?? null, ]; }