connection(); $accountsByCompany = []; foreach ($connection->table('fin_accounts')->get(['company_name', 'account_short', 'account_type']) as $account) { $company = trim((string) $account->company_name); $accountShort = trim((string) $account->account_short); if ($company === '' || $accountShort === '') { continue; } $accountsByCompany[$company][$accountShort] = [ 'account_short' => $accountShort, 'account_type' => trim((string) $account->account_type), ]; } $rows = $connection->table('fin_account_map') ->where('source', 'payable') ->where(function ($query): void { $query->whereNull('account_short')->orWhere('account_short', ''); }) ->orderBy('source_value') ->get(['id', 'source_value']); $candidates = []; $ambiguous = []; $missing = []; foreach ($rows as $row) { $company = trim((string) $row->source_value); $accounts = array_values($accountsByCompany[$company] ?? []); if ($accounts === []) { $missing[] = $company; continue; } if (count($accounts) === 1) { $candidates[] = $this->candidate($row, $accounts[0]['account_short'], 'single_account'); continue; } $publicAccounts = array_values(array_filter( $accounts, static fn (array $account): bool => $account['account_type'] === '公户', )); if (count($publicAccounts) === 1) { $candidates[] = $this->candidate($row, $publicAccounts[0]['account_short'], 'single_public_account'); continue; } $ambiguous[] = [ 'company' => $company, 'accounts' => array_values(array_map( static fn (array $account): string => $account['account_short'], $accounts, )), ]; } return [ 'total' => $rows->count(), 'eligible' => count($candidates), 'ambiguous' => count($ambiguous), 'missing' => count($missing), 'candidates' => $candidates, 'ambiguous_values' => $ambiguous, 'missing_values' => $missing, ]; } public function apply(?User $user = null): array { $preview = $this->preview(); $connection = $this->connection(); $userName = (string) ($user?->name ?? ''); $userId = (int) ($user?->id ?? 0); $now = now(); $mapped = 0; if ($preview['candidates'] !== []) { $connection->transaction(function () use ($connection, $preview, $userName, $userId, $now, &$mapped): void { foreach ($preview['candidates'] as $candidate) { $updated = $connection->table('fin_account_map') ->where('id', $candidate['id']) ->where(function ($query): void { $query->whereNull('account_short')->orWhere('account_short', ''); }) ->update([ 'account_short' => $candidate['account_short'], 'updated_by' => $userName, 'updated_at' => $now, ]); if ($updated !== 1) { continue; } $connection->table('fin_account_map_events')->insert([ 'source' => 'payable', 'source_value' => $candidate['source_value'], 'old_account_short' => null, 'new_account_short' => $candidate['account_short'], 'user_id' => $userId, 'user_name' => $userName, 'created_at' => $now, ]); $mapped++; } }); } if ($mapped > 0) { app(FinanceLedgerCalculator::class)->recalculate(); } return [ 'total' => $preview['total'], 'mapped' => $mapped, 'ambiguous' => $preview['ambiguous'], 'missing' => $preview['missing'], ]; } private function candidate(object $row, string $accountShort, string $matchType): array { return [ 'id' => (int) $row->id, 'source_value' => trim((string) $row->source_value), 'account_short' => $accountShort, 'match_type' => $matchType, ]; } private function connection() { return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror')); } }