user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); } public function test_normal_products_page_renders_mirror_rows_and_filters(): void { $this->grantAdminPermission(); $state = [ 'status' => 'succeeded', 'last_succeeded_at' => '2026-08-15 17:51:03', 'last_full_succeeded_at' => '2026-08-10 03:15:00', 'mirror_row_count' => 547217, ]; $repository = \Mockery::mock(ProductCatalogMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnTrue(); $repository->shouldReceive('state')->once()->andReturn($state); $repository->shouldReceive('syncStatus')->once()->with($state)->andReturn([ 'code' => 'healthy', 'lag_minutes' => 4, ]); $repository->shouldReceive('ready')->once()->andReturnTrue(); $repository->shouldReceive('paginate') ->once() ->withArgs(static function (array $filters, int $page, int $limit): bool { return $filters['i_id'] === 'STYLE-001' && $filters['sku_id'] === 'SKU-001' && $filters['keyword'] === '胡桃' && $filters['enabled'] === '1' && $filters['stock_disabled'] === '0' && $filters['sale_price_min'] === '800' && $filters['sale_price_max'] === '900' && $page === 1 && $limit <= 50; }) ->andReturn([ 'data' => [(object) [ 'autoid' => 1001, 'i_id' => 'STYLE-001', 'sku_id' => 'SKU-001', 'name' => '岩板餐桌', 'short_name' => '餐桌', 'properties_value' => '1.4米', 'color' => '胡桃色', 'sale_price' => 899, 'cost_price' => 500, 'purchase_price' => 480, 'purchase_order_unit_price' => 65, 'purchase_order_finished_at' => '2026-08-20 11:03:05', 'purchase_order_id' => 827380, 'data_sources' => ['商品及库存管理(普通商品资料)', '1688代发单价'], 'market_price' => 1099, 'brand' => '北木', 'category' => '住宅家具', 'supplier_name' => '新北供应商', 'enabled' => 1, 'stock_disabled' => 0, 'modified' => '2026-08-15 10:00:00', ]], 'total' => 1, 'purchase_price_sync_status' => [ 'code' => 'ready', 'initialized' => true, 'last_succeeded_at' => '2026-08-21 12:00:00', ], ]); $this->app->instance(ProductCatalogMirrorRepository::class, $repository); $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'product-inventory', 'page' => 'normal-products', 'i_id' => 'STYLE-001', 'sku_id' => 'SKU-001', 'keyword' => '胡桃', 'enabled' => '1', 'stock_disabled' => '0', 'sale_price_min' => '900', 'sale_price_max' => '800', ])); $response ->assertOk() ->assertSee('data-product-catalog-page', false) ->assertSeeText('普通商品资料') ->assertSee('data-sync-state="healthy"', false) ->assertSeeText('同步正常') ->assertSeeText('最近同步 2026-08-15 17:51:03') ->assertSeeText('数据延迟 4 分钟') ->assertSeeText('最近校准 2026-08-10 03:15:00') ->assertSeeText('STYLE-001') ->assertSeeText('SKU-001') ->assertSeeText('岩板餐桌') ->assertSeeText('899.00') ->assertSeeText('普通商品采购价') ->assertSeeText('1688代发单价') ->assertSeeText('数据来源') ->assertSeeText('商品及库存管理(普通商品资料)') ->assertSeeText('1688代发单价') ->assertSeeText('65.00') ->assertSeeText('导出') ->assertDontSeeText('1688代发单价尚未完成首次同步') ->assertSeeText('正常同步') ->assertDontSee('pic_url'); } public function test_normal_products_export_uses_all_filters_and_includes_data_source_column(): void { $this->grantAdminPermission(); $repository = \Mockery::mock(ProductCatalogMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnTrue(); $repository->shouldReceive('ready')->once()->andReturnTrue(); $repository->shouldReceive('exportRows') ->once() ->withArgs(static function (array $filters): bool { return $filters['sku_id'] === 'SKU-001' && $filters['supplier_name'] === '新北供应商' && $filters['sale_price_min'] === '800' && $filters['sale_price_max'] === '900'; }) ->andReturn((static function (): \Generator { yield (object) [ 'autoid' => 1001, 'i_id' => 'STYLE-001', 'sku_id' => 'SKU-001', 'name' => '岩板餐桌', 'short_name' => '餐桌', 'properties_value' => '1.4米', 'color' => '胡桃色', 'brand' => '北木', 'category' => '住宅家具', 'supplier_name' => '新北供应商', 'sale_price' => 899, 'cost_price' => 500, 'purchase_price' => 480, 'purchase_order_unit_price' => 65, 'data_sources' => ['商品及库存管理(普通商品资料)', '1688代发单价'], 'market_price' => 1099, 'enabled' => 1, 'stock_disabled' => 0, 'modified' => '2026-08-15 10:00:00', ]; })()); $this->app->instance(ProductCatalogMirrorRepository::class, $repository); $response = $this->loginAs()->get(route('business.product-inventory.normal-products.export', [ 'sku_id' => 'SKU-001', 'supplier_name' => '新北供应商', 'sale_price_min' => '900', 'sale_price_max' => '800', ])); $response->assertOk(); $this->assertStringContainsString('attachment;', (string) $response->headers->get('content-disposition')); $content = $response->streamedContent(); $this->assertStringStartsWith(chr(0xEF) . chr(0xBB) . chr(0xBF), $content); $this->assertStringContainsString('数据来源', $content); $this->assertStringContainsString('商品及库存管理(普通商品资料);1688代发单价', $content); $this->assertStringContainsString('SKU-001', $content); } public function test_normal_products_page_marks_purchase_prices_as_not_yet_synchronized(): void { $this->grantAdminPermission(); $state = [ 'status' => 'succeeded', 'initial_completed_at' => '2026-08-14 21:15:36', 'last_succeeded_at' => '2026-08-21 12:00:00', 'mirror_row_count' => 547217, ]; $repository = \Mockery::mock(ProductCatalogMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnTrue(); $repository->shouldReceive('state')->once()->andReturn($state); $repository->shouldReceive('syncStatus')->once()->with($state)->andReturn([ 'code' => 'healthy', 'lag_minutes' => 1, ]); $repository->shouldReceive('ready')->once()->andReturnTrue(); $repository->shouldReceive('paginate')->once()->andReturn([ 'data' => [], 'total' => 0, 'purchase_price_sync_status' => [ 'code' => 'pending', 'initialized' => false, 'last_succeeded_at' => null, ], ]); $this->app->instance(ProductCatalogMirrorRepository::class, $repository); $this->loginAs()->get(route('business.pages.show', [ 'section' => 'product-inventory', 'page' => 'normal-products', ])) ->assertOk() ->assertSee('data-purchase-price-sync-state="pending"', false) ->assertSeeText('1688代发单价尚未完成首次同步') ->assertSeeText('当前仅显示普通商品采购价'); } public function test_normal_products_page_does_not_treat_initial_sync_as_empty_data(): void { $this->grantAdminPermission(); $state = ['status' => 'running']; $repository = \Mockery::mock(ProductCatalogMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnTrue(); $repository->shouldReceive('state')->once()->andReturn($state); $repository->shouldReceive('syncStatus')->once()->with($state)->andReturn([ 'code' => 'initializing', 'lag_minutes' => null, ]); $repository->shouldReceive('ready')->once()->andReturnFalse(); $repository->shouldNotReceive('paginate'); $this->app->instance(ProductCatalogMirrorRepository::class, $repository); $this->loginAs()->get(route('business.pages.show', [ 'section' => 'product-inventory', 'page' => 'normal-products', ])) ->assertOk() ->assertSeeText('普通商品资料正在执行首次全量同步') ->assertSeeText('首次同步中') ->assertDontSeeText('当前镜像中没有普通商品资料。'); } public function test_normal_products_page_reports_an_unavailable_mirror(): void { $this->grantAdminPermission(); $repository = \Mockery::mock(ProductCatalogMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnFalse(); $repository->shouldNotReceive('state'); $repository->shouldNotReceive('syncStatus'); $repository->shouldNotReceive('ready'); $repository->shouldNotReceive('paginate'); $this->app->instance(ProductCatalogMirrorRepository::class, $repository); $this->loginAs()->get(route('business.pages.show', [ 'section' => 'product-inventory', 'page' => 'normal-products', ])) ->assertOk() ->assertSeeText('共享业务镜像尚未建立') ->assertDontSeeText('当前镜像中没有普通商品资料。'); } public function test_normal_products_page_keeps_old_rows_visible_after_a_sync_failure(): void { $this->grantAdminPermission(); $state = [ 'status' => 'failed', 'run_mode' => 'incremental', 'initial_completed_at' => '2026-08-14 21:15:36', 'last_succeeded_at' => '2026-08-15 17:51:03', 'mirror_row_count' => 547217, ]; $repository = \Mockery::mock(ProductCatalogMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnTrue(); $repository->shouldReceive('state')->once()->andReturn($state); $repository->shouldReceive('syncStatus')->once()->with($state)->andReturn([ 'code' => 'failed', 'lag_minutes' => 18, ]); $repository->shouldReceive('ready')->once()->andReturnTrue(); $repository->shouldReceive('paginate')->once()->andReturn([ 'data' => [(object) [ 'autoid' => 1001, 'i_id' => 'STYLE-OLD', 'sku_id' => 'SKU-OLD', 'name' => '最近成功同步的商品', 'short_name' => null, 'properties_value' => null, 'color' => null, 'sale_price' => 100, 'cost_price' => null, 'purchase_price' => null, 'market_price' => null, 'brand' => null, 'category' => null, 'supplier_name' => null, 'enabled' => 1, 'stock_disabled' => 0, 'modified' => '2026-08-15 17:45:00', ]], 'total' => 1, ]); $this->app->instance(ProductCatalogMirrorRepository::class, $repository); $this->loginAs()->get(route('business.pages.show', [ 'section' => 'product-inventory', 'page' => 'normal-products', ])) ->assertOk() ->assertSee('data-sync-state="failed"', false) ->assertSeeText('同步失败,显示最近数据') ->assertSeeText('最近成功同步的商品'); } public function test_normal_products_export_reports_an_unavailable_mirror(): void { $this->grantAdminPermission(); $repository = \Mockery::mock(ProductCatalogMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnFalse(); $repository->shouldNotReceive('ready'); $repository->shouldNotReceive('exportRows'); $this->app->instance(ProductCatalogMirrorRepository::class, $repository); $this->withExceptionHandling() ->loginAs() ->get(route('business.product-inventory.normal-products.export')) ->assertServiceUnavailable(); } public function test_normal_products_export_rejects_users_without_sidebar_permission(): void { $subject = 'user_' . $this->user->id; setting()->set('system.sidebar_permissions', json_encode([ 'configured' => [$subject], 'subjects' => [ $subject => ['company-management/basic-information'], ], ], JSON_UNESCAPED_UNICODE)); $this->withExceptionHandling() ->loginAs() ->get(route('business.product-inventory.normal-products.export')) ->assertForbidden(); } public function test_normal_products_page_rejects_users_without_sidebar_permission(): void { $subject = 'user_' . $this->user->id; setting()->set('system.sidebar_permissions', json_encode([ 'configured' => [$subject], 'subjects' => [ $subject => ['company-management/basic-information'], ], ], JSON_UNESCAPED_UNICODE)); $this->withExceptionHandling() ->loginAs() ->get(route('business.pages.show', [ 'section' => 'product-inventory', 'page' => 'normal-products', ])) ->assertForbidden(); } }