CaiWuXiTong/tests/Unit/EcommerceFinance/PlantingReimbursementAuditorTest.php
limeng f78149e630 feat(种菜报销): 接入钉钉群机器人(取消通知 @ 发起人、每天 10:25 推送汇总),看板职级为空改为取消 (#1150)
- 工作台新增「钉钉机器人设置」:Webhook、加签密钥加密存公司设置,页面不回显;支持自定义关键词、汇总单独发群、@ 报销人花名
- 核对后点「发取消通知到钉钉群」:按发起人一人一条,按钉钉花名册(花名 → userid)@ 发起人;同一批已发过的不重复发,可勾选重发
- 新增 planting-reimbursement:daily-digest,每天 10:25(宿主机 cron)生成当天批次,把待核对条数/金额、可报销合计、取消/待复查数、自检结果和工作台链接推到群里;数据停更或看板未上传会提醒
- 种菜监控看板提示「职级为空」的单改为取消工单,群通知原因写「职级为空」(2026-11-13 报销人确认)
- 没上传花名册时,收款人在职改按钉钉花名册在职真名判断;两边都取不到时不做该项校验并提示,避免误判
涉及:只读聚工单镜像与钉钉花名册副本;钉钉群机器人发消息;不涉及迁移,不写聚工单。

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VANWYsNUbu2piMtCKymoyd
2026-11-13 11:15:04 +08:00

171 lines
9.6 KiB
PHP

<?php
namespace Tests\Unit\EcommerceFinance;
use Modules\EcommerceFinance\Services\PlantingReimbursementAuditor as Auditor;
use PHPUnit\Framework\TestCase;
class PlantingReimbursementAuditorTest extends TestCase
{
private function row(string $orderNo, float $amount, array $patch = []): array
{
return array_replace([
'work_order_no' => 'GD' . $orderNo, 'online_order_no' => $orderNo, 'shop_name' => '京喜-星光闪闪京喜自营工厂店',
'initiator' => '星璃', 'payee_name' => '王敏佳', 'payee_account' => '6214000011112222', 'payee_bank' => '招商银行',
'payee_mobile' => '13800000000', 'task_payment' => (string) $amount, 'task_freight' => '0.00', 'task_commission' => '0.00',
'jingxi_price_diff' => '0.00', 'task_deduction' => '0.00', 'task_empty_package_fee' => '0.00',
'reimbursement_amount' => (string) $amount, 'actual_paid_amount' => (string) $amount, 'erp_order_label' => '',
'paid_at' => '2026-11-11 10:00:00', 'status' => '处理中',
], $patch);
}
private function audit(array $rows, array $refs = []): array
{
return (new Auditor())->audit($rows, $refs + [
'reimbursed' => [],
'commission_people' => ['白菜' => true],
'roster' => ['王敏佳' => true, '姚佳炜' => true, '李润' => true],
'roster_exceptions' => ['柴嘉伟(建行)', '柴嘉伟'],
'profit_centers' => ['京喜-星光闪闪京喜自营工厂店' => '运营中心A', '天猫-欧意朗旗舰店' => '运营中心A', '天猫-HX旗舰店101' => '运营中心G'],
'monitor' => null,
], '2026-11-12');
}
private function decisions(array $result): array
{
return array_column($result['rows'], 'decision', 'online_order_no');
}
public function test_same_day_daily_over_limit_keeps_orders_by_paid_time_until_limit(): void
{
$rows = [];
$monitor = [];
foreach ([1, 2, 3, 4, 5] as $i) {
$rows[] = $this->row("10{$i}", 300, ['paid_at' => "2026-11-11 1{$i}:00:00"]);
$monitor["10{$i}"] = ['risk' => '超额', 'hint' => '当日累计 1500.00 超过上限 800.00', 'risk_type' => '每日超额', 'owner' => '星璃', 'daily_limit' => '800'];
}
$result = $this->audit($rows, ['monitor' => $monitor]);
$this->assertSame(['101' => 'pass', '102' => 'pass', '103' => 'cancel', '104' => 'cancel', '105' => 'cancel'], $this->decisions($result));
$this->assertSame('600.00', $result['summary']['total']);
$this->assertSame(3, $result['summary']['counts']['cancel']);
$this->assertStringContainsString('@星璃 以上订单本次不予报销', $result['summary']['notices'][0]['message']);
}
public function test_over_limit_on_different_days_cancels_all(): void
{
$monitor = [
'1' => ['risk' => '超额', 'risk_type' => '每日超额', 'owner' => '星璃', 'daily_limit' => '800'],
'2' => ['risk' => '超额', 'risk_type' => '每日超额', 'owner' => '星璃', 'daily_limit' => '800'],
];
$result = $this->audit([
$this->row('1', 100, ['paid_at' => '2026-11-10 09:00:00']),
$this->row('2', 100, ['paid_at' => '2026-11-11 09:00:00']),
], ['monitor' => $monitor]);
$this->assertSame(['1' => 'cancel', '2' => 'cancel'], $this->decisions($result));
}
public function test_rule_failures_cancel_and_exceptions_pass(): void
{
$result = $this->audit([
$this->row('201', 100, ['shop_name' => '天猫-欧意朗旗舰店', 'initiator' => '白菜', 'payee_name' => '姚佳炜']),
$this->row('202', 100, ['initiator' => '观海', 'task_commission' => '5.00', 'reimbursement_amount' => '105.00']),
$this->row('203', 89.5, ['shop_name' => '天猫-欧意朗旗舰店', 'erp_order_label' => '工单同步,以旧换新', 'actual_paid_amount' => '50.00']),
$this->row('204', 10, ['shop_name' => '天猫-HX旗舰店101', 'payee_name' => '柴嘉伟(建行)']),
$this->row('205', 10, ['payee_name' => '离职的人']),
$this->row('206', 10, ['reimbursement_amount' => '12.00']),
$this->row('207', 10, ['work_order_no' => 'GD207A']),
$this->row('207', 10, ['work_order_no' => 'GD207B']),
$this->row('208', 10, ['shop_name' => '拼多多-新店']),
$this->row('209', 120, ['shop_name' => '京东-某店', 'erp_order_label' => '以旧换新', 'actual_paid_amount' => '50.00']),
], ['reimbursed' => ['201' => '2026-11-03']]);
$rows = [];
foreach ($result['rows'] as $row) {
$rows[$row['work_order_no']] = $row;
}
$this->assertSame('cancel', $rows['GD201']['decision']);
$this->assertStringContainsString('2026-11-03', $rows['GD201']['issue_text']);
$this->assertSame('cancel', $rows['GD202']['decision']);
$this->assertStringContainsString('报销佣金人员', $rows['GD202']['issue_text']);
$this->assertSame('cancel', $rows['GD203']['decision']);
$this->assertStringContainsString('买家实付', $rows['GD203']['issue_text']);
$this->assertSame('pass', $rows['GD204']['decision']);
$this->assertSame('cancel', $rows['GD205']['decision']);
$this->assertSame('cancel', $rows['GD206']['decision']);
$this->assertSame('pass', $rows['GD207A']['decision']);
$this->assertSame('cancel', $rows['GD207B']['decision']);
// 查不到利润中心只提示,不取消。
$this->assertSame('pass', $rows['GD208']['decision']);
$this->assertStringContainsString('利润中心', $rows['GD208']['issue_text']);
// 以旧换新只校验天猫、淘宝店。
$this->assertSame('pass', $rows['GD209']['decision']);
}
public function test_trade_in_uses_lowest_buyer_paid_amount(): void
{
$result = $this->audit([
$this->row('3317016073022051663', 89.5, ['shop_name' => '天猫-欧意朗旗舰店', 'erp_order_label' => '工单同步,以旧换新·国家补贴', 'actual_paid_amount' => '89.50']),
], ['monitor' => ['3317016073022051663' => ['risk' => '正常', 'order_paid' => '76.08']]]);
$this->assertSame('cancel', $result['rows'][0]['decision']);
$this->assertStringContainsString('76.08', $result['rows'][0]['issue_text']);
// 工单里标签为空、买家实付更低 → 待复查,去聚水潭确认。
$missing = $this->audit([
$this->row('1', 89.5, ['shop_name' => '天猫-欧意朗旗舰店', 'erp_order_label' => '', 'buyer_paid_amount' => '76.08']),
$this->row('2', 89.5, ['shop_name' => '天猫-欧意朗旗舰店', 'erp_order_label' => '工单同步', 'buyer_paid_amount' => '76.08']),
]);
$this->assertSame(['1' => 'recheck', '2' => 'pass'], $this->decisions($missing));
}
public function test_missing_roster_skips_payee_check(): void
{
$result = $this->audit([$this->row('1', 10, ['payee_name' => '任何人'])], ['roster' => null]);
$this->assertSame(['1' => 'pass'], $this->decisions($result));
}
public function test_monitor_missing_is_recheck(): void
{
$result = $this->audit([$this->row('1', 10), $this->row('2', 10)], ['monitor' => ['1' => ['risk' => '正常']]]);
$this->assertSame(['1' => 'pass', '2' => 'recheck'], $this->decisions($result));
// 看板提示职级为空 → 取消,原因写明职级为空。
$rank = $this->audit([$this->row('3', 10)], ['monitor' => ['3' => ['risk' => '待补', 'risk_type' => '职级为空', 'hint' => '职级为空,暂不判断平台规则', 'owner' => '星璃']]]);
$this->assertSame('cancel', $rank['rows'][0]['decision']);
$this->assertStringContainsString('职级为空', $rank['summary']['notices'][0]['message']);
$this->assertSame(1, $result['summary']['counts']['recheck']);
}
public function test_summary_groups_ding_lines_cards_and_pivots(): void
{
$result = $this->audit([
$this->row('1', 60, ['shop_name' => '天猫-欧意朗旗舰店', 'initiator' => '白菜', 'payee_name' => '姚佳炜']),
$this->row('2', 40.5, ['shop_name' => '天猫-欧意朗旗舰店', 'initiator' => '白菜', 'payee_name' => '姚佳炜']),
$this->row('3', 10, ['shop_name' => '天猫-HX旗舰店101', 'initiator' => '观海', 'payee_name' => '李润']),
$this->row('4', 20, ['initiator' => '观海', 'payee_name' => '李润']),
]);
$summary = $result['summary'];
$this->assertSame('2026-11-11', $summary['bill_date']);
$this->assertSame('11.11种菜报销', $summary['purpose']);
$this->assertSame('100.50', $summary['group_amounts']['三大天猫']);
$this->assertSame('30.00', $summary['group_amounts']['其他']);
$this->assertSame('130.50', $summary['total']);
$this->assertSame([['运营中心A', '欧意朗', 10050], ['运营中心A', '其他', 2000], ['运营中心G', '其他', 1000]], array_map(
static fn (array $line): array => [$line['department'], $line['shop'], $line['cents']],
$summary['ding_lines'],
));
$this->assertCount(1, $summary['pivots']['三大天猫']);
$this->assertCount(2, $summary['pivots']['其他']);
$this->assertCount(2, $summary['cards']['其他']);
$this->assertSame('招商银行', $summary['cards']['三大天猫'][0]['payee_bank']);
$this->assertSame(['count' => 4, 'amount' => '130.50'], $summary['source']);
$this->assertSame([true, true, true, true, true, true], array_column($summary['checks'], 'ok'));
}
}