pdo = $pdo; $this->companyId = $company_id; } // ── ETL-aware aggregate helpers ─────────────────────────────────────────── // All three return: [acc_code => ['debit' => float, 'credit' => float]] /** * Sum debits/credits per account for journal_date in [from_date, to_date]. * Uses ETL for interior whole months; raw for boundary months. */ private function aggRange(string $from_date, string $to_date, int $dept_id = 0): array { $from_month = substr($from_date, 0, 7); $to_month = substr($to_date, 0, 7); $same_month = ($from_month === $to_month); $dept_filter = $dept_id ? 'AND i.department_id = :dept_id' : ''; $out = []; $merge = function (array $rows) use (&$out) { foreach ($rows as $r) { $c = (int)$r['acc_code']; $out[$c]['debit'] = ($out[$c]['debit'] ?? 0.0) + (float)$r['d']; $out[$c]['credit'] = ($out[$c]['credit'] ?? 0.0) + (float)$r['c']; } }; if ($same_month || $dept_id !== 0) { $sth = $this->pdo->prepare(" SELECT i.account_code AS acc_code, SUM(i.debit) AS d, SUM(i.credit) AS c FROM td_gl_item i JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id WHERE i.company_id = :cid AND g.journal_date BETWEEN :fd AND :td AND g.source_type != 'voided' $dept_filter GROUP BY i.account_code "); $p = [':cid' => $this->companyId, ':fd' => $from_date, ':td' => $to_date]; if ($dept_id) $p[':dept_id'] = $dept_id; $sth->execute($p); $merge($sth->fetchAll(PDO::FETCH_ASSOC)); return $out; } // ETL: interior months only (strictly between boundaries) $sth = $this->pdo->prepare(" SELECT acc_code, SUM(debit) AS d, SUM(credit) AS c FROM etl_gl_summary WHERE company_id = :cid AND period > :fm AND period < :tm GROUP BY acc_code "); $sth->execute([':cid' => $this->companyId, ':fm' => $from_month, ':tm' => $to_month]); $merge($sth->fetchAll(PDO::FETCH_ASSOC)); // Raw: start boundary month (from_date → last day of from_month) $from_month_end = date('Y-m-t', strtotime($from_date)); $sth = $this->pdo->prepare(" SELECT i.account_code AS acc_code, SUM(i.debit) AS d, SUM(i.credit) AS c FROM td_gl_item i JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id WHERE i.company_id = :cid AND g.journal_date BETWEEN :fd AND :fe AND g.source_type != 'voided' GROUP BY i.account_code "); $sth->execute([':cid' => $this->companyId, ':fd' => $from_date, ':fe' => $from_month_end]); $merge($sth->fetchAll(PDO::FETCH_ASSOC)); // Raw: end boundary month (1st of to_month → to_date) $sth = $this->pdo->prepare(" SELECT i.account_code AS acc_code, SUM(i.debit) AS d, SUM(i.credit) AS c FROM td_gl_item i JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id WHERE i.company_id = :cid AND g.journal_date BETWEEN :ts AND :td AND g.source_type != 'voided' GROUP BY i.account_code "); $sth->execute([':cid' => $this->companyId, ':ts' => $to_month . '-01', ':td' => $to_date]); $merge($sth->fetchAll(PDO::FETCH_ASSOC)); return $out; } /** * Sum debits/credits per account for journal_date strictly BEFORE before_date. * Uses ETL for months prior to the month containing before_date; raw within that month. */ private function aggBefore(string $before_date, int $dept_id = 0): array { $month = substr($before_date, 0, 7); $month_start = $month . '-01'; $dept_filter = $dept_id ? 'AND i.department_id = :dept_id' : ''; $out = []; $merge = function (array $rows) use (&$out) { foreach ($rows as $r) { $c = (int)$r['acc_code']; $out[$c]['debit'] = ($out[$c]['debit'] ?? 0.0) + (float)$r['d']; $out[$c]['credit'] = ($out[$c]['credit'] ?? 0.0) + (float)$r['c']; } }; if ($dept_id !== 0) { $sth = $this->pdo->prepare(" SELECT i.account_code AS acc_code, SUM(i.debit) AS d, SUM(i.credit) AS c FROM td_gl_item i JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id WHERE i.company_id = :cid AND g.journal_date < :bd AND g.source_type != 'voided' $dept_filter GROUP BY i.account_code "); $p = [':cid' => $this->companyId, ':bd' => $before_date]; if ($dept_id) $p[':dept_id'] = $dept_id; $sth->execute($p); $merge($sth->fetchAll(PDO::FETCH_ASSOC)); return $out; } // ETL: all periods strictly before the month containing before_date $sth = $this->pdo->prepare(" SELECT acc_code, SUM(debit) AS d, SUM(credit) AS c FROM etl_gl_summary WHERE company_id = :cid AND period < :month GROUP BY acc_code "); $sth->execute([':cid' => $this->companyId, ':month' => $month]); $merge($sth->fetchAll(PDO::FETCH_ASSOC)); // Raw: within before_date's month, up to but not including before_date if ($before_date > $month_start) { $sth = $this->pdo->prepare(" SELECT i.account_code AS acc_code, SUM(i.debit) AS d, SUM(i.credit) AS c FROM td_gl_item i JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id WHERE i.company_id = :cid AND g.journal_date >= :ms AND g.journal_date < :bd AND g.source_type != 'voided' GROUP BY i.account_code "); $sth->execute([':cid' => $this->companyId, ':ms' => $month_start, ':bd' => $before_date]); $merge($sth->fetchAll(PDO::FETCH_ASSOC)); } return $out; } /** * Sum debits/credits per account for all journal_date up to and including as_of_date. * Uses ETL for months before as_of_month; raw within as_of_month. */ private function aggUpTo(string $as_of_date, int $dept_id = 0): array { $month = substr($as_of_date, 0, 7); $month_start = $month . '-01'; $dept_filter = $dept_id ? 'AND i.department_id = :dept_id' : ''; $out = []; $merge = function (array $rows) use (&$out) { foreach ($rows as $r) { $c = (int)$r['acc_code']; $out[$c]['debit'] = ($out[$c]['debit'] ?? 0.0) + (float)$r['d']; $out[$c]['credit'] = ($out[$c]['credit'] ?? 0.0) + (float)$r['c']; } }; if ($dept_id !== 0) { $sth = $this->pdo->prepare(" SELECT i.account_code AS acc_code, SUM(i.debit) AS d, SUM(i.credit) AS c FROM td_gl_item i JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id WHERE i.company_id = :cid AND g.journal_date <= :as_of AND g.source_type != 'voided' $dept_filter GROUP BY i.account_code "); $p = [':cid' => $this->companyId, ':as_of' => $as_of_date]; if ($dept_id) $p[':dept_id'] = $dept_id; $sth->execute($p); $merge($sth->fetchAll(PDO::FETCH_ASSOC)); return $out; } // ETL: all periods strictly before as_of_month $sth = $this->pdo->prepare(" SELECT acc_code, SUM(debit) AS d, SUM(credit) AS c FROM etl_gl_summary WHERE company_id = :cid AND period < :month GROUP BY acc_code "); $sth->execute([':cid' => $this->companyId, ':month' => $month]); $merge($sth->fetchAll(PDO::FETCH_ASSOC)); // Raw: within as_of_month up to as_of_date $sth = $this->pdo->prepare(" SELECT i.account_code AS acc_code, SUM(i.debit) AS d, SUM(i.credit) AS c FROM td_gl_item i JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id WHERE i.company_id = :cid AND g.journal_date BETWEEN :ms AND :as_of AND g.source_type != 'voided' GROUP BY i.account_code "); $sth->execute([':cid' => $this->companyId, ':ms' => $month_start, ':as_of' => $as_of_date]); $merge($sth->fetchAll(PDO::FETCH_ASSOC)); return $out; } // ── Trial Balance ───────────────────────────────────────────────────────── // Returns every posting account with brought-forward balance, period movement, // and carry-forward balance for the selected date range. private function normalizeDate(string $date, bool $end_of_month = false): string { $date = trim($date); if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) { return $date; } if (preg_match('/^\d{4}-\d{2}$/', $date)) { return $end_of_month ? date('Y-m-t', strtotime($date . '-01')) : $date . '-01'; } return date('Y-m-d'); } public function getTrialBalance(string $from_date, string $to_date, int $dept_id = 0): array { $from_date = $this->normalizeDate($from_date); $to_date = $this->normalizeDate($to_date, true); $opening_agg = $this->aggBefore($from_date, $dept_id); $period_agg = $this->aggRange($from_date, $to_date, $dept_id); $sth = $this->pdo->prepare(" SELECT account_code, account_name, account_type FROM md_account WHERE company_id = :cid AND is_posting = 1 AND status = 1 ORDER BY account_code "); $sth->execute([':cid' => $this->companyId]); $rows = []; $total_opening_debit = 0.0; $total_opening_credit = 0.0; $total_period_debit = 0.0; $total_period_credit = 0.0; $total_closing_debit = 0.0; $total_closing_credit = 0.0; foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $a) { $code = (int)$a['account_code']; $od = $opening_agg[$code]['debit'] ?? 0.0; $oc = $opening_agg[$code]['credit'] ?? 0.0; $pd = $period_agg[$code]['debit'] ?? 0.0; $pc = $period_agg[$code]['credit'] ?? 0.0; if ($od == 0 && $oc == 0 && $pd == 0 && $pc == 0) continue; $opening_balance = $od - $oc; $closing_balance = $opening_balance + $pd - $pc; $a['opening_debit'] = $opening_balance > 0 ? $opening_balance : 0.0; $a['opening_credit'] = $opening_balance < 0 ? abs($opening_balance) : 0.0; $a['period_debit'] = $pd; $a['period_credit'] = $pc; $a['closing_debit'] = $closing_balance > 0 ? $closing_balance : 0.0; $a['closing_credit'] = $closing_balance < 0 ? abs($closing_balance) : 0.0; $rows[] = $a; $total_opening_debit += $a['opening_debit']; $total_opening_credit += $a['opening_credit']; $total_period_debit += $pd; $total_period_credit += $pc; $total_closing_debit += $a['closing_debit']; $total_closing_credit += $a['closing_credit']; } return [ 'rows' => $rows, 'total_opening_debit' => $total_opening_debit, 'total_opening_credit' => $total_opening_credit, 'total_period_debit' => $total_period_debit, 'total_period_credit' => $total_period_credit, 'total_closing_debit' => $total_closing_debit, 'total_closing_credit' => $total_closing_credit, 'total_debit' => $total_closing_debit, 'total_credit' => $total_closing_credit, 'from_date' => $from_date, 'to_date' => $to_date, 'from_period' => substr($from_date, 0, 7), 'to_period' => substr($to_date, 0, 7), ]; } // ── Profit & Loss ───────────────────────────────────────────────────────── // Returns revenue and expense accounts for the date range. // revenue net = credit - debit; expense net = debit - credit (normal balances). public function getProfitAndLoss(string $from_date, string $to_date, int $dept_id = 0): array { $from_date = $this->normalizeDate($from_date); $to_date = $this->normalizeDate($to_date, true); $agg = $this->aggRange($from_date, $to_date, $dept_id); $sth = $this->pdo->prepare(" SELECT account_code, account_name, account_type FROM md_account WHERE company_id = :cid AND account_type IN ('revenue','expense') AND is_posting = 1 ORDER BY account_type DESC, account_code "); $sth->execute([':cid' => $this->companyId]); $rows = []; $total_revenue = 0.0; $total_expense = 0.0; foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $a) { $code = (int)$a['account_code']; $d = $agg[$code]['debit'] ?? 0.0; $c = $agg[$code]['credit'] ?? 0.0; if ($d == 0 && $c == 0) continue; $a['total_debit'] = $d; $a['total_credit'] = $c; $rows[] = $a; if ($a['account_type'] === 'revenue') $total_revenue += $c - $d; if ($a['account_type'] === 'expense') $total_expense += $d - $c; } return [ 'rows' => $rows, 'total_revenue' => $total_revenue, 'total_expense' => $total_expense, 'net_profit' => $total_revenue - $total_expense, 'from_date' => $from_date, 'to_date' => $to_date, 'from_period' => substr($from_date, 0, 7), 'to_period' => substr($to_date, 0, 7), ]; } // ── Balance Sheet ───────────────────────────────────────────────────────── // Cumulative balances for asset/liability/equity accounts up to as_of_period. // Retained earnings = cumulative net P&L (revenue - expense) through that period. public function getBalanceSheet(string $as_of_date, int $dept_id = 0): array { $as_of_date = $this->normalizeDate($as_of_date, true); $agg = $this->aggUpTo($as_of_date, $dept_id); $sth = $this->pdo->prepare(" SELECT account_code, account_name, account_type FROM md_account WHERE company_id = :cid AND account_type IN ('asset','liability','equity','revenue','expense') AND is_posting = 1 AND status = 1 ORDER BY account_type, account_code "); $sth->execute([':cid' => $this->companyId]); $rows = []; $net_revenue = 0.0; $net_expense = 0.0; foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $a) { $code = (int)$a['account_code']; $d = $agg[$code]['debit'] ?? 0.0; $c = $agg[$code]['credit'] ?? 0.0; if (in_array($a['account_type'], ['asset', 'liability', 'equity'])) { if ($d == 0 && $c == 0) continue; $a['total_debit'] = $d; $a['total_credit'] = $c; $rows[] = $a; } else { if ($a['account_type'] === 'revenue') $net_revenue += $c - $d; if ($a['account_type'] === 'expense') $net_expense += $d - $c; } } return [ 'rows' => $rows, 'retained_earnings' => $net_revenue - $net_expense, 'as_of_date' => $as_of_date, 'as_of_period' => substr($as_of_date, 0, 7), ]; } // ── Monthly Trend ───────────────────────────────────────────────────────── // Revenue vs expense per period for the last N months (defaults to 6). // Reads from etl_gl_summary; falls back to td_gl_item for periods not yet in ETL. public function getMonthlyTrend(int $months = 6): array { $from = date('Y-m', strtotime('-' . ($months - 1) . ' months')); $sth = $this->pdo->prepare(" SELECT period, acc_code, SUM(debit) AS d, SUM(credit) AS c FROM etl_gl_summary WHERE company_id = :cid AND acc_code >= 4000 AND period >= :from GROUP BY period, acc_code UNION ALL SELECT g.period, i.account_code AS acc_code, SUM(i.debit) AS d, SUM(i.credit) AS c FROM td_gl_item i JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id LEFT JOIN ( SELECT DISTINCT period FROM etl_gl_summary WHERE company_id = :cid2 ) etl ON etl.period = g.period WHERE i.company_id = :cid3 AND i.account_code >= 4000 AND g.source_type != 'voided' AND g.period >= :from2 AND etl.period IS NULL GROUP BY g.period, i.account_code "); $sth->execute([ ':cid' => $this->companyId, ':from' => $from, ':cid2' => $this->companyId, ':cid3' => $this->companyId, ':from2' => $from, ]); $by_period = []; foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) { $p = $row['period']; $code = (int)$row['acc_code']; if (!isset($by_period[$p])) $by_period[$p] = ['period' => $p, 'revenue' => 0.0, 'expense' => 0.0]; if ($code >= 4000 && $code < 5000) $by_period[$p]['revenue'] += (float)$row['c'] - (float)$row['d']; else $by_period[$p]['expense'] += (float)$row['d'] - (float)$row['c']; } ksort($by_period); return array_values($by_period); } // ── Dashboard focused methods ───────────────────────────────────────────── // One method per dashboard card/section. Used by separate REST endpoints. public function getDashboardPL(): array { // ETL first — all periods present in etl_gl_summary // Fallback — periods in td_gl but missing from ETL (gap fill) $sth = $this->pdo->prepare(" SELECT acc_code, SUM(debit) AS d, SUM(credit) AS c FROM etl_gl_summary WHERE company_id = :cid AND acc_code >= 4000 GROUP BY acc_code UNION ALL SELECT i.account_code AS acc_code, SUM(i.debit) AS d, SUM(i.credit) AS c FROM td_gl_item i JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id LEFT JOIN ( SELECT DISTINCT period FROM etl_gl_summary WHERE company_id = :cid2 ) etl ON etl.period = g.period WHERE i.company_id = :cid3 AND i.account_code >= 4000 AND g.source_type != 'voided' AND etl.period IS NULL GROUP BY i.account_code "); $sth->execute([ ':cid' => $this->companyId, ':cid2' => $this->companyId, ':cid3' => $this->companyId, ]); $revenue = 0.0; $expense = 0.0; foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) { $code = (int)$row['acc_code']; if ($code >= 4000 && $code < 5000) $revenue += (float)$row['c'] - (float)$row['d']; else $expense += (float)$row['d'] - (float)$row['c']; } return [ 'revenue' => $revenue, 'expense' => $expense, 'net_profit' => $revenue - $expense, ]; } public function getDashboardJournalCount(): int { $sth = $this->pdo->prepare("SELECT COUNT(*) FROM td_gl WHERE company_id = :cid"); $sth->execute([':cid' => $this->companyId]); return (int)$sth->fetchColumn(); } public function getDashboardBySource(): array { $sth = $this->pdo->prepare(" SELECT source_type, COUNT(*) AS cnt FROM td_gl WHERE company_id = :cid GROUP BY source_type ORDER BY cnt DESC "); $sth->execute([':cid' => $this->companyId]); return $sth->fetchAll(PDO::FETCH_ASSOC); } public function getDashboardRecentEntries(int $limit = 10): array { $limit = max(1, min(100, $limit)); $sth = $this->pdo->prepare(" SELECT g.id, g.source_type, COALESCE(g.reference, '') AS reference, COALESCE(g.description, '') AS description, COALESCE(g.journal_date, DATE(g.created_at)) AS journal_date, g.period, SUM(i.debit) AS total_debit, SUM(i.credit) AS total_credit FROM td_gl g JOIN td_gl_item i ON i.gl_id = g.id AND i.company_id = g.company_id WHERE g.company_id = :cid AND g.source_type != 'voided' GROUP BY g.id, g.source_type, g.reference, g.description, g.journal_date, g.period, g.created_at ORDER BY g.created_at DESC LIMIT {$limit} "); $sth->execute([':cid' => $this->companyId]); return $sth->fetchAll(PDO::FETCH_ASSOC); } public function getDashboardSummary(int $trend_months = 6, int $recent_limit = 10): array { $sth = $this->pdo->prepare(" SELECT a.account_type, COALESCE(SUM(i.debit), 0) AS total_debit, COALESCE(SUM(i.credit), 0) AS total_credit FROM md_account a JOIN td_gl_item i ON i.company_id = a.company_id AND i.account_code = a.account_code JOIN td_gl g ON g.company_id = i.company_id AND g.id = i.gl_id WHERE a.company_id = :cid AND a.account_type IN ('revenue', 'expense') AND a.is_posting = 1 GROUP BY a.account_type "); $sth->execute([':cid' => $this->companyId]); $total_revenue = 0.0; $total_expense = 0.0; foreach ($sth->fetchAll(PDO::FETCH_ASSOC) as $row) { if ($row['account_type'] === 'revenue') { $total_revenue += (float)$row['total_credit'] - (float)$row['total_debit']; } if ($row['account_type'] === 'expense') { $total_expense += (float)$row['total_debit'] - (float)$row['total_credit']; } } $sth = $this->pdo->prepare("SELECT COUNT(*) FROM td_gl WHERE company_id = :cid"); $sth->execute([':cid' => $this->companyId]); $journal_count = (int)$sth->fetchColumn(); $sth = $this->pdo->prepare(" SELECT source_type, COUNT(*) AS cnt FROM td_gl WHERE company_id = :cid GROUP BY source_type ORDER BY cnt DESC "); $sth->execute([':cid' => $this->companyId]); $by_source = $sth->fetchAll(PDO::FETCH_ASSOC); $sth = $this->pdo->prepare(" SELECT g.id, g.source_type, COALESCE(g.reference, '') AS reference, COALESCE(g.description, '') AS description, COALESCE(g.journal_date, DATE(g.created_at)) AS journal_date, g.period, SUM(i.debit) AS total_debit, SUM(i.credit) AS total_credit FROM td_gl g JOIN td_gl_item i ON i.gl_id = g.id AND i.company_id = g.company_id WHERE g.company_id = :cid GROUP BY g.id, g.source_type, g.reference, g.description, g.journal_date, g.period, g.created_at ORDER BY g.created_at DESC LIMIT {$recent_limit} "); $sth->execute([':cid' => $this->companyId]); $recent = $sth->fetchAll(PDO::FETCH_ASSOC); return [ 'revenue' => $total_revenue, 'expense' => $total_expense, 'net_profit' => $total_revenue - $total_expense, 'journal_count' => $journal_count, 'trend' => $this->getMonthlyTrend($trend_months), 'by_source' => $by_source, 'recent' => $recent, ]; } // Returns periods where td_gl is newer than etl_gl_summary (drift detected) // or where ETL is missing entirely. Used to signal which periods need a rebuild. private function getEtlGaps(string $from_period = ''): array { $period_filter = $from_period ? "AND g.period >= :from_period" : ""; $sql = " SELECT DISTINCT g.period FROM td_gl g LEFT JOIN ( SELECT period, MIN(source_updated_at) AS etl_ts FROM etl_gl_summary WHERE company_id = :cid2 GROUP BY period ) etl ON etl.period = g.period WHERE g.company_id = :cid AND g.source_type NOT IN ('voided', 'reversal') $period_filter AND ( etl.period IS NULL OR g.updated_at > etl.etl_ts ) ORDER BY g.period "; $params = [':cid' => $this->companyId, ':cid2' => $this->companyId]; if ($from_period) $params[':from_period'] = $from_period; $sth = $this->pdo->prepare($sql); $sth->execute($params); return $sth->fetchAll(PDO::FETCH_COLUMN); } // ── GL Movement ─────────────────────────────────────────────────────────── // Per-account ledger: opening balance + chronological lines with running balance. public function getGLMovement(string $account_code, string $from_date, string $to_date, int $dept_id = 0): array { $from_date = $this->normalizeDate($from_date); $to_date = $this->normalizeDate($to_date, true); // Opening balance = all movements before from_date $sth = $this->pdo->prepare(" SELECT COALESCE(SUM(i.debit), 0) AS opening_debit, COALESCE(SUM(i.credit), 0) AS opening_credit FROM td_gl_item i JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id WHERE i.company_id = :cid AND i.account_code = :code AND (:dept_id = 0 OR i.department_id = :dept_id) AND COALESCE(g.journal_date, DATE(g.created_at)) < :from_date "); $sth->execute([ ':cid' => $this->companyId, ':code' => $account_code, ':from_date' => $from_date, ':dept_id' => $dept_id, ]); $ob = $sth->fetch(PDO::FETCH_ASSOC); $opening_balance = (float)$ob['opening_debit'] - (float)$ob['opening_credit']; // Period lines $sth = $this->pdo->prepare(" SELECT g.id AS gl_id, g.source_type, COALESCE(g.reference, '') AS reference, COALESCE(g.description, '') AS gl_description, g.period, i.department_id, COALESCE(d.dept_code, '') AS dept_code, COALESCE(d.dept_name, '') AS dept_name, COALESCE(g.journal_date, DATE(g.created_at)) AS entry_date, DATE_FORMAT(g.created_at, '%d/%m/%Y %H:%i') AS posted_at, i.debit, i.credit, COALESCE(i.description, '') AS line_description FROM td_gl_item i JOIN td_gl g ON g.id = i.gl_id AND g.company_id = i.company_id LEFT JOIN md_department d ON d.company_id = i.company_id AND d.id = i.department_id WHERE i.company_id = :cid AND i.account_code = :code AND (:dept_id = 0 OR i.department_id = :dept_id) AND COALESCE(g.journal_date, DATE(g.created_at)) BETWEEN :from_date AND :to_date ORDER BY g.journal_date ASC, g.id ASC, i.id ASC "); $sth->execute([ ':cid' => $this->companyId, ':code' => $account_code, ':from_date' => $from_date, ':to_date' => $to_date, ':dept_id' => $dept_id, ]); $rows = $sth->fetchAll(PDO::FETCH_ASSOC); // Account metadata $sth = $this->pdo->prepare( "SELECT account_name, account_type FROM md_account WHERE company_id = :cid AND account_code = :code LIMIT 1" ); $sth->execute([':cid' => $this->companyId, ':code' => $account_code]); $acct = $sth->fetch(PDO::FETCH_ASSOC); return [ 'account_code' => $account_code, 'account_name' => $acct['account_name'] ?? '', 'account_type' => $acct['account_type'] ?? '', 'opening_balance' => $opening_balance, 'rows' => $rows, 'from_date' => $from_date, 'to_date' => $to_date, 'from_period' => substr($from_date, 0, 7), 'to_period' => substr($to_date, 0, 7), ]; } }