204 lines
8.0 KiB
PHP
204 lines
8.0 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Business;
|
||
|
||
use Illuminate\Support\Facades\File;
|
||
use Modules\EcommerceFinance\Services\AccountLedgerRemarkMap;
|
||
use Modules\EcommerceFinance\Services\AccountMappingMatcher;
|
||
use Modules\EcommerceFinance\Services\AccountMappingStore;
|
||
use Modules\EcommerceFinance\Services\AccountMappingValidator;
|
||
use Tests\Feature\FeatureTestCase;
|
||
|
||
/**
|
||
* 科目映射表的「区分条件」匹配:业务备注 / 商品名称关键词。
|
||
*
|
||
* 数据同样落在 storage_path('app/account-mapping'),所以每个用例都把 storage 指到临时目录。
|
||
*/
|
||
class AccountMappingMatcherTest extends FeatureTestCase
|
||
{
|
||
private string $storageRoot = '';
|
||
|
||
private AccountMappingStore $store;
|
||
|
||
private AccountMappingMatcher $matcher;
|
||
|
||
protected function setUp(): void
|
||
{
|
||
parent::setUp();
|
||
|
||
$this->storageRoot = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'account-mapping-matcher-' . uniqid();
|
||
File::makeDirectory($this->storageRoot . '/app', 0777, true, true);
|
||
$this->app->useStoragePath($this->storageRoot);
|
||
|
||
$this->store = app(AccountMappingStore::class);
|
||
$this->matcher = app(AccountMappingMatcher::class);
|
||
}
|
||
|
||
protected function tearDown(): void
|
||
{
|
||
if ($this->storageRoot !== '' && File::isDirectory($this->storageRoot)) {
|
||
File::deleteDirectory($this->storageRoot);
|
||
}
|
||
|
||
parent::tearDown();
|
||
}
|
||
|
||
/** @param array<string,string> $values */
|
||
private function rule(array $values): array
|
||
{
|
||
return $this->store->create($values + [
|
||
'platform' => '测试平台',
|
||
'item' => '测试项目',
|
||
'category' => '费用',
|
||
'dAcct' => '销售费用',
|
||
'dSub' => '运费',
|
||
'cAcct' => '其他货币资金',
|
||
'cSub' => '',
|
||
'basis' => '支出基准',
|
||
]);
|
||
}
|
||
|
||
/** 同项目两条规则:带业务备注的优先于不带条件的兜底规则 */
|
||
public function test_specific_rule_wins_over_fallback(): void
|
||
{
|
||
$fallback = $this->rule(['item' => '在线支付']);
|
||
$erp = $this->rule(['item' => '在线支付', 'memo' => 'ERP软件费', 'dAcct' => '管理费用', 'dSub' => '软件服务费']);
|
||
|
||
$hit = $this->matcher->match('测试平台', '在线支付', ['ERP软件费'], '聚水潭ERP-专业版');
|
||
$this->assertNotNull($hit);
|
||
$this->assertTrue($hit['matched']);
|
||
$this->assertSame($erp['id'], $hit['rule']['id']);
|
||
|
||
$miss = $this->matcher->match('测试平台', '在线支付', ['推广充值'], 'P4P充值包');
|
||
$this->assertNotNull($miss);
|
||
$this->assertFalse($miss['matched']);
|
||
$this->assertSame($fallback['id'], $miss['rule']['id']);
|
||
$this->assertStringContainsString('兜底', $miss['reason']);
|
||
}
|
||
|
||
/** 商品名称关键词比业务备注更具体:两个都命中时取商品名称那条 */
|
||
public function test_goods_keyword_is_more_specific_than_remark(): void
|
||
{
|
||
$byMemo = $this->rule(['item' => '在线支付', 'memo' => '推广充值']);
|
||
$byGoods = $this->rule(['item' => '在线支付', 'memo' => '推广充值', 'goods' => 'P4P充值包']);
|
||
|
||
$hit = $this->matcher->match('测试平台', '在线支付', ['推广充值'], 'P4P充值包-2026年7月');
|
||
$this->assertNotNull($hit);
|
||
$this->assertSame($byGoods['id'], $hit['rule']['id']);
|
||
|
||
$other = $this->matcher->match('测试平台', '在线支付', ['推广充值'], '万相台充值');
|
||
$this->assertNotNull($other);
|
||
$this->assertSame($byMemo['id'], $other['rule']['id']);
|
||
}
|
||
|
||
/** 月份式备注(2607退款 / 202607订单)不参与匹配,否则规则会随月份膨胀 */
|
||
public function test_period_labels_do_not_match_business_remark(): void
|
||
{
|
||
$rule = $this->rule(['item' => '退款', 'memo' => '退款']);
|
||
|
||
$this->assertTrue($this->matcher->isPeriodLabel('2607退款'));
|
||
$this->assertTrue($this->matcher->isPeriodLabel('202607订单'));
|
||
|
||
$period = $this->matcher->match('测试平台', '退款', ['2607退款']);
|
||
$this->assertNotNull($period);
|
||
$this->assertFalse($period['matched']);
|
||
|
||
$business = $this->matcher->match('测试平台', '退款', ['退款']);
|
||
$this->assertNotNull($business);
|
||
$this->assertTrue($business['matched']);
|
||
$this->assertSame($rule['id'], $business['rule']['id']);
|
||
}
|
||
|
||
/** 商品名称关键词支持空格分隔的多个词,全部命中才算 */
|
||
public function test_goods_keyword_supports_multiple_terms(): void
|
||
{
|
||
$rule = $this->rule(['item' => '在线支付', 'goods' => '聚水潭 ERP']);
|
||
|
||
$hit = $this->matcher->match('测试平台', '在线支付', [], '聚水潭ERP-专业版-订单付款');
|
||
$this->assertNotNull($hit);
|
||
$this->assertTrue($hit['matched']);
|
||
$this->assertSame($rule['id'], $hit['rule']['id']);
|
||
|
||
$miss = $this->matcher->match('测试平台', '在线支付', [], '聚水潭-专业版');
|
||
$this->assertNotNull($miss);
|
||
$this->assertFalse($miss['matched']);
|
||
}
|
||
|
||
public function test_performance_fee_item_uses_the_matched_rule_fee_memo(): void
|
||
{
|
||
$rule = $this->rule([
|
||
'platform' => '京东',
|
||
'item' => '平台服务费',
|
||
'memo' => '平台服务费',
|
||
'feeMemo' => '平台服务费',
|
||
'category' => '费用',
|
||
]);
|
||
|
||
$matched = app(AccountLedgerRemarkMap::class)->accountMappingMatch(
|
||
'京东-测试店铺',
|
||
'jd-transactions',
|
||
'平台服务费',
|
||
);
|
||
|
||
$this->assertNotNull($matched);
|
||
$this->assertTrue($matched['matched']);
|
||
$this->assertSame($rule['id'], $matched['rule']['id']);
|
||
$this->assertSame('平台服务费', $matched['rule']['feeMemo']);
|
||
}
|
||
|
||
/** 校验:同项目、条件不同 = 正常;条件相同 = 才算重复 */
|
||
public function test_validator_flags_only_same_condition_pairs(): void
|
||
{
|
||
$first = $this->rule(['item' => '交易分账_货款', 'memo' => '订单收入']);
|
||
$second = $this->rule(['item' => '交易分账_货款', 'memo' => '退款']);
|
||
|
||
$this->assertNotContains('duplicate_pair', $this->issueCodes([$first['id'], $second['id']]), '条件不同的两条不该报重复');
|
||
|
||
$third = $this->rule(['item' => '交易分账_货款', 'memo' => '退款']);
|
||
|
||
$this->assertContains('duplicate_pair', $this->issueCodes([$second['id'], $third['id']]), '条件相同的两条要报重复');
|
||
}
|
||
|
||
/** 「-」表示不限:存进去按空处理,匹配时不会拿它当关键词去找 */
|
||
public function test_dash_means_no_condition(): void
|
||
{
|
||
$rule = $this->rule(['item' => '在线支付', 'memo' => '-', 'goods' => '-']);
|
||
|
||
$this->assertSame('', $rule['memo']);
|
||
$this->assertSame('', $rule['goods']);
|
||
|
||
$hit = $this->matcher->match('测试平台', '在线支付', ['推广充值'], 'P4P充值包');
|
||
$this->assertNotNull($hit);
|
||
$this->assertFalse($hit['matched']);
|
||
$this->assertSame($rule['id'], $hit['rule']['id']);
|
||
}
|
||
|
||
/** 校验:同项目下有兜底规则时给出提示,提醒它只做兜底 */
|
||
public function test_validator_hints_fallback_rule(): void
|
||
{
|
||
$fallback = $this->rule(['item' => '在线支付']);
|
||
$specific = $this->rule(['item' => '在线支付', 'memo' => 'ERP软件费']);
|
||
|
||
$codes = $this->issueCodes([$fallback['id']]);
|
||
$this->assertContains('fallback_rule', $codes);
|
||
$this->assertNotContains('duplicate_pair', $codes);
|
||
|
||
$this->assertNotContains('duplicate_pair', $this->issueCodes([$specific['id']]));
|
||
}
|
||
|
||
/** @param array<int,int> $ids @return array<int,string> */
|
||
private function issueCodes(array $ids): array
|
||
{
|
||
$result = app(AccountMappingValidator::class)->validate($this->store->all());
|
||
|
||
$codes = [];
|
||
foreach ($ids as $id) {
|
||
foreach ($result['issues'][$id] ?? [] as $issue) {
|
||
$codes[] = $issue['code'];
|
||
}
|
||
}
|
||
|
||
return $codes;
|
||
}
|
||
}
|