[ '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']); } 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 $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 $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 $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 $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 $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 $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 $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 $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); } }