'Asset cost account', 'accum_account_code' => 'Accumulated depreciation / amortization account', 'expense_account_code' => 'Depreciation / amortization expense account', 'gain_account_code' => 'Gain on disposal account', 'loss_account_code' => 'Loss on disposal account', ]; private PDO $pdo; private int $companyId; public function __construct(PDO $pdo, int $company_id) { $this->pdo = $pdo; $this->companyId = $company_id; } public static function isPostingAccount(PDO $pdo, int $company_id, string $account_code): bool { if ($account_code === '') return false; $sth = $pdo->prepare( "SELECT COUNT(*) FROM md_account WHERE company_id = :cid AND account_code = :code AND is_posting = 1 AND status = 1" ); $sth->execute([':cid' => $company_id, ':code' => $account_code]); return (int)$sth->fetchColumn() > 0; } public function getAll(): array { $sth = $this->pdo->prepare( "SELECT c.*, COUNT(a.id) AS asset_count FROM md_asset_category c LEFT JOIN td_asset a ON a.company_id = c.company_id AND a.category_id = c.id AND a.status <> 4 WHERE c.company_id = :cid GROUP BY c.id ORDER BY c.category_code ASC" ); $sth->execute([':cid' => $this->companyId]); return $sth->fetchAll(PDO::FETCH_ASSOC); } public function getById(int $id): ?array { $sth = $this->pdo->prepare( "SELECT * FROM md_asset_category WHERE company_id = :cid AND id = :id LIMIT 1" ); $sth->execute([':cid' => $this->companyId, ':id' => $id]); return $sth->fetch(PDO::FETCH_ASSOC) ?: null; } public function save(array $data): int { $id = (int)($data['id'] ?? 0); $code = strtoupper(trim((string)($data['category_code'] ?? ''))); $name = trim((string)($data['category_name'] ?? '')); $class = trim((string)($data['asset_class'] ?? '')); $life = (int)($data['useful_life_months'] ?? 0); if ($code === '' || $name === '') throw new Exception('Category code and name are required.'); if (!in_array($class, self::CLASSES, true)) throw new Exception('Select tangible or intangible.'); if ($life < 1 || $life > 1200) throw new Exception('Default useful life must be between 1 and 1200 months.'); $accounts = []; foreach (self::ACCOUNT_FIELDS as $field => $label) { $account_code = trim((string)($data[$field] ?? '')); if ($account_code === '') throw new Exception("{$label} is required."); if (!self::isPostingAccount($this->pdo, $this->companyId, $account_code)) { throw new Exception("{$label} {$account_code} is not an active posting account."); } $accounts[$field] = $account_code; } $sth = $this->pdo->prepare( "SELECT COUNT(*) FROM md_asset_category WHERE company_id = :cid AND category_code = :code AND id <> :id" ); $sth->execute([':cid' => $this->companyId, ':code' => $code, ':id' => $id]); if ((int)$sth->fetchColumn() > 0) throw new Exception("Category code {$code} already exists."); $params = array_merge([ ':cid' => $this->companyId, ':code' => $code, ':name' => $name, ':class' => $class, ':life' => $life, ':desc' => trim((string)($data['description'] ?? '')), ':status' => (int)($data['status'] ?? 1) === 1 ? 1 : 0, ], [ ':cost_acc' => $accounts['cost_account_code'], ':accum_acc' => $accounts['accum_account_code'], ':expense_acc' => $accounts['expense_account_code'], ':gain_acc' => $accounts['gain_account_code'], ':loss_acc' => $accounts['loss_account_code'], ]); if ($id) { $existing = $this->getById($id); if (!$existing) throw new Exception('Category not found.'); if ($existing['asset_class'] !== $class && $this->countAssets($id) > 0) { throw new Exception('The asset class cannot change once assets use this category.'); } $params[':id'] = $id; $this->pdo->prepare( "UPDATE md_asset_category SET category_code = :code, category_name = :name, asset_class = :class, useful_life_months = :life, description = :desc, status = :status, cost_account_code = :cost_acc, accum_account_code = :accum_acc, expense_account_code = :expense_acc, gain_account_code = :gain_acc, loss_account_code = :loss_acc, updated_at = NOW() WHERE company_id = :cid AND id = :id" )->execute($params); return $id; } $this->pdo->prepare( "INSERT INTO md_asset_category (company_id, category_code, category_name, asset_class, useful_life_months, description, status, cost_account_code, accum_account_code, expense_account_code, gain_account_code, loss_account_code, created_at) VALUES (:cid, :code, :name, :class, :life, :desc, :status, :cost_acc, :accum_acc, :expense_acc, :gain_acc, :loss_acc, NOW())" )->execute($params); return (int)$this->pdo->lastInsertId(); } /** Categories are only deactivated: existing assets keep their accounts. */ public function deactivate(int $id): void { $this->pdo->prepare( "UPDATE md_asset_category SET status = 0, updated_at = NOW() WHERE company_id = :cid AND id = :id" )->execute([':cid' => $this->companyId, ':id' => $id]); } public function getStats(): array { $sth = $this->pdo->prepare( "SELECT COUNT(*) AS total, COALESCE(SUM(status = 1), 0) AS active, COALESCE(SUM(status = 0), 0) AS inactive FROM md_asset_category WHERE company_id = :cid" ); $sth->execute([':cid' => $this->companyId]); return $sth->fetch(PDO::FETCH_ASSOC) ?: []; } private function countAssets(int $category_id): int { $sth = $this->pdo->prepare( "SELECT COUNT(*) FROM td_asset WHERE company_id = :cid AND category_id = :id AND status <> 4" ); $sth->execute([':cid' => $this->companyId, ':id' => $category_id]); return (int)$sth->fetchColumn(); } }