CaiWuXiTong/tests/Feature/Console/ImportDingProcessDetailsTest.php

272 lines
10 KiB
PHP

<?php
namespace Tests\Feature\Console;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Services\DingProcessDetailMirrorImporter;
use RuntimeException;
use Tests\TestCase;
class ImportDingProcessDetailsTest extends TestCase
{
private string $fixture;
/** @var list<string> */
private array $tempFiles = [];
protected function setUp(): void
{
parent::setUp();
config([
'database.connections.business_mirror' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
],
'upstream_mirrors.connection' => 'business_mirror',
'upstream_mirrors.ding_process_details.scope' => 'personal_permission',
]);
DB::purge('business_mirror');
$this->createMirrorTables();
$this->fixture = base_path('tests/Fixtures/ding_process_details_sample.csv');
}
protected function tearDown(): void
{
foreach ($this->tempFiles as $file) {
if (is_file($file)) {
unlink($file);
}
}
parent::tearDown();
}
public function test_imports_csv_and_publishes_mirror_state(): void
{
$result = app(DingProcessDetailMirrorImporter::class)
->import($this->fixture, 'personal_permission', true);
$this->assertSame('succeeded', $result['status']);
$this->assertSame(4, $result['rows']);
$this->assertSame(0, $result['invalid']);
$this->assertSame(0, $result['placeholders']);
$this->assertSame(4, DB::connection('business_mirror')
->table('ding_process_detail_mirror')->count());
$row = DB::connection('business_mirror')
->table('ding_process_detail_mirror')
->where('business_id', '202601011200000000001')
->where('line_no', 1)
->first();
$this->assertNotNull($row);
$this->assertSame('张三', $row->originator_name);
$this->assertSame('已完成', $row->status);
$this->assertSame('公摊日常报销', $row->document_type);
$this->assertSame('5602.04', $row->account_code);
$this->assertSame('管理费用_办公费', $row->account_name);
$this->assertEqualsWithDelta(100.0, (float) $row->amount, 0.001);
$this->assertSame('202601', $row->business_month);
$this->assertSame('personal_permission', $row->source_scope);
$state = DB::connection('business_mirror')
->table('business_mirror_table_states')
->where('dataset_key', 'ding.process_details.personal_permission')
->first();
$this->assertNotNull($state);
$this->assertSame('succeeded', $state->status);
$this->assertSame('ding_process_detail_mirror', $state->target_table);
$this->assertNotNull($state->initial_completed_at);
$this->assertSame(4, (int) $state->mirror_row_count);
}
public function test_import_is_idempotent_by_business_id_and_line_number(): void
{
$importer = app(DingProcessDetailMirrorImporter::class);
$importer->import($this->fixture, 'personal_permission', true);
$importer->import($this->fixture, 'personal_permission', false);
$this->assertSame(4, DB::connection('business_mirror')
->table('ding_process_detail_mirror')->count());
}
public function test_replace_removes_rows_that_are_not_in_the_new_file(): void
{
$importer = app(DingProcessDetailMirrorImporter::class);
$importer->import($this->fixture, 'personal_permission', true);
$replacement = $this->makeCsv([[
'1',
'202603011200000000003',
'王五',
'已完成',
'店铺日常报销',
'替换后的流程',
'1',
'5601.01',
'销售费用',
'10.00',
'1002.01.003',
'银行-测试3',
'10.00',
'银行卡',
'2026-03-02 00:00',
]]);
$importer->import($replacement, 'personal_permission', true);
$this->assertSame(1, DB::connection('business_mirror')
->table('ding_process_detail_mirror')->count());
$this->assertSame(
'202603011200000000003',
(string) DB::connection('business_mirror')
->table('ding_process_detail_mirror')
->value('business_id'),
);
}
public function test_missing_headers_does_not_delete_existing_rows(): void
{
$importer = app(DingProcessDetailMirrorImporter::class);
$importer->import($this->fixture, 'personal_permission', true);
$path = tempnam(sys_get_temp_dir(), 'ding-process-bad-');
file_put_contents($path, "序号,审批编号\n1,202603011200000000003\n");
$this->tempFiles[] = $path;
try {
$importer->import($path, 'personal_permission', true);
$this->fail('缺少表头时应抛出异常。');
} catch (RuntimeException $exception) {
$this->assertStringContainsString('缺少列', $exception->getMessage());
}
$this->assertSame(4, DB::connection('business_mirror')
->table('ding_process_detail_mirror')->count());
}
public function test_placeholder_rows_are_skipped_and_the_better_duplicate_wins(): void
{
$path = $this->makeCsv([
[
'1', '202604011200000000004', '赵六', '已完成', '公摊日常报销', '真实费用',
'1', '5602.04', '管理费用_办公费', '100.00', '1002.01.001', '华夏银行-测试',
'100.00', '银行卡', '2026-04-02 00:00',
],
[
'2', '202604011200000000004', '赵六', '已完成', '公摊日常报销', '251',
'1', '251', '251', '', '251', '251', '251', '251', '251',
],
[
'3', '202604011200000000004', '赵六', '已完成', '公摊日常报销', '251',
'1', '1221.05', '251', '0.00', '1002.01.001', '华夏银行-测试',
'100.00', '银行卡', '2026-04-02 00:00',
],
[
'4', '202604021200000000005', '钱七', '已完成', '店铺日常报销', '251',
'1', '251', '251', '', '251', '251', '251', '251', '251',
],
]);
$result = app(DingProcessDetailMirrorImporter::class)
->import($path, 'personal_permission', true);
$this->assertSame(1, $result['rows']);
$this->assertSame(2, $result['placeholders']);
$this->assertSame(1, $result['duplicates']);
$row = DB::connection('business_mirror')
->table('ding_process_detail_mirror')
->first();
$this->assertSame('5602.04', $row->account_code);
$this->assertSame('管理费用_办公费', $row->account_name);
$this->assertEqualsWithDelta(100.0, (float) $row->amount, 0.001);
}
public function test_artisan_command_imports_the_file(): void
{
$this->artisan('ding:import-process-details', [
'file' => $this->fixture,
'--scope' => 'personal_permission',
'--replace' => true,
])->assertSuccessful();
$this->assertSame(4, DB::connection('business_mirror')
->table('ding_process_detail_mirror')->count());
}
private function createMirrorTables(): void
{
Schema::connection('business_mirror')->create(
'business_mirror_table_states',
function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->string('source_key');
$table->string('source_object');
$table->string('target_table');
$table->string('status')->default('pending');
$table->string('run_mode')->nullable();
$table->string('generation')->nullable();
$table->json('cursor')->nullable();
$table->json('high_watermark')->nullable();
$table->dateTime('window_started_at')->nullable();
$table->dateTime('window_ends_at')->nullable();
$table->dateTime('initial_completed_at')->nullable();
$table->dateTime('last_full_succeeded_at')->nullable();
$table->dateTime('last_attempted_at')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
$table->unsignedBigInteger('last_upserted_rows')->default(0);
$table->unsignedBigInteger('mirror_row_count')->default(0);
$table->string('last_error_code')->nullable();
$table->timestamps();
},
);
Schema::connection('business_mirror')->create(
'ding_process_detail_mirror',
function (Blueprint $table): void {
$table->bigIncrements('id');
$table->string('business_id', 32);
$table->unsignedInteger('line_no');
$table->string('business_month', 6)->index();
$table->string('originator_name', 100)->nullable()->index();
$table->string('status', 20)->nullable()->index();
$table->string('document_type', 100)->nullable()->index();
$table->text('purpose')->nullable();
$table->string('account_code', 64)->nullable();
$table->string('account_name', 255)->nullable();
$table->decimal('amount', 18, 2)->nullable();
$table->string('bank_account_code', 255)->nullable();
$table->string('bank_account_name', 255)->nullable();
$table->decimal('bank_amount', 18, 2)->nullable();
$table->string('payment_method', 50)->nullable();
$table->string('paid_at', 50)->nullable();
$table->string('source_scope', 40)->default('personal_permission')->index();
$table->dateTime('synced_at', 6);
$table->timestamps();
$table->unique(['business_id', 'line_no']);
},
);
}
/** @param list<list<string>> $rows */
private function makeCsv(array $rows): string
{
$path = tempnam(sys_get_temp_dir(), 'ding-process-');
$handle = fopen($path, 'wb');
fputcsv($handle, DingProcessDetailMirrorImporter::HEADERS);
foreach ($rows as $row) {
fputcsv($handle, $row);
}
fclose($handle);
$this->tempFiles[] = $path;
return $path;
}
}