CaiWuXiTong/modules/EcommerceFinance/Services/AccountMappingValidator.php

197 lines
7.7 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\Services;
/**
* 科目映射表校验。
*
* 三档含义(不是拍脑袋的严重度):
* 错误 = 会让程序生成凭证失败,或让账务数字直接错 → 必须改
* 警告 = 账能做、程序能跑,但口径不对 → 影响扣除、税负、可比性
* 提示 = 只是建议 → 做账时留意
*
* 校验只针对「一笔账单按某条规则生成分录」这一件事,不评价业务口径优劣。
*/
class AccountMappingValidator
{
public function __construct(private readonly AccountMappingCoa $coa)
{
}
/**
* @param array<int,array<string,mixed>> $rules
* @return array{issues:array<int,array<int,array{level:string,code:string,message:string}>>,summary:array<string,int>}
*/
public function validate(array $rules): array
{
$issues = [];
$dupIndex = [];
foreach ($rules as $rule) {
$dupIndex[implode('|', [
trim((string) ($rule['platform'] ?? '')),
trim((string) ($rule['item'] ?? '')),
])][] = (int) $rule['id'];
}
foreach ($rules as $rule) {
$id = (int) $rule['id'];
$list = [];
$list = array_merge($list, $this->accountIssues($rule, 'dAcct', 'dSub', '借方'));
$list = array_merge($list, $this->accountIssues($rule, 'cAcct', 'cSub', '贷方'));
// 方向基准
$basis = trim((string) ($rule['basis'] ?? ''));
$category = trim((string) ($rule['category'] ?? ''));
if ($basis === '') {
$list[] = $this->issue('错误', 'basis_missing', '没有填方向基准,程序无法判断该照抄还是互换。');
} elseif (! in_array($basis, AccountMappingStore::BASISES, true)) {
$list[] = $this->issue('错误', 'basis_invalid', "方向基准「{$basis}」不是有效值,只能是支出基准或收入基准。");
} elseif ($category !== '' && isset(AccountMappingStore::CATEGORY_BASIS[$category])) {
$expected = AccountMappingStore::CATEGORY_BASIS[$category];
if ($expected !== $basis) {
$list[] = $this->issue(
'警告',
'basis_mismatch',
"大类「{$category}」按口径应是「{$expected}」,这里填的是「{$basis}」,账单收支与基准不一致时方向会反。"
);
}
}
if ($category === '') {
$list[] = $this->issue('警告', 'category_missing', '没有填大类,方向基准失去参照,也无法按大类筛选。');
}
// 票据口径
$invoice = trim((string) ($rule['invoice'] ?? ''));
if ($invoice === '') {
$list[] = $this->issue('提示', 'invoice_missing', '票据口径为空,汇算清缴时不好判断调增项。');
} elseif (! in_array($invoice, AccountMappingStore::INVOICES, true)) {
$list[] = $this->issue('提示', 'invoice_unknown', "票据口径「{$invoice}」不在常用取值(0.06 / 不开票 / -)内。");
}
// 借贷两边同一个科目
$d = trim((string) ($rule['dAcct'] ?? ''));
$c = trim((string) ($rule['cAcct'] ?? ''));
$ds = trim((string) ($rule['dSub'] ?? ''));
$cs = trim((string) ($rule['cSub'] ?? ''));
if ($d !== '' && $d === $c && $ds === $cs) {
$list[] = $this->issue('警告', 'same_account', '借方和贷方是同一个科目,这笔业务不会有损益影响。');
}
// 同平台同项目重复
$pairKey = trim((string) ($rule['platform'] ?? '')) . '|' . trim((string) ($rule['item'] ?? ''));
if (count($dupIndex[$pairKey] ?? []) > 1) {
$others = array_values(array_diff($dupIndex[$pairKey], [$id]));
$list[] = $this->issue(
'提示',
'duplicate_pair',
'同一平台下这个账单项目还有第 ' . implode('、', $others) . ' 条规则;若单据上无法区分,程序会取先出现的那条。'
);
}
if ($list !== []) {
$issues[$id] = $list;
}
}
$errRows = 0;
$warnRows = 0;
$errCount = 0;
$warnCount = 0;
$tipCount = 0;
foreach ($issues as $list) {
$levels = array_column($list, 'level');
if (in_array('错误', $levels, true)) {
$errRows++;
} elseif (in_array('警告', $levels, true)) {
$warnRows++;
}
foreach ($levels as $level) {
if ($level === '错误') {
$errCount++;
} elseif ($level === '警告') {
$warnCount++;
} else {
$tipCount++;
}
}
}
$total = count($rules);
return [
'issues' => $issues,
'summary' => [
'total' => $total,
'error_rows' => $errRows,
'warning_rows' => $warnRows,
'ok_rows' => max($total - $errRows - $warnRows, 0),
'error_count' => $errCount,
'warning_count' => $warnCount,
'tip_count' => $tipCount,
],
];
}
/**
* @return array<int,array{level:string,code:string,message:string}>
*/
private function accountIssues(array $rule, string $acctField, string $subField, string $side): array
{
$list = [];
$acct = trim((string) ($rule[$acctField] ?? ''));
$sub = trim((string) ($rule[$subField] ?? ''));
if ($acct === '') {
$list[] = $this->issue('错误', 'account_missing', "{$side}科目为空,生成不了分录。");
return $list;
}
$base = $this->coa->toCode($acct);
if ($base === null) {
$anywhere = $this->coa->anywhereByName($acct);
$hint = $anywhere === [] ? '账套里没有这个名称的科目,需要先建科目或改成账套已有的一级科目。'
: '它只存在于 ' . implode('、', $anywhere) . ',不是一级科目,应放到对应的一级科目下面。';
$list[] = $this->issue('错误', 'account_unknown', "{$side}科目「{$acct}」在账套里找不到:{$hint}");
return $list;
}
if ($sub === '' || $sub === $acct) {
return $list;
}
$hits = $this->coa->resolve($acct, $sub);
if (count($hits) === 1) {
return $list;
}
if ($hits === []) {
$anywhere = $this->coa->anywhereByName($sub);
$hint = $anywhere === []
? '账套里没有这个名称,需要新建,或改用已有明细。'
: '它在 ' . implode('、', $anywhere) . ',不挂在「' . $acct . '」下面。';
$list[] = $this->issue('警告', 'sub_unknown', "{$side}二级科目「{$sub}」找不到:{$hint}");
return $list;
}
$list[] = $this->issue(
'警告',
'sub_ambiguous',
"{$side}二级科目「{$sub}」在「{$acct}」下有 " . count($hits) . ' 个(' . implode('、', $hits) . '),程序无法自动选,建议改写为编码。'
);
return $list;
}
/** @return array{level:string,code:string,message:string} */
private function issue(string $level, string $code, string $message): array
{
return ['level' => $level, 'code' => $code, 'message' => $message];
}
}