shouldReceive('settle')->zeroOrMoreTimes()->andReturn([ 'ready' => false, 'complete' => false, 'message' => null, 'inbound_fee' => null, 'outbound_fee' => null, 'storage_fee' => null, 'total_fee' => null, ]); $this->app->instance(ThirdPartyWarehouseStorageSettlementService::class, $settlement); } protected function tearDown(): void { Carbon::setTestNow(); parent::tearDown(); } public function test_page_passes_a_daily_period_and_filters_to_the_mirror(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); setting()->set('default.list_limit', 25); $filters = [ 'dimension' => 'day', 'period' => '2026-08-15', 'sku_code' => 'BG002950N956V01', 'product_name' => '餐椅', 'supplier' => '测试供应商', 'category' => '家具', ]; $repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnTrue(); $repository->shouldReceive('ready')->once()->andReturnTrue(); $repository->shouldReceive('paginate')->once()->with($filters, 1, 25)->andReturn([ 'data' => [$this->row()], 'total' => 1, 'summary' => [ 'count' => 1, 'opening_qty' => 0, 'closing_qty' => 10, ], 'state' => ['status' => 'succeeded'], 'sync' => [ 'code' => 'healthy', 'last_succeeded_at' => '2026-09-11 03:20:00', 'lag_minutes' => 10, ], ]); $this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository); $this->loginAs(); $this->get(route('business.pages.show', [ 'section' => 'courier-payables', 'page' => 'third-party-warehouse-storage', ]) . '?' . http_build_query($filters)) ->assertOk() ->assertSee('三方仓进销存') ->assertSee('查询维度:商品 + 按天') ->assertSee('value="day" checked', false) ->assertSee('type="date"', false) ->assertSee('value="2026-08-15"', false) ->assertSee('BG002950N956V01') ->assertSee('测试供应商') ->assertSee('每日 00:00 自动同步'); } public function test_invalid_daily_period_falls_back_to_the_previous_complete_day(): void { Carbon::setTestNow('2026-09-11 16:00:00'); $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnTrue(); $repository->shouldReceive('ready')->once()->andReturnTrue(); $repository->shouldReceive('paginate')->once()->with( Mockery::on(static fn (array $filters): bool => $filters['dimension'] === 'day' && $filters['period'] === '2026-09-10'), 1, Mockery::type('int'), )->andReturn($this->emptyResult()); $this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository); $this->loginAs(); $this->get(route('business.pages.show', [ 'section' => 'courier-payables', 'page' => 'third-party-warehouse-storage', 'dimension' => 'day', 'period' => '2026-02-31', ]))->assertOk() ->assertSee('value="2026-09-10"', false); } public function test_period_before_supported_start_date_is_clamped(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class); $repository->shouldReceive('available')->twice()->andReturnTrue(); $repository->shouldReceive('ready')->twice()->andReturnTrue(); $repository->shouldReceive('paginate')->once()->with( Mockery::on(static fn (array $filters): bool => $filters['dimension'] === 'day' && $filters['period'] === '2026-08-01'), 1, Mockery::type('int'), )->andReturn($this->emptyResult()); $repository->shouldReceive('paginate')->once()->with( Mockery::on(static fn (array $filters): bool => $filters['dimension'] === 'month' && $filters['period'] === '2026-08'), 1, Mockery::type('int'), )->andReturn($this->emptyResult()); $this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository); $this->loginAs(); $route = route('business.pages.show', [ 'section' => 'courier-payables', 'page' => 'third-party-warehouse-storage', ]); $this->get($route . '?dimension=day&period=2026-07-31') ->assertOk() ->assertSee('value="2026-08-01"', false); $this->get($route . '?dimension=month&period=2026-07') ->assertOk() ->assertSee('value="2026-08"', false); } public function test_export_keeps_daily_filters_and_only_accepts_declared_fields(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $filters = [ 'dimension' => 'day', 'period' => '2026-08-15', 'sku_code' => 'BG002950N956V01', 'product_name' => '', 'supplier' => '', 'category' => '', ]; $repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnTrue(); $repository->shouldReceive('ready')->once()->andReturnTrue(); $repository->shouldReceive('exportRows')->once()->with($filters)->andReturn((static function (): \Generator { yield self::row(); })()); $this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository); $response = $this->loginAs()->get(route( 'business.courier-payables.third-party-warehouse-storage.export', $filters + ['fields' => 'sku_code,closing_qty,not_allowed'], )); $response->assertOk(); $this->assertStringContainsString('text/csv', (string) $response->headers->get('content-type')); $content = $response->streamedContent(); $this->assertStringStartsWith(chr(0xEF) . chr(0xBB) . chr(0xBF), $content); $this->assertStringContainsString('商品编码,期末数量', $content); $this->assertStringContainsString("'BG002950N956V01,10", $content); $this->assertStringNotContainsString('货主', $content); $this->assertStringNotContainsString('not_allowed', $content); } public function test_page_explains_that_an_uninitialized_mirror_is_not_empty_business_data(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnTrue(); $repository->shouldReceive('ready')->once()->andReturnFalse(); $repository->shouldReceive('state')->once()->andReturn(['status' => 'pending']); $repository->shouldReceive('syncStatus')->once()->andReturn([ 'code' => 'pending', 'last_succeeded_at' => null, 'lag_minutes' => null, ]); $repository->shouldNotReceive('paginate'); $this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository); $this->loginAs(); $this->get(route('business.pages.show', [ 'section' => 'courier-payables', 'page' => 'third-party-warehouse-storage', ]))->assertOk() ->assertSee('首次同步尚未完成') ->assertSee('不会显示空白数据冒充正式结果'); } public function test_query_timeout_preserves_filters_and_explains_the_actual_failure(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class); $repository->shouldReceive('available')->once()->andReturnTrue(); $repository->shouldReceive('ready')->once()->andReturnTrue(); $repository->shouldReceive('paginate')->once()->andThrow(new \RuntimeException('max_statement_time exceeded')); $this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository); $this->loginAs()->get(route('business.pages.show', [ 'section' => 'courier-payables', 'page' => 'third-party-warehouse-storage', 'period' => '2026-09', 'sku_code' => 'SKU-TIMEOUT', ]))->assertOk()->assertSee('三方仓进销存查询超时') ->assertSee('value="2026-09"', false)->assertSee('value="SKU-TIMEOUT"', false) ->assertDontSee('登录已过期'); } /** @return array */ private static function row(): array { return [ 'owner' => '常州简尚家居有限公司', 'date' => '2026-08-15', 'sku_code' => 'BG002950N956V01', 'image_url' => '', 'product_name' => '测试餐椅', 'short_name' => '餐椅', 'supplier' => '测试供应商', 'category' => '家具', 'virtual_category' => '', 'supplier_style_no' => 'SUP-001', 'color_spec' => '原木色', 'brand' => '测试品牌', 'unit' => '件', 'opening_qty' => 0, 'closing_qty' => 10, 'processing_in' => 0, 'processing_out' => 0, 'purchase_in' => 34, 'purchase_return' => 0, 'other_in' => 0, 'other_return' => 0, 'sales_out' => -24, 'sales_return' => 0, 'stocktake' => 0, 'transfer_out' => 0, 'transfer_in' => 0, 'opening_aux' => 0, 'other_out' => 0, ]; } /** @return array */ private function emptyResult(): array { return [ 'data' => [], 'total' => 0, 'summary' => [ 'count' => 0, 'opening_qty' => 0, 'closing_qty' => 0, ], 'state' => ['status' => 'succeeded'], 'sync' => [ 'code' => 'healthy', 'last_succeeded_at' => '2026-09-11 03:20:00', 'lag_minutes' => 10, ], ]; } }