237 lines
9.1 KiB
PHP
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;
|
|
}
|
|
}
|