168 lines
9.4 KiB
PHP
168 lines
9.4 KiB
PHP
<?php
|
||
namespace Tests\Unit;
|
||
|
||
use Illuminate\Config\Repository;
|
||
use Illuminate\Database\Capsule\Manager;
|
||
use Illuminate\Foundation\Application;
|
||
use Illuminate\Support\Facades\Facade;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Schema;
|
||
use Illuminate\Translation\ArrayLoader;
|
||
use Illuminate\Translation\Translator;
|
||
use Illuminate\Validation\Factory;
|
||
use Illuminate\Validation\ValidationException;
|
||
use Modules\EcommerceFinance\Services\AccessoryReshipCourierStore;
|
||
use Modules\EcommerceFinance\Services\AccessoryReshipCourierImporter;
|
||
use Modules\EcommerceFinance\Services\AccessoryReshipRateCalculator;
|
||
use Modules\EcommerceFinance\Services\AccessoryReshipSelectionStore;
|
||
use PHPUnit\Framework\TestCase;
|
||
|
||
foreach (['AccessoryReshipCourierStore', 'AccessoryReshipCourierImporter', 'AccessoryReshipRateCalculator', 'AccessoryReshipSelectionStore'] as $class) {
|
||
require_once dirname(__DIR__, 2) . '/modules/EcommerceFinance/Services/' . $class . '.php';
|
||
}
|
||
|
||
class AccessoryReshipCourierConfigTest extends TestCase
|
||
{
|
||
private AccessoryReshipCourierStore $store;
|
||
|
||
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());
|
||
$app->bind('db.schema', fn () => $db->getConnection()->getSchemaBuilder());
|
||
$app->instance('validator', new Factory(new Translator(new ArrayLoader, 'zh_CN'), $app));
|
||
Facade::clearResolvedInstances();
|
||
Facade::setFacadeApplication($app);
|
||
$migration = require dirname(__DIR__, 2) . '/database/migrations/2026_10_16_220000_create_accessory_reship_courier_configs.php';
|
||
$migration->up();
|
||
$this->store = new AccessoryReshipCourierStore;
|
||
}
|
||
|
||
private function row(string $code = 'TEST', string $name = '测试快递', int $rate = 5000): array
|
||
{
|
||
return ['courier_code' => $code, 'courier_name' => $name, 'rate_basis_points' => $rate, 'source_created_at' => '2025-11-07 17:03:30', 'creator' => '原创建人'];
|
||
}
|
||
|
||
public function test_import_preserves_source_metadata_and_is_idempotent(): void
|
||
{
|
||
$this->assertSame(['added' => 1, 'skipped' => 0], $this->store->import(1, [$this->row()], '导入操作人', 'hash'));
|
||
$this->assertSame(['added' => 0, 'skipped' => 1], $this->store->import(1, [$this->row()], '导入操作人', 'hash'));
|
||
$row = DB::table(AccessoryReshipCourierStore::TABLE)->first();
|
||
$this->assertSame('2025-11-07 17:03:30', $row->source_created_at);
|
||
$this->assertSame('原创建人', $row->creator);
|
||
$this->assertSame(1, DB::table(AccessoryReshipCourierStore::TABLE)->count());
|
||
$this->assertSame(2, DB::table(AccessoryReshipCourierStore::EVENTS)->count());
|
||
}
|
||
|
||
public function test_import_conflict_rolls_back_entire_batch_and_log(): void
|
||
{
|
||
$this->store->import(1, [$this->row()], '操作人', 'hash');
|
||
try {
|
||
$this->store->import(1, [$this->row('NEW', '新增快递'), $this->row('TEST', '不同名称')], '操作人', 'hash2');
|
||
$this->fail('必须拒绝冲突');
|
||
} catch (ValidationException) {
|
||
$this->assertSame(1, DB::table(AccessoryReshipCourierStore::TABLE)->count());
|
||
$this->assertSame(1, DB::table(AccessoryReshipCourierStore::EVENTS)->count());
|
||
}
|
||
}
|
||
|
||
public function test_crud_soft_delete_restore_and_company_isolation(): void
|
||
{
|
||
$id = $this->store->save(1, $this->row(), '操作人');
|
||
$this->store->save(1, $this->row(rate: 7000), '编辑人', $id);
|
||
$this->assertSame(7000, (int) $this->store->activeRates(1)['测试快递']['rate_basis_points']);
|
||
$this->assertSame([], $this->store->activeRates(2));
|
||
$this->store->changeDeleted(1, $id, true, '操作人');
|
||
$this->assertSame([], $this->store->activeRates(1));
|
||
$this->store->changeDeleted(1, $id, false, '操作人');
|
||
$this->assertCount(1, $this->store->activeRates(1));
|
||
$this->assertSame(4, DB::table(AccessoryReshipCourierStore::EVENTS)->count());
|
||
try {
|
||
$this->store->save(2, $this->row(), '其他公司', $id);
|
||
$this->fail('不能修改其他公司');
|
||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) {
|
||
$this->assertSame(404, $exception->getStatusCode());
|
||
}
|
||
}
|
||
|
||
public function test_rate_calculation_zero_missing_conflicting_and_rounding(): void
|
||
{
|
||
$calculator = new AccessoryReshipRateCalculator;
|
||
$row = ['status' => 'matched', 'freight_cents' => 1235, 'matches' => [['courier' => '测试快递'], ['courier' => '另一快递']]];
|
||
$rates = ['测试快递' => $this->row(rate: 5000), '另一快递' => $this->row('SECOND', '另一快递', 5000)];
|
||
$this->assertSame('selection_required', $calculator->apply($row, $rates)['deduction_state']);
|
||
$result = $calculator->apply($row, $rates, '测试快递');
|
||
$this->assertSame(618, $result['deduction_cents']);
|
||
$this->assertSame('ready', $result['deduction_state']);
|
||
$rates['另一快递']['rate_basis_points'] = 10000;
|
||
$this->assertSame('selection_required', $calculator->apply($row, $rates)['deduction_state']);
|
||
$this->assertNull($calculator->apply($row, $rates)['deduction_cents']);
|
||
$this->assertSame('missing_config', $calculator->apply($row, [], '测试快递')['deduction_state']);
|
||
$this->assertSame(1235, $calculator->apply($row, $rates, '另一快递')['deduction_cents']);
|
||
$this->assertSame('selection_required', $calculator->apply($row, $rates, '失效快递')['deduction_state']);
|
||
$row['matches'] = [['courier' => '测试快递']];
|
||
$rates['测试快递']['rate_basis_points'] = 0;
|
||
$this->assertSame(0, $calculator->apply($row, $rates)['deduction_cents']);
|
||
$this->assertSame('zero_rate', $calculator->apply($row, $rates)['deduction_state']);
|
||
$this->assertSame(865, AccessoryReshipRateCalculator::amount(1235, 7000));
|
||
$row['status'] = 'unmatched';
|
||
$this->assertSame(0, $calculator->apply($row, $rates)['deduction_cents']);
|
||
}
|
||
|
||
public function test_same_courier_selects_one_waybill_without_confirmation(): void
|
||
{
|
||
$row = ['status' => 'matched', 'freight_cents' => 1235, 'matches' => [
|
||
['courier' => '测试快递', 'express_no' => 'FIRST', 'month' => '2026-09'],
|
||
['courier' => '测试快递', 'express_no' => 'SECOND', 'month' => '2026-08'],
|
||
]];
|
||
$result = (new AccessoryReshipRateCalculator)->apply($row, ['测试快递' => $this->row()]);
|
||
$this->assertSame('ready', $result['deduction_state']);
|
||
$this->assertSame(618, $result['deduction_cents']);
|
||
$this->assertCount(1, $result['matches']);
|
||
$this->assertSame('FIRST', $result['matches'][0]['express_no']);
|
||
$this->assertStringNotContainsString('核对', $result['deduction_note']);
|
||
}
|
||
|
||
public function test_selection_survives_reload_and_is_scoped_to_company_month_and_package(): void
|
||
{
|
||
$migration = require dirname(__DIR__, 2) . '/database/migrations/2026_10_16_230000_create_accessory_reship_selections.php';
|
||
$migration->up();
|
||
$store = new AccessoryReshipSelectionStore;
|
||
$key = AccessoryReshipSelectionStore::key(['source_id' => 1, 'express_no' => 'BF001']);
|
||
$other = AccessoryReshipSelectionStore::key(['source_id' => 2, 'express_no' => 'BF002']);
|
||
$this->assertNotSame($key, $other);
|
||
$store->save(1, '2026-10', $key, '快递A', '操作人');
|
||
$store->save(1, '2026-10', $key, '快递B', '操作人');
|
||
$store->save(1, '2026-10', $other, '快递C', '操作人');
|
||
$this->assertSame([$key => '快递B', $other => '快递C'], (new AccessoryReshipSelectionStore)->forMonth(1, '2026-10'));
|
||
$this->assertSame([], $store->forMonth(2, '2026-10'));
|
||
$this->assertSame([], $store->forMonth(1, '2026-09'));
|
||
$this->assertSame(2, DB::table(AccessoryReshipSelectionStore::TABLE)->count());
|
||
}
|
||
|
||
public function test_importer_reads_string_and_numeric_percent_and_retains_dates(): void
|
||
{
|
||
$book = new \PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||
$sheet = $book->getActiveSheet();
|
||
$sheet->fromArray([['快递代码', '快递公司', '创建时间', '创建人', '扣款比率(%)'], ['A', '快递A', '2025-11-07 17:03:30', '原创建人', '50.00%'], ['B', '快递B', '2025-11-08 12:00:00', '原创建人', 0.7]]);
|
||
$sheet->getStyle('E3')->getNumberFormat()->setFormatCode('0.00%');
|
||
$path = tempnam(sys_get_temp_dir(), 'reship-config-');
|
||
try {
|
||
(new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($book))->save($path);
|
||
$rows = (new AccessoryReshipCourierImporter)->read($path);
|
||
$this->assertSame([5000, 7000], array_column($rows, 'rate_basis_points'));
|
||
$this->assertSame('2025-11-07 17:03:30', $rows[0]['source_created_at']);
|
||
$sheet->setCellValue('E3', '100.01%');
|
||
(new \PhpOffice\PhpSpreadsheet\Writer\Xlsx($book))->save($path);
|
||
$this->expectException(ValidationException::class);
|
||
(new AccessoryReshipCourierImporter)->read($path);
|
||
} finally {
|
||
$book->disconnectWorksheets();
|
||
unlink($path);
|
||
}
|
||
}
|
||
}
|