pdo = $pdo; $this->companyId = $company_id; $this->postingWindowGuard = $postingWindowGuard; } // ── public ─────────────────────────────────────────────────────────────── public function getBySource(string $source_type, int $source_id): ?array { $sth = $this->pdo->prepare( "SELECT * FROM td_gl WHERE company_id = :cid AND source_type = :source_type AND source_id = :source_id LIMIT 1" ); $sth->execute([ ':cid' => $this->companyId, ':source_type' => $source_type, ':source_id' => $source_id, ]); return $sth->fetch(PDO::FETCH_ASSOC) ?: null; } public function post(string $source_type, int $source_id, int $formula_id, string $period, array $lines, array $meta = []): void { $journal_date = $this->resolveJournalDate($meta['journal_date'] ?? null, $period); $this->assertPostingWindow($journal_date, 'GL posting'); $now = date('Y-m-d H:i:s'); $sth = $this->pdo->prepare( "INSERT INTO td_gl (company_id, source_type, source_id, reference, description, journal_date, formula_id, period, current_version, history, created_at, updated_at) VALUES (:cid, :source_type, :source_id, :reference, :description, :journal_date, :formula_id, :period, 1, '[]', :created_at, :updated_at)" ); $sth->execute([ ':cid' => $this->companyId, ':source_type' => $source_type, ':source_id' => $source_id, ':reference' => $meta['reference'] ?? '', ':description' => $meta['description'] ?? '', ':journal_date' => $journal_date, ':formula_id' => $formula_id, ':period' => $period, ':created_at' => $now, ':updated_at' => $now, ]); $this->insertLines((int)$this->pdo->lastInsertId(), $lines); } public function replaceManual(int $gl_id, string $reference, string $description, string $journal_date, string $period, array $lines): void { $this->assertPostingWindow($journal_date, 'Manual journal'); $now = date('Y-m-d H:i:s'); $sth = $this->pdo->prepare( "SELECT id, current_version, formula_id, history FROM td_gl WHERE company_id = :cid AND id = :gl_id AND source_type = 'manual' FOR UPDATE" ); $sth->execute([':cid' => $this->companyId, ':gl_id' => $gl_id]); $gl = $sth->fetch(PDO::FETCH_ASSOC); if (!$gl) throw new Exception("Manual GL entry #$gl_id not found."); $version = (int)$gl['current_version']; $history = json_decode($gl['history'], true) ?: []; $history[] = [ 'version' => $version, 'formula_id' => 0, 'replaced_at' => $now, 'lines' => $this->getLines($gl_id), ]; $this->pdo->prepare( "DELETE FROM td_gl_item WHERE company_id = :cid AND gl_id = :gl_id" )->execute([':cid' => $this->companyId, ':gl_id' => $gl_id]); $this->pdo->prepare( "UPDATE td_gl SET reference = :reference, description = :description, journal_date = :journal_date, period = :period, current_version = :current_version, history = :history, updated_at = :updated_at WHERE id = :id" )->execute([ ':reference' => $reference, ':description' => $description, ':journal_date' => $journal_date, ':period' => $period, ':current_version' => $version + 1, ':history' => json_encode($history), ':updated_at' => $now, ':id' => $gl_id, ]); $this->insertLines($gl_id, $lines); } public function postManual(string $reference, string $description, string $journal_date, string $period, array $lines): int { $this->assertPostingWindow($journal_date, 'Manual journal'); $now = date('Y-m-d H:i:s'); $sth = $this->pdo->prepare( "INSERT INTO td_gl (company_id, source_type, source_id, reference, description, journal_date, formula_id, period, current_version, history, created_at, updated_at) VALUES (:cid, 'manual', 0, :reference, :description, :journal_date, 0, :period, 1, '[]', :created_at, :updated_at)" ); $sth->execute([ ':cid' => $this->companyId, ':reference' => $reference, ':description' => $description, ':journal_date' => $journal_date, ':period' => $period, ':created_at' => $now, ':updated_at' => $now, ]); $gl_id = (int)$this->pdo->lastInsertId(); $this->insertLines($gl_id, $lines); return $gl_id; } public function replace(string $source_type, int $source_id, int $formula_id, string $period, array $lines, array $meta = []): void { $now = date('Y-m-d H:i:s'); $sth = $this->pdo->prepare( "SELECT id, current_version, formula_id, history, journal_date FROM td_gl WHERE company_id = :cid AND source_type = :source_type AND source_id = :source_id FOR UPDATE" ); $sth->execute([ ':cid' => $this->companyId, ':source_type' => $source_type, ':source_id' => $source_id, ]); $gl = $sth->fetch(PDO::FETCH_ASSOC); if (!$gl) throw new Exception("GL record not found for replace."); $journal_date = $this->resolveJournalDate($meta['journal_date'] ?? ($gl['journal_date'] ?? null), $period); $this->assertPostingWindow($journal_date, 'GL posting'); $gl_id = (int)$gl['id']; $version = (int)$gl['current_version']; $history = json_decode($gl['history'], true) ?: []; // Snapshot current lines into history $history[] = [ 'version' => $version, 'formula_id' => (int)$gl['formula_id'], 'replaced_at' => $now, 'lines' => $this->getLines($gl_id), ]; $this->pdo->prepare( "DELETE FROM td_gl_item WHERE company_id = :cid AND gl_id = :gl_id" )->execute([':cid' => $this->companyId, ':gl_id' => $gl_id]); $this->pdo->prepare( "UPDATE td_gl SET formula_id = :formula_id, journal_date = :journal_date, period = :period, current_version = :current_version, history = :history, updated_at = :updated_at WHERE id = :id" )->execute([ ':formula_id' => $formula_id, ':journal_date' => $journal_date, ':period' => $period, ':current_version' => $version + 1, ':history' => json_encode($history), ':updated_at' => $now, ':id' => $gl_id, ]); $this->insertLines($gl_id, $lines); } public function delete(string $source_type, int $source_id): void { $sth = $this->pdo->prepare( "SELECT id, journal_date, period FROM td_gl WHERE company_id = :cid AND source_type = :source_type AND source_id = :source_id LIMIT 1" ); $sth->execute([ ':cid' => $this->companyId, ':source_type' => $source_type, ':source_id' => $source_id, ]); $gl = $sth->fetch(PDO::FETCH_ASSOC); $gl_id = (int)($gl['id'] ?? 0); if (!$gl_id) return; $journal_date = $this->resolveJournalDate($gl['journal_date'] ?? null, (string)($gl['period'] ?? '')); $this->assertPostingWindow($journal_date, 'GL posting deletion'); $this->pdo->prepare( "DELETE FROM td_gl_item WHERE company_id = :cid AND gl_id = :gl_id" )->execute([':cid' => $this->companyId, ':gl_id' => $gl_id]); $this->pdo->prepare( "DELETE FROM td_gl WHERE id = :id AND company_id = :cid" )->execute([':id' => $gl_id, ':cid' => $this->companyId]); } // ── private ────────────────────────────────────────────────────────────── private function getLines(int $gl_id): array { $sth = $this->pdo->prepare( "SELECT account_code, department_id, debit, credit, description FROM td_gl_item WHERE company_id = :cid AND gl_id = :gl_id ORDER BY id ASC" ); $sth->execute([':cid' => $this->companyId, ':gl_id' => $gl_id]); return $sth->fetchAll(PDO::FETCH_ASSOC); } private function assertPostingWindow(?string $journal_date, string $context): void { if ($this->postingWindowGuard) { $this->postingWindowGuard->assertOpenDate($journal_date, $context); } } private function resolveJournalDate(?string $journal_date, string $period): string { $journal_date = trim((string)$journal_date); if ($journal_date !== '') { return $journal_date; } $period = trim($period); return preg_match('/^\d{4}-\d{2}$/', $period) ? "{$period}-01" : ''; } private function insertLines(int $gl_id, array $lines): void { $sth = $this->pdo->prepare( "INSERT INTO td_gl_item (company_id, gl_id, account_code, department_id, debit, credit, description) VALUES (:cid, :gl_id, :account_code, :dept_id, :debit, :credit, :description)" ); foreach ($lines as $line) { $sth->execute([ ':cid' => $this->companyId, ':gl_id' => $gl_id, ':account_code' => $line['account_code'], ':dept_id' => (int)($line['department_id'] ?? 0), ':debit' => (float)($line['debit'] ?? 0), ':credit' => (float)($line['credit'] ?? 0), ':description' => (string)($line['description'] ?? ''), ]); } } }