CaiWuXiTong/tests/Feature/Console/TaobaoAlipaySettlementBillImporterTest.php

170 lines
9.6 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\TaobaoAlipaySettlementBillRepository;
use Modules\EcommerceFinance\Services\TaobaoAlipaySettlementBillImporter;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use RuntimeException;
use Tests\TestCase;
class TaobaoAlipaySettlementBillImporterTest 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_authoritative_rows_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', '456.78', '交易收款'),
$this->row('REC-EXPENSE', '2026-08-02 12:00:00', '0', '25.00', '431.78', '退款'),
]);
$result = app(TaobaoAlipaySettlementBillImporter::class)->import(
$path, '2026-08-01', '2026-08-31', 2, '100.00', '25.00', 100,
);
$this->assertSame('succeeded', $result['status']);
$this->assertSame(2, $result['row_count']);
$this->assertSame(1, $result['store_count']);
$this->assertSame(2, $result['bill_item_count']);
$this->assertSame('100.00', $result['income_total']);
$this->assertSame('25.00', $result['expense_total']);
$this->assertSame('75.00', $result['net_total']);
$this->assertDatabaseMissing('taobao_alipay_bill_rows', ['record_code' => 'OLD-IN-PERIOD'], 'business_mirror');
$this->assertDatabaseHas('taobao_alipay_bill_rows', ['record_code' => 'KEEP-OUTSIDE-PERIOD'], 'business_mirror');
$this->assertTrue(app(TaobaoAlipaySettlementBillRepository::class)->ready());
$unchanged = app(TaobaoAlipaySettlementBillImporter::class)->import(
$path, '2026-08-01', '2026-08-31', 2, '100.00', '25.00', 100,
);
$this->assertSame('unchanged', $unchanged['status']);
$this->assertSame(3, DB::connection('business_mirror')->table('taobao_alipay_bill_rows')->count());
}
public function test_control_mismatch_and_duplicate_code_keep_active_rows(): 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(TaobaoAlipaySettlementBillImporter::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('taobao_alipay_bill_rows', ['record_code' => 'ACTIVE-ROW'], 'business_mirror');
$this->assertDatabaseMissing('taobao_alipay_bill_rows', ['record_code' => 'NEW-ROW'], 'business_mirror');
$this->assertSame(0, DB::connection('business_mirror')->table('taobao_alipay_bill_staging')->count());
$duplicate = $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', '30.00', '交易收款'),
]);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('记录编码重复');
app(TaobaoAlipaySettlementBillImporter::class)->import(
$duplicate, '2026-08-01', '2026-08-31', 2, '30.00', '0.00', 100,
);
}
private function createTables(): void
{
$schema = Schema::connection('business_mirror');
$schema->create('taobao_alipay_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->unsignedInteger('store_count')->default(0); $table->unsignedInteger('bill_item_count')->default(0);
$table->json('business_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('taobao_alipay_bill_staging', function (Blueprint $table): void {
$table->bigIncrements('id'); $table->uuid('generation'); $this->billColumns($table);
$table->unique(['generation', 'record_code']);
});
$schema->create('taobao_alipay_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('alipay_account')->nullable(); $table->string('account_order_no', 191)->nullable(); $table->string('business_serial_no', 191)->nullable();
$table->string('merchant_order_no', 191)->nullable(); $table->string('business_base_order_no', 191)->nullable(); $table->string('goods_name', 1000)->nullable();
$table->string('counterparty_account', 255)->nullable(); $table->dateTime('occurred_at');
$table->decimal('income_amount', 20, 6)->default(0); $table->decimal('expense_amount', 20, 6)->default(0); $table->decimal('balance_amount', 20, 6)->default(0);
$table->string('business_type')->nullable(); $table->string('remark', 1000)->nullable(); $table->string('online_order_no', 191)->nullable(); $table->string('online_real_store')->nullable();
$table->string('business_description', 1000)->nullable(); $table->string('business_bill_source', 255)->nullable(); $table->string('business_order_no', 191)->nullable();
$table->dateTime('source_created_at')->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('taobao_alipay_bill_rows')->insert([
'record_code' => $recordCode, 'store_name' => '原有店铺', 'bill_item' => '原有流水', 'occurred_at' => $occurredAt,
'income_amount' => 1, 'expense_amount' => 0, 'balance_amount' => 1, 'source_row_number' => 1,
'source_generation' => 'existing-generation', 'source_sha256' => str_repeat('a', 64), 'imported_at' => '2026-09-01 00:00:00',
]);
}
private function row(string $recordCode, string $occurredAt, string $income, string $expense, string $balance, string $businessType): array
{
return [
$recordCode, '淘宝-测试店铺', '', $businessType === '退款' ? '退款' : '交易收款', '2088480341979438',
'ACCOUNT-' . $recordCode, 'SERIAL-' . $recordCode, 'MERCHANT-' . $recordCode, 'BASE-' . $recordCode,
'测试商品', 'buyer@example.test', $occurredAt, $income, $expense, $balance, $businessType, '',
'ONLINE-' . $recordCode, '淘宝-测试店铺', '业务描述', '支付宝', 'BIZ-' . $recordCode, '2026-09-01 10:00:00', '2026-09-01 10:01:00', 'RPA',
];
}
private function xlsx(array $rows): string
{
$temporary = tempnam(sys_get_temp_dir(), 'taobao-alipay-authoritative-');
$path = $temporary . '.xlsx'; rename($temporary, $path);
$spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); $sheet->setTitle('淘系支付宝账户');
$sheet->fromArray([[
'记录编码', '原始店铺', '自定义备注', '账单项目', '支付宝账号', '账务订单号', '业务流水号', '商户订单号', '业务基础订单号', '商品名称', '对方账号', '发生时间', '收入', '支出', '余额', '业务类型', '备注', '线上单号', '线上单号对应实际店铺', '业务描述', '业务账单来源', '业务订单号', '创建时间', '修改时间', '修改人',
], ...$rows]);
(new Xlsx($spreadsheet))->save($path); $spreadsheet->disconnectWorksheets(); $this->files[] = $path;
return $path;
}
}