CaiWuXiTong/modules/EcommerceFinance/Services/AccountMappingValidator.php

228 lines
9.1 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'];
}
// 同一「平台 + 账单项目」下,用区分条件(业务备注 + 商品名称)分组:
// 条件相同的两条 = 真重复(提示);
// 条件不同的多条 = 按条件区分(正常,不报)。
$conditionIndex = [];
foreach ($rules as $rule) {
$conditionIndex[$this->pairKey($rule)][$this->conditionKey($rule)][] = (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) {
$sameCondition = array_values(array_diff($conditionIndex[$pairKey][$this->conditionKey($rule)] ?? [], [$id]));
if ($sameCondition !== []) {
$list[] = $this->issue(
'提示',
'duplicate_pair',
'同一平台、同一账单项目、区分条件也相同的第 ' . implode('、', $sameCondition) . ' 条规则属于重复,建议合并。'
);
}
if (trim((string) ($rule['memo'] ?? '')) === '' && trim((string) ($rule['goods'] ?? '')) === '') {
$list[] = $this->issue(
'提示',
'fallback_rule',
'这条没有区分条件,属于兜底规则:只有在同项目下其它带条件的规则都没命中时才会用它。'
);
}
}
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];
}
/** 平台 + 账单项目 */
private function pairKey(array $rule): string
{
return trim((string) ($rule['platform'] ?? '')) . '|' . trim((string) ($rule['item'] ?? ''));
}
/** 区分条件:业务备注关键词 + 商品名称关键词 */
private function conditionKey(array $rule): string
{
return trim((string) ($rule['memo'] ?? '')) . '|' . trim((string) ($rule['goods'] ?? ''));
}
}