CaiWuXiTong/tests/Feature/Console/ThirdPartyWarehouseStorageAuthoritativeAnchorImportTest.php
limeng 179b2cdbcc 接入并校准三方仓进销存 (#176)
新增三方仓进销存共享镜像、按日查询汇总导出、每日零点增量同步和权威历史锚点校准。
2026-09-12 20:29:43 +08:00

237 lines
9.1 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\ThirdPartyWarehouseStorageAuthoritativeAnchorImporter;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Tests\TestCase;
class ThirdPartyWarehouseStorageAuthoritativeAnchorImportTest extends TestCase
{
/** @var list<string> */
private array $temporaryFiles = [];
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->temporaryFiles as $file) {
@unlink($file);
}
parent::tearDown();
}
public function test_it_imports_authoritative_daily_rows_and_month_end_anchor(): void
{
$path = $this->validWorkbook();
$this->artisan('business-mirror:import-third-party-warehouse-anchor', ['path' => $path])
->assertSuccessful();
$state = DB::connection('business_mirror')
->table('third_party_warehouse_authoritative_anchor_states')
->where('dataset_key', ThirdPartyWarehouseStorageAuthoritativeAnchorImporter::DATASET_KEY)
->first();
$this->assertSame('succeeded', $state->status);
$this->assertSame('2026-08-01', $state->source_period_start);
$this->assertSame('2026-08-31', $state->source_period_end);
$this->assertSame(32, (int) $state->source_row_count);
$this->assertSame(2, (int) $state->anchor_sku_count);
$this->assertSame('2026-09-01 00:00:00', $state->anchor_at);
$this->assertDatabaseHas('third_party_warehouse_authoritative_anchors', [
'generation' => $state->active_generation,
'sku_code' => 'SKU-A',
'qty' => 41,
'anchor_at' => '2026-09-01 00:00:00',
], 'business_mirror');
$this->assertDatabaseHas('third_party_warehouse_authoritative_anchors', [
'generation' => $state->active_generation,
'sku_code' => 'CODE+PLUS',
'qty' => 7,
], 'business_mirror');
$this->assertSame(
32,
DB::connection('business_mirror')->table('third_party_warehouse_authoritative_rows')
->where('generation', $state->active_generation)->count(),
);
$this->assertDatabaseHas('third_party_warehouse_authoritative_rows', [
'generation' => $state->active_generation,
'report_date' => '2026-08-01',
'sku_code' => 'SKU-A',
'opening_qty' => 10,
'closing_qty' => 11,
'purchase_in' => 1,
], 'business_mirror');
}
public function test_invalid_replacement_keeps_the_previous_active_generation(): void
{
$valid = app(ThirdPartyWarehouseStorageAuthoritativeAnchorImporter::class)
->import($this->validWorkbook());
$invalid = $this->workbook([
$this->headers(),
$this->row('2026-08-01', 'SKU-A', 10, 9, ['销售出仓' => -2]),
]);
$this->artisan('business-mirror:import-third-party-warehouse-anchor', ['path' => $invalid])
->expectsOutputToContain('已保留上一版有效数据')
->assertFailed();
$state = DB::connection('business_mirror')
->table('third_party_warehouse_authoritative_anchor_states')
->first();
$this->assertSame('failed', $state->status);
$this->assertSame($valid['generation'], $state->active_generation);
$this->assertNull($state->pending_generation);
$this->assertSame(
2,
DB::connection('business_mirror')
->table('third_party_warehouse_authoritative_anchors')
->where('generation', $valid['generation'])
->count(),
);
$this->assertSame(
32,
DB::connection('business_mirror')
->table('third_party_warehouse_authoritative_rows')
->where('generation', $valid['generation'])
->count(),
);
}
private function createTables(): void
{
$schema = Schema::connection('business_mirror');
$schema->create('third_party_warehouse_authoritative_anchor_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->string('status');
$table->string('active_generation')->nullable();
$table->string('pending_generation')->nullable();
$table->dateTime('anchor_at')->nullable();
$table->string('source_filename')->nullable();
$table->string('source_sha256')->nullable();
$table->date('source_period_start')->nullable();
$table->date('source_period_end')->nullable();
$table->unsignedBigInteger('source_row_count')->default(0);
$table->unsignedBigInteger('anchor_sku_count')->default(0);
$table->dateTime('last_attempted_at')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
$table->string('last_error_code')->nullable();
$table->timestamps();
});
$schema->create('third_party_warehouse_authoritative_anchors', function (Blueprint $table): void {
$table->string('generation');
$table->string('sku_code');
$table->decimal('qty', 20, 4);
$table->dateTime('anchor_at');
$table->dateTime('imported_at');
$table->primary(['generation', 'sku_code']);
});
$schema->create('third_party_warehouse_authoritative_rows', function (Blueprint $table): void {
$table->string('generation');
$table->date('report_date');
$table->string('sku_code');
$table->decimal('opening_qty', 20, 4);
$table->decimal('closing_qty', 20, 4);
foreach ([
'processing_in', 'processing_out', 'purchase_in', 'purchase_return',
'other_in', 'other_return', 'sales_out', 'sales_return', 'stocktake',
'transfer_out', 'transfer_in', 'opening_aux', 'other_out',
] as $column) {
$table->decimal($column, 20, 4)->default(0);
}
$table->dateTime('imported_at');
$table->primary(['generation', 'report_date', 'sku_code']);
});
}
private function validWorkbook(): string
{
$rows = [$this->headers()];
for ($day = 1; $day <= 31; $day++) {
$rows[] = $this->row(
sprintf('2026-08-%02d', $day),
'SKU-A',
10 + $day - 1,
10 + $day,
['采购进仓' => 1],
);
}
// 聚水潭会省略部分没有变化的月末日期;每个商品取自身最后一行期末数。
$rows[] = $this->row('2026-08-30', 'CODE_x002B_PLUS', 7, 7);
return $this->workbook($rows);
}
/** @return list<string> */
private function headers(): array
{
return [
'货主', '日期', '商品编码', '图片', '商品名称', '商品简称', '供应商',
'分类', '虚拟分类', '供应商款号', '颜色规格', '品牌', '单位', '期初数量',
'期末数量', '加工进仓', '加工出仓', '采购进仓', '采购退货', '其它进仓',
'其它退货', '销售出仓', '销售退货', '盘点', '调拨出', '调拨入', '期初',
'其它出库',
];
}
/** @param array<string,int|float> $movements @return list<string|int|float|null> */
private function row(
string $date,
string $sku,
float $opening,
float $closing,
array $movements = [],
): array {
$row = array_fill(0, 28, null);
$row[0] = '常州简尚家居有限公司';
$row[1] = $date;
$row[2] = $sku;
$row[13] = $opening;
$row[14] = $closing;
foreach ([
'加工进仓', '加工出仓', '采购进仓', '采购退货', '其它进仓', '其它退货',
'销售出仓', '销售退货', '盘点', '调拨出', '调拨入', '期初', '其它出库',
] as $offset => $header) {
$row[15 + $offset] = $movements[$header] ?? 0;
}
return $row;
}
/** @param list<list<string|int|float|null>> $rows */
private function workbook(array $rows): string
{
$temporary = tempnam(sys_get_temp_dir(), 'tpw-anchor-');
$path = $temporary . '.xlsx';
rename($temporary, $path);
$spreadsheet = new Spreadsheet();
$spreadsheet->getActiveSheet()->fromArray($rows);
(new Xlsx($spreadsheet))->save($path);
$spreadsheet->disconnectWorksheets();
$this->temporaryFiles[] = $path;
return $path;
}
}