createMock(JushuitanOpenApiClient::class); $upstream->expects($this->never())->method('getCostPriceOperationHistory'); $mirror = $this->createMock(JushuitanCostPriceHistoryMirrorRepository::class); $mirror->method('available')->willReturn(true); $mirror->method('state')->willReturn(null); $result = (new JushuitanCostPriceHistorySynchronizer($upstream, $mirror))->sync(); $this->assertSame('requires_full_sync', $result['status']); } public function test_full_sync_uses_stable_time_windows_and_page_cursor(): void { Carbon::setTestNow('2026-08-15 00:00:00'); config()->set('services.jushuitan.cost_price_history_start', '2026-08-01 00:00:00'); config()->set('services.jushuitan.cost_price_history_window_days', 30); $responses = [[ 'records' => [['operation_id' => '10', 'sku_id' => 'SKU-10']], 'source_rows' => 2, 'current_page' => 1, 'has_next' => true, ], [ 'records' => [['operation_id' => '20', 'sku_id' => 'SKU-20']], 'source_rows' => 1, 'current_page' => 2, 'has_next' => false, ], [ 'records' => [], 'source_rows' => 1, 'current_page' => 1, 'has_next' => false, ]]; $apiCalls = []; $upstream = $this->createMock(JushuitanOpenApiClient::class); $upstream->expects($this->exactly(3))->method('getCostPriceOperationHistory') ->willReturnCallback(function (Carbon $start, Carbon $end, int $page, int $pageSize) use (&$responses, &$apiCalls): array { $apiCalls[] = [$start->format('Y-m-d H:i:s'), $end->format('Y-m-d H:i:s'), $page, $pageSize]; return array_shift($responses); }); $storedPages = []; $mirror = $this->createMock(JushuitanCostPriceHistoryMirrorRepository::class); $mirror->method('available')->willReturn(true); $mirror->method('state')->willReturn(null); $mirror->expects($this->once())->method('startRun') ->with('full', $this->isType('string'), $this->isInstanceOf(Carbon::class), $this->isInstanceOf(Carbon::class), false) ->willReturn([ 'cursor_window_started_at' => '2026-08-01 00:00:00', 'cursor_page' => 1, 'last_processed_source_rows' => 0, 'last_upserted_rows' => 0, ]); $mirror->expects($this->exactly(3))->method('storePage') ->willReturnCallback(function (...$arguments) use (&$storedPages): void { $storedPages[] = $arguments; }); $mirror->expects($this->once())->method('complete') ->with('full', $this->isType('string'), $this->isInstanceOf(Carbon::class), 4, 2); $result = (new JushuitanCostPriceHistorySynchronizer($upstream, $mirror))->sync(true, 2000, 0); $this->assertSame([ ['2026-08-01 00:00:00', '2026-08-08 00:00:00', 1, 50], ['2026-08-01 00:00:00', '2026-08-08 00:00:00', 2, 50], ['2026-08-08 00:00:00', '2026-08-15 00:00:00', 1, 50], ], $apiCalls); $this->assertSame('2026-08-01 00:00:00', $storedPages[0][1]->format('Y-m-d H:i:s')); $this->assertSame(2, $storedPages[0][2]); $this->assertSame('2026-08-08 00:00:00', $storedPages[1][1]->format('Y-m-d H:i:s')); $this->assertSame(1, $storedPages[1][2]); $this->assertSame('succeeded', $result['status']); $this->assertSame(4, $result['source_rows']); $this->assertSame(2, $result['rows']); } public function test_api_failure_marks_run_failed_without_completing_or_storing_a_page(): void { Carbon::setTestNow('2026-09-14 00:00:00'); $upstream = $this->createMock(JushuitanOpenApiClient::class); $upstream->expects($this->once())->method('getCostPriceOperationHistory') ->willThrowException(new RuntimeException('upstream_unavailable')); $mirror = $this->createMock(JushuitanCostPriceHistoryMirrorRepository::class); $mirror->method('available')->willReturn(true); $mirror->method('state')->willReturn([ 'initial_completed_at' => '2026-09-13 00:00:00', 'last_succeeded_at' => '2026-09-13 00:00:00', ]); $mirror->expects($this->once())->method('startRun')->willReturn([ 'cursor_window_started_at' => '2026-09-11 00:00:00', 'cursor_page' => 1, 'last_processed_source_rows' => 0, 'last_upserted_rows' => 0, ]); $mirror->expects($this->never())->method('storePage'); $mirror->expects($this->never())->method('complete'); $mirror->expects($this->once())->method('fail')->with('upstream_unavailable'); $this->expectExceptionMessage('upstream_unavailable'); (new JushuitanCostPriceHistorySynchronizer($upstream, $mirror))->sync(); } }