set('services.jushuitan', [ 'base_url' => 'https://jushuitan.example.test/api/open/query.aspx', 'partner_id' => 'test-partner-id', 'partner_key' => 'test-partner-key', 'token' => 'test-token', 'timeout' => 20, 'product_operation_history_method' => 'worklog.query', ]); Http::preventStrayRequests(); } protected function tearDown(): void { Carbon::setTestNow(); parent::tearDown(); } public function test_it_posts_signed_json_and_keeps_only_cost_price_operations(): void { Http::fake([ 'https://jushuitan.example.test/*' => Http::response([ 'code' => 0, 'page_size' => 2, 'page_index' => 1, 'has_next' => false, 'datas' => [[ 'autoid' => 101, 'type' => '商品与库存管理导入', 'remark' => '采购价: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, 'type' => '编辑', 'remark' => '商品名称:旧名称->新名称', '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-08 00:00:00'), 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 { parse_str((string) parse_url($request->url(), PHP_URL_QUERY), $query); $timestamp = (string) Carbon::now()->timestamp; return $request->method() === 'POST' && str_starts_with($request->url(), 'https://jushuitan.example.test/api/open/query.aspx?') && str_starts_with((string) $request->header('Content-Type')[0], 'application/json') && $query === [ 'method' => 'worklog.query', 'partnerid' => 'test-partner-id', 'token' => 'test-token', 'ts' => $timestamp, 'sign' => md5('worklog.querytest-partner-idtokentest-tokents' . $timestamp . 'test-partner-key'), ] && $request->data() === [ 'page_index' => 1, 'page_size' => 50, 'modified_begin' => '2026-06-01 00:00:00', 'modified_end' => '2026-06-08 00:00:00', ]; }); } public function test_it_preserves_unparsed_cost_operation_content_without_inventing_prices(): void { Http::fake([ '*' => Http::response([ 'code' => 0, 'page_index' => 1, 'has_next' => false, 'datas' => [[ 'autoid' => 103, 'remark' => '采购价已由批量任务更新', '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.token', null); try { (new JushuitanOpenApiClient())->getCostPriceOperationHistory(now(), now(), 1, 100); $this->fail('Expected missing credential exception.'); } catch (RuntimeException $exception) { $this->assertSame('jushuitan_classic_api_credentials_missing', $exception->getMessage()); } config()->set('services.jushuitan.token', 'test-token'); Http::fake(['*' => Http::response(['code' => 10001, 'msg' => 'denied'])]); $this->expectExceptionMessage('jushuitan_api_code_10001'); (new JushuitanOpenApiClient())->getCostPriceOperationHistory(now(), now(), 1, 100); } public function test_it_reports_an_invalid_classic_partner_id_without_exposing_upstream_text(): void { Http::fake(['*' => Http::response([ 'code' => 100, 'msg' => '验证失败! partnerid 无效', ])]); $this->expectExceptionMessage('jushuitan_classic_api_partner_id_invalid'); (new JushuitanOpenApiClient())->getCostPriceOperationHistory(now(), now(), 1, 50); } public function test_new_platform_app_credentials_do_not_replace_classic_credentials(): void { config()->set('services.jushuitan.partner_id', null); config()->set('services.jushuitan.partner_key', null); config()->set('services.jushuitan.token', null); config()->set('services.jushuitan.app_key', 'new-platform-app-key'); config()->set('services.jushuitan.app_secret', 'new-platform-app-secret'); config()->set('services.jushuitan.access_token', 'new-platform-access-token'); try { (new JushuitanOpenApiClient())->getCostPriceOperationHistory(now(), now(), 1, 50); $this->fail('Expected missing classic credential exception.'); } catch (RuntimeException $exception) { $this->assertSame('jushuitan_classic_api_credentials_missing', $exception->getMessage()); } Http::assertNothingSent(); } }