set('services.jushuitan', [ 'base_url' => 'https://jushuitan.example.test', 'app_key' => 'test-app-key', 'app_secret' => 'test-app-secret', 'access_token' => 'test-access-token', 'timeout' => 20, 'product_operation_history_path' => '/open/webapi/itemapi/itemsku/queryitemskuopshistory', ]); Http::preventStrayRequests(); } protected function tearDown(): void { Carbon::setTestNow(); parent::tearDown(); } public function test_it_posts_signed_form_and_keeps_only_cost_price_operations(): void { Http::fake([ 'https://jushuitan.example.test/*' => Http::response([ 'code' => 0, 'data' => [ 'page' => ['current_page' => 1, 'page_size' => 2000, 'count' => 2, 'pages' => 1], 'list' => [[ 'autoid' => 101, 'operation_type' => '商品与库存管理导入', 'operation_description' => '采购价:72.2222->65', 'created' => '2026-06-06 14:32:02.993', 'creator_name' => '简尚', 'sku_id' => '15961G', 'i_id' => 'ZWJ0111', 'name' => '测试商品', 'properties_value' => '胡桃色', 'sku_type' => 'normal', ], [ 'autoid' => 102, 'operation_type' => '编辑', 'operation_description' => '商品名称:旧名称->新名称', 'created' => '2026-06-06 14:33:00', 'creator_name' => '测试人', 'sku_id' => '15961G', ]], ], ]), ]); $result = (new JushuitanOpenApiClient())->getCostPriceOperationHistory( Carbon::parse('2026-06-01 00:00:00'), Carbon::parse('2026-06-30 23:59:59'), 1, 2000, ); $this->assertSame(2, $result['source_rows']); $this->assertCount(1, $result['records']); $this->assertSame('101', $result['records'][0]['operation_id']); $this->assertSame(72.2222, $result['records'][0]['before_price']); $this->assertSame(65.0, $result['records'][0]['after_price']); $this->assertEqualsWithDelta(-7.2222, $result['records'][0]['diff_amount'], 0.00001); $this->assertSame('简尚', $result['records'][0]['operator_name']); Http::assertSent(function (Request $request): bool { $data = $request->data(); $sign = $data['sign'] ?? null; unset($data['sign']); ksort($data, SORT_STRING); $plain = 'test-app-secret'; foreach ($data as $key => $value) { $plain .= $key . (string) $value; } $biz = json_decode((string) ($data['biz'] ?? ''), true); return $request->method() === 'POST' && $request->url() === 'https://jushuitan.example.test/open/webapi/itemapi/itemsku/queryitemskuopshistory' && str_starts_with((string) $request->header('Content-Type')[0], 'application/x-www-form-urlencoded') && $sign === md5($plain) && $biz === [ 'current_page' => 1, 'page_size' => 2000, 'start_time' => '2026-06-01 00:00:00', 'end_time' => '2026-06-30 23:59:59', ]; }); } public function test_it_preserves_unparsed_cost_operation_content_without_inventing_prices(): void { Http::fake([ '*' => Http::response([ 'code' => 0, 'data' => [ 'page' => ['current_page' => 1, 'count' => 1, 'pages' => 1], 'list' => [[ 'autoid' => 103, 'operation_description' => '采购价已由批量任务更新', 'created' => '2026-06-07 08:00:00', 'sku_id' => 'SKU-UNPARSED', ]], ], ]), ]); $result = (new JushuitanOpenApiClient())->getCostPriceOperationHistory( Carbon::parse('2026-06-01'), Carbon::parse('2026-06-30'), 1, 100, ); $this->assertCount(1, $result['records']); $this->assertNull($result['records'][0]['before_price']); $this->assertNull($result['records'][0]['after_price']); $this->assertSame('采购价已由批量任务更新', $result['records'][0]['operation_description']); } public function test_it_rejects_missing_credentials_and_upstream_error_codes(): void { config()->set('services.jushuitan.access_token', null); try { (new JushuitanOpenApiClient())->getCostPriceOperationHistory(now(), now(), 1, 100); $this->fail('Expected missing credential exception.'); } catch (RuntimeException $exception) { $this->assertSame('jushuitan_api_credentials_missing', $exception->getMessage()); } config()->set('services.jushuitan.access_token', 'test-access-token'); Http::fake(['*' => Http::response(['code' => 10001, 'msg' => 'denied'])]); $this->expectExceptionMessage('jushuitan_api_code_10001'); (new JushuitanOpenApiClient())->getCostPriceOperationHistory(now(), now(), 1, 100); } }