330 lines
13 KiB
PHP
330 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* GlManager
|
|
*
|
|
* Owns all read/write operations on td_gl and td_gl_item.
|
|
* Called by PostingEngine after building the journal lines.
|
|
*
|
|
* Lifecycle:
|
|
* post() — first-time GL creation; throws if a record already exists.
|
|
* replace() — snapshot current lines into td_gl.history, then delete + re-insert.
|
|
* delete() — create a reversal journal entry (debit/credit swapped), then mark the original
|
|
* source_type = 'voided' so it stays in the table for audit trail but is invisible
|
|
* to getBySource() lookups.
|
|
*/
|
|
class GlManager
|
|
{
|
|
private PDO $pdo;
|
|
private int $companyId;
|
|
private ?PostingWindowGuard $postingWindowGuard;
|
|
|
|
public function __construct(PDO $pdo, int $company_id, ?PostingWindowGuard $postingWindowGuard = null)
|
|
{
|
|
$this->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, reference, description, journal_date, period, formula_id
|
|
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 void');
|
|
|
|
$original_lines = $this->getLines($gl_id);
|
|
|
|
if (!empty($original_lines)) {
|
|
$reversal_lines = array_map(fn($line) => [
|
|
'account_code' => $line['account_code'],
|
|
'department_id' => (int)$line['department_id'],
|
|
'debit' => (float)$line['credit'],
|
|
'credit' => (float)$line['debit'],
|
|
'description' => 'VOID: ' . $line['description'],
|
|
], $original_lines);
|
|
|
|
$now = date('Y-m-d H:i:s');
|
|
$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, 'reversal', :source_id, :reference, :description, :journal_date,
|
|
:formula_id, :period, 1, '[]', :created_at, :updated_at)"
|
|
)->execute([
|
|
':cid' => $this->companyId,
|
|
':source_id' => $gl_id,
|
|
':reference' => 'VOID/' . ($gl['reference'] ?? ''),
|
|
':description' => 'Void: ' . ($gl['description'] ?? ''),
|
|
':journal_date' => $journal_date,
|
|
':formula_id' => (int)($gl['formula_id'] ?? 0),
|
|
':period' => $gl['period'] ?? '',
|
|
':created_at' => $now,
|
|
':updated_at' => $now,
|
|
]);
|
|
$this->insertLines((int)$this->pdo->lastInsertId(), $reversal_lines);
|
|
}
|
|
|
|
// Mark original as voided — keeps audit trail; getBySource() won't match 'voided' source_type
|
|
$this->pdo->prepare(
|
|
"UPDATE td_gl SET source_type = 'voided', updated_at = NOW()
|
|
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'] ?? ''),
|
|
]);
|
|
}
|
|
}
|
|
}
|