ultimatepos/Modules/Accounting/Services/AccountingHealthService.php

208 lines
7.5 KiB
PHP

<?php
namespace Modules\Accounting\Services;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
class AccountingHealthService
{
public function assess(int $businessId): array
{
$checks = [];
$checks[] = $this->checkTable('accounting_accounts', $businessId, 'chart_of_accounts');
$checks[] = $this->checkGlActivity($businessId);
$checks[] = $this->checkPostingIntegrity($businessId);
$checks[] = $this->checkUnlinkedPaymentAccounts($businessId);
$checks[] = $this->checkFailedPostings($businessId);
$checks[] = $this->checkClosedPeriods($businessId);
$checks[] = $this->checkStaleExchangeRates($businessId);
$checks[] = $this->checkEntityTagging($businessId);
$checks[] = $this->checkOperationalMapping($businessId);
$score = 0;
foreach ($checks as $check) {
$score += $check['score'];
}
return [
'score' => min(100, $score),
'status' => $score >= 80 ? 'healthy' : ($score >= 50 ? 'watch' : 'critical'),
'checks' => $checks,
];
}
protected function checkTable(string $table, int $businessId, string $label): array
{
if (! Schema::hasTable($table)) {
return ['key' => $label, 'ok' => false, 'score' => 0, 'message' => "Missing table: {$table}"];
}
$count = DB::table($table)->where('business_id', $businessId)->count();
return [
'key' => $label,
'ok' => $count > 0,
'score' => $count > 0 ? 15 : 5,
'message' => $count > 0 ? "{$count} records" : 'No records yet',
];
}
protected function checkGlActivity(int $businessId): array
{
if (! Schema::hasTable('accounting_accounts_transactions')) {
return ['key' => 'gl_activity', 'ok' => false, 'score' => 0, 'message' => 'GL transactions table missing'];
}
$count = DB::table('accounting_accounts_transactions as aat')
->join('accounting_accounts as aa', 'aa.id', '=', 'aat.accounting_account_id')
->where('aa.business_id', $businessId)
->count();
return [
'key' => 'gl_activity',
'ok' => $count > 0,
'score' => $count > 0 ? 20 : 5,
'message' => "{$count} GL lines",
];
}
protected function checkPostingIntegrity(int $businessId): array
{
if (! Schema::hasTable('accounting_posting_logs')) {
return ['key' => 'posting_integrity', 'ok' => true, 'score' => 10, 'message' => 'Posting log not migrated yet'];
}
$duplicates = DB::table('accounting_posting_logs')
->where('business_id', $businessId)
->where('status', 'posted')
->select('posting_key', DB::raw('COUNT(*) as c'))
->groupBy('posting_key')
->having('c', '>', 1)
->count();
return [
'key' => 'posting_integrity',
'ok' => $duplicates === 0,
'score' => $duplicates === 0 ? 20 : 0,
'message' => $duplicates === 0 ? 'No duplicate postings' : "{$duplicates} duplicate posting keys",
];
}
protected function checkUnlinkedPaymentAccounts(int $businessId): array
{
if (! Schema::hasColumn('accounts', 'accounting_account_id')) {
return ['key' => 'payment_gl_link', 'ok' => true, 'score' => 10, 'message' => 'Link column not migrated yet'];
}
$unlinked = DB::table('accounts')
->where('business_id', $businessId)
->whereNull('accounting_account_id')
->count();
return [
'key' => 'payment_gl_link',
'ok' => $unlinked === 0,
'score' => $unlinked === 0 ? 15 : 5,
'message' => $unlinked === 0 ? 'All payment accounts linked' : "{$unlinked} unlinked payment accounts",
];
}
protected function checkFailedPostings(int $businessId): array
{
if (! Schema::hasTable('accounting_posting_logs')) {
return ['key' => 'failed_postings', 'ok' => true, 'score' => 10, 'message' => 'N/A'];
}
$failed = DB::table('accounting_posting_logs')
->where('business_id', $businessId)
->where('status', 'failed')
->count();
return [
'key' => 'failed_postings',
'ok' => $failed === 0,
'score' => $failed === 0 ? 10 : 0,
'message' => $failed === 0 ? 'No failed postings' : "{$failed} failed postings",
];
}
protected function checkClosedPeriods(int $businessId): array
{
if (! Schema::hasTable('accounting_fiscal_periods')) {
return ['key' => 'fiscal_periods', 'ok' => true, 'score' => 10, 'message' => 'Fiscal periods not migrated yet'];
}
$count = DB::table('accounting_fiscal_periods')->where('business_id', $businessId)->count();
return [
'key' => 'fiscal_periods',
'ok' => $count > 0,
'score' => $count > 0 ? 10 : 5,
'message' => "{$count} fiscal periods configured",
];
}
protected function checkStaleExchangeRates(int $businessId): array
{
if (! Schema::hasTable('accounting_exchange_rates') || ! class_exists(ExchangeRateService::class)) {
return ['key' => 'fx_rates', 'ok' => true, 'score' => 10, 'message' => 'FX table not available'];
}
$summary = app(ExchangeRateService::class)->staleRateSummary($businessId, 2);
return [
'key' => 'fx_rates',
'ok' => ($summary['total_issues'] ?? 0) === 0,
'score' => ($summary['total_issues'] ?? 0) === 0 ? 10 : 0,
'message' => ($summary['total_issues'] ?? 0) === 0
? 'Exchange rates are current'
: ($summary['total_issues'].' stale/missing FX pair(s)'),
];
}
protected function checkEntityTagging(int $businessId): array
{
if (! class_exists(HoldingEntityResolutionService::class)) {
return ['key' => 'entity_tagging', 'ok' => true, 'score' => 5, 'message' => 'N/A'];
}
$tagging = app(HoldingEntityResolutionService::class)->assessTagging($businessId);
if (($tagging['entities'] ?? 0) === 0) {
return ['key' => 'entity_tagging', 'ok' => true, 'score' => 5, 'message' => 'No holding entities'];
}
$ready = (bool) ($tagging['ready'] ?? false);
return [
'key' => 'entity_tagging',
'ok' => $ready,
'score' => $ready ? 10 : 0,
'message' => $ready
? ($tagging['tagged_pct'].'% GL lines tagged by entity')
: ($tagging['untagged_lines'].' untagged GL line(s)'),
];
}
protected function checkOperationalMapping(int $businessId): array
{
if (! class_exists(OperationalTransactionPostingService::class)) {
return ['key' => 'operational_mapping', 'ok' => true, 'score' => 5, 'message' => 'N/A'];
}
$gaps = app(OperationalTransactionPostingService::class)->assessGaps($businessId);
$total = (int) ($gaps['total_unmapped'] ?? 0);
return [
'key' => 'operational_mapping',
'ok' => $total === 0,
'score' => $total === 0 ? 10 : max(0, 10 - min(10, $total)),
'message' => $total === 0
? 'Operational transactions mapped ('.($gaps['unified_posting'] ? 'unified' : 'legacy').')'
: "{$total} unmapped operational transaction(s) in last 90 days",
];
}
}