CaiWuXiTong/tests/Unit/SupplierAccessoryFreightTest.php

224 lines
14 KiB
PHP

<?php
namespace Tests\Unit;
use Illuminate\Config\Repository;
use Illuminate\Database\Capsule\Manager;
use Illuminate\Foundation\Application;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Facade;
use Modules\EcommerceFinance\Contracts\BusinessMirrorDataset;
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
use Modules\EcommerceFinance\Repositories\ProductCatalogMirrorRepository;
use Modules\EcommerceFinance\Repositories\ShippingOrderRepository;
use Modules\EcommerceFinance\Repositories\SupplierAccessoryFreightRepository;
use Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository;
use Modules\EcommerceFinance\Services\AccessoryReshipMatcher;
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
use Modules\EcommerceFinance\Services\SupplierAccessoryFreightMatcher;
use Modules\EcommerceFinance\View\SupplierAccessoryFreightPageData;
use Mockery;
use PHPUnit\Framework\TestCase;
foreach (['Contracts/BusinessMirrorDataset', 'Repositories/GenericBusinessMirrorRepository', 'Repositories/ShippingOrderRepository', 'Repositories/ProductCatalogMirrorRepository', 'Repositories/SupplierManagementMirrorRepository', 'Repositories/SupplierAccessoryFreightRepository', 'Services/BusinessMirrorDatasetRegistry', 'Services/AccessoryReshipMatcher', 'Services/SupplierAccessoryFreightMatcher', 'View/SupplierAccessoryFreightPageData'] as $file) {
require_once dirname(__DIR__, 2) . '/modules/EcommerceFinance/' . $file . '.php';
}
require_once dirname(__DIR__, 2) . '/app/Http/Controllers/Business/SupplierAccessoryFreight.php';
class SupplierAccessoryFreightTest extends TestCase
{
protected function setUp(): void
{
$app = new Application(dirname(__DIR__, 2));
$app->instance('config', new Repository(['app' => ['platform_timezone' => 'Asia/Shanghai']]));
$db = new Manager($app);
$db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '']);
$app->instance('db', $db->getDatabaseManager());
Facade::clearResolvedInstances();
Facade::setFacadeApplication($app);
$app->instance('request', Request::create('/?month=2026-09&state=all'));
$app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Illuminate\Routing\ResponseFactory(
Mockery::mock(\Illuminate\Contracts\View\Factory::class), Mockery::mock(\Illuminate\Routing\Redirector::class)
));
}
protected function tearDown(): void { Mockery::close(); parent::tearDown(); }
private function row(array $patch = []): array
{
return array_replace(['source_id' => 1, 'trade_no' => '123456789012345678', 'express_no' => 'BF001', 'express_name1' => '补发快递',
'ship_time' => '2026-09-15', 'barcode' => 'GBGJ-SJ038024N1410V02-10,GBGJ-SJ038024N1410V02-2', 'freight' => '12.35'], $patch);
}
public function test_four_province_spellings_deduct_full_freight_once(): void
{
foreach (['山东', '山东省', '河北', '河北省'] as $province) {
$result = (new SupplierAccessoryFreightMatcher)->match($this->row(), [['name' => '成品', 'supplier_name' => '供应商甲'], ['name' => '成品', 'supplier_name' => '供应商甲']], [['linkerprovience' => $province]]);
$this->assertSame('GBGJ-SJ038024N1410V02', $result['normalized_barcode']);
$this->assertSame('ready', $result['state']);
$this->assertSame(1235, $result['deduction_cents']);
}
}
public function test_external_courier_always_excluded_and_keeps_supplier_details(): void
{
foreach (['express_name1', 'express_name2'] as $field) {
foreach (['12.35', null, '0'] as $freight) {
$row = (new SupplierAccessoryFreightMatcher)->match($this->row([$field => ' 外采快递 ', 'freight' => $freight]), [['name'=>'成品','supplier_name'=>'甲']], [['linkerprovience'=>'河北省']]);
$this->assertSame('excluded', $row['state']);
$this->assertSame(0, $row['deduction_cents']);
$this->assertSame('甲', $row['supplier_name']);
$this->assertSame('外采快递不扣配件运费', $row['reason']);
}
}
$row = (new SupplierAccessoryFreightMatcher)->match($this->row(['express_name1'=>'外采快递']), [], []);
$this->assertSame('excluded', $row['state']);
$this->assertSame(0, $row['deduction_cents']);
$normal = (new SupplierAccessoryFreightMatcher)->match($this->row(), [['name'=>'成品','supplier_name'=>'甲']], [['linkerprovience'=>'河北省']]);
$this->assertSame(1235, $normal['deduction_cents']);
}
public function test_missing_or_ambiguous_product_supplier_province_never_deducts(): void
{
$product = [['name' => '成品', 'supplier_name' => '甲']];
foreach ([ [[], []], [[['name' => '成品', 'supplier_name' => '']], []], [$product, []],
[[...$product, ['name' => '成品', 'supplier_name' => '乙']], [['linkerprovience' => '山东']]],
[$product, [['linkerprovience' => '']]], [$product, [['linkerprovience' => '山东'], ['linkerprovience' => '河北']]],
[$product, [['linkerprovience' => '山东'], ['linkerprovience' => '']]] ] as [$products, $suppliers]) {
$result = (new SupplierAccessoryFreightMatcher)->match($this->row(), $products, $suppliers);
$this->assertSame('pending', $result['state']);
$this->assertSame(0, $result['deduction_cents']);
}
}
public function test_other_provinces_zero_invalid_and_equivalent_duplicate_provinces(): void
{
$matcher = new SupplierAccessoryFreightMatcher;
$products = [['name' => '成品', 'supplier_name' => '甲']];
foreach (['江苏省', '山东济南', '河北省石家庄市'] as $province) {
$result = $matcher->match($this->row(), $products, [['linkerprovience' => $province]]);
$this->assertSame('excluded', $result['state']);
$this->assertSame(0, $result['deduction_cents']);
}
foreach ([null, '', '-5', 'abc'] as $amount) {
$result = $matcher->match($this->row(['freight' => $amount]), $products, [['linkerprovience' => '河北']]);
$this->assertSame('pending', $result['state']);
$this->assertSame(0, $result['deduction_cents']);
}
$result = $matcher->match($this->row(['freight' => '0']), $products, [['linkerprovience' => '河北']]);
$this->assertSame('excluded', $result['state']);
$result = $matcher->match($this->row(['freight' => '0.10']), $products, [['linkerprovience' => ' 山东 '], ['linkerprovience' => '山东省']]);
$this->assertSame('ready', $result['state']);
$this->assertSame(10, $result['deduction_cents']);
}
public function test_repository_uses_product_supplier_not_shipping_supplier_and_ignores_combinations(): void
{
$connection = app('db')->connection();
$connection->statement('CREATE TABLE products (sku_id TEXT, sku_type TEXT, name TEXT, supplier_name TEXT)');
$connection->statement('CREATE TABLE suppliers (scmname TEXT, linkerprovience TEXT)');
$connection->table('products')->insert([
['sku_id' => 'GBGJ-SJ038024N1410V02', 'sku_type' => 'normal', 'name' => '成品', 'supplier_name' => '商品供应商'],
['sku_id' => 'GBGJ-SJ038024N1410V02', 'sku_type' => 'combine', 'name' => '组合', 'supplier_name' => '其他供应商'],
]);
$connection->table('suppliers')->insert(['scmname' => '商品供应商', 'linkerprovience' => '河北省']);
$registry = Mockery::mock(BusinessMirrorDatasetRegistry::class);
foreach ([ProductCatalogMirrorRepository::DATASET_KEY => 'products', SupplierManagementMirrorRepository::DATASET => 'suppliers'] as $key => $table) {
$dataset = Mockery::mock(BusinessMirrorDataset::class);
$dataset->shouldReceive('targetTable')->andReturn($table);
$registry->shouldReceive('get')->with($key)->andReturn($dataset);
}
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
$mirror->shouldReceive('ready')->andReturnTrue();
$mirror->shouldReceive('connection')->andReturn($connection);
$shipping = Mockery::mock(ShippingOrderRepository::class);
$shipping->shouldReceive('accessoryFreightBatches')->once()->with('2026-09-01', '2026-10-01')->andReturn((function () { yield [$this->row(['supplier_name' => '错误的历史供应商'])]; })());
$repo = new SupplierAccessoryFreightRepository($shipping, $registry, $mirror, new SupplierAccessoryFreightMatcher);
$rows = iterator_to_array($repo->rows('2026-09'));
$this->assertCount(1, $rows);
$this->assertSame('商品供应商', $rows[0]['supplier_name']);
$this->assertSame(1235, $rows[0]['deduction_cents']);
}
private function data(): SupplierAccessoryFreightPageData
{
$repository = Mockery::mock(SupplierAccessoryFreightRepository::class);
$repository->shouldReceive('rows')->with('2026-09')->andReturnUsing(function () {
$matcher = new SupplierAccessoryFreightMatcher;
for ($i = 0; $i < 105; $i++) yield $matcher->match($this->row(['source_id' => $i, 'freight' => '0.10']), [['name' => '=危险商品', 'supplier_name' => '山东供应商']], [['linkerprovience' => '山东']]);
yield $matcher->match($this->row(['source_id' => 200]), [], []);
});
return new SupplierAccessoryFreightPageData($repository);
}
public function test_summary_pagination_and_filters_use_full_month_without_multiplying_amount(): void
{
app()->instance('request', Request::create('/?month=2026-09&state=all&page=2'));
$page = $this->data()->page();
$this->assertNull($page['freightError']);
$this->assertCount(50, $page['freightRows']);
$this->assertSame(106, $page['freightRows']->total());
$this->assertSame(1050, $page['freightSummary']['amount']);
app()->instance('request', Request::create('/?month=2026-09&state=ready&supplier_name=山东&page=3'));
$page = $this->data()->page();
$this->assertCount(5, $page['freightRows']);
$this->assertSame(105, $page['freightRows']->total());
$this->assertSame(0, $page['freightSummary']['pending']);
$this->assertSame(1050, $page['freightSummary']['amount']);
}
public function test_csv_matches_filters_preserves_long_ids_and_escapes_formulas(): void
{
app()->instance('request', Request::create('/?month=2026-09&state=pending'));
$controller = new \App\Http\Controllers\Business\SupplierAccessoryFreight;
$response = $controller->export($this->data());
ob_start(); $response->sendContent(); $csv = ob_get_clean();
$this->assertStringStartsWith("\xEF\xBB\xBF", $csv);
$this->assertSame(2, substr_count($csv, "\n"));
$this->assertStringContainsString("'123456789012345678", $csv);
$this->assertStringContainsString('待核实', $csv);
app()->instance('request', Request::create('/?month=2026-09&state=ready'));
$response = $controller->export($this->data());
ob_start(); $response->sendContent(); $csv = ob_get_clean();
$this->assertStringContainsString("'=危险商品", $csv);
app()->instance('request', Request::create('/?month=2026-09&state=ready&columns=deduction,supplier_name'));
$response = $controller->export($this->data());
ob_start(); $response->sendContent(); $csv = ob_get_clean();
$lines = explode("\n", trim(substr($csv, 3)));
$this->assertSame(['业务月份', '发货记录ID', '仓库', '扣款金额(元)', '供应商'], str_getcsv($lines[0]));
$this->assertCount(5, str_getcsv($lines[1]));
}
public function test_filters_accept_province_alias_and_reject_invalid_month_state_and_columns(): void
{
app()->instance('request', Request::create('/?month=2026-09&state=all&province=山东省'));
$this->assertSame('山东', $this->data()->filters()['province']);
foreach (['month=2026-13', 'month[]=2026-09', 'month=1999-01', 'month=2026-09&state=unknown', 'month=2026-09&supplier_name[]=x', 'month=2026-09&columns=', 'month=2026-09&columns=state,state', 'month=2026-09&columns=password'] as $query) {
app()->instance('request', Request::create('/?' . $query));
try {
$this->data()->filters();
$this->data()->exportColumns();
$this->fail('应拒绝无效条件');
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) {
$this->assertSame(422, $exception->getStatusCode());
}
}
}
public function test_data_error_clears_partial_rows_and_totals(): void
{
$repository = Mockery::mock(SupplierAccessoryFreightRepository::class);
$repository->shouldReceive('rows')->andReturnUsing(function () {
yield (new SupplierAccessoryFreightMatcher)->match($this->row(), [['name'=>'商品','supplier_name'=>'甲']], [['linkerprovience'=>'河北']]);
throw new \RuntimeException('读取失败');
});
$handler = Mockery::mock(\Illuminate\Contracts\Debug\ExceptionHandler::class);
$handler->shouldReceive('report')->once();
app()->instance(\Illuminate\Contracts\Debug\ExceptionHandler::class, $handler);
$page = (new SupplierAccessoryFreightPageData($repository))->page();
$this->assertNotNull($page['freightError']);
$this->assertSame(0, $page['freightRows']->total());
$this->assertSame(0, $page['freightSummary']['amount']);
}
}