CaiWuXiTong/tests/Unit/EcommerceFinance/ThirdPartyWarehouseStorageMirrorRepositoryTest.php

1355 lines
61 KiB
PHP

<?php
namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Repositories\ThirdPartyWarehouseStorageMirrorRepository;
use RuntimeException;
use Tests\TestCase;
class ThirdPartyWarehouseStorageMirrorRepositoryTest extends TestCase
{
private const HEADER_GENERATION = '11111111-1111-1111-1111-111111111111';
private const ITEM_GENERATION = '22222222-2222-2222-2222-222222222222';
private const INVENTORY_GENERATION = '33333333-3333-3333-3333-333333333333';
private ThirdPartyWarehouseStorageMirrorRepository $repository;
private int $nextHeaderId = 1;
private int $nextItemId = 1;
protected function setUp(): void
{
parent::setUp();
config([
'database.connections.business_mirror' => [
'driver' => 'sqlite',
'database' => ':memory:',
'prefix' => '',
],
'upstream_mirrors.connection' => 'business_mirror',
'upstream_mirrors.third_party_warehouse_storage.owner_name' => '常州简尚家居有限公司',
'upstream_mirrors.third_party_warehouse_storage.stale_after_minutes' => 1560,
]);
DB::purge('business_mirror');
$this->createMirrorTables();
$this->createProductTable();
$this->createWarehouseTable();
$this->createDependencyStateTable();
$this->repository = new ThirdPartyWarehouseStorageMirrorRepository(app('db'));
}
public function test_it_refuses_queries_until_all_active_generations_finish_initial_sync(): void
{
$this->insertState([
'status' => 'running',
'run_mode' => 'full',
'active_header_generation' => self::HEADER_GENERATION,
'active_item_generation' => self::ITEM_GENERATION,
'active_inventory_generation' => null,
'initial_completed_at' => null,
]);
$this->assertTrue($this->repository->available());
$this->assertFalse($this->repository->ready());
$this->assertSame('initializing', $this->repository->syncStatus()['code']);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('third_party_warehouse_mirror_not_ready');
$this->repository->paginate($this->filters(), 1, 25);
}
public function test_failed_refresh_keeps_reading_only_active_generations(): void
{
$this->markReady(['status' => 'failed', 'last_error_code' => 'upstream_timeout']);
$this->insertProduct('SKU-A', ['autoid' => 10]);
$this->insertInventory('SKU-A', 6, '2026-09-01 00:00:00');
$this->insertInventory('SKU-OLD', 99, '2026-09-01 00:00:00', [
'generation' => '99999999-9999-9999-9999-999999999999',
]);
$this->insertMovement('SKU-A', '采购进仓', 1, 1, '2026-08-10 10:00:00');
$this->insertMovement('SKU-OLD', '采购进仓', 99, 1, '2026-08-10 10:00:00', [
'header_generation' => '99999999-9999-9999-9999-999999999999',
'item_generation' => '99999999-9999-9999-9999-999999999999',
]);
$result = $this->repository->paginate($this->filters(), 1, 25);
$this->assertTrue($this->repository->ready());
$this->assertSame('failed', $result['sync']['code']);
// 月份维度按“日期 + 商品”展开,因此 8 月对应 31 条日明细。
$this->assertSame(31, $result['total']);
$this->assertSame('SKU-A', $result['data'][0]['sku_code']);
}
public function test_month_filter_returns_daily_rows_using_signed_movements_and_each_sku_snapshot_anchor(): void
{
$this->markReady();
$this->insertProduct('SKU-MONTH', [
'name' => '月报商品',
'short_name' => null,
'supplier_name' => '供应商甲',
'category' => '桌类',
'purchase_price' => 30,
]);
$this->insertInventory('SKU-MONTH', 25, '2026-09-03 12:00:00');
$this->insertMovement('SKU-MONTH', '采购进仓', 10, 1, '2026-08-01 00:00:00');
$this->insertMovement('SKU-MONTH', '销售出仓', 8, -1, '2026-08-15 12:00:00');
$this->insertMovement('SKU-MONTH', '其它进仓', 2, 1, '2026-08-31 23:59:59');
$this->insertMovement('SKU-MONTH', '采购进仓', 5, 1, '2026-09-01 00:00:00');
$this->insertMovement('SKU-MONTH', '销售出仓', 4, -1, '2026-09-02 00:00:00');
// 在该 SKU 自己的快照时间之后,不能参与从快照回推历史余额。
$this->insertMovement('SKU-MONTH', '采购进仓', 100, 1, '2026-09-04 00:00:00');
// 不提供统一 period 时仍兼容旧页面的 month 参数。
$rows = $this->repository->paginate($this->filters(['period' => '']), 1, 100)['data'];
$first = collect($rows)->firstWhere('date', '2026-08-01');
$salesDay = collect($rows)->firstWhere('date', '2026-08-15');
$last = collect($rows)->firstWhere('date', '2026-08-31');
$this->assertCount(31, $rows);
$this->assertSame('常州简尚家居有限公司', $first['owner']);
$this->assertSame('月报商品', $first['short_name']);
$this->assertSame(10.0, $first['purchase_in']);
$this->assertSame(20.0, $first['opening_qty']);
$this->assertSame(30.0, $first['closing_qty']);
$this->assertSame(-8.0, $salesDay['sales_out']);
$this->assertSame(30.0, $salesDay['opening_qty']);
$this->assertSame(22.0, $salesDay['closing_qty']);
$this->assertSame(2.0, $last['other_in']);
$this->assertSame(22.0, $last['opening_qty']);
$this->assertSame(24.0, $last['closing_qty']);
$this->assertCount(28, $first);
$this->insertProduct('SKU-EARLY', ['autoid' => 20, 'purchase_price' => 2]);
$this->insertInventory('SKU-EARLY', 10, '2026-08-10 00:00:00');
$this->insertMovement('SKU-EARLY', '采购进仓', 5, 1, '2026-08-20 00:00:00');
$early = collect($this->repository->paginate($this->filters([
'dimension' => 'day',
'period' => '2026-08-20',
]), 1, 25)['data'])->firstWhere('sku_code', 'SKU-EARLY');
$this->assertSame(10.0, $early['opening_qty']);
$this->assertSame(15.0, $early['closing_qty']);
}
public function test_inventory_report_aggregates_the_same_confirmed_movements_by_product_warehouse_and_month(): void
{
$this->markReady();
$this->insertProduct('SKU-MONTHLY', ['category' => '住宅家具 - 书桌']);
$this->insertInventory('SKU-MONTHLY', 12, '2026-09-01 00:00:00');
$this->insertMovement('SKU-MONTHLY', '采购进仓', 5, 1, '2026-08-02 10:00:00');
$this->insertMovement('SKU-MONTHLY', '销售出仓', 3, -1, '2026-08-15 10:00:00');
$this->insertMovement('SKU-MONTHLY', '销售退货', 2, 1, '2026-08-20 10:00:00');
$result = $this->repository->paginateMonthlyInventoryReport([
'month' => '2026-08',
'product_code' => 'SKU-MONTHLY',
'warehouse_provider' => '南昌樵舍',
'category' => '书桌',
], 1, 25);
$row = $result['data'][0];
$this->assertSame(1, $result['total']);
$this->assertSame('2026-08-01', $row->month);
$this->assertSame('三方-南昌樵舍仓', $row->warehouse_provider);
$this->assertSame('三方-南昌樵舍仓主仓', $row->warehouse);
$this->assertSame('SKU-MONTHLY', $row->product_code);
$this->assertSame('住宅家具 - 书桌', $row->category);
$this->assertSame(8.0, (float) $row->opening_quantity);
$this->assertSame(12.0, (float) $row->closing_quantity);
$this->assertSame(5.0, (float) $row->purchase_in);
$this->assertSame(-3.0, (float) $row->sales_out);
$this->assertSame(2.0, (float) $row->sales_return);
}
public function test_inventory_report_prefers_authoritative_month_rows_with_exact_opening_quantities(): void
{
$this->markReady();
$this->insertProduct('SKU-AUTH', ['category' => '权威分类']);
$this->insertInventory('SKU-AUTH', 999, '2026-09-01 00:00:00');
$this->insertMovement('SKU-AUTH', '采购进仓', 500, 1, '2026-08-20 10:00:00', [
'warehouse_id' => 15038426,
'wh_id' => 3,
]);
$this->insertMovement('SKU-AUTH', '销售出仓', 2, -1, '2026-08-21 10:00:00', [
'warehouse_id' => 15038426,
'wh_id' => 3,
]);
$this->insertMovement('SKU-AUTH', '采购进仓', 90, 1, '2026-09-01 00:00:00', [
'warehouse_id' => 15038426,
'wh_id' => 3,
]);
$this->insertInventoryReportAuthoritativeRow([
'warehouse_provider' => '简尚-崔桥工厂仓',
'warehouse' => '简尚-崔桥工厂仓进货仓',
'product_code' => 'SKU-AUTH',
'category' => '权威分类',
'opening_quantity' => 14,
'closing_quantity' => 20,
'purchase_in' => 6,
]);
$result = $this->repository->paginateMonthlyInventoryReport([
'month' => '2026-08',
'product_code' => 'SKU-AUTH',
'warehouse_provider' => '崔桥',
'category' => '权威',
], 1, 25);
$row = $result['data'][0];
$this->assertSame(1, $result['total']);
$this->assertSame('简尚-崔桥工厂仓进货仓', $row->warehouse);
$this->assertSame(14.0, (float) $row->opening_quantity);
$this->assertSame(20.0, (float) $row->closing_quantity);
$this->assertSame(6.0, (float) $row->purchase_in);
$this->assertSame(0.0, (float) $row->sales_out);
}
public function test_inventory_report_export_reads_current_authoritative_month_without_movement_scan(): void
{
$this->markReady();
$this->insertProduct('SKU-AUTH-EXPORT');
$this->insertMovement('SKU-AUTH-EXPORT', '采购进仓', 999, 1, '2026-08-20 10:00:00');
$this->insertInventoryReportAuthoritativeRow([
'warehouse_provider' => '简尚-崔桥工厂仓',
'warehouse' => '简尚-崔桥工厂仓进货仓',
'product_code' => 'SKU-AUTH-EXPORT',
'opening_quantity' => 14,
'closing_quantity' => 20,
'purchase_in' => 6,
]);
$rows = iterator_to_array($this->repository->exportMonthlyInventoryReportRows([
'month' => '2026-08',
'product_code' => 'SKU-AUTH-EXPORT',
'warehouse_provider' => '',
'category' => '',
], 1), false);
$this->assertCount(1, $rows);
$this->assertSame(14.0, (float) $rows[0]->opening_quantity);
$this->assertSame(6.0, (float) $rows[0]->purchase_in);
$this->assertSame(20.0, (float) $rows[0]->closing_quantity);
}
public function test_inventory_report_rolls_previous_month_closing_into_next_month_opening(): void
{
$this->markReady();
$this->insertProduct('SKU-ROLL-FORWARD');
$this->insertInventory('SKU-ROLL-FORWARD', 999, '2026-09-11 03:21:00');
$this->insertInventoryReportAuthoritativeRow([
'warehouse_provider' => '三方-南昌樵舍仓',
'warehouse' => '三方-南昌樵舍仓主仓',
'product_code' => 'SKU-ROLL-FORWARD',
'opening_quantity' => 14,
'closing_quantity' => 20,
'purchase_in' => 6,
]);
$this->insertMovement('SKU-ROLL-FORWARD', '采购进仓', 3, 1, '2026-09-02 10:00:00');
$this->insertInventory('SKU-ROLL-FORWARD', 5, '2026-09-11 03:21:00', [
'wms_co_id' => 15038426,
]);
$this->insertMovement('SKU-ROLL-FORWARD', '调拨入', 5, 1, '2026-09-03 10:00:00', [
'warehouse_id' => 15038426,
]);
$db = DB::connection('business_mirror');
$db->flushQueryLog();
$db->enableQueryLog();
$filters = [
'month' => '2026-09',
'product_code' => 'SKU-ROLL-FORWARD',
'warehouse_provider' => '',
'category' => '',
];
$result = $this->repository->paginateMonthlyInventoryReport($filters, 1, 25);
$queries = $db->getQueryLog();
$db->disableQueryLog();
$rows = collect($result['data'])->keyBy('warehouse');
$this->assertSame(2, $result['total']);
$this->assertSame(20.0, (float) $rows['三方-南昌樵舍仓主仓']->opening_quantity);
$this->assertSame(23.0, (float) $rows['三方-南昌樵舍仓主仓']->closing_quantity);
$this->assertSame(3.0, (float) $rows['三方-南昌樵舍仓主仓']->purchase_in);
$this->assertSame(0.0, (float) $rows['简尚-崔桥工厂仓主仓']->opening_quantity);
$this->assertSame(5.0, (float) $rows['简尚-崔桥工厂仓主仓']->closing_quantity);
$this->assertSame(5.0, (float) $rows['简尚-崔桥工厂仓主仓']->transfer_in);
$countQueries = array_values(array_filter(
$queries,
static fn (array $query): bool => str_contains(strtolower($query['query']), 'count(*)'),
));
$this->assertCount(3, $countQueries);
$this->assertFalse(collect($queries)->contains(
static fn (array $query): bool => str_contains(strtolower($query['query']), 'count(*) over()'),
));
$candidatePage = collect($queries)
->first(static fn (array $query): bool => str_contains($query['query'], '"prior"."report_month"'));
$this->assertNotNull($candidatePage);
$this->assertStringNotContainsString('strftime', strtolower($candidatePage['query']));
$db->flushQueryLog();
$db->enableQueryLog();
$cached = $this->repository->paginateMonthlyInventoryReport($filters, 1, 25);
$cachedQueries = $db->getQueryLog();
$db->disableQueryLog();
$this->assertSame($result['total'], $cached['total']);
$this->assertCount(0, array_filter(
$cachedQueries,
static fn (array $query): bool => str_contains(strtolower($query['query']), 'report_total'),
));
}
public function test_inventory_report_rolls_multiple_months_from_the_july_authoritative_baseline(): void
{
$this->markReady();
$this->insertProduct('SKU-ROLL-MULTI');
$this->insertInventory('SKU-ROLL-MULTI', 999, '2026-09-11 03:21:00');
$this->insertInventoryReportAuthoritativeRow([
'source_month' => '2026-07-01',
'report_month' => '2026-07-01',
'warehouse_provider' => '三方-南昌樵舍仓',
'warehouse' => '三方-南昌樵舍仓主仓',
'product_code' => 'SKU-ROLL-MULTI',
'opening_quantity' => 10,
'closing_quantity' => 12,
]);
$this->insertMovement('SKU-ROLL-MULTI', '采购进仓', 3, 1, '2026-08-02 10:00:00');
$this->insertMovement('SKU-ROLL-MULTI', '销售出仓', 2, -1, '2026-09-03 10:00:00');
$result = $this->repository->paginateMonthlyInventoryReport([
'month' => '2026-09',
'product_code' => 'SKU-ROLL-MULTI',
'warehouse_provider' => '',
'category' => '',
], 1, 25);
$row = $result['data'][0];
$this->assertSame(1, $result['total']);
$this->assertSame(15.0, (float) $row->opening_quantity);
$this->assertSame(13.0, (float) $row->closing_quantity);
$this->assertSame(-2.0, (float) $row->sales_out);
}
public function test_inventory_report_includes_confirmed_movement_subwarehouse_without_current_stock(): void
{
$this->markReady();
$this->insertProduct('CTG008616N1152V01');
$this->insertInventoryReportAuthoritativeRow([
'source_month' => '2026-07-01',
'report_month' => '2026-07-01',
'warehouse_provider' => '简尚-崔桥工厂仓',
'warehouse' => '简尚-崔桥工厂仓主仓',
'product_code' => 'OTHER-SKU',
]);
$this->insertInventory('CTG008616N1152V01', 39, '2026-09-11 03:21:00', [
'wms_co_id' => 15038426,
'in_qty' => 0,
]);
foreach ([30, 50, 30] as $quantity) {
$this->insertMovement('CTG008616N1152V01', '采购进仓', $quantity, 1, '2026-08-23 10:00:00', [
'warehouse_id' => 15038426,
'wh_id' => 3,
]);
}
// 按确认的进出仓明细口径汇总日期、类型、仓储方、仓库、商品和带符号数量。
$this->insertMovement('CTG008616N1152V01', '销售出仓', 1, -1, '2026-08-24 09:00:00', [
'warehouse_id' => 15038426,
'wh_id' => 3,
]);
$this->insertMovement('CTG008616N1152V01', '销售出仓', 2, -1, '2026-08-24 09:30:00', [
'warehouse_id' => 15038426,
'wh_id' => 3,
]);
$this->insertMovement('CTG008616N1152V01', '销售出仓', 999, -1, '2026-08-24 10:00:00', [
'warehouse_id' => 15038426,
'wh_id' => 3,
]);
$this->insertMovement('CTG008616N1152V01', '采购进仓', 999, 1, '2026-08-25 10:00:00', [
'warehouse_id' => 15038426,
'wh_id' => 3,
'status' => 'WaitConfirm',
]);
$filters = [
'month' => '2026-08',
'product_code' => 'CTG008616N1152V01',
'warehouse_provider' => '崔桥工厂仓',
'category' => '',
];
$august = $this->repository->paginateMonthlyInventoryReport($filters, 1, 25);
$rows = collect($august['data'])->keyBy('warehouse');
$this->assertSame(2, $august['total']);
$this->assertSame(0.0, (float) $rows['简尚-崔桥工厂仓进货仓']->opening_quantity);
$this->assertSame(110.0, (float) $rows['简尚-崔桥工厂仓进货仓']->purchase_in);
$this->assertSame(-1002.0, (float) $rows['简尚-崔桥工厂仓进货仓']->sales_out);
$this->assertSame(0.0, (float) $rows['简尚-崔桥工厂仓进货仓']->transfer_out);
$this->assertSame(-892.0, (float) $rows['简尚-崔桥工厂仓进货仓']->closing_quantity);
$secondPage = $this->repository->paginateMonthlyInventoryReport($filters, 2, 1);
$this->assertSame(2, $secondPage['total']);
$this->assertSame('简尚-崔桥工厂仓进货仓', $secondPage['data'][0]->warehouse);
$categorized = $this->repository->paginateMonthlyInventoryReport(
array_merge($filters, ['category' => '测试分类']), 1, 25,
);
$this->assertSame(2, $categorized['total']);
$this->assertSame(-892.0, (float) collect($categorized['data'])
->firstWhere('warehouse', '简尚-崔桥工厂仓进货仓')->closing_quantity);
$exported = iterator_to_array($this->repository->exportMonthlyInventoryReportRows($filters, 1));
$this->assertCount(2, $exported);
$this->assertSame(110.0, (float) collect($exported)->firstWhere('warehouse', '简尚-崔桥工厂仓进货仓')->purchase_in);
$september = $this->repository->paginateMonthlyInventoryReport(
array_merge($filters, ['month' => '2026-09']), 1, 25,
);
$nextRow = collect($september['data'])->firstWhere('warehouse', '简尚-崔桥工厂仓进货仓');
$this->assertSame(-892.0, (float) $nextRow->opening_quantity);
$this->assertSame(-892.0, (float) $nextRow->closing_quantity);
}
public function test_inventory_report_product_code_filter_is_exact(): void
{
$this->markReady();
$this->insertProduct('15962G');
$this->insertProduct('15962G-1');
$this->insertInventory('15962G', 1, '2026-09-01 00:00:00');
$this->insertInventory('15962G-1', 1, '2026-09-01 00:00:00');
$result = $this->repository->paginateMonthlyInventoryReport([
'month' => '2026-08',
'product_code' => '15962G',
'warehouse_provider' => '',
'category' => '',
], 1, 25);
$this->assertSame(1, $result['total']);
$this->assertSame(['15962G'], collect($result['data'])->pluck('product_code')->all());
}
public function test_inventory_report_splits_main_return_inbound_and_other_subwarehouses(): void
{
$this->markReady();
$this->insertProduct('SKU-SUBWAREHOUSE', ['category' => '测试分类']);
$this->insertInventory('SKU-SUBWAREHOUSE', 10, '2026-09-01 00:00:00', [
'return_qty' => 4,
'in_qty' => 3,
'customize_qty_1' => 2,
]);
$this->insertMovement('SKU-SUBWAREHOUSE', '销售退货', 2, 1, '2026-08-20 10:00:00', ['wh_id' => 2]);
$this->insertMovement('SKU-SUBWAREHOUSE', '采购进仓', 3, 1, '2026-08-21 10:00:00', ['wh_id' => 3]);
$result = $this->repository->paginateMonthlyInventoryReport([
'month' => '2026-08',
'product_code' => 'SKU-SUBWAREHOUSE',
'warehouse_provider' => '',
'category' => '',
], 1, 25);
$rows = collect($result['data'])->keyBy('warehouse');
$this->assertSame(4, $result['total']);
$this->assertSame([
'三方-南昌樵舍仓主仓',
'三方-南昌樵舍仓销退仓',
'三方-南昌樵舍仓进货仓',
'三方-南昌樵舍仓自定义仓1',
], $rows->keys()->all());
$this->assertSame(2.0, (float) $rows['三方-南昌樵舍仓销退仓']->opening_quantity);
$this->assertSame(4.0, (float) $rows['三方-南昌樵舍仓销退仓']->closing_quantity);
$this->assertSame(2.0, (float) $rows['三方-南昌樵舍仓销退仓']->sales_return);
$this->assertSame(0.0, (float) $rows['三方-南昌樵舍仓进货仓']->opening_quantity);
$this->assertSame(3.0, (float) $rows['三方-南昌樵舍仓进货仓']->closing_quantity);
}
public function test_inventory_report_uses_export_warehouse_names_for_special_subwarehouses(): void
{
$this->markReady();
DB::connection('business_mirror')->table('ops_jst_warehouse_info')->insert([
'Id' => 3,
'wms_co_id' => 15033302,
'name' => '简尚-横林二仓',
]);
$this->insertProduct('SKU-SPECIAL-WAREHOUSE');
$this->insertInventory('SKU-SPECIAL-WAREHOUSE', 8, '2026-09-01 00:00:00', [
'wms_co_id' => 15033302,
'customize_qty_1' => 5,
'customize_qty_2' => 2,
]);
$result = $this->repository->paginateMonthlyInventoryReport([
'month' => '2026-08',
'product_code' => 'SKU-SPECIAL-WAREHOUSE',
'warehouse_provider' => '',
'category' => '',
], 1, 25);
$this->assertSame([
'简尚-横林二仓主仓',
'客退临时上架仓',
'数量校验仓',
], collect($result['data'])->pluck('warehouse')->all());
}
public function test_inventory_report_uses_accessory_warehouse_name_for_company_inventory(): void
{
$this->markReady();
DB::connection('business_mirror')->table('ops_jst_warehouse_info')->insert([
'Id' => 3,
'wms_co_id' => 14138566,
'name' => '常州简尚家居有限公司',
]);
$this->insertProduct('121630-49');
$this->insertInventory('121630-49', -2, '2026-09-01 00:00:00', [
'wms_co_id' => 14138566,
]);
$result = $this->repository->paginateMonthlyInventoryReport([
'month' => '2026-08',
'product_code' => '121630-49',
'warehouse_provider' => '',
'category' => '',
], 1, 25);
$this->assertSame('配件单仓', $result['data'][0]->warehouse);
}
public function test_inventory_report_reads_and_filters_warehouses_outside_the_third_party_scope(): void
{
$this->markReady();
$this->insertProduct('SKU-ALL-WAREHOUSES', ['category' => '住宅家具 - 餐边柜']);
$this->insertInventory('SKU-ALL-WAREHOUSES', 9, '2026-09-01 00:00:00', [
'wms_co_id' => 15038426,
]);
$this->insertMovement('SKU-ALL-WAREHOUSES', '销售出仓', 20, -1, '2026-08-15 10:00:00', [
'warehouse_id' => 15038426,
]);
$result = $this->repository->paginateMonthlyInventoryReport([
'month' => '2026-08',
'product_code' => 'SKU-ALL-WAREHOUSES',
'warehouse_provider' => '崔桥',
'category' => '餐边柜',
], 1, 25);
$this->assertSame(1, $result['total']);
$this->assertSame('简尚-崔桥工厂仓', $result['data'][0]->warehouse_provider);
$this->assertSame('SKU-ALL-WAREHOUSES', $result['data'][0]->product_code);
$this->assertSame(-20.0, (float) $result['data'][0]->sales_out);
}
public function test_inventory_report_uses_only_product_codes_matched_to_the_operations_replica_catalog(): void
{
$this->markReady();
$this->insertProduct('SKU-STOCK');
$this->insertInventory('SKU-STOCK', 2, '2026-09-01 00:00:00');
$this->insertInventory('LEGACY-CODE-NOT-IN-CATALOG', 7, '2026-09-01 00:00:00');
$result = $this->repository->paginateMonthlyInventoryReport([
'month' => '2026-08',
'product_code' => '',
'warehouse_provider' => '',
'category' => '',
], 1, 25);
$codes = collect($result['data'])->pluck('product_code')->all();
$this->assertSame(1, $result['total']);
$this->assertSame(['SKU-STOCK'], $codes);
$this->assertNotContains('LEGACY-CODE-NOT-IN-CATALOG', $codes);
}
public function test_inventory_report_export_reads_roll_forward_rows_without_total_count(): void
{
$this->markReady();
$this->insertProduct('SKU-EXPORT');
$this->insertInventory('SKU-EXPORT', 999, '2026-09-11 03:21:00');
$this->insertInventoryReportAuthoritativeRow([
'source_month' => '2026-07-01',
'report_month' => '2026-07-01',
'warehouse_provider' => '三方-南昌樵舍仓',
'warehouse' => '三方-南昌樵舍仓主仓',
'product_code' => 'SKU-EXPORT',
'opening_quantity' => 10,
'closing_quantity' => 12,
]);
$this->insertMovement('SKU-EXPORT', '采购进仓', 3, 1, '2026-08-02 10:00:00');
$rows = iterator_to_array($this->repository->exportMonthlyInventoryReportRows([
'month' => '2026-08',
'product_code' => 'SKU-EXPORT',
'warehouse_provider' => '',
'category' => '',
], 1), false);
$this->assertCount(1, $rows);
$this->assertSame('SKU-EXPORT', $rows[0]->product_code);
$this->assertSame(12.0, (float) $rows[0]->opening_quantity);
$this->assertSame(15.0, (float) $rows[0]->closing_quantity);
}
public function test_third_party_page_remains_ready_when_only_the_all_warehouse_dimension_is_missing(): void
{
$this->markReady();
DB::connection('business_mirror')->table('business_mirror_table_states')
->where('dataset_key', 'operations_replica.rpt.jst_warehouseInfo')
->delete();
Schema::connection('business_mirror')->drop('ops_jst_warehouse_info');
$this->assertTrue($this->repository->ready());
$this->assertFalse($this->repository->allWarehousesReady());
}
public function test_monthly_inventory_report_supports_july_without_changing_flow_page_cutoff(): void
{
$this->markReady();
$this->insertProduct('SKU-CUTOFF');
$this->insertInventory('SKU-CUTOFF', 10, '2026-08-01 00:00:00');
$this->insertMovement('SKU-CUTOFF', '采购进仓', 2, 1, '2026-07-15 12:00:00');
$this->insertMovement('SKU-CUTOFF', '采购进仓', 2, 1, '2026-08-01 12:00:00');
$day = $this->repository->paginate($this->filters([
'dimension' => 'day',
'period' => '2026-07-01',
]), 1, 25)['data'][0];
$inventoryReport = $this->repository->paginateMonthlyInventoryReport([
'month' => '2026-07',
'product_code' => 'SKU-CUTOFF',
'warehouse_provider' => '',
'category' => '',
], 1, 25);
$this->assertSame('2026-08-01', $day['date']);
$this->assertSame(1, $inventoryReport['total']);
$this->assertSame('2026-07-01', $inventoryReport['data'][0]->month);
$this->assertSame(8.0, (float) $inventoryReport['data'][0]->opening_quantity);
$this->assertSame(10.0, (float) $inventoryReport['data'][0]->closing_quantity);
}
public function test_product_catalog_must_finish_initial_sync_before_product_fields_are_exposed(): void
{
$this->markReady();
DB::connection('business_mirror')->table('business_mirror_table_states')
->where('dataset_key', 'operations_replica.rpt.jst_sku')
->update(['initial_completed_at' => null, 'status' => 'running']);
$this->assertFalse($this->repository->ready());
$this->assertSame('dependency_pending', $this->repository->syncStatus()['code']);
}
public function test_product_join_only_reads_normal_product_rows(): void
{
$this->markReady();
$this->insertProduct('SKU-SCOPE', [
'autoid' => 10,
'name' => '普通商品',
]);
$this->insertProduct('SKU-SCOPE', [
'autoid' => 10,
'sku_type' => 'combine',
'name' => '组合商品',
]);
$this->insertInventory('SKU-SCOPE', 1, '2026-08-15 12:00:00');
$result = $this->repository->paginate($this->filters([
'dimension' => 'day',
'period' => '2026-08-15',
]), 1, 25);
$this->assertSame(1, $result['total']);
$this->assertCount(1, $result['data']);
$this->assertSame('普通商品', $result['data'][0]['product_name']);
}
public function test_summary_without_product_filters_does_not_join_the_product_catalog(): void
{
$this->markReady();
$this->insertProduct('SKU-SUMMARY');
$this->insertInventory('SKU-SUMMARY', 1, '2026-08-15 12:00:00');
$queries = [];
DB::connection('business_mirror')->listen(function ($query) use (&$queries): void {
$queries[] = $query->sql;
});
$summary = $this->repository->summary($this->filters());
$this->assertSame(31, $summary['count']);
$this->assertFalse(collect($queries)->contains(
static fn (string $sql): bool => str_contains($sql, 'ops_jst_sku'),
));
}
public function test_day_dimension_uses_half_open_boundary(): void
{
$this->markReady();
$this->insertProduct('SKU-DAY', ['name' => '按天商品', 'purchase_price' => 10]);
$this->insertInventory('SKU-DAY', 22, '2026-08-17 00:00:00');
$this->insertMovement('SKU-DAY', '采购进仓', 10, 1, '2026-08-15 00:00:00');
$this->insertMovement('SKU-DAY', '销售出仓', 4, -1, '2026-08-15 23:59:59');
// 次日零点不属于 8 月 15 日,但属于日末到库存锚点之间的回推区间。
$this->insertMovement('SKU-DAY', '采购进仓', 3, 1, '2026-08-16 00:00:00');
$row = $this->repository->paginate($this->filters([
'dimension' => 'day',
'period' => '2026-08-15',
'month' => '',
]), 1, 25)['data'][0];
$this->assertSame(10.0, $row['purchase_in']);
$this->assertSame(-4.0, $row['sales_out']);
$this->assertSame(19.0, $row['closing_qty']);
$this->assertSame(13.0, $row['opening_qty']);
}
public function test_authoritative_rows_override_historical_stream_and_anchor_rolls_into_september(): void
{
$this->markReady();
$this->insertProduct('SKU-A');
// 实时库存会持续变化;存在权威月末锚点后不能再用它回推历史。
$this->insertInventory('SKU-A', 999, '2026-09-11 03:21:00');
$this->insertAuthoritativeAnchor('SKU-A', 20, '2026-09-01 00:00:00');
$this->insertAuthoritativeRow('SKU-A', '2026-08-01', 17, 22, ['purchase_in' => 5]);
$this->insertAuthoritativeRow('SKU-A', '2026-08-24', 22, 20, ['sales_out' => -2]);
$this->insertAuthoritativeRow('SKU-A', '2026-08-31', 20, 20);
$this->insertMovement('SKU-A', '采购进仓', 5, 1, '2026-08-01 00:00:00');
$this->insertMovement('SKU-A', '销售出仓', 2, -1, '2026-08-24 12:00:00', [
'status' => 'OuterConfirming',
]);
$this->insertMovement('SKU-A', '采购进仓', 3, 1, '2026-09-01 00:00:00');
$august = $this->repository->paginate($this->filters(), 1, 100)['data'];
$augustFirst = collect($august)->firstWhere('date', '2026-08-01');
$augustOuterConfirming = collect($august)->firstWhere('date', '2026-08-24');
$augustLast = collect($august)->firstWhere('date', '2026-08-31');
$septemberFirst = $this->repository->paginate($this->filters([
'dimension' => 'day',
'period' => '2026-09-01',
]), 1, 25)['data'][0];
$this->assertSame(17.0, $augustFirst['opening_qty']);
$this->assertSame(22.0, $augustFirst['closing_qty']);
$this->assertSame(-2.0, $augustOuterConfirming['sales_out']);
$this->assertSame(20.0, $augustOuterConfirming['closing_qty']);
$this->assertSame(20.0, $augustLast['closing_qty']);
$this->assertSame(20.0, $septemberFirst['opening_qty']);
$this->assertSame(3.0, $septemberFirst['purchase_in']);
$this->assertSame(23.0, $septemberFirst['closing_qty']);
DB::connection('business_mirror')->table('third_party_warehouse_authoritative_anchor_states')
->update(['status' => 'failed']);
$afterFailedReplacement = $this->repository->paginate($this->filters([
'dimension' => 'day',
'period' => '2026-08-31',
]), 1, 25)['data'][0];
$this->assertSame(20.0, $afterFailedReplacement['closing_qty']);
}
public function test_it_maps_all_twelve_types_and_excludes_wrong_scope_and_unknown_types(): void
{
$this->markReady();
$this->insertProduct('SKU-TYPES');
$this->insertInventory('SKU-TYPES', 50, '2026-09-01 00:00:00');
$types = [
'加工进仓' => [1, 1],
'加工出仓' => [2, -1],
'采购进仓' => [3, 1],
'采购退货' => [4, -1],
'其它进仓' => [5, 1],
'其它退货' => [6, -1],
'销售出仓' => [7, -1],
'销售退货' => [8, 1],
'盘点' => [9, -1],
'调拨出' => [10, -1],
'调拨入' => [11, 1],
'其它出库' => [12, -1],
];
foreach ($types as $type => [$qty, $qtyType]) {
$this->insertMovement('SKU-TYPES', $type, $qty, $qtyType, '2026-08-20 12:00:00');
}
$this->insertMovement('SKU-TYPES', '维修', 500, -1, '2026-08-20 12:00:00');
$this->insertMovement('SKU-TYPES', '虚拟仓调拨入', 500, 1, '2026-08-20 12:00:00');
$this->insertMovement('SKU-TYPES', '装箱快递', 500, -1, '2026-08-20 12:00:00');
$this->insertMovement('SKU-TYPES', '采购进仓', 500, 1, '2026-08-20 12:00:00', ['status' => 'WaitConfirm']);
$this->insertMovement('SKU-TYPES', '采购进仓', 500, 1, '2026-08-20 12:00:00', ['warehouse_id' => 999]);
// 同一单据出现更新版本时只认最新头记录,避免重复累计旧版本。
$this->insertMovement('SKU-TYPES', '采购进仓', 600, 1, '2026-08-20 12:00:00', [
'io_id' => 9000,
'header_id' => 9000,
]);
$this->insertHeaderOnly(9001, 9000, '采购进仓', '2026-08-20 12:00:00', 'WaitConfirm');
$row = $this->repository->paginate($this->filters([
'dimension' => 'day',
'period' => '2026-08-20',
]), 1, 25)['data'][0];
$expected = [
'processing_in' => 1.0,
'processing_out' => -2.0,
'purchase_in' => 3.0,
'purchase_return' => -4.0,
'other_in' => 5.0,
'other_return' => -6.0,
'sales_out' => -7.0,
'sales_return' => 8.0,
'stocktake' => -9.0,
'transfer_out' => -10.0,
'transfer_in' => 11.0,
'other_out' => -12.0,
];
foreach ($expected as $column => $value) {
$this->assertSame($value, $row[$column], $column);
}
}
public function test_filters_paging_summary_options_and_export_share_the_non_zero_scope(): void
{
$this->markReady();
foreach ([
['SKU-C', '椅子 C', '供应商乙', '椅类', 3],
['SKU-A', '桌子 A', '供应商甲', '桌类', 1],
['SKU-B', '桌子 B', '供应商甲', '桌类', 2],
] as [$sku, $name, $supplier, $category, $qty]) {
$this->insertProduct($sku, [
'name' => $name,
'supplier_name' => $supplier,
'category' => $category,
]);
$this->insertInventory($sku, $qty, '2026-09-01 00:00:00');
}
$this->insertProduct('SKU-ZERO', ['name' => '零库存', 'supplier_name' => '供应商甲', 'category' => '桌类']);
$this->insertInventory('SKU-ZERO', 0, '2026-09-01 00:00:00');
// 没有库存快照但存在相关流水的 SKU 也必须进入候选集合。
$this->insertProduct('SKU-FLOW', ['name' => '桌子流水', 'supplier_name' => '供应商甲', 'category' => '桌类']);
$this->insertMovement('SKU-FLOW', '销售出仓', 2, -1, '2026-08-10 00:00:00');
$this->insertMovement('SKU-FLOW', '销售出仓', 3, -1, '2026-09-02 00:00:00');
$filters = $this->filters([
'dimension' => 'day',
'period' => '2026-08-10',
'product_name' => '桌子',
'supplier' => '供应商甲',
'category' => '桌类',
]);
$pageOne = $this->repository->paginate($filters, 1, 1);
$pageTwo = $this->repository->paginate($filters, 2, 1);
$summary = $this->repository->summary($filters);
$options = $this->repository->filterOptions($this->filters(['product_name' => '桌子']));
$export = iterator_to_array($this->repository->exportRows($filters, 1), false);
$this->assertSame(3, $pageOne['total']);
$this->assertSame('SKU-A', $pageOne['data'][0]['sku_code']);
$this->assertSame('SKU-B', $pageTwo['data'][0]['sku_code']);
$this->assertSame([
'count' => 3,
'opening_qty' => 8.0,
'closing_qty' => 6.0,
], $summary);
$this->assertSame(['供应商甲'], $options['suppliers']);
$this->assertSame(['桌类'], $options['categories']);
$this->assertSame(['SKU-A', 'SKU-B', 'SKU-FLOW'], array_column($export, 'sku_code'));
$this->assertSame(31, $this->repository->summary($this->filters(['sku_code' => 'SKU-B']))['count']);
}
public function test_export_keeps_its_initial_generation_when_publication_switches_between_batches(): void
{
$this->markReady();
foreach (['SKU-A', 'SKU-B'] as $sku) {
$this->insertProduct($sku);
$this->insertInventory($sku, 1, '2026-09-01 00:00:00');
}
$export = $this->repository->exportRows($this->filters(), 1);
$this->assertTrue($export->valid());
$this->assertSame('SKU-A', $export->current()['sku_code']);
DB::connection('business_mirror')->table('third_party_warehouse_sync_states')->update([
'active_header_generation' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'active_item_generation' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'active_inventory_generation' => 'cccccccc-cccc-cccc-cccc-cccccccccccc',
]);
$export->next();
$this->assertTrue($export->valid());
$this->assertSame('SKU-B', $export->current()['sku_code']);
}
public function test_daily_aggregation_matches_the_original_balance_query_across_snapshot_boundaries(): void
{
$this->markReady();
$this->insertProduct('SKU-FRACTION');
$this->insertInventory('SKU-FRACTION', 1.2345, '2026-09-03 12:00:00');
foreach ([
['采购进仓', 0.1234, 1, '2026-09-01 00:00:00'],
['销售出仓', 0.2345, -1, '2026-09-02 23:59:59'],
['采购进仓', 0.3456, 1, '2026-09-03 11:59:59'],
['采购进仓', 0.4567, 1, '2026-09-03 12:00:00'],
['销售出仓', 0.5678, -1, '2026-09-03 12:00:01'],
['采购进仓', 0.6789, 1, '2026-09-04 00:00:00'],
] as [$type, $qty, $direction, $time]) {
$this->insertMovement('SKU-FRACTION', $type, $qty, $direction, $time);
}
$filters = $this->filters(['period' => '2026-09']);
$queryMethod = new \ReflectionMethod($this->repository, 'filteredReportQuery');
$mapMethod = new \ReflectionMethod($this->repository, 'mapRow');
$expected = $queryMethod->invoke($this->repository, $filters, $this->repository->state())
->orderBy('report_date')->orderBy('sku_code')->get()
->map(fn ($row) => $mapMethod->invoke($this->repository, $row))->all();
$actual = $this->repository->paginate($filters, 1, 100);
$this->assertSame($expected, $actual['data']);
$this->assertSame(count($expected), $actual['total']);
$this->assertSame($expected, iterator_to_array($this->repository->exportRows($filters, 1), false));
}
public function test_paging_reuses_quantities_and_refreshes_after_a_new_mirror_publication(): void
{
$this->markReady();
$this->insertProduct('SKU-CACHE');
$this->insertInventory('SKU-CACHE', 5, '2026-09-01 00:00:00');
$filters = $this->filters(['period' => '2026-09']);
$first = $this->repository->paginate($filters, 1, 1);
DB::connection('business_mirror')->enableQueryLog();
$second = $this->repository->paginate($filters, 2, 1);
$queries = DB::connection('business_mirror')->getQueryLog();
DB::connection('business_mirror')->disableQueryLog();
$this->assertSame('2026-09-02', $second['data'][0]['date']);
$this->assertSame($first['summary'], $second['summary']);
$this->assertFalse(collect($queries)->contains(fn ($q) => str_contains($q['query'], 'from "third_party_warehouse_inout_items"')
|| str_contains($q['query'], 'join "third_party_warehouse_inout_items"')));
$this->insertMovement('SKU-CACHE', '采购进仓', 2, 1, '2026-09-01 12:00:00');
DB::connection('business_mirror')->table('third_party_warehouse_sync_states')
->update(['last_succeeded_at' => '2026-09-12 01:00:00']);
$refreshed = $this->repository->paginate($filters, 1, 1);
$this->assertSame(7.0, $refreshed['data'][0]['closing_qty']);
DB::connection('business_mirror')->table('ops_jst_sku')->update(['supplier_name' => '新供应商']);
$filtered = $this->repository->paginate($filters + ['unused' => 'ignored'], 1, 1);
$this->assertSame('新供应商', $filtered['data'][0]['supplier']);
}
public function test_cancelled_fractional_movements_do_not_leave_phantom_zero_inventory_rows(): void
{
$this->markReady();
$this->insertProduct('SKU-ZERO');
$this->insertInventory('SKU-ZERO', 0.3, '2026-09-01 00:00:00');
$this->insertMovement('SKU-ZERO', '销售出仓', 0.1, -1, '2026-09-01 08:00:00');
$this->insertMovement('SKU-ZERO', '销售出仓', 0.2, -1, '2026-09-01 09:00:00');
$result = $this->repository->paginate($this->filters(['period' => '2026-09']), 1, 100);
$this->assertSame(1, $result['total']);
$this->assertSame(0.0, $result['data'][0]['closing_qty']);
$this->assertSame(0.3, $result['data'][0]['opening_qty']);
}
private function createMirrorTables(): void
{
$schema = Schema::connection('business_mirror');
$schema->create('third_party_warehouse_sync_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->unsignedTinyInteger('scope_version')->default(1);
$table->string('status')->nullable();
$table->string('run_mode')->nullable();
$table->string('active_header_generation')->nullable();
$table->string('active_item_generation')->nullable();
$table->string('active_inventory_generation')->nullable();
$table->dateTime('initial_completed_at')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
$table->string('last_error_code')->nullable();
});
$schema->create('third_party_warehouse_inout_headers', function (Blueprint $table): void {
$table->id('_mirror_id');
$table->string('generation');
$table->unsignedBigInteger('id');
$table->unsignedBigInteger('io_id');
$table->unsignedBigInteger('wms_co_id');
$table->unsignedBigInteger('wh_id')->default(0);
$table->string('status')->nullable();
$table->string('io_type')->nullable();
$table->dateTime('occurred_at')->nullable();
$table->dateTime('source_modified_at')->nullable();
});
$schema->create('third_party_warehouse_inout_items', function (Blueprint $table): void {
$table->id('_mirror_id');
$table->string('generation');
$table->unsignedBigInteger('id');
$table->unsignedBigInteger('io_id');
$table->string('sku_code');
$table->decimal('qty', 20, 4);
$table->smallInteger('qty_type');
$table->dateTime('source_modified_at')->nullable();
});
$schema->create('third_party_warehouse_inventory_snapshots', function (Blueprint $table): void {
$table->id('_mirror_id');
$table->string('generation');
$table->unsignedBigInteger('id');
$table->unsignedBigInteger('wms_co_id');
$table->string('sku_code');
$table->decimal('qty', 20, 4);
$table->dateTime('source_modified_at')->nullable();
$table->dateTime('captured_at')->nullable();
});
$schema->create('third_party_warehouse_inventory_subwarehouse_snapshots', function (Blueprint $table): void {
$table->id('_mirror_id');
$table->string('generation');
$table->unsignedBigInteger('id');
$table->unsignedBigInteger('wms_co_id');
$table->unsignedSmallInteger('wh_id');
$table->string('sku_code');
$table->decimal('qty', 20, 4);
$table->dateTime('captured_at')->nullable();
});
$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->dateTime('anchor_at')->nullable();
$table->date('source_period_start')->nullable();
$table->date('source_period_end')->nullable();
});
$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->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');
foreach ([
'opening_qty', 'closing_qty', '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->primary(['generation', 'report_date', 'sku_code']);
});
$schema->create('inventory_report_authoritative_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->string('status');
$table->string('active_generation')->nullable();
$table->date('source_month')->nullable();
});
$schema->create('inventory_report_authoritative_rows', function (Blueprint $table): void {
$table->string('generation');
$table->string('row_key');
$table->date('report_month');
$table->string('owner');
$table->string('warehouse_provider');
$table->string('warehouse');
$table->string('product_code');
$table->string('category')->nullable();
foreach ([
'opening_quantity', 'closing_quantity', 'processing_in', 'processing_out',
'purchase_in', 'purchase_return', 'other_in', 'other_return', 'sales_out',
'sales_return', 'stocktake', 'transfer_out', 'transfer_in', 'opening', 'other_out',
] as $column) {
$table->decimal($column, 20, 4)->default(0);
}
$table->primary(['generation', 'row_key']);
});
}
private function createProductTable(): void
{
Schema::connection('business_mirror')->create('ops_jst_sku', function (Blueprint $table): void {
$table->unsignedBigInteger('autoid');
$table->string('sku_id')->nullable();
$table->string('sku_type')->nullable();
foreach ([
'pic_big', 'pic', 'name', 'short_name', 'supplier_name', 'category',
'vc_name', 'supplier_sku_id', 'properties_value', 'color', 'brand',
'unit', 'other_price_1', 'other_price_2', 'other_price_3',
'other_price_4', 'other_1', 'other_2', 'other_3', 'other_4',
'other_5', 'other_6', 'other_7', 'other_8', 'other_9', 'other_10',
] as $column) {
$table->string($column)->nullable();
}
$table->decimal('pack_qty', 20, 4)->nullable();
$table->decimal('purchase_price', 20, 4)->nullable();
});
}
private function createDependencyStateTable(): void
{
$schema = Schema::connection('business_mirror');
$schema->create('business_mirror_table_states', function (Blueprint $table): void {
$table->string('dataset_key')->primary();
$table->string('status')->nullable();
$table->dateTime('initial_completed_at')->nullable();
$table->dateTime('last_succeeded_at')->nullable();
});
DB::connection('business_mirror')->table('business_mirror_table_states')->insert([
[
'dataset_key' => 'operations_replica.rpt.jst_sku',
'status' => 'succeeded',
'initial_completed_at' => '2026-09-01 03:00:00',
'last_succeeded_at' => '2026-09-11 03:00:00',
],
[
'dataset_key' => 'operations_replica.rpt.jst_warehouseInfo',
'status' => 'succeeded',
'initial_completed_at' => '2026-09-01 03:00:00',
'last_succeeded_at' => '2026-09-11 03:00:00',
],
]);
}
private function createWarehouseTable(): void
{
Schema::connection('business_mirror')->create('ops_jst_warehouse_info', function (Blueprint $table): void {
$table->unsignedBigInteger('Id');
$table->unsignedBigInteger('wms_co_id');
$table->string('name')->nullable();
});
DB::connection('business_mirror')->table('ops_jst_warehouse_info')->insert([
['Id' => 1, 'wms_co_id' => ThirdPartyWarehouseStorageMirrorRepository::WAREHOUSE_ID, 'name' => '三方-南昌樵舍仓'],
['Id' => 2, 'wms_co_id' => 15038426, 'name' => '简尚-崔桥工厂仓'],
]);
}
/** @param array<string,mixed> $overrides */
private function markReady(array $overrides = []): void
{
$this->insertState(array_merge([
'scope_version' => 2,
'status' => 'succeeded',
'run_mode' => null,
'active_header_generation' => self::HEADER_GENERATION,
'active_item_generation' => self::ITEM_GENERATION,
'active_inventory_generation' => self::INVENTORY_GENERATION,
'initial_completed_at' => '2026-09-11 03:20:00',
'last_succeeded_at' => '2026-09-11 03:21:00',
'last_error_code' => null,
], $overrides));
}
/** @param array<string,mixed> $values */
private function insertState(array $values): void
{
DB::connection('business_mirror')->table('third_party_warehouse_sync_states')->insert(
['dataset_key' => ThirdPartyWarehouseStorageMirrorRepository::DATASET_KEY] + $values,
);
}
/** @param array<string,mixed> $overrides */
private function insertProduct(string $sku, array $overrides = []): void
{
DB::connection('business_mirror')->table('ops_jst_sku')->insert(array_merge([
'autoid' => 1,
'sku_id' => $sku,
'sku_type' => 'normal',
'pic_big' => 'https://example.test/' . $sku . '.jpg',
'pic' => null,
'name' => '测试商品',
'short_name' => '测试简称',
'supplier_name' => '测试供应商',
'category' => '测试分类',
'vc_name' => '测试虚拟分类',
'supplier_sku_id' => 'SUP-' . $sku,
'properties_value' => '原木色',
'color' => null,
'brand' => '测试品牌',
'unit' => '件',
'purchase_price' => 55,
'other_price_1' => '采购价',
'other_price_2' => '按需叫货',
'other_price_3' => null,
'other_price_4' => null,
'pack_qty' => 1,
'other_1' => '型号',
'other_2' => '板材',
'other_3' => '采购',
'other_4' => null,
'other_5' => '在产',
'other_6' => null,
'other_7' => 'ITEM-' . $sku,
'other_8' => '单包',
'other_9' => 'DRAW-' . $sku,
'other_10' => '木',
], $overrides));
}
/** @param array<string,mixed> $overrides */
private function insertInventory(string $sku, float $qty, string $capturedAt, array $overrides = []): void
{
static $id = 1;
$subwarehouseValues = array_merge([
'qty' => $qty,
'return_qty' => 0,
'in_qty' => 0,
'defective_qty' => 0,
'customize_qty_1' => 0,
'customize_qty_2' => 0,
'customize_qty_3' => 0,
], array_intersect_key($overrides, array_flip([
'qty', 'return_qty', 'in_qty', 'defective_qty',
'customize_qty_1', 'customize_qty_2', 'customize_qty_3',
])));
$values = array_merge([
'generation' => self::INVENTORY_GENERATION,
'id' => $id++,
'wms_co_id' => ThirdPartyWarehouseStorageMirrorRepository::WAREHOUSE_ID,
'sku_code' => $sku,
'qty' => $qty,
'captured_at' => $capturedAt,
], array_diff_key($overrides, $subwarehouseValues));
DB::connection('business_mirror')->table('third_party_warehouse_inventory_snapshots')->insert($values);
foreach ([
1 => 'qty', 2 => 'return_qty', 3 => 'in_qty', 4 => 'defective_qty',
6 => 'customize_qty_1', 7 => 'customize_qty_2', 8 => 'customize_qty_3',
] as $warehouseId => $column) {
DB::connection('business_mirror')->table('third_party_warehouse_inventory_subwarehouse_snapshots')->insert([
'generation' => $values['generation'],
'id' => $values['id'],
'wms_co_id' => $values['wms_co_id'],
'wh_id' => $warehouseId,
'sku_code' => $values['sku_code'],
'qty' => $subwarehouseValues[$column],
'captured_at' => $values['captured_at'],
]);
}
}
private function insertAuthoritativeAnchor(string $sku, float $qty, string $anchorAt): void
{
$generation = '44444444-4444-4444-4444-444444444444';
DB::connection('business_mirror')->table('third_party_warehouse_authoritative_anchor_states')->insert([
'dataset_key' => 'jiansun.third-party-warehouse-storage.authoritative-anchor',
'status' => 'succeeded',
'active_generation' => $generation,
'anchor_at' => $anchorAt,
'source_period_start' => '2026-08-01',
'source_period_end' => '2026-08-31',
]);
DB::connection('business_mirror')->table('third_party_warehouse_authoritative_anchors')->insert([
'generation' => $generation,
'sku_code' => $sku,
'qty' => $qty,
'anchor_at' => $anchorAt,
]);
}
/** @param array<string,mixed> $overrides */
private function insertInventoryReportAuthoritativeRow(array $overrides = []): void
{
$generation = '55555555-5555-5555-5555-555555555555';
$sourceMonth = (string) ($overrides['source_month'] ?? '2026-08-01');
unset($overrides['source_month']);
DB::connection('business_mirror')->table('inventory_report_authoritative_states')->insert([
'dataset_key' => 'jiansun.inventory-report.authoritative-month',
'status' => 'succeeded',
'active_generation' => $generation,
'source_month' => $sourceMonth,
]);
DB::connection('business_mirror')->table('inventory_report_authoritative_rows')->insert(array_merge([
'generation' => $generation,
'row_key' => hash('sha256', 'authoritative-row'),
'report_month' => $sourceMonth,
'owner' => '常州简尚家居有限公司',
'warehouse_provider' => '仓储方',
'warehouse' => '子仓',
'product_code' => 'SKU-AUTH',
'category' => '分类',
'opening_quantity' => 0,
'closing_quantity' => 0,
], $overrides));
}
/** @param array<string,float|int> $movements */
private function insertAuthoritativeRow(
string $sku,
string $date,
float $opening,
float $closing,
array $movements = [],
): void {
DB::connection('business_mirror')->table('third_party_warehouse_authoritative_rows')->insert(array_merge([
'generation' => '44444444-4444-4444-4444-444444444444',
'report_date' => $date,
'sku_code' => $sku,
'opening_qty' => $opening,
'closing_qty' => $closing,
], $movements));
}
/** @param array<string,mixed> $overrides */
private function insertMovement(
string $sku,
string $type,
float $qty,
int $qtyType,
string $occurredAt,
array $overrides = [],
): void {
$ioId = $overrides['io_id'] ?? (1000 + $this->nextHeaderId);
$headerGeneration = $overrides['header_generation'] ?? self::HEADER_GENERATION;
$itemGeneration = $overrides['item_generation'] ?? self::ITEM_GENERATION;
DB::connection('business_mirror')->table('third_party_warehouse_inout_headers')->insert([
'generation' => $headerGeneration,
'id' => $overrides['header_id'] ?? $this->nextHeaderId++,
'io_id' => $ioId,
'wms_co_id' => $overrides['warehouse_id'] ?? ThirdPartyWarehouseStorageMirrorRepository::WAREHOUSE_ID,
'wh_id' => $overrides['wh_id'] ?? 1,
'status' => $overrides['status'] ?? 'Confirmed',
'io_type' => $type,
'occurred_at' => $occurredAt,
]);
DB::connection('business_mirror')->table('third_party_warehouse_inout_items')->insert([
'generation' => $itemGeneration,
'id' => $this->nextItemId++,
'io_id' => $ioId,
'sku_code' => $sku,
'qty' => $qty,
'qty_type' => $qtyType,
]);
}
private function insertHeaderOnly(
int $id,
int $ioId,
string $type,
string $occurredAt,
string $status,
): void {
DB::connection('business_mirror')->table('third_party_warehouse_inout_headers')->insert([
'generation' => self::HEADER_GENERATION,
'id' => $id,
'io_id' => $ioId,
'wms_co_id' => ThirdPartyWarehouseStorageMirrorRepository::WAREHOUSE_ID,
'status' => $status,
'io_type' => $type,
'occurred_at' => $occurredAt,
]);
}
/** @param array<string,mixed> $overrides */
private function filters(array $overrides = []): array
{
return array_merge([
'dimension' => 'month',
'period' => '2026-08',
'month' => '2026-08',
'date' => '',
'sku_code' => '',
'product_name' => '',
'supplier' => '',
'category' => '',
], $overrides);
}
}