createMock(UpstreamReadOnlyQuery::class); $query->expects($this->once())->method('select')->willReturnCallback( function (string $sql, array $bindings): array { UpstreamReadOnlyQuery::assertReadOnlySql($sql); $this->assertStringContainsString('[rpt].[Bulk_Price_Limit]', $sql); $this->assertStringContainsString('[product_code] LIKE ?', $sql); $this->assertStringContainsString("WHEN 'COMPLETED' THEN N'已完成'", $sql); $this->assertStringContainsString("WHEN 'agree' THEN N'已同意'", $sql); $this->assertStringContainsString('[process_end_date] < DATEADD(day, 1, CAST(? AS date))', $sql); $this->assertStringContainsString('OFFSET 2 ROWS FETCH NEXT 3 ROWS ONLY', $sql); $this->assertSame( ['%ABC\\%\\_%', '2026-07-01', '2026-07-31'], $bindings, ); return [ [ 'barcode' => 'A', 'minimum_price' => '10.00', 'price_limit' => '11.50', 'purchase_price' => '7.25', 'shipping_fee' => '2.00', 'jingxi_limit_price' => '9.50', ], [ 'barcode' => 'B', 'minimum_price' => '', 'price_limit' => null, 'purchase_price' => '0.00', 'shipping_fee' => null, 'jingxi_limit_price' => '', ], [ 'barcode' => 'C', 'minimum_price' => '3.00', 'price_limit' => '4.00', 'purchase_price' => '2.00', 'shipping_fee' => '1.00', 'jingxi_limit_price' => '3.50', ], ]; }, ); $factory = $this->createMock(UpstreamReadOnlyQueryFactory::class); $factory->expects($this->once())->method('forSource')->with('operations_replica')->willReturn($query); $result = (new ProductPriceRepository($factory))->paginate([ 'barcode' => 'ABC%_', 'completed_from' => '2026-07-01', 'completed_to' => '2026-07-31', ], 2, 2); $this->assertCount(3, $result['rows']); $this->assertSame(10.0, $result['rows'][0]['minimum_price']); $this->assertSame(11.5, $result['rows'][0]['price_limit']); $this->assertNull($result['rows'][1]['minimum_price']); $this->assertSame(0.0, $result['rows'][1]['purchase_price']); } public function test_mirror_batch_uses_stable_keyset_cursor_and_bounded_window(): void { $query = $this->createMock(UpstreamReadOnlyQuery::class); $query->expects($this->once())->method('select')->willReturnCallback( function (string $sql, array $bindings): array { UpstreamReadOnlyQuery::assertReadOnlySql($sql); $this->assertStringContainsString('SELECT TOP 200', $sql); $this->assertStringContainsString("COALESCE([businessId], '') AS business_id", $sql); $this->assertStringContainsString('[process_end_date] > CAST(? AS datetime2(7))', $sql); $this->assertStringContainsString('CONVERT(varchar(33), [process_end_date], 126) AS cursor_completed_at', $sql); $this->assertStringNotContainsString('OFFSET', $sql); $this->assertSame([ '2026-08-01 00:00:00', '2026-08-14 00:00:00', '2026-08-10 12:00:00', '2026-08-10 12:00:00', 'B100', '2026-08-10 12:00:00', 'B100', 'SKU100', ], $bindings); return [[ 'business_id' => 'B101', 'barcode' => 'SKU101', 'completed_at' => '2026-08-10 12:01:00', 'minimum_price' => '10.25', 'price_limit' => null, 'purchase_price' => '8.00', 'shipping_fee' => '1.00', 'jingxi_limit_price' => '9.00', ]]; }, ); $factory = $this->createMock(UpstreamReadOnlyQueryFactory::class); $factory->method('forSource')->with('operations_replica')->willReturn($query); $rows = (new ProductPriceRepository($factory))->mirrorBatch([ 'completed_at' => '2026-08-10 12:00:00', 'business_id' => 'B100', 'barcode' => 'SKU100', ], '2026-08-01 00:00:00', '2026-08-14 00:00:00', 200); $this->assertSame('B101', $rows[0]['business_id']); $this->assertSame(10.25, $rows[0]['minimum_price']); } }