1244 lines
61 KiB
PHP
1244 lines
61 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Business;
|
|
|
|
use App\Jobs\Business\ArchiveCourierAgreementSignature;
|
|
use App\Models\Auth\Permission;
|
|
use App\Models\Business\CourierAgreementSignature;
|
|
use App\Models\Business\CourierQuoteChange;
|
|
use App\Models\Business\CourierQuoteSignature;
|
|
use App\Models\Business\CourierQuoteVersion;
|
|
use App\Support\CourierAgreementPdf;
|
|
use DOMDocument;
|
|
use DOMXPath;
|
|
use Illuminate\Http\Client\Request as ClientRequest;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Queue;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Tests\Feature\FeatureTestCase;
|
|
|
|
class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Storage::fake('uploads');
|
|
config([
|
|
'services.esign.environment' => 'sandbox',
|
|
'services.esign.base_url' => 'https://smlopenapi.esign.test',
|
|
'services.esign.app_id' => 'sandbox-app-id',
|
|
'services.esign.app_secret' => 'sandbox-app-secret',
|
|
'services.esign.notify_url' => null,
|
|
]);
|
|
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
|
}
|
|
|
|
public function test_account_proof_uses_its_own_document_mapping_and_pushes_once(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$payload = $this->accountProofPayload('request-001');
|
|
|
|
$this->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertRedirect(route('business.pages.show', [
|
|
'section' => 'courier-payables',
|
|
'page' => 'agreement-signing',
|
|
'agreement_view' => 'tasks',
|
|
]));
|
|
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertSame('TPL-ACCOUNT-CERT-V2', $signature->template_code);
|
|
$this->assertSame(CourierAgreementSignature::STATUS_SIGNING, $signature->status);
|
|
$this->assertSame('flow-agreement-001', $signature->esign_sign_flow_id);
|
|
$this->assertSame('新北中通快递有限公司', $signature->organization_name);
|
|
$this->assertSame('2026-09-21', $signature->payload['signature_date']);
|
|
Storage::disk('uploads')->assertExists($signature->source_pdf_path);
|
|
$this->assertSame('新北中通快递有限公司', $signature->payload['account_proof_account_name']);
|
|
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
if ($request->url() !== 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file') {
|
|
return false;
|
|
}
|
|
|
|
return data_get($request->data(), 'signers.0.signerType') === 1
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo.orgName') === '新北中通快递有限公司'
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo.transactorInfo.psnInfo.psnName') === '张经理';
|
|
});
|
|
|
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)->assertRedirect();
|
|
Http::assertSentCount(4);
|
|
$this->assertSame(1, CourierAgreementSignature::query()->count());
|
|
}
|
|
|
|
public function test_account_proof_accepts_private_account_holder_name_different_from_company_name(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$payload = $this->accountProofPayload('request-private-holder');
|
|
$payload['account_proof_account_name'] = '李四';
|
|
|
|
$this->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertRedirect();
|
|
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertSame('李四', $signature->payload['account_proof_account_name']);
|
|
$this->assertNotSame($this->company->name, $signature->payload['account_proof_account_name']);
|
|
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
if ($request->url() !== 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file') {
|
|
return false;
|
|
}
|
|
|
|
return data_get($request->data(), 'signers.0.signerType') === 1
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo.orgName') === '新北中通快递有限公司'
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo.transactorInfo.psnInfo.psnName') === '张经理'
|
|
&& data_get($request->data(), 'signers.0.psnSignerInfo') === null;
|
|
});
|
|
}
|
|
|
|
public function test_account_change_keeps_courier_company_signer_when_payee_changes(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$payload = $this->accountChangePayload('request-account-change');
|
|
$payload['new_account_name'] = '李四';
|
|
|
|
$this->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertRedirect();
|
|
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertSame('新北中通快递有限公司', $signature->organization_name);
|
|
$this->assertSame('李四', $signature->payload['new_account_name']);
|
|
$this->assertSame('张经理', $signature->operator_name);
|
|
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
if ($request->url() !== 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file') {
|
|
return false;
|
|
}
|
|
|
|
return data_get($request->data(), 'signers.0.signerType') === 1
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo.orgName') === '新北中通快递有限公司'
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo.transactorInfo.psnInfo.psnName') === '张经理'
|
|
&& data_get($request->data(), 'signers.0.psnSignerInfo') === null;
|
|
});
|
|
}
|
|
|
|
public function test_account_proof_requires_esign_real_name_separate_from_payee_name(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$payload = $this->accountProofPayload('request-missing-operator-name');
|
|
unset($payload['operator_name']);
|
|
|
|
$this->withExceptionHandling()
|
|
->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertSessionHasErrors('operator_name');
|
|
|
|
$this->assertDatabaseCount('business_courier_agreement_signatures', 0);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_account_change_requires_counterparty_esign_organization_name(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$payload = $this->accountChangePayload('request-missing-counterparty-organization');
|
|
unset($payload['counterparty_organization_name']);
|
|
|
|
$this->withExceptionHandling()
|
|
->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertSessionHasErrors('counterparty_organization_name');
|
|
|
|
$this->assertDatabaseCount('business_courier_agreement_signatures', 0);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_account_proof_can_be_signed_by_a_personal_counterparty(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$payload = $this->accountProofPayload('request-person-counterparty');
|
|
$payload['counterparty_signer_type'] = 'person';
|
|
$payload['counterparty_organization_name'] = '李斯';
|
|
unset($payload['operator_name']);
|
|
|
|
$this->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertRedirect();
|
|
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertSame('person', $signature->payload['counterparty_signer_type']);
|
|
$this->assertSame('李斯', $signature->operator_name);
|
|
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
if ($request->url() !== 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file') {
|
|
return false;
|
|
}
|
|
|
|
return data_get($request->data(), 'signers.0.signerType') === 0
|
|
&& data_get($request->data(), 'signers.0.psnSignerInfo.psnInfo.psnName') === '李斯'
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo') === null;
|
|
});
|
|
}
|
|
|
|
public function test_entity_change_sends_two_enterprise_signers_with_their_real_names(): void
|
|
{
|
|
$this->fakeEsign();
|
|
|
|
$payload = [
|
|
'idempotency_key' => 'entity-change-two-enterprise-signers',
|
|
'courier_name' => '新北中通',
|
|
'document_type' => 'entity-change',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-10-10',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'operator_name' => '旧法人',
|
|
'secondary_esign_account' => '13900139000',
|
|
'secondary_operator_name' => '新法人',
|
|
'archive_mode' => 'auto',
|
|
'former_company_name' => '原公司有限公司',
|
|
'entity_change_date' => '2026-10-01',
|
|
'changed_company_name' => '新公司有限公司',
|
|
'changed_company_start_date' => '2026-10-10',
|
|
];
|
|
|
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)->assertRedirect();
|
|
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertSame('旧法人', $signature->payload['former_legal_representative']);
|
|
$this->assertSame('新法人', $signature->payload['secondary_operator_name']);
|
|
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
if ($request->url() !== 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file') {
|
|
return false;
|
|
}
|
|
|
|
$signers = $request->data()['signers'] ?? [];
|
|
|
|
return count($signers) === 2
|
|
&& data_get($signers, '0.signerType') === 1
|
|
&& data_get($signers, '0.orgSignerInfo.orgName') === '原公司有限公司'
|
|
&& data_get($signers, '0.orgSignerInfo.transactorInfo.psnInfo.psnName') === '旧法人'
|
|
&& data_get($signers, '1.signerType') === 1
|
|
&& data_get($signers, '1.orgSignerInfo.orgName') === '新公司有限公司'
|
|
&& data_get($signers, '1.orgSignerInfo.transactorInfo.psnInfo.psnName') === '新法人';
|
|
});
|
|
}
|
|
|
|
public function test_deregistered_entity_change_sends_the_former_person_and_current_enterprise(): void
|
|
{
|
|
$this->fakeEsign();
|
|
|
|
$payload = [
|
|
'idempotency_key' => 'entity-change-deregistered-former-person',
|
|
'courier_name' => '新北中通',
|
|
'document_type' => 'entity-change',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-10-10',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'operator_name' => '原法人',
|
|
'secondary_esign_account' => '13900139000',
|
|
'secondary_operator_name' => '新法人',
|
|
'archive_mode' => 'auto',
|
|
'entity_change_old_company_deregistered' => '1',
|
|
'former_company_name' => '已注销原公司有限公司',
|
|
'entity_change_date' => '2026-10-01',
|
|
'changed_company_name' => '新公司有限公司',
|
|
'changed_company_start_date' => '2026-10-10',
|
|
];
|
|
|
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)->assertRedirect();
|
|
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertTrue($signature->payload['entity_change_old_company_deregistered']);
|
|
$this->assertSame('原法人', $signature->payload['former_legal_representative']);
|
|
$this->assertSame('13900139000', $signature->payload['secondary_esign_account']);
|
|
$this->assertSame('新法人', $signature->payload['secondary_operator_name']);
|
|
$html = view('business.pdf.courier-agreement', [
|
|
'signature' => $signature,
|
|
'type' => $signature->document_type,
|
|
'title' => $signature->documentLabel(),
|
|
'payload' => $signature->payload,
|
|
'signatureDate' => '2026年10月10日',
|
|
'fontPath' => str_replace('\\', '/', resource_path('fonts/DroidSansFallback.ttf')),
|
|
])->render();
|
|
$this->assertStringContainsString('原公司法人个人签字:原法人', strip_tags($html));
|
|
$this->assertStringContainsString('变更后公司法人签字盖公章:新公司有限公司(签署人:新法人)', strip_tags($html));
|
|
$this->assertStringNotContainsString('变更前公司法人签字盖公章', $html);
|
|
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
if ($request->url() !== 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file') {
|
|
return false;
|
|
}
|
|
|
|
$signers = $request->data()['signers'] ?? [];
|
|
|
|
return count($signers) === 2
|
|
&& data_get($signers, '0.signerType') === 0
|
|
&& data_get($signers, '0.psnSignerInfo.psnAccount') === '13800138000'
|
|
&& data_get($signers, '0.psnSignerInfo.psnInfo.psnName') === '原法人'
|
|
&& data_get($signers, '0.orgSignerInfo') === null
|
|
&& data_get($signers, '1.signerType') === 1
|
|
&& data_get($signers, '1.orgSignerInfo.orgName') === '新公司有限公司'
|
|
&& data_get($signers, '1.orgSignerInfo.transactorInfo.psnAccount') === '13900139000'
|
|
&& data_get($signers, '1.orgSignerInfo.transactorInfo.psnInfo.psnName') === '新法人';
|
|
});
|
|
}
|
|
|
|
public function test_deregistered_entity_change_rejects_missing_current_signer_before_creating_a_task(): void
|
|
{
|
|
Http::fake();
|
|
$payload = [
|
|
'idempotency_key' => 'entity-change-missing-current-signer',
|
|
'courier_name' => '新北中通',
|
|
'document_type' => 'entity-change',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-10-10',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'operator_name' => '原法人',
|
|
'archive_mode' => 'auto',
|
|
'entity_change_old_company_deregistered' => '1',
|
|
'former_company_name' => '已注销原公司有限公司',
|
|
'entity_change_date' => '2026-10-01',
|
|
'changed_company_name' => '新公司有限公司',
|
|
'changed_company_start_date' => '2026-10-10',
|
|
'secondary_esign_account' => '13900139000',
|
|
'secondary_operator_name' => '新法人',
|
|
];
|
|
|
|
$this->withExceptionHandling()->loginAs();
|
|
foreach (['secondary_esign_account', 'secondary_operator_name'] as $field) {
|
|
$missing = $payload;
|
|
unset($missing[$field]);
|
|
$this->postJson(route('business.courier-agreement-signatures.store'), $missing)
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors($field);
|
|
}
|
|
$payload['secondary_esign_account'] = 'not-an-account';
|
|
$this->postJson(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors('secondary_esign_account');
|
|
$this->assertDatabaseCount('business_courier_agreement_signatures', 0);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_cooperation_contract_maps_party_variables_and_creates_two_signers(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$payload = $this->cooperationContractPayload('cooperation-001');
|
|
|
|
$this->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertRedirect();
|
|
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertSame('cooperation-contract', $signature->document_type);
|
|
$this->assertSame('TPL-COOPERATION-CONTRACT-V1', $signature->template_code);
|
|
$this->assertSame('18052733960', $signature->payload['secondary_esign_account']);
|
|
$this->assertSame('李经理', $signature->payload['secondary_operator_name']);
|
|
$this->assertSame('0519-88886666', $signature->payload['cooperation_party_a_phone']);
|
|
$this->assertSame('2026-12-31', $signature->payload['cooperation_end_date']);
|
|
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
if ($request->url() !== 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file') {
|
|
return false;
|
|
}
|
|
|
|
$json = $request->data();
|
|
$signers = $json['signers'] ?? [];
|
|
|
|
return count($signers) === 2
|
|
&& ($signers[0]['orgSignerInfo']['orgName'] ?? null) === $this->company->name
|
|
&& ($signers[0]['orgSignerInfo']['transactorInfo']['psnAccount'] ?? null) === '18052733960'
|
|
&& ($signers[0]['orgSignerInfo']['transactorInfo']['psnInfo']['psnName'] ?? null) === '李经理'
|
|
&& ($signers[1]['orgSignerInfo']['orgName'] ?? null) === '新北中通快递有限公司'
|
|
&& ($signers[1]['orgSignerInfo']['transactorInfo']['psnAccount'] ?? null) === '13800138000'
|
|
&& ($signers[1]['orgSignerInfo']['transactorInfo']['psnInfo']['psnName'] ?? null) === '张经理';
|
|
});
|
|
}
|
|
|
|
public function test_cooperation_pdf_preserves_clauses_and_uses_a_supported_font_for_static_numbers(): void
|
|
{
|
|
$payload = $this->cooperationContractPayload('cooperation-font');
|
|
$payload['signature_date'] = '2026-10-10';
|
|
$signature = new CourierAgreementSignature([
|
|
'business_no' => 'AGR-FONT-REGRESSION',
|
|
'template_code' => 'TPL-COOPERATION-CONTRACT-V1',
|
|
'document_type' => 'cooperation-contract',
|
|
'payload' => $payload,
|
|
]);
|
|
$html = view('business.pdf.courier-agreement', [
|
|
'signature' => $signature,
|
|
'type' => $signature->document_type,
|
|
'title' => '服务协议',
|
|
'payload' => $payload,
|
|
'signatureDate' => '2026年10月10日',
|
|
'fontPath' => str_replace('\\', '/', resource_path('fonts/DroidSansFallback.ttf')),
|
|
])->render();
|
|
$original = new DOMDocument();
|
|
$original->loadHTML($html, LIBXML_NOWARNING | LIBXML_NOERROR);
|
|
$pdf = app('dompdf.wrapper');
|
|
$document = null;
|
|
$wrapper = \Mockery::mock($pdf);
|
|
$wrapper->shouldReceive('output')->once()->andReturnUsing(function () use ($pdf, &$document): string {
|
|
$document = clone $pdf->getDomPDF()->getDom();
|
|
|
|
return $pdf->output();
|
|
});
|
|
$this->app->instance('dompdf.wrapper', $wrapper);
|
|
|
|
$wordBreakPattern = \Dompdf\FrameReflower\Text::$_wordbreak_pattern;
|
|
$bytes = app(CourierAgreementPdf::class)->render($signature);
|
|
|
|
$this->assertStringStartsWith('%PDF-', $bytes);
|
|
$this->assertSame($wordBreakPattern, \Dompdf\FrameReflower\Text::$_wordbreak_pattern);
|
|
$text = $document->getElementsByTagName('body')->item(0)->textContent;
|
|
$this->assertSame(
|
|
preg_replace('/\s+/u', '', $original->getElementsByTagName('body')->item(0)->textContent),
|
|
preg_replace('/\s+/u', '', $text),
|
|
);
|
|
$this->assertStringContainsString('4.6.1、针对甲方客服录入', $text);
|
|
$this->assertStringContainsString('95%', $text);
|
|
$this->assertStringContainsString('5000元/次', $text);
|
|
$xpath = new DOMXPath($document);
|
|
foreach ($xpath->query('//body//text()[not(ancestor::*[contains(concat(" ", normalize-space(@class), " "), " latin ")])]') as $node) {
|
|
$this->assertDoesNotMatchRegularExpression('/[\x21-\x7E\x{2018}-\x{201F}]/u', $node->nodeValue);
|
|
}
|
|
}
|
|
|
|
public function test_cooperation_contract_uses_the_persons_real_name_instead_of_the_courier_name(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), [
|
|
'idempotency_key' => 'cooperation-person',
|
|
'courier_name' => '新北中通',
|
|
'document_type' => 'cooperation-contract',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-10-10',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'counterparty_signer_type' => 'person',
|
|
'secondary_operator_name' => '我方经办人',
|
|
'archive_mode' => 'auto',
|
|
'cooperation_party_a_name' => $this->company->name,
|
|
'cooperation_party_a_phone' => '0519-88886666',
|
|
'cooperation_party_b_name' => '张三',
|
|
'cooperation_party_b_phone' => '13800138000',
|
|
'cooperation_start_date' => '2026-10-10',
|
|
'cooperation_end_date' => '2027-10-09',
|
|
])->assertRedirect()->assertSessionHasNoErrors();
|
|
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertSame('张三', $signature->operator_name);
|
|
Http::assertSent(fn (ClientRequest $request): bool =>
|
|
str_ends_with($request->url(), '/v3/sign-flow/create-by-file')
|
|
&& count($request->data()['signers']) === 2
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo.transactorInfo.psnInfo.psnName') === '我方经办人'
|
|
&& data_get($request->data(), 'signers.1.signerType') === 0
|
|
&& data_get($request->data(), 'signers.1.psnSignerInfo.psnInfo.psnName') === '张三'
|
|
&& data_get($request->data(), 'signers.1.psnSignerInfo.psnAccount') === '13800138000'
|
|
&& data_get($request->data(), 'signers.1.orgSignerInfo') === null
|
|
);
|
|
}
|
|
|
|
public function test_registered_entity_change_rejects_missing_former_signer_real_name(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$payload = [
|
|
'idempotency_key' => 'entity-change-missing-former-signer-name',
|
|
'courier_name' => '新北中通',
|
|
'document_type' => 'entity-change',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-10-10',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'archive_mode' => 'auto',
|
|
'former_company_name' => '原公司有限公司',
|
|
'entity_change_date' => '2026-10-01',
|
|
'changed_company_name' => '新公司有限公司',
|
|
'changed_company_start_date' => '2026-10-10',
|
|
'secondary_esign_account' => '13900139000',
|
|
'secondary_operator_name' => '新法人',
|
|
];
|
|
|
|
$this->withExceptionHandling()->loginAs()
|
|
->from(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'create']))
|
|
->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertSessionHasErrors('operator_name');
|
|
|
|
$this->assertSame(0, CourierAgreementSignature::query()->count());
|
|
}
|
|
|
|
public static function authorizationSignerTypes(): array
|
|
{
|
|
return [['person', 'person'], ['organization', 'person'], ['person', 'organization'], ['organization', 'organization']];
|
|
}
|
|
|
|
#[\PHPUnit\Framework\Attributes\DataProvider('authorizationSignerTypes')]
|
|
public function test_authorization_supports_independent_person_and_enterprise_signers(string $principalType, string $trusteeType): void
|
|
{
|
|
$this->fakeEsign();
|
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), [
|
|
'idempotency_key' => 'authorization-' . $principalType . '-' . $trusteeType,
|
|
'courier_name' => '新北中通',
|
|
'document_type' => 'authorization',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-10-10',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'secondary_esign_account' => '13900139000',
|
|
'counterparty_signer_type' => $principalType,
|
|
'secondary_signer_type' => $trusteeType,
|
|
'operator_name' => $principalType === 'organization' ? '张三' : null,
|
|
'secondary_operator_name' => $trusteeType === 'organization' ? '李四' : null,
|
|
'archive_mode' => 'auto',
|
|
'principal_name' => $principalType === 'person' ? '张三' : '委托公司有限公司',
|
|
'principal_id_number' => 'TEST-PRINCIPAL-ID',
|
|
'trustee_name' => $trusteeType === 'person' ? '李四' : '受托公司有限公司',
|
|
'trustee_id_number' => 'TEST-TRUSTEE-ID',
|
|
'authorization_items' => ['1', '4'],
|
|
'bill_counterparty' => $this->company->name,
|
|
'loan_counterparty' => $this->company->name,
|
|
'payment_counterparty' => $this->company->name,
|
|
'authorization_start_date' => '2026-10-10',
|
|
'authorization_end_date' => '2027-10-09',
|
|
])->assertRedirect()->assertSessionHasNoErrors();
|
|
|
|
Http::assertSent(function (ClientRequest $request) use ($principalType, $trusteeType): bool {
|
|
if (! str_ends_with($request->url(), '/v3/sign-flow/create-by-file')) {
|
|
return false;
|
|
}
|
|
$signers = $request->data()['signers'];
|
|
foreach ([[$principalType, '张三', '13800138000'], [$trusteeType, '李四', '13900139000']] as $index => [$type, $name, $account]) {
|
|
$identityPath = $type === 'person' ? 'psnSignerInfo' : 'orgSignerInfo.transactorInfo';
|
|
if (data_get($signers, "$index.signerType") !== ($type === 'person' ? 0 : 1)
|
|
|| data_get($signers, "$index.$identityPath.psnInfo.psnName") !== $name
|
|
|| data_get($signers, "$index.$identityPath.psnAccount") !== $account
|
|
|| ($type === 'person' && data_get($signers, "$index.orgSignerInfo") !== null)) {
|
|
return false;
|
|
}
|
|
}
|
|
return count($signers) === 2;
|
|
});
|
|
}
|
|
|
|
public function test_cooperation_contract_requires_both_real_names_before_creating_a_task(): void
|
|
{
|
|
$this->fakeEsign();
|
|
foreach (['operator_name', 'secondary_operator_name'] as $field) {
|
|
$payload = $this->cooperationContractPayload('missing-' . $field);
|
|
$payload[$field] = ' ';
|
|
$this->withExceptionHandling()->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertSessionHasErrors($field);
|
|
}
|
|
$this->assertSame(0, CourierAgreementSignature::query()->count());
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_cooperation_contract_rejects_company_names_as_personal_names(): void
|
|
{
|
|
$this->fakeEsign();
|
|
foreach (['operator_name' => 'cooperation_party_b_name', 'secondary_operator_name' => 'cooperation_party_a_name'] as $field => $companyField) {
|
|
$payload = $this->cooperationContractPayload('company-' . $field);
|
|
$payload[$field] = $payload[$companyField];
|
|
$this->withExceptionHandling()->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertSessionHasErrors($field);
|
|
}
|
|
$this->assertSame(0, CourierAgreementSignature::query()->count());
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_shared_contract_accepts_multiple_couriers_and_preserves_note_in_pdf(): void
|
|
{
|
|
$this->fakeEsign();
|
|
|
|
$this->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), [
|
|
'idempotency_key' => 'shared-contract-multiple-001',
|
|
'courier_name' => '新北中通',
|
|
'document_type' => 'shared-contract',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => '常州电优闪供应链管理有限公司',
|
|
'signature_date' => '2026-09-21',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'operator_name' => '张经理',
|
|
'archive_mode' => 'auto',
|
|
'contract_partner_name' => '宿迁天枢物流科技有限公司',
|
|
'shared_courier_names' => ['常州圆通其他', '常州圆通-丁楼'],
|
|
'shared_note' => '请在3个工作日内完成签章。',
|
|
])
|
|
->assertRedirect();
|
|
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertSame(['常州圆通其他', '常州圆通-丁楼'], $signature->payload['shared_courier_names']);
|
|
$this->assertSame('宿迁天枢物流科技有限公司', $signature->organization_name);
|
|
$this->assertSame('organization', $signature->payload['counterparty_signer_type']);
|
|
$this->assertSame('张经理', $signature->operator_name);
|
|
$this->assertSame('常州圆通其他', $signature->payload['shared_courier_name']);
|
|
$this->assertSame('请在3个工作日内完成签章。', $signature->payload['shared_note']);
|
|
$pdf = Storage::disk('uploads')->get($signature->source_pdf_path);
|
|
$this->assertStringStartsWith('%PDF-', $pdf);
|
|
$html = view('business.pdf.courier-agreement', [
|
|
'signature' => $signature,
|
|
'type' => $signature->document_type,
|
|
'title' => $signature->documentLabel(),
|
|
'payload' => $signature->payload,
|
|
'signatureDate' => '2026年9月21日',
|
|
'fontPath' => str_replace('\\', '/', resource_path('fonts/DroidSansFallback.ttf')),
|
|
])->render();
|
|
foreach (['常州电优闪供应链管理有限公司', '宿迁天枢物流科技有限公司', '常州圆通其他', '常州圆通-丁楼', '请在3个工作日内完成签章。'] as $text) {
|
|
$this->assertStringContainsString($text, strip_tags($html));
|
|
}
|
|
$this->assertStringContainsString('与 常州电优闪供应链管理有限公司 合作的', strip_tags($html));
|
|
$this->assertStringNotContainsString('与 宿迁天枢物流科技有限公司 合作的', strip_tags($html));
|
|
$this->assertStringContainsString('均归属于 宿迁天枢物流科技有限公司', strip_tags($html));
|
|
$this->assertStringContainsString('确认企业:宿迁天枢物流科技有限公司', strip_tags($html));
|
|
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
if ($request->url() !== 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file') {
|
|
return false;
|
|
}
|
|
|
|
return count($request->data()['signers'] ?? []) === 1
|
|
&& data_get($request->data(), 'signers.0.signerType') === 1
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo.orgName') === '宿迁天枢物流科技有限公司'
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo.transactorInfo.psnAccount') === '13800138000'
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo.transactorInfo.psnInfo.psnName') === '张经理';
|
|
});
|
|
}
|
|
|
|
public function test_shared_contract_can_be_confirmed_by_a_person_without_an_enterprise_seal(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$payload = [
|
|
'idempotency_key' => 'shared-contract-person',
|
|
'courier_name' => '常州圆通其他',
|
|
'document_type' => 'shared-contract',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => '常州电优闪供应链管理有限公司',
|
|
'signature_date' => '2026-09-21',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'archive_mode' => 'auto',
|
|
'counterparty_signer_type' => 'person',
|
|
'contract_partner_name' => '张三',
|
|
'shared_courier_names' => ['常州圆通其他', '常州圆通-丁楼'],
|
|
];
|
|
|
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)->assertRedirect();
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertSame('person', $signature->payload['counterparty_signer_type']);
|
|
$this->assertSame('张三', $signature->organization_name);
|
|
$this->assertSame('张三', $signature->operator_name);
|
|
$this->assertSame($payload['shared_courier_names'], $signature->payload['shared_courier_names']);
|
|
$this->assertStringStartsWith('%PDF-', Storage::disk('uploads')->get($signature->source_pdf_path));
|
|
$html = view('business.pdf.courier-agreement', [
|
|
'signature' => $signature,
|
|
'type' => $signature->document_type,
|
|
'title' => $signature->documentLabel(),
|
|
'payload' => $signature->payload,
|
|
'signatureDate' => '2026年9月21日',
|
|
'fontPath' => str_replace('\\', '/', resource_path('fonts/DroidSansFallback.ttf')),
|
|
])->render();
|
|
$this->assertStringContainsString('与 常州电优闪供应链管理有限公司 合作的', strip_tags($html));
|
|
$this->assertStringContainsString('均归属于 张三', strip_tags($html));
|
|
$this->assertStringContainsString('确认人(个人签字):张三', strip_tags($html));
|
|
$this->assertStringNotContainsString('法人', $html);
|
|
$this->assertStringNotContainsString('公司盖章', $html);
|
|
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
return $request->url() === 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file'
|
|
&& count($request->data()['signers'] ?? []) === 1
|
|
&& data_get($request->data(), 'signers.0.signerType') === 0
|
|
&& data_get($request->data(), 'signers.0.psnSignerInfo.psnAccount') === '13800138000'
|
|
&& data_get($request->data(), 'signers.0.psnSignerInfo.psnInfo.psnName') === '张三'
|
|
&& data_get($request->data(), 'signers.0.orgSignerInfo') === null;
|
|
});
|
|
|
|
$this->post(route('business.courier-agreement-signatures.store'), $payload)->assertRedirect();
|
|
$this->assertDatabaseCount('business_courier_agreement_signatures', 1);
|
|
Http::assertSentCount(4);
|
|
}
|
|
|
|
public function test_shared_contract_rejects_missing_real_name_without_creating_a_task_or_remote_request(): void
|
|
{
|
|
Http::fake();
|
|
$payload = [
|
|
'idempotency_key' => 'shared-contract-missing-name',
|
|
'courier_name' => '快递公司乙',
|
|
'document_type' => 'shared-contract',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-09-21',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'archive_mode' => 'auto',
|
|
'contract_partner_name' => '合同主体有限公司',
|
|
'shared_courier_names' => ['快递公司乙', '快递公司丙'],
|
|
];
|
|
|
|
$this->withExceptionHandling()->loginAs()
|
|
->postJson(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors('operator_name');
|
|
|
|
$payload['operator_name'] = ' ';
|
|
$this->postJson(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors('operator_name');
|
|
|
|
$payload['counterparty_signer_type'] = 'person';
|
|
unset($payload['operator_name']);
|
|
$payload['contract_partner_name'] = ' ';
|
|
$this->postJson(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors('contract_partner_name');
|
|
|
|
$payload['contract_partner_name'] = '张三';
|
|
$payload['counterparty_signer_type'] = 'invalid';
|
|
$this->postJson(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertStatus(422)
|
|
->assertJsonValidationErrors('counterparty_signer_type');
|
|
|
|
$this->assertDatabaseCount('business_courier_agreement_signatures', 0);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_quote_confirmation_binds_an_approved_version_and_uses_only_the_courier_signer(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$version = CourierQuoteVersion::create([
|
|
'company_id' => $this->company->id,
|
|
'version_code' => 'QB-CONF-001',
|
|
'source_quote_id' => 'QB-001',
|
|
'source_change_id' => CourierQuoteChange::create([
|
|
'company_id' => $this->company->id,
|
|
'number' => 'BG-CONF-001',
|
|
'source_quote_id' => 'QB-001',
|
|
'source_sheet_name' => '新北中通报价',
|
|
'carrier_name' => '新北中通',
|
|
'quote_type' => '标准报价',
|
|
'change_type' => '面单调整',
|
|
'status' => CourierQuoteChange::STATUS_APPROVED,
|
|
'effective_date' => now()->addDay()->toDateString(),
|
|
'summary' => '报价确认单测试。',
|
|
'created_by' => $this->user->id,
|
|
'approved_by' => $this->user->id,
|
|
'approved_at' => now(),
|
|
])->id,
|
|
'sheet_name' => '新北中通报价',
|
|
'company_name' => $this->company->name,
|
|
'carrier_name' => '新北中通',
|
|
'quote_type' => '标准报价',
|
|
'effective_date' => now()->addDay()->toDateString(),
|
|
'values_json' => json_encode([
|
|
['地区', '面单费', '计泡系数'],
|
|
['广东省', 6.5, 1.2],
|
|
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
|
|
'formulas_json' => json_encode([[], []], JSON_THROW_ON_ERROR),
|
|
'status' => 'approved',
|
|
'signature_status' => '未签署',
|
|
'created_by' => $this->user->id,
|
|
'approved_by' => $this->user->id,
|
|
]);
|
|
|
|
$this->loginAs()
|
|
->get(route('business.courier-agreement-signatures.quotes.preview', $version))
|
|
->assertOk()
|
|
->assertJsonPath('version_code', 'QB-CONF-001')
|
|
->assertJsonPath('carrier_name', '新北中通')
|
|
->assertJsonPath('face_fee', '6.5')
|
|
->assertJsonPath('bubble_coefficient', '1.2')
|
|
->assertJsonPath('values.1.0', '广东省');
|
|
|
|
$this->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), [
|
|
'idempotency_key' => 'quote-confirmation-001',
|
|
'courier_name' => '新北中通',
|
|
'document_type' => 'quote-confirmation',
|
|
'quote_version_id' => $version->id,
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-09-21',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'operator_name' => '李经理',
|
|
'archive_mode' => 'auto',
|
|
])
|
|
->assertRedirect();
|
|
|
|
$signature = CourierQuoteSignature::query()->firstOrFail();
|
|
$this->assertDatabaseCount('business_courier_agreement_signatures', 0);
|
|
$this->assertSame($version->id, $signature->quote_version_id);
|
|
$this->assertSame(CourierQuoteSignature::STATUS_SIGNING, $signature->status);
|
|
$this->assertSame('待签署', $version->fresh()->signature_status);
|
|
Storage::disk('uploads')->assertExists($signature->source_pdf_path);
|
|
$this->assertStringStartsWith('%PDF-', Storage::disk('uploads')->get($signature->source_pdf_path));
|
|
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
if ($request->url() !== 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file') {
|
|
return false;
|
|
}
|
|
|
|
$signers = $request->data()['signers'] ?? [];
|
|
|
|
return count($signers) === 1
|
|
&& ($signers[0]['signerType'] ?? null) === 1
|
|
&& ($signers[0]['orgSignerInfo']['orgName'] ?? null) === '新北中通'
|
|
&& ($signers[0]['orgSignerInfo']['transactorInfo']['psnAccount'] ?? null) === '13800138000';
|
|
});
|
|
}
|
|
|
|
public function test_signature_date_is_required_for_every_signing_request(): void
|
|
{
|
|
$payload = $this->accountProofPayload('missing-signature-date');
|
|
unset($payload['signature_date']);
|
|
|
|
$this->withExceptionHandling()->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertSessionHasErrors('signature_date');
|
|
|
|
$this->assertDatabaseCount('business_courier_agreement_signatures', 0);
|
|
}
|
|
|
|
public function test_all_agreement_templates_render_the_selected_signature_date(): void
|
|
{
|
|
$payload = [
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-09-21',
|
|
'account_proof_effective_date' => '2026-09-01',
|
|
'account_proof_account_name' => '测试收款人',
|
|
'account_proof_bank_name' => '测试银行',
|
|
'account_proof_bank_account' => '6222000012345678',
|
|
'account_change_date' => '2026-09-01',
|
|
'old_account_name' => '旧账户',
|
|
'old_account_number' => '6222000011111111',
|
|
'old_account_stop_date' => '2026-09-01',
|
|
'new_account_effective_date' => '2026-09-02',
|
|
'new_account_name' => '新账户',
|
|
'new_bank_name' => '新银行',
|
|
'new_bank_account' => '6222000022222222',
|
|
'former_company_name' => '变更前公司',
|
|
'entity_change_date' => '2026-09-01',
|
|
'changed_company_name' => '变更后公司',
|
|
'changed_company_start_date' => '2026-09-02',
|
|
'contract_partner_name' => '快递公司甲',
|
|
'shared_courier_names' => ['快递公司乙'],
|
|
'cooperation_party_a_name' => '甲方公司',
|
|
'cooperation_party_a_phone' => '0519-88886666',
|
|
'cooperation_party_b_name' => '乙方公司',
|
|
'cooperation_party_b_phone' => '0519-88889999',
|
|
'cooperation_start_date' => '2026-09-01',
|
|
'cooperation_end_date' => '2027-08-31',
|
|
'principal_name' => '委托方',
|
|
'principal_id_number' => '91320400TEST',
|
|
'trustee_name' => '受托方',
|
|
'trustee_id_number' => '320400TEST',
|
|
'authorization_items' => ['1'],
|
|
'bill_counterparty' => '对账方',
|
|
'loan_counterparty' => '借支方',
|
|
'payment_counterparty' => '付款方',
|
|
'authorization_start_date' => '2026-09-01',
|
|
'authorization_end_date' => '2027-08-31',
|
|
];
|
|
|
|
foreach (['account-proof', 'account-change', 'entity-change', 'shared-contract', 'cooperation-contract', 'authorization'] as $type) {
|
|
$signature = new CourierAgreementSignature([
|
|
'business_no' => 'AGR-DATE-' . $type,
|
|
'template_code' => CourierAgreementSignature::DOCUMENTS[$type]['code'],
|
|
'document_type' => $type,
|
|
]);
|
|
$html = view('business.pdf.courier-agreement', [
|
|
'signature' => $signature,
|
|
'type' => $type,
|
|
'title' => CourierAgreementSignature::DOCUMENTS[$type]['name'],
|
|
'payload' => $payload,
|
|
'signatureDate' => '2026年9月21日',
|
|
'fontPath' => str_replace('\\', '/', resource_path('fonts/DroidSansFallback.ttf')),
|
|
])->render();
|
|
|
|
$this->assertStringContainsString('2026年9月21日', strip_tags($html), "{$type} 未输出签署日期变量");
|
|
$this->assertStringNotContainsString('签署后生成', $html, "{$type} 仍包含旧占位文案");
|
|
}
|
|
}
|
|
|
|
public function test_verified_agreement_callback_is_idempotent_and_queues_archive(): void
|
|
{
|
|
Queue::fake();
|
|
$signature = CourierAgreementSignature::create([
|
|
'company_id' => $this->company->id,
|
|
'business_no' => 'AGR-CALLBACK-001',
|
|
'idempotency_key' => 'callback-001',
|
|
'document_type' => 'account-proof',
|
|
'template_code' => 'TPL-ACCOUNT-CERT-V2',
|
|
'template_version' => '2',
|
|
'source_file_name' => '收款账户证明文件.doc',
|
|
'status' => CourierAgreementSignature::STATUS_SIGNING,
|
|
'courier_name' => '新北中通',
|
|
'company_full_name' => $this->company->name,
|
|
'organization_name' => '新北中通快递有限公司',
|
|
'operator_name' => '张经理',
|
|
'operator_account' => '13800138000',
|
|
'payload' => ['company_full_name' => $this->company->name, 'courier_name' => '新北中通'],
|
|
'environment' => 'sandbox',
|
|
'source_pdf_path' => 'courier-agreements/test/source.pdf',
|
|
'source_pdf_sha256' => hash('sha256', '%PDF-source'),
|
|
'esign_file_id' => 'file-agreement-001',
|
|
'esign_sign_flow_id' => 'flow-agreement-callback',
|
|
'remote_status' => '1',
|
|
'created_by' => $this->user->id,
|
|
'updated_by' => $this->user->id,
|
|
]);
|
|
$payload = json_encode([
|
|
'action' => 'SIGN_FLOW_COMPLETE',
|
|
'signFlowId' => 'flow-agreement-callback',
|
|
'signFlowStatus' => '2',
|
|
], JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
|
|
$timestamp = (string) now()->getTimestampMs();
|
|
$callbackSignature = hash_hmac('sha256', $timestamp . '12' . $payload, 'sandbox-app-secret');
|
|
|
|
$server = [
|
|
'HTTP_X_TSIGN_OPEN_TIMESTAMP' => $timestamp,
|
|
'HTTP_X_TSIGN_OPEN_SIGNATURE_ALGORITHM' => 'HmacSHA256',
|
|
'HTTP_X_TSIGN_OPEN_APP_ID' => 'sandbox-app-id',
|
|
'HTTP_X_TSIGN_OPEN_SIGNATURE' => $callbackSignature,
|
|
'CONTENT_TYPE' => 'application/json',
|
|
];
|
|
$this->call('POST', route('integrations.esign.callback') . '?zeta=2&alpha=1', [], [], [], $server, $payload)
|
|
->assertOk();
|
|
|
|
$signature->refresh();
|
|
$this->assertSame(CourierAgreementSignature::STATUS_COMPLETED, $signature->status);
|
|
$this->assertSame(hash('sha256', $payload), $signature->callback_payload_hash);
|
|
Queue::assertPushed(ArchiveCourierAgreementSignature::class, 1);
|
|
$this->assertNull(app(\App\Support\CourierAgreementSignatureService::class)->acceptCallback(
|
|
'flow-agreement-callback', '2', hash('sha256', $payload),
|
|
));
|
|
|
|
$this->call('POST', route('integrations.esign.callback') . '?zeta=2&alpha=1', [], [], [], $server, $payload)
|
|
->assertOk();
|
|
Queue::assertPushed(ArchiveCourierAgreementSignature::class, 1);
|
|
}
|
|
|
|
public function test_polling_archives_mainland_agreement_without_completion_certificate(): void
|
|
{
|
|
$signature = CourierAgreementSignature::create([
|
|
'company_id' => $this->company->id,
|
|
'business_no' => 'AGR-MAINLAND-001',
|
|
'idempotency_key' => 'mainland-001',
|
|
'document_type' => 'account-proof',
|
|
'template_code' => 'TPL-ACCOUNT-CERT-V2',
|
|
'template_version' => '2',
|
|
'source_file_name' => '收款账户证明文件.doc',
|
|
'status' => CourierAgreementSignature::STATUS_SIGNING,
|
|
'courier_name' => '新北中通',
|
|
'company_full_name' => $this->company->name,
|
|
'organization_name' => '新北中通快递有限公司',
|
|
'operator_name' => '张经理',
|
|
'operator_account' => '13800138000',
|
|
'payload' => ['archive_mode' => 'auto'],
|
|
'environment' => 'sandbox',
|
|
'source_pdf_path' => 'courier-agreements/mainland/source.pdf',
|
|
'source_pdf_sha256' => hash('sha256', '%PDF-source'),
|
|
'esign_file_id' => 'file-agreement-mainland',
|
|
'esign_sign_flow_id' => 'flow-agreement-mainland',
|
|
'remote_status' => '1',
|
|
'created_by' => $this->user->id,
|
|
'updated_by' => $this->user->id,
|
|
]);
|
|
Http::fake([
|
|
'https://smlopenapi.esign.test/v3/sign-flow/flow-agreement-mainland/detail' => Http::response([
|
|
'code' => 0,
|
|
'data' => ['signFlowStatus' => 2],
|
|
]),
|
|
'https://smlopenapi.esign.test/v3/sign-flow/flow-agreement-mainland/file-download-url' => Http::response([
|
|
'code' => 0,
|
|
'data' => [
|
|
'files' => [['downloadUrl' => 'https://download.esign.test/agreement-mainland']],
|
|
'certificateDownloadUrl' => null,
|
|
],
|
|
]),
|
|
'https://download.esign.test/agreement-mainland' => Http::response('%PDF-agreement-mainland'),
|
|
]);
|
|
|
|
$result = app(\App\Support\CourierAgreementSignatureService::class)
|
|
->synchronize($signature, $this->user->id);
|
|
|
|
$this->assertSame(CourierAgreementSignature::STATUS_ARCHIVED, $result->status);
|
|
Storage::disk('uploads')->assertExists($result->signed_pdf_path);
|
|
$this->assertNull($result->certificate_path);
|
|
$this->assertNull($result->certificate_sha256);
|
|
}
|
|
|
|
public function test_source_download_uses_existing_local_history_file_without_remote_request(): void
|
|
{
|
|
Http::fake();
|
|
$bytes = '%PDF-local-history';
|
|
$signature = $this->agreementSignature([
|
|
'source_pdf_sha256' => hash('sha256', $bytes),
|
|
]);
|
|
Storage::disk('uploads')->put($signature->source_pdf_path, $bytes);
|
|
|
|
$this->loginAs()
|
|
->get(route('business.courier-agreement-signatures.download', [$signature, 'source']))
|
|
->assertOk()
|
|
->assertDownload();
|
|
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_source_download_restores_missing_history_file_from_esign_after_hash_verification(): void
|
|
{
|
|
$bytes = '%PDF-remote-history';
|
|
$signature = $this->agreementSignature([
|
|
'source_pdf_sha256' => hash('sha256', $bytes),
|
|
'esign_file_id' => 'file-history-001',
|
|
]);
|
|
Http::fake([
|
|
'https://smlopenapi.esign.test/v3/files/file-history-001' => Http::response([
|
|
'code' => 0,
|
|
'data' => ['fileDownloadUrl' => 'https://download.esign.test/source-history-001'],
|
|
]),
|
|
'https://download.esign.test/source-history-001' => Http::response($bytes),
|
|
]);
|
|
|
|
$this->loginAs()
|
|
->get(route('business.courier-agreement-signatures.download', [$signature, 'source']))
|
|
->assertOk()
|
|
->assertDownload();
|
|
|
|
Storage::disk('uploads')->assertExists($signature->source_pdf_path);
|
|
$this->assertSame($bytes, Storage::disk('uploads')->get($signature->source_pdf_path));
|
|
Http::assertSentCount(2);
|
|
}
|
|
|
|
public function test_source_download_rejects_remote_history_file_when_hash_does_not_match(): void
|
|
{
|
|
$signature = $this->agreementSignature([
|
|
'source_pdf_sha256' => hash('sha256', '%PDF-expected-history'),
|
|
'esign_file_id' => 'file-history-mismatch',
|
|
]);
|
|
Http::fake([
|
|
'https://smlopenapi.esign.test/v3/files/file-history-mismatch' => Http::response([
|
|
'code' => 0,
|
|
'data' => ['downloadUrl' => 'https://download.esign.test/source-history-mismatch'],
|
|
]),
|
|
'https://download.esign.test/source-history-mismatch' => Http::response('%PDF-different-history'),
|
|
]);
|
|
|
|
$this->withExceptionHandling()->loginAs()
|
|
->get(route('business.courier-agreement-signatures.download', [$signature, 'source']))
|
|
->assertRedirect(route('business.pages.show', [
|
|
'section' => 'courier-payables',
|
|
'page' => 'agreement-signing',
|
|
'agreement_view' => 'tasks',
|
|
]))
|
|
->assertSessionHas('error', 'E 签宝历史原文件与系统留存摘要不一致,已停止自动恢复。');
|
|
|
|
Storage::disk('uploads')->assertMissing($signature->source_pdf_path);
|
|
}
|
|
|
|
public function test_source_download_explains_when_missing_history_was_never_uploaded(): void
|
|
{
|
|
$signature = $this->agreementSignature();
|
|
|
|
$this->withExceptionHandling()->loginAs()
|
|
->get(route('business.courier-agreement-signatures.download', [$signature, 'source']))
|
|
->assertRedirect(route('business.pages.show', [
|
|
'section' => 'courier-payables',
|
|
'page' => 'agreement-signing',
|
|
'agreement_view' => 'tasks',
|
|
]))
|
|
->assertSessionHas('error', '本地历史文件缺失,且该任务未成功上传至 E 签宝,无法自动恢复。');
|
|
}
|
|
|
|
public function test_esign_business_error_is_saved_as_failed_and_preserves_uploaded_file_id(): void
|
|
{
|
|
Http::fake(function (ClientRequest $request) {
|
|
return match (true) {
|
|
$request->url() === 'https://smlopenapi.esign.test/v3/files/file-upload-url' => Http::response([
|
|
'code' => 0,
|
|
'data' => ['fileId' => 'file-business-error', 'fileUploadUrl' => 'https://upload.esign.test/business-error'],
|
|
]),
|
|
$request->url() === 'https://upload.esign.test/business-error' => Http::response(['ok' => true]),
|
|
$request->url() === 'https://smlopenapi.esign.test/v3/files/file-business-error' => Http::response([
|
|
'code' => 0,
|
|
'data' => ['status' => 2],
|
|
]),
|
|
$request->url() === 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file' => Http::response([
|
|
'code' => 1435002,
|
|
'message' => '签署账号 18810726376 与身份信息不一致',
|
|
]),
|
|
default => Http::response(['code' => 404], 404),
|
|
};
|
|
});
|
|
|
|
$this->withExceptionHandling()->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $this->accountProofPayload('business-error'))
|
|
->assertSessionHasErrors('signature');
|
|
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertSame(CourierAgreementSignature::STATUS_FAILED, $signature->status);
|
|
$this->assertSame('file-business-error', $signature->esign_file_id);
|
|
$this->assertStringContainsString('1435002', $signature->last_error);
|
|
$this->assertStringContainsString('188****6376', $signature->last_error);
|
|
$this->assertStringNotContainsString('18810726376', $signature->last_error);
|
|
}
|
|
|
|
public function test_server_error_during_flow_creation_keeps_unknown_status(): void
|
|
{
|
|
Http::fake(function (ClientRequest $request) {
|
|
return match (true) {
|
|
$request->url() === 'https://smlopenapi.esign.test/v3/files/file-upload-url' => Http::response([
|
|
'code' => 0,
|
|
'data' => ['fileId' => 'file-server-error', 'fileUploadUrl' => 'https://upload.esign.test/server-error'],
|
|
]),
|
|
$request->url() === 'https://upload.esign.test/server-error' => Http::response(['ok' => true]),
|
|
$request->url() === 'https://smlopenapi.esign.test/v3/files/file-server-error' => Http::response([
|
|
'code' => 0,
|
|
'data' => ['status' => 2],
|
|
]),
|
|
$request->url() === 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file' => Http::response([], 500),
|
|
default => Http::response(['code' => 404], 404),
|
|
};
|
|
});
|
|
|
|
$this->withExceptionHandling()->loginAs()
|
|
->post(route('business.courier-agreement-signatures.store'), $this->accountProofPayload('server-error'))
|
|
->assertSessionHasErrors('signature');
|
|
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertSame(CourierAgreementSignature::STATUS_UNKNOWN, $signature->status);
|
|
$this->assertSame('file-server-error', $signature->esign_file_id);
|
|
}
|
|
|
|
private function fakeEsign(): void
|
|
{
|
|
Http::fake(function (ClientRequest $request) {
|
|
return match (true) {
|
|
$request->url() === 'https://smlopenapi.esign.test/v3/files/file-upload-url' => Http::response([
|
|
'code' => 0,
|
|
'data' => ['fileId' => 'file-agreement-001', 'fileUploadUrl' => 'https://upload.esign.test/agreement-001'],
|
|
]),
|
|
$request->url() === 'https://upload.esign.test/agreement-001' => Http::response(['ok' => true]),
|
|
$request->url() === 'https://smlopenapi.esign.test/v3/files/file-agreement-001' => Http::response([
|
|
'code' => 0,
|
|
'data' => ['status' => 2],
|
|
]),
|
|
$request->url() === 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file' => Http::response([
|
|
'code' => 0,
|
|
'data' => ['signFlowId' => 'flow-agreement-001'],
|
|
]),
|
|
default => Http::response(['code' => 404], 404),
|
|
};
|
|
});
|
|
}
|
|
|
|
private function accountProofPayload(string $idempotencyKey): array
|
|
{
|
|
return [
|
|
'idempotency_key' => $idempotencyKey,
|
|
'courier_name' => '新北中通',
|
|
'document_type' => 'account-proof',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-09-21',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'operator_name' => '张经理',
|
|
'counterparty_organization_name' => '新北中通快递有限公司',
|
|
'archive_mode' => 'auto',
|
|
'account_proof_effective_date' => now()->toDateString(),
|
|
'account_proof_account_name' => '新北中通快递有限公司',
|
|
'account_proof_bank_name' => '中国工商银行',
|
|
'account_proof_bank_account' => '6222000012345678',
|
|
];
|
|
}
|
|
|
|
private function cooperationContractPayload(string $idempotencyKey): array
|
|
{
|
|
return [
|
|
'idempotency_key' => $idempotencyKey,
|
|
'courier_name' => '新北中通',
|
|
'document_type' => 'cooperation-contract',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-09-21',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'secondary_esign_account' => '13900139000',
|
|
'operator_name' => '张经理',
|
|
'secondary_operator_name' => '李经理',
|
|
'archive_mode' => 'auto',
|
|
'cooperation_party_a_name' => $this->company->name,
|
|
'cooperation_party_a_phone' => '0519-88886666',
|
|
'cooperation_party_b_name' => '新北中通快递有限公司',
|
|
'cooperation_party_b_phone' => '18112896651',
|
|
'cooperation_start_date' => '2026-01-01',
|
|
'cooperation_end_date' => '2026-12-31',
|
|
];
|
|
}
|
|
|
|
private function accountChangePayload(string $idempotencyKey): array
|
|
{
|
|
return [
|
|
'idempotency_key' => $idempotencyKey,
|
|
'courier_name' => '新北中通',
|
|
'document_type' => 'account-change',
|
|
'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name,
|
|
'signature_date' => '2026-09-21',
|
|
'signing_deadline' => now()->addDays(7)->toDateString(),
|
|
'esign_account' => '13800138000',
|
|
'operator_name' => '张经理',
|
|
'counterparty_organization_name' => '新北中通快递有限公司',
|
|
'archive_mode' => 'auto',
|
|
'account_change_date' => now()->toDateString(),
|
|
'old_account_name' => '新北中通快递有限公司',
|
|
'old_account_number' => '6222000012345678',
|
|
'old_account_stop_date' => now()->toDateString(),
|
|
'new_account_effective_date' => now()->addDay()->toDateString(),
|
|
'new_account_name' => '新北中通快递有限公司',
|
|
'new_bank_name' => '中国工商银行',
|
|
'new_bank_account' => '6222000098765432',
|
|
];
|
|
}
|
|
|
|
private function agreementSignature(array $attributes = []): CourierAgreementSignature
|
|
{
|
|
return CourierAgreementSignature::create(array_merge([
|
|
'company_id' => $this->company->id,
|
|
'business_no' => 'AGR-DOWNLOAD-HISTORY-001',
|
|
'idempotency_key' => 'download-history-001',
|
|
'document_type' => 'account-proof',
|
|
'template_code' => 'TPL-ACCOUNT-CERT-V2',
|
|
'template_version' => '2',
|
|
'source_file_name' => '收款账户证明文件.doc',
|
|
'status' => CourierAgreementSignature::STATUS_FAILED,
|
|
'courier_name' => '测试快递',
|
|
'company_full_name' => $this->company->name,
|
|
'organization_name' => '测试快递有限公司',
|
|
'operator_name' => '测试人员',
|
|
'operator_account' => '13800138000',
|
|
'payload' => ['signing_subject' => $this->company->name],
|
|
'environment' => 'sandbox',
|
|
'source_pdf_path' => 'courier-agreements/history/source.pdf',
|
|
'source_pdf_sha256' => hash('sha256', '%PDF-history-source'),
|
|
'created_by' => $this->user->id,
|
|
'updated_by' => $this->user->id,
|
|
], $attributes));
|
|
}
|
|
}
|