CaiWuXiTong/tests/Feature/Console/PinduoduoSettlementBillImporterTest.php

349 lines
14 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\Repositories\PinduoduoSettlementBillRepository;
use Modules\EcommerceFinance\Services\PinduoduoSettlementBillImporter;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use RuntimeException;
use Tests\TestCase;
class PinduoduoSettlementBillImporterTest extends TestCase
{
/** @var list<string> */
private array $files = [];
protected function setUp(): void
{
parent::setUp();
config([
'database.connections.business_mirror' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
],
'upstream_mirrors.connection' => 'business_mirror',
]);
DB::purge('business_mirror');
$this->createTables();
}
protected function tearDown(): void
{
foreach ($this->files as $file) {
@unlink($file);
}
parent::tearDown();
}
public function test_it_imports_current_jushengsuan_headers_keeps_all_accounts_and_is_idempotent(): void
{
$this->insertActiveRow('OLD-IN-PERIOD', '2026-08-15 12:00:00');
$this->insertActiveRow('KEEP-OUTSIDE-PERIOD', '2026-07-31 23:59:59');
$path = $this->xlsx([
$this->row('REC-INCOME', '2026-08-01 00:00:19', '100.00', '0', '100.00'),
$this->row('REC-EXPENSE', '2026-08-02 12:00:00', '0', '25.00', '-25.00', '拼多多货款账户', '退款'),
$this->row('REC-ZERO', '2026-08-03 12:00:00', '0', '0', '0'),
$this->row('REC-MARKETING', '2026-08-04 12:00:00', '0', '3.00', '-3.00', '拼多多营销账户'),
]);
$importer = app(PinduoduoSettlementBillImporter::class);
$result = $importer->import(
$path,
'2026-08-01',
'2026-08-31',
4,
'100.00',
'28.00',
100,
);
$this->assertSame('succeeded', $result['status']);
$this->assertSame(4, $result['row_count']);
$this->assertSame(0, $result['excluded_account_rows']);
$this->assertSame(1, $result['store_count']);
$this->assertSame(2, $result['bill_item_count']);
$this->assertSame([
'拼多多货款账户' => 3,
'拼多多营销账户' => 1,
], $result['account_type_counts']);
$this->assertSame('100.00', $result['income_total']);
$this->assertSame('28.00', $result['expense_total']);
$this->assertSame('72.00', $result['net_total']);
$this->assertDatabaseMissing('pdd_settlement_bill_rows', ['record_code' => 'OLD-IN-PERIOD'], 'business_mirror');
$this->assertDatabaseHas('pdd_settlement_bill_rows', ['record_code' => 'KEEP-OUTSIDE-PERIOD'], 'business_mirror');
$this->assertDatabaseHas('pdd_settlement_bill_rows', [
'record_code' => 'REC-MARKETING',
'account_name' => '拼多多营销账户',
], 'business_mirror');
$repository = app(PinduoduoSettlementBillRepository::class);
$this->assertTrue($repository->ready());
$this->assertSame([
'income_total' => 100.0,
'expense_total' => 28.0,
'cnt' => 4,
], $repository->summarize(null, '2026-08-01', '2026-08-31', null));
$this->assertSame(1, $repository->paginate(null, '2026-08-01', '2026-08-31', 'income', 1, 25)['total']);
$this->assertSame(2, $repository->paginate(null, '2026-08-01', '2026-08-31', 'expense', 1, 25)['total']);
$zero = $repository->paginate(null, '2026-08-03', '2026-08-03', null, 1, 25);
$this->assertSame(0, $zero['rows'][0]['flowType']);
$unchanged = $importer->import(
$path,
'2026-08-01',
'2026-08-31',
4,
'100.00',
'28.00',
100,
);
$this->assertSame('unchanged', $unchanged['status']);
$this->assertSame(5, DB::connection('business_mirror')->table('pdd_settlement_bill_rows')->count());
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('本次账期或权威控制数与历史记录不一致');
$importer->import($path, '2026-08-01', '2026-08-31', 5, '100.00', '28.00', 100);
}
public function test_control_mismatch_does_not_replace_the_active_period(): void
{
$this->insertActiveRow('ACTIVE-ROW', '2026-08-15 12:00:00');
$path = $this->xlsx([
$this->row('NEW-ROW', '2026-08-16 12:00:00', '10.00', '0', '10.00'),
]);
try {
app(PinduoduoSettlementBillImporter::class)->import(
$path,
'2026-08-01',
'2026-08-31',
2,
'10.00',
'0.00',
100,
);
$this->fail('控制数不一致时必须拒绝激活。');
} catch (RuntimeException $exception) {
$this->assertStringContainsString('权威控制数不一致', $exception->getMessage());
}
$this->assertDatabaseHas('pdd_settlement_bill_rows', ['record_code' => 'ACTIVE-ROW'], 'business_mirror');
$this->assertDatabaseMissing('pdd_settlement_bill_rows', ['record_code' => 'NEW-ROW'], 'business_mirror');
$this->assertSame('failed', DB::connection('business_mirror')->table('pdd_settlement_bill_imports')->value('status'));
$this->assertSame(2, DB::connection('business_mirror')->table('pdd_settlement_bill_imports')->value('expected_row_count'));
$this->assertSame(1, DB::connection('business_mirror')->table('pdd_settlement_bill_imports')->value('row_count'));
$this->assertSame(0, DB::connection('business_mirror')->table('pdd_settlement_bill_staging')->count());
}
public function test_duplicate_record_codes_are_rejected_without_replacing_active_rows(): void
{
$this->insertActiveRow('ACTIVE-ROW', '2026-08-15 12:00:00');
$path = $this->xlsx([
$this->row('DUPLICATE', '2026-08-16 12:00:00', '10.00', '0', '10.00'),
$this->row('DUPLICATE', '2026-08-17 12:00:00', '20.00', '0', '20.00'),
]);
$failure = null;
try {
app(PinduoduoSettlementBillImporter::class)->import(
$path,
'2026-08-01',
'2026-08-31',
2,
'30.00',
'0.00',
100,
);
} catch (\Throwable $exception) {
$failure = $exception;
}
$this->assertNotNull($failure, '重复记录编码必须拒绝导入。');
$this->assertDatabaseHas('pdd_settlement_bill_rows', ['record_code' => 'ACTIVE-ROW'], 'business_mirror');
$this->assertDatabaseMissing('pdd_settlement_bill_rows', ['record_code' => 'DUPLICATE'], 'business_mirror');
$this->assertSame(0, DB::connection('business_mirror')->table('pdd_settlement_bill_staging')->count());
}
public function test_out_of_period_and_inconsistent_amount_rows_are_rejected(): void
{
$outOfPeriod = $this->xlsx([
$this->row('OUTSIDE', '2026-09-01 00:00:00', '10.00', '0', '10.00'),
]);
$badAmount = $this->xlsx([
$this->row('BAD-AMOUNT', '2026-08-10 00:00:00', '10.00', '2.00', '9.00'),
]);
$importer = app(PinduoduoSettlementBillImporter::class);
foreach ([
[$outOfPeriod, '发生时间超出指定账期'],
[$badAmount, '收入、支出与金额不一致'],
] as [$path, $message]) {
try {
$importer->import($path, '2026-08-01', '2026-08-31', 1, '10.00', '0.00', 100);
$this->fail($message . '时必须拒绝导入。');
} catch (RuntimeException $exception) {
$this->assertStringContainsString($message, $exception->getMessage());
}
}
$this->assertSame(0, DB::connection('business_mirror')->table('pdd_settlement_bill_rows')->count());
$this->assertSame(0, DB::connection('business_mirror')->table('pdd_settlement_bill_staging')->count());
}
public function test_blank_account_type_is_rejected(): void
{
$path = $this->xlsx([
$this->row('BLANK-ACCOUNT', '2026-08-10 00:00:00', '10.00', '0', '10.00', ''),
]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('缺少账户类型');
app(PinduoduoSettlementBillImporter::class)->import(
$path,
'2026-08-01',
'2026-08-31',
1,
'10.00',
'0.00',
100,
);
}
private function createTables(): void
{
$schema = Schema::connection('business_mirror');
$schema->create('pdd_settlement_bill_imports', function (Blueprint $table): void {
$table->uuid('generation')->primary();
$table->string('source_filename');
$table->char('source_sha256', 64)->unique();
$table->date('period_start');
$table->date('period_end');
$table->string('status', 32)->default('running');
$table->unsignedBigInteger('expected_row_count');
$table->decimal('expected_income_total', 20, 2);
$table->decimal('expected_expense_total', 20, 2);
$table->unsignedBigInteger('row_count')->default(0);
$table->unsignedBigInteger('excluded_account_rows')->default(0);
$table->unsignedInteger('store_count')->default(0);
$table->unsignedInteger('bill_item_count')->default(0);
$table->json('account_type_counts')->nullable();
$table->decimal('income_total', 20, 2)->default(0);
$table->decimal('expense_total', 20, 2)->default(0);
$table->decimal('net_total', 20, 2)->default(0);
$table->string('last_error_code', 120)->nullable();
$table->timestamp('completed_at')->nullable();
$table->timestamps();
});
$schema->create('pdd_settlement_bill_staging', function (Blueprint $table): void {
$table->bigIncrements('id');
$table->uuid('generation');
$this->billColumns($table);
$table->unique(['generation', 'record_code']);
});
$schema->create('pdd_settlement_bill_rows', function (Blueprint $table): void {
$table->string('record_code', 191)->primary();
$this->billColumns($table, false);
$table->uuid('source_generation');
$table->char('source_sha256', 64);
$table->timestamp('imported_at');
});
}
private function billColumns(Blueprint $table, bool $includeRecordCode = true): void
{
if ($includeRecordCode) {
$table->string('record_code', 191);
}
$table->string('store_name')->nullable();
$table->string('custom_remark')->nullable();
$table->string('bill_item')->nullable();
$table->string('merchant_order_no', 191)->nullable();
$table->string('online_order_no', 191)->nullable();
$table->string('business_type')->nullable();
$table->string('business_description', 1000)->nullable();
$table->string('account_name')->nullable();
$table->dateTime('occurred_at');
$table->decimal('income_amount', 20, 2)->default(0);
$table->decimal('expense_amount', 20, 2)->default(0);
$table->decimal('signed_amount', 20, 2)->default(0);
$table->string('remark', 1000)->nullable();
$table->dateTime('source_updated_at')->nullable();
$table->string('source_modified_by')->nullable();
$table->unsignedBigInteger('source_row_number');
}
private function insertActiveRow(string $recordCode, string $occurredAt): void
{
DB::connection('business_mirror')->table('pdd_settlement_bill_rows')->insert([
'record_code' => $recordCode,
'store_name' => '原有店铺',
'bill_item' => '原有流水',
'account_name' => PinduoduoSettlementBillImporter::ACCOUNT_NAME,
'occurred_at' => $occurredAt,
'income_amount' => 1,
'expense_amount' => 0,
'signed_amount' => 1,
'source_row_number' => 1,
'source_generation' => 'existing-generation',
'source_sha256' => str_repeat('a', 64),
'imported_at' => '2026-09-01 00:00:00',
]);
}
/** @return list<string> */
private function row(
string $recordCode,
string $occurredAt,
string $income,
string $expense,
string $signed,
string $account = PinduoduoSettlementBillImporter::ACCOUNT_NAME,
string $billItem = '技术服务费',
): array {
return [
$recordCode,
'拼多多-测试店铺',
'',
$billItem,
'ORDER-' . $recordCode,
'ONLINE-' . $recordCode,
$billItem,
'001|' . $billItem,
$account,
$occurredAt,
$income,
$expense,
$signed,
'-',
'2026-09-07 17:07:15',
'RPA',
];
}
/** @param list<list<string>> $rows */
private function xlsx(array $rows): string
{
$temporary = tempnam(sys_get_temp_dir(), 'pdd-authoritative-');
$path = $temporary . '.xlsx';
rename($temporary, $path);
$spreadsheet = new Spreadsheet();
$spreadsheet->getActiveSheet()->fromArray(array_merge([[
'记录编码', '店铺', '自定义备注', '账单项目', '商户订单号', '线上单号',
'财务类型', '业务描述', '类型', '发生时间', '收入金额', '支出金额', '金额',
'备注', '修改时间', '修改人',
]], $rows));
(new Xlsx($spreadsheet))->save($path);
$spreadsheet->disconnectWorksheets();
$this->files[] = $path;
return $path;
}
}