You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

190 lines
7.1 KiB

<?php
namespace Tests\Unit\Services;
use App\Models\Company;
use App\Models\CompanyQccAccount;
use App\Services\QccCallService;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use RuntimeException;
use Tests\TestCase;
class QccCallServiceTest extends TestCase
{
protected function setUp(): void
{
parent::setUp();
config([
'database.default' => 'sqlite',
'database.connections.sqlite.database' => ':memory:',
'audit.enabled' => false,
]);
DB::purge('sqlite');
Schema::create('companies', function (Blueprint $table) {
$table->id();
$table->string('company_name')->nullable();
$table->string('credit_code')->nullable();
$table->timestamps();
$table->softDeletes();
});
Schema::create('company_qcc_accounts', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('company_id')->unique();
$table->string('status', 20);
$table->unsignedBigInteger('selected_by')->nullable();
$table->dateTime('selected_at');
$table->dateTime('first_success_at')->nullable();
$table->string('first_success_source')->nullable();
$table->string('first_success_ref')->nullable();
$table->text('unknown_note')->nullable();
$table->timestamps();
});
}
protected function tearDown(): void
{
Schema::dropIfExists('company_qcc_accounts');
Schema::dropIfExists('companies');
parent::tearDown();
}
public function test_first_success_marks_candidate_occupied_only_once(): void
{
$company = $this->company();
$account = $this->selectedAccount($company);
$service = new QccCallService();
$service->recordSuccess($company->id, 'enterprise-profile', 'request-first');
$firstSuccessAt = $account->fresh()->first_success_at;
$service->recordSuccess($company->id, 'risk-check', 'request-second');
$account = $account->fresh();
$this->assertSame(CompanyQccAccount::STATUS_OCCUPIED, $account->status);
$this->assertSame('enterprise-profile', $account->first_success_source);
$this->assertSame('request-first', $account->first_success_ref);
$this->assertTrue($firstSuccessAt->equalTo($account->first_success_at));
}
public function test_unknown_candidate_blocks_follow_up_calls(): void
{
$company = $this->company();
$account = $this->selectedAccount($company);
$service = new QccCallService();
try {
$service->call($company, 'enterprise-profile', 'request-timeout', function () {
throw new RuntimeException('timeout');
}, fn($result) => $result === true);
$this->fail('Expected timeout exception was not thrown.');
} catch (RuntimeException $exception) {
$this->assertSame('timeout', $exception->getMessage());
}
$this->assertSame(CompanyQccAccount::STATUS_UNKNOWN, $account->fresh()->status);
$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('待人工确认');
$service->call($company, 'enterprise-profile', 'request-retry', fn() => true, fn($result) => $result === true);
}
public function test_later_timeout_does_not_downgrade_occupied_account(): void
{
$company = $this->company();
$account = $this->selectedAccount($company, CompanyQccAccount::STATUS_OCCUPIED);
$service = new QccCallService();
try {
$service->call($company, 'enterprise-profile', 'request-timeout', function () {
throw new RuntimeException('timeout');
}, fn($result) => $result === true);
$this->fail('Expected timeout exception was not thrown.');
} catch (RuntimeException $exception) {
$this->assertSame('timeout', $exception->getMessage());
}
$this->assertSame(CompanyQccAccount::STATUS_OCCUPIED, $account->fresh()->status);
}
public function test_indeterminate_response_marks_selected_account_unknown_but_explicit_failure_does_not(): void
{
$company = $this->company();
$account = $this->selectedAccount($company);
$service = new QccCallService();
$service->call(
$company,
'enterprise-profile',
'request-business-failed',
fn() => ['code' => 400],
fn($result) => $result['code'] === 200,
fn($result) => !array_key_exists('code', $result)
);
$this->assertSame(CompanyQccAccount::STATUS_SELECTED, $account->fresh()->status);
$service->call(
$company,
'enterprise-profile',
'request-unparseable',
fn() => [],
fn($result) => ($result['code'] ?? null) === 200,
fn($result) => !array_key_exists('code', $result) ? '元禾响应缺少业务状态码' : false
);
$account = $account->fresh();
$this->assertSame(CompanyQccAccount::STATUS_UNKNOWN, $account->status);
$this->assertSame('元禾响应缺少业务状态码', $account->unknown_note);
}
public function test_result_classifier_exception_marks_selected_account_unknown(): void
{
$company = $this->company();
$account = $this->selectedAccount($company);
$service = new QccCallService();
try {
$service->call(
$company,
'enterprise-profile',
'request-classifier-error',
fn() => [],
function () {
throw new RuntimeException('响应字段不完整');
}
);
$this->fail('Expected classifier exception was not thrown.');
} catch (RuntimeException $exception) {
$this->assertSame('响应字段不完整', $exception->getMessage());
}
$account = $account->fresh();
$this->assertSame(CompanyQccAccount::STATUS_UNKNOWN, $account->status);
$this->assertStringContainsString('调用结果判定异常', $account->unknown_note);
}
protected function company(): Company
{
$id = DB::table('companies')->insertGetId([
'company_name' => '测试企业',
'credit_code' => '91320100TEST00001',
'created_at' => now(),
'updated_at' => now(),
]);
return Company::findOrFail($id);
}
protected function selectedAccount(Company $company, string $status = CompanyQccAccount::STATUS_SELECTED): CompanyQccAccount
{
return CompanyQccAccount::create([
'company_id' => $company->id,
'status' => $status,
'selected_at' => now(),
'first_success_at' => $status === CompanyQccAccount::STATUS_OCCUPIED ? now()->subMinute() : null,
'first_success_source' => $status === CompanyQccAccount::STATUS_OCCUPIED ? 'seed' : null,
'first_success_ref' => $status === CompanyQccAccount::STATUS_OCCUPIED ? 'seed-ref' : null,
]);
}
}