'production', 'services.esign.base_url' => 'https://openapi.esign.test', 'services.esign.app_id' => 'production-app-id', 'services.esign.app_secret' => 'production-app-secret', 'services.esign.notify_url' => null, 'services.esign.polling_enabled' => true, ]); } public function test_it_polls_only_due_non_terminal_flows_without_a_callback(): void { $quote = $this->quoteSignature([ 'status' => CourierQuoteSignature::STATUS_SIGNING, 'esign_sign_flow_id' => 'quote-flow-due', 'last_synced_at' => now()->subMinutes(5), ]); $this->quoteSignature([ 'status' => CourierQuoteSignature::STATUS_ARCHIVED, 'esign_sign_flow_id' => 'quote-flow-archived', 'last_synced_at' => now()->subMinutes(5), ]); $agreement = $this->agreementSignature([ 'status' => CourierAgreementSignature::STATUS_UNKNOWN, 'esign_sign_flow_id' => 'agreement-flow-due', 'last_synced_at' => null, ]); $this->agreementSignature([ 'status' => CourierAgreementSignature::STATUS_SIGNING, 'esign_sign_flow_id' => 'agreement-flow-recent', 'last_synced_at' => now(), ]); Http::fake([ 'https://openapi.esign.test/v3/sign-flow/*/detail' => Http::response([ 'code' => 0, 'data' => ['signFlowStatus' => 1], ]), ]); $this->artisan('esign:poll-signatures', ['--limit' => 10, '--min-age' => 120]) ->expectsOutputToContain('候选 2,成功 2,失败 0') ->assertSuccessful(); Http::assertSentCount(2); $this->assertNotNull($quote->fresh()->last_synced_at); $this->assertNotNull($agreement->fresh()->last_synced_at); } public function test_it_skips_polling_until_the_private_switch_is_enabled(): void { config(['services.esign.polling_enabled' => false]); Http::preventStrayRequests(); $this->artisan('esign:poll-signatures') ->expectsOutputToContain('定时轮询未启用') ->assertSuccessful(); Http::assertNothingSent(); } public function test_it_does_not_poll_flows_created_in_another_environment(): void { $this->quoteSignature([ 'environment' => 'sandbox', 'esign_sign_flow_id' => 'sandbox-quote-flow', 'last_synced_at' => now()->subMinutes(5), ]); $this->agreementSignature([ 'environment' => 'sandbox', 'esign_sign_flow_id' => 'sandbox-agreement-flow', 'last_synced_at' => now()->subMinutes(5), ]); Http::preventStrayRequests(); $this->artisan('esign:poll-signatures', ['--limit' => 10, '--min-age' => 0]) ->expectsOutputToContain('候选 0,成功 0,失败 0') ->assertSuccessful(); Http::assertNothingSent(); } public function test_it_limits_the_total_number_of_remote_queries_per_run(): void { $this->agreementSignature([ 'esign_sign_flow_id' => 'agreement-flow-first', 'last_synced_at' => now()->subMinutes(10), ]); $this->agreementSignature([ 'esign_sign_flow_id' => 'agreement-flow-second', 'last_synced_at' => now()->subMinutes(5), ]); Http::fake([ 'https://openapi.esign.test/v3/sign-flow/*/detail' => Http::response([ 'code' => 0, 'data' => ['signFlowStatus' => 1], ]), ]); $this->artisan('esign:poll-signatures', ['--limit' => 1, '--min-age' => 0]) ->expectsOutputToContain('候选 1,成功 1,失败 0') ->assertSuccessful(); Http::assertSentCount(1); } public function test_it_continues_after_one_remote_query_fails(): void { $failed = $this->agreementSignature([ 'esign_sign_flow_id' => 'agreement-flow-failed', 'last_synced_at' => now()->subMinutes(10), ]); $succeeded = $this->agreementSignature([ 'esign_sign_flow_id' => 'agreement-flow-succeeded', 'last_synced_at' => now()->subMinutes(5), ]); Http::fake(fn ($request) => str_contains($request->url(), 'agreement-flow-failed') ? Http::response(['code' => 500, 'message' => 'temporary failure'], 503) : Http::response(['code' => 0, 'data' => ['signFlowStatus' => 1]])); $this->artisan('esign:poll-signatures', ['--limit' => 10, '--min-age' => 0]) ->expectsOutputToContain('候选 2,成功 1,失败 1') ->assertFailed(); Http::assertSentCount(2); $this->assertNotNull($failed->fresh()->last_error); $this->assertNotNull($succeeded->fresh()->last_synced_at); } private function quoteSignature(array $overrides = []): CourierQuoteSignature { return CourierQuoteSignature::create(array_merge([ 'company_id' => $this->company->id, 'quote_version_id' => random_int(1000, 999999), 'provider' => 'esign', 'environment' => 'production', 'status' => CourierQuoteSignature::STATUS_SIGNING, 'organization_name' => '测试快递公司', 'source_pdf_path' => 'quotes/source.pdf', 'source_pdf_sha256' => str_repeat('a', 64), 'source_pdf_generated_at' => now(), 'created_by' => $this->user->id, 'updated_by' => $this->user->id, ], $overrides)); } private function agreementSignature(array $overrides = []): CourierAgreementSignature { $sequence = CourierAgreementSignature::count() + 1; return CourierAgreementSignature::create(array_merge([ 'company_id' => $this->company->id, 'business_no' => 'ESIGN-POLL-' . $sequence, 'idempotency_key' => 'esign-poll-' . $sequence, 'document_type' => 'account-proof', 'template_code' => 'TPL-ACCOUNT-CERT-V2', 'template_version' => '2', 'source_file_name' => 'account-proof.doc', 'status' => CourierAgreementSignature::STATUS_SIGNING, 'courier_name' => '测试快递', 'company_full_name' => '测试公司', 'organization_name' => '测试快递公司', 'operator_account' => '13800138000', 'payload' => ['archive_mode' => 'auto'], 'environment' => 'production', 'source_pdf_path' => 'agreements/source.pdf', 'source_pdf_sha256' => str_repeat('b', 64), 'created_by' => $this->user->id, 'updated_by' => $this->user->id, ], $overrides)); } }