224 lines
14 KiB
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']);
|
|
}
|
|
}
|