assertSame(['mode' => Rule::MODE_NONE, 'effective_from' => null, 'effective_to' => null], Rule::parseRuleText('不记账')); $this->assertSame(Rule::MODE_NORMAL, Rule::parseRuleText('正常记账')['mode']); $this->assertSame(Rule::MODE_DROPSHIP_WRONG, Rule::parseRuleText('代发仓发货错需记账,其他原因均不记账')['mode']); $this->assertSame(Rule::MODE_DROPSHIP_WRONG, Rule::parseRuleText('厂家暂未出货,后期若建立代发仓,发货错需记账,其他原因均不记账')['mode']); $this->assertSame(['mode' => Rule::MODE_NONE, 'effective_from' => '2025-03-01', 'effective_to' => '2026-02-28'], Rule::parseRuleText('不记账(2025.3月-2026.2月)')); $this->assertSame(['mode' => Rule::MODE_NONE, 'effective_from' => '2025-03-01', 'effective_to' => '2027-12-30'], Rule::parseRuleText('不记账(2025.3月-2027.12.30)')); $this->assertSame(Rule::MODE_DROPSHIP_WRONG, Rule::parseRuleText('仅代发仓发错货记账')['mode']); } public function test_rejects_unknown_rule_text(): void { $this->expectException(InvalidArgumentException::class); Rule::parseRuleText('看情况'); } private function row(array $patch = []): array { return array_replace([ 'started_at' => '2026-09-05 10:00:00', 'reason_level_1' => '厂家记账', 'reason_level_2' => '孔位错误', 'shipping_warehouse' => '常州主仓', 'supplier' => '甲厂', ], $patch); } private function configs(): array { return [ '甲厂' => ['mode' => Rule::MODE_NORMAL, 'effective_from' => null, 'effective_to' => null], '乙厂' => ['mode' => Rule::MODE_DROPSHIP_WRONG, 'effective_from' => null, 'effective_to' => null], '丙厂' => ['mode' => Rule::MODE_NONE, 'effective_from' => null, 'effective_to' => null], '丁厂' => ['mode' => Rule::MODE_NONE, 'effective_from' => '2025-03-01', 'effective_to' => '2026-02-28'], ]; } private function judge(array $patch): string { return Rule::decide($this->row($patch), $this->configs())['deduction_status']; } public function test_supplier_reasons_follow_supplier_config(): void { $this->assertSame(Rule::STATUS_DEDUCT, $this->judge([])); $this->assertSame(Rule::STATUS_SKIP, $this->judge(['supplier' => '乙厂'])); $this->assertSame(Rule::STATUS_SKIP, $this->judge(['supplier' => '丙厂'])); // 配置表中没有的供应商视同正常记账,可扣款。 $this->assertSame(Rule::STATUS_DEDUCT, $this->judge(['supplier' => '未知厂'])); $this->assertSame(Rule::STATUS_REVIEW, $this->judge(['supplier' => ''])); // 限期不记账:期外视同不在配置表,可扣款。 $this->assertSame(Rule::STATUS_DEDUCT, $this->judge(['supplier' => '丁厂'])); $this->assertSame(Rule::STATUS_SKIP, $this->judge(['supplier' => '丁厂', 'started_at' => '2026-02-28 23:00:00'])); } public function test_warehouse_reasons_only_deduct_dropship_wrong_shipment(): void { $wrong = ['reason_level_1' => '仓库记账', 'reason_level_2' => '仓库发错货(面单照片,实物照片,条形码照片)', 'shipping_warehouse' => '曹县洪耀代发仓']; $this->assertSame(Rule::STATUS_DEDUCT, $this->judge($wrong + ['supplier' => '乙厂'])); $this->assertSame(Rule::STATUS_DEDUCT, $this->judge($wrong)); $this->assertSame(Rule::STATUS_SKIP, $this->judge($wrong + ['supplier' => '丙厂'])); $this->assertSame(Rule::STATUS_DEDUCT, $this->judge($wrong + ['supplier' => '未知厂'])); // 非代发仓、或代发仓的其他仓库记账原因:一律不作为厂家扣款项,无需配置。 $this->assertSame(Rule::STATUS_SKIP, $this->judge(['shipping_warehouse' => '常州主仓'] + $wrong + ['supplier' => '乙厂'])); $this->assertSame(Rule::STATUS_SKIP, $this->judge(['reason_level_2' => '仓库双面单'] + $wrong + ['supplier' => '乙厂'])); $this->assertSame(Rule::STATUS_SKIP, $this->judge(['reason_level_2' => '退货运费'] + $wrong + ['supplier' => '未知厂'])); } public function test_multiple_suppliers_need_review_when_outcomes_differ(): void { $this->assertSame(Rule::STATUS_DEDUCT, $this->judge(['supplier' => "甲厂\n甲厂"])); $result = Rule::decide($this->row(['supplier' => "甲厂\n丙厂"]), $this->configs()); $this->assertSame(Rule::STATUS_REVIEW, $result['deduction_status']); $this->assertStringContainsString('甲厂:扣款', $result['deduction_basis']); $this->assertSame("正常记账\n不记账", $result['supplier_config']); } public function test_small_compensation_uses_bookkeeping_divided_by_buyer_paid_and_preserves_notes(): void { foreach (['补偿款项', '记账全款', '记账全额'] as $type) { $row = $this->row(['bookkeeping_type' => $type, 'amount' => '30.00', 'buyer_paid_amount' => '100', 'remark' => '协商补偿']); $result = Rule::decide($row, $this->configs()); $this->assertSame('0.00', $result['amount']); $this->assertSame(Rule::STATUS_SKIP, $result['deduction_status']); $this->assertSame("协商补偿\n小于50且小于0.4,记账改0", $result['remark']); $this->assertSame('30.00', $row['amount']); } $multiple = Rule::decide($this->row(['bookkeeping_type' => '补偿款项', 'amount' => '30.00', 'buyer_paid_amount' => "40\n60"]), []); $this->assertSame('0.00', $multiple['amount']); } public function test_small_compensation_requires_both_strict_limits_and_complete_positive_paid_amount(): void { foreach ([ ['50.00', '200'], ['49.99', '124.975'], ['40.00', '100'], ['39.99', '99'], ['30.00', '10'], ['30.00', ''], ['30.00', '0'], ['30.00', '-100'], ['30.00', "100\n"], ['30.00', "\n100"], ['30.00', 'invalid'], ['0.00', '100'], ['-1.00', '100'], ['', '100'], ] as [$amount, $paid]) { $result = Rule::decide($this->row(['bookkeeping_type' => '补偿款项', 'amount' => $amount, 'buyer_paid_amount' => $paid]), $this->configs()); $this->assertArrayNotHasKey('amount', $result, "$amount / $paid"); } $hit = Rule::decide($this->row(['bookkeeping_type' => '补偿款项', 'amount' => '39.99', 'buyer_paid_amount' => '100']), $this->configs()); $this->assertSame('0.00', $hit['amount']); $freight = Rule::decide($this->row(['bookkeeping_type' => '运费损耗', 'amount' => '10.00', 'buyer_paid_amount' => '100']), $this->configs()); $this->assertArrayNotHasKey('amount', $freight); } public function test_second_round_uses_order_actual_paid_when_buyer_paid_round_does_not_hit(): void { foreach (['10', '', '0', "100\n"] as $buyer) { $result = Rule::decide($this->row(['bookkeeping_type' => '补偿款项', 'amount' => '30.00', 'buyer_paid_amount' => $buyer, 'actual_paid_amount' => '100.00']), $this->configs()); $this->assertSame('0.00', $result['amount']); $this->assertSame(Rule::STATUS_SKIP, $result['deduction_status']); } $equal = Rule::decide($this->row(['bookkeeping_type' => '记账全款', 'amount' => '40.00', 'buyer_paid_amount' => '50', 'actual_paid_amount' => '100.00']), $this->configs()); $this->assertArrayNotHasKey('amount', $equal); } }