CaiWuXiTong/tests/Unit/AccessoryReshipPageDataTest.php

122 lines
8.6 KiB
PHP

<?php
namespace Tests\Unit;
use Illuminate\Container\Container;
use Illuminate\Http\Request;
use Modules\EcommerceFinance\Repositories\ShippingOrderRepository;
use Modules\EcommerceFinance\Services\AccessoryReshipMatcher;
use Modules\EcommerceFinance\View\AccessoryReshipPageData;
use Modules\EcommerceFinance\Services\AccessoryReshipCourierStore;
use Modules\EcommerceFinance\Services\AccessoryReshipRateCalculator;
use Modules\EcommerceFinance\Services\AccessoryReshipSelectionStore;
use PHPUnit\Framework\TestCase;
use Mockery;
require_once dirname(__DIR__, 2) . '/modules/EcommerceFinance/Repositories/ShippingOrderRepository.php';
require_once dirname(__DIR__, 2) . '/modules/EcommerceFinance/Services/AccessoryReshipMatcher.php';
require_once dirname(__DIR__, 2) . '/modules/EcommerceFinance/View/AccessoryReshipPageData.php';
require_once dirname(__DIR__, 2) . '/modules/EcommerceFinance/Services/AccessoryReshipCourierStore.php';
require_once dirname(__DIR__, 2) . '/modules/EcommerceFinance/Services/AccessoryReshipRateCalculator.php';
require_once dirname(__DIR__, 2) . '/modules/EcommerceFinance/Services/AccessoryReshipSelectionStore.php';
class AccessoryReshipPageDataTest extends TestCase
{
public function test_deductible_category_excludes_special_reship_couriers_and_nonpositive_amounts(): void
{
$calculator = new AccessoryReshipRateCalculator;
$base = ['status' => 'matched', 'freight_cents' => 1000, 'matches' => [['courier' => '首发快递']]];
$rates = ['首发快递' => ['rate_basis_points' => 5000]];
foreach (['外采快递', '线下快递'] as $name) {
foreach (['express_name1', 'express_name2'] as $field) {
$row = $calculator->apply($base + [$field => ' ' . $name . ' '], $rates);
$this->assertSame('excluded_courier', $row['deduction_state']);
$this->assertSame(0, $row['deduction_cents']);
$this->assertFalse(AccessoryReshipPageData::inCategory($row, 'ready'));
$this->assertTrue(AccessoryReshipPageData::inCategory($row, 'all'));
}
}
$ready = $calculator->apply($base + ['express_name1' => '正常快递'], $rates);
$this->assertTrue(AccessoryReshipPageData::inCategory($ready, 'ready'));
$this->assertSame(500, $ready['deduction_cents']);
foreach ([0, 1, null] as $freight) {
$row = $calculator->apply(array_replace($base, ['freight_cents' => $freight]), ['首发快递' => ['rate_basis_points' => 1]]);
$this->assertFalse(AccessoryReshipPageData::inCategory($row, 'ready'));
}
foreach (['zero_rate', 'selection_required', 'missing_config', 'invalid_amount', 'unmatched'] as $state) {
$this->assertFalse(AccessoryReshipPageData::inCategory(array_replace($ready, ['deduction_state' => $state]), 'ready'));
}
}
private function selections(): AccessoryReshipSelectionStore
{
$store = Mockery::mock(AccessoryReshipSelectionStore::class);
$store->shouldReceive('forMonth')->andReturn([]);
return $store;
}
private function configs(): AccessoryReshipCourierStore
{
$store = Mockery::mock(AccessoryReshipCourierStore::class);
$store->shouldReceive('activeRates')->andReturn(['快递' => ['courier_code' => 'TEST', 'rate_basis_points' => 5000], '顺丰' => ['courier_code' => 'SF', 'rate_basis_points' => 10000]]);
return $store;
}
protected function tearDown(): void { Mockery::close(); parent::tearDown(); }
public function test_january_uses_current_month_and_previous_two_months(): void
{
$repo = Mockery::mock(ShippingOrderRepository::class);
$repo->shouldReceive('accessoryReshipBatches')->once()->with('2026-01-01', '2026-02-01', '2025-11-01')->andReturn((function () { yield ['accessories' => [['trade_no' => '001', 'express_no' => 'BF001', 'express_name1' => '补发快递', 'ship_time' => '2026-09-01', 'barcode' => 'SKU-1', 'freight' => '2.50']], 'originals' => []]; })());
$data = new AccessoryReshipPageData($repo, new AccessoryReshipMatcher, $this->configs(), new AccessoryReshipRateCalculator, $this->selections());
$rows = iterator_to_array($data->rows(['month' => '2026-01']));
$this->assertSame('unmatched', $rows[0]['status']);
$this->assertSame(0, $rows[0]['deduction_cents']);
}
public function test_summary_does_not_duplicate_candidate_freight_and_paginates(): void
{
$request = Request::create('/workspace/courier-payables/accessory-reship-deductions?month=2026-09&match_status=all&page=2');
Container::getInstance()->instance('request', $request);
$accessories = [];
for ($i = 0; $i < 105; $i++) $accessories[] = ['trade_no' => '001', 'express_no' => 'BF001', 'express_name1' => '补发快递', 'ship_time' => '2026-09-01', 'barcode' => 'SKU-1', 'freight' => '0.10'];
$original = ['trade_no' => '001', 'barcode' => 'SKU', 'express_no' => 'FIRST', 'express_name1' => '快递', 'ship_time' => '2026-08-01', 'warehouse_name' => '成品仓'];
$repo = Mockery::mock(ShippingOrderRepository::class);
$repo->shouldReceive('accessoryReshipBatches')->andReturn((function () use ($accessories, $original) { yield ['accessories' => $accessories, 'originals' => [$original, array_replace($original, ['express_no' => 'SECOND'])]]; })());
$data = (new AccessoryReshipPageData($repo, new AccessoryReshipMatcher, $this->configs(), new AccessoryReshipRateCalculator, $this->selections()))->page();
$this->assertCount(50, $data['reshipRows']);
$this->assertSame(105, $data['reshipRows']->total());
$this->assertSame(105, $data['reshipSummary']['matched']);
$this->assertSame(525, $data['reshipSummary']['amount']);
$this->assertSame(105, $data['reshipSummary']['ready']);
$this->assertSame(0, $data['reshipSummary']['ambiguous']);
}
public function test_filters_search_all_rows_and_keep_summary_pagination_consistent(): void
{
Container::getInstance()->instance('request', Request::create('/?month=2026-09&match_status=all&courier=补发&original_month=2026-08&page=2'));
$accessories = [];
for ($i = 0; $i < 105; $i++) $accessories[] = ['trade_no'=>'001', 'express_no'=>'BF'.$i, 'express_name1'=>'补发快递', 'ship_time'=>'2026-09-15', 'barcode'=>'SKU-1', 'freight'=>'0.10'];
$accessories[] = array_replace($accessories[0], ['trade_no'=>'NO-MATCH']);
$repo = Mockery::mock(ShippingOrderRepository::class);
$repo->shouldReceive('accessoryReshipBatches')->andReturn((function () use ($accessories) { yield ['accessories'=>$accessories, 'originals'=>[['trade_no'=>'001','barcode'=>'SKU','express_no'=>'SF001','express_name1'=>'顺丰','ship_time'=>'2026-08-01','warehouse_name'=>'成品仓']]]; })());
$data = (new AccessoryReshipPageData($repo, new AccessoryReshipMatcher, $this->configs(), new AccessoryReshipRateCalculator, $this->selections()))->page();
$this->assertNull($data['reshipError']);
$this->assertCount(50, $data['reshipRows']);
$this->assertSame(105, $data['reshipRows']->total());
$this->assertSame(105, $data['reshipSummary']['all']);
$this->assertSame(1050, $data['reshipSummary']['amount']);
}
public function test_filters_require_same_candidate_and_support_fields_and_unknown_amount(): void
{
$data = new AccessoryReshipPageData(Mockery::mock(ShippingOrderRepository::class), new AccessoryReshipMatcher, $this->configs(), new AccessoryReshipRateCalculator, $this->selections());
$row = ['trade_no'=>'T001','express_no'=>'BF001','express_name1'=>'','express_name2'=>'补发快递','ship_time'=>'2026-09-15','barcode'=>'SKU-1','normalized_barcode'=>'SKU','freight_cents'=>1230,'deduction_cents'=>1230,'matches'=>[['courier'=>'顺丰','express_no'=>'SF001','month'=>'2026-08'],['courier'=>'圆通','express_no'=>'YT001','month'=>'2026-09']]];
$this->assertTrue($data->matchesFilters($row, ['trade_no'=>'T001','express_no'=>'bf001','courier'=>'补发','ship_date'=>'2026-09-15','barcode'=>'SKU-1','normalized_barcode'=>'SKU','freight'=>'12.30','deduction_amount'=>'12.30','original_courier'=>'顺丰','original_express_no'=>'sf001','original_month'=>'2026-08']));
$this->assertFalse($data->matchesFilters($row, ['original_courier'=>'顺丰','original_express_no'=>'YT001']));
$this->assertFalse($data->matchesFilters($row, ['trade_no'=>'missing']));
$row['freight_cents'] = $row['deduction_cents'] = null;
$this->assertTrue($data->matchesFilters($row, ['freight'=>'待核实','deduction_amount'=>'待核实']));
$this->assertFalse($data->matchesFilters($row, ['deduction_amount'=>'0.00']));
}
}