237 lines
14 KiB
PHP
237 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Business;
|
|
|
|
use App\Models\Auth\Permission;
|
|
use App\Models\Business\CourierAgreementSignature;
|
|
use App\Support\CourierAgreementCredentials;
|
|
use App\Support\CourierAgreementSignatureService;
|
|
use Illuminate\Http\Client\Request as ClientRequest;
|
|
use Illuminate\Http\UploadedFile;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use RuntimeException;
|
|
use Tests\Feature\FeatureTestCase;
|
|
|
|
class CourierAgreementCredentialsTest extends FeatureTestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Storage::fake('uploads');
|
|
config(['app.key' => 'base64:' . base64_encode(str_repeat('k', 32)),
|
|
'services.esign.environment' => 'sandbox', 'services.esign.base_url' => 'https://smlopenapi.esign.test',
|
|
'services.esign.app_id' => 'sandbox-test-id', 'services.esign.app_secret' => 'sandbox-test-secret']);
|
|
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
|
}
|
|
|
|
public function test_documents_are_optional_and_account_change_rejects_credentials(): void
|
|
{
|
|
foreach (['authorization', 'entity-change', 'account-change'] as $type) {
|
|
$this->assertTrue(Validator::make([], CourierAgreementCredentials::rules($type, false))->passes());
|
|
}
|
|
$this->assertTrue(Validator::make(['credential_required' => 1], CourierAgreementCredentials::rules('account-change', true))->fails());
|
|
$this->assertTrue(Validator::make(['credential_required' => 1], CourierAgreementCredentials::rules('authorization', true))->fails());
|
|
}
|
|
|
|
public function test_page_renders_optional_uploads_and_live_sheet_preview(): void
|
|
{
|
|
$couriers = \Mockery::mock(\Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository::class);
|
|
$couriers->shouldReceive('names')->andReturn(['测试快递']);
|
|
$couriers->shouldReceive('signingAccountsByName')->andReturn(['测试快递' => '13800138000']);
|
|
$this->app->instance(\Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository::class, $couriers);
|
|
$suppliers = \Mockery::mock(\Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository::class);
|
|
$suppliers->shouldReceive('signingOptions')->andReturn([]);
|
|
$this->app->instance(\Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository::class, $suppliers);
|
|
$response = $this->loginAs()->get(route('business.pages.show', [
|
|
'section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'create',
|
|
]));
|
|
$response->assertOk()->assertSee('enctype="multipart/form-data"', false)
|
|
->assertSee('data-credential-preview', false)->assertSee('data-credential-required disabled', false)
|
|
->assertSeeText('本次需要补充证件并签署留档');
|
|
if ($qa = getenv('CAIWU_CREDENTIAL_QA')) {
|
|
file_put_contents($qa . '/page.html', $response->getContent());
|
|
}
|
|
}
|
|
|
|
public function test_authorization_sends_three_files_and_each_sheet_has_only_one_signature_field(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$response = $this->loginAs()->post(route('business.courier-agreement-signatures.store'), $this->payload('authorization'));
|
|
$response->assertRedirect()->assertSessionHasNoErrors();
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertCount(2, $signature->credentialDocuments());
|
|
foreach ($signature->credentialDocuments() as $document) {
|
|
$this->assertCount(2, $document['originals']);
|
|
Storage::disk('uploads')->assertExists($document['source_path']);
|
|
}
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
if (! str_ends_with($request->url(), '/create-by-file')) return false;
|
|
$data = $request->data();
|
|
$this->assertCount(3, $data['docs']);
|
|
$this->assertCount(3, $data['signers'][0]['signFields']);
|
|
$this->assertCount(1, $data['signers'][1]['signFields']);
|
|
foreach (array_slice($data['signers'][0]['signFields'], 1) as $field) {
|
|
$this->assertSame('1', $field['normalSignFieldConfig']['signFieldPosition']['positionPage']);
|
|
$this->assertFalse($field['normalSignFieldConfig']['freeMode']);
|
|
}
|
|
return true;
|
|
});
|
|
// Reposting the same request must not create a second signing flow.
|
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $this->payload('authorization'))->assertRedirect();
|
|
$this->assertCount(1, Http::recorded(fn (ClientRequest $request) => str_ends_with($request->url(), '/create-by-file')));
|
|
}
|
|
|
|
public function test_missing_back_side_stops_request_before_sending_files(): void
|
|
{
|
|
Http::fake();
|
|
$payload = $this->payload('authorization');
|
|
unset($payload['credential_images']['id_back']);
|
|
$this->withExceptionHandling()->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertSessionHasErrors('credential_images.id_back');
|
|
$this->assertDatabaseCount('business_courier_agreement_signatures', 0);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_duplicate_processing_request_does_not_create_another_flow(): void
|
|
{
|
|
Http::fake();
|
|
$payload = $this->payload('authorization');
|
|
$key = 'agreement-' . hash('sha256', $this->company->id . '|' . $payload['idempotency_key']);
|
|
CourierAgreementSignature::create(['company_id' => $this->company->id, 'business_no' => 'AGR-IN-PROGRESS',
|
|
'idempotency_key' => $key, 'document_type' => 'authorization', 'template_code' => 'TPL-AUTHORIZATION-V1',
|
|
'template_version' => '1', 'source_file_name' => '授权委托书.pdf', 'status' => 'sending',
|
|
'courier_name' => '测试快递', 'company_full_name' => $this->company->name,
|
|
'organization_name' => '测试委托公司', 'payload' => [], 'environment' => 'sandbox',
|
|
'source_pdf_path' => 'pending/source.pdf', 'source_pdf_sha256' => '',
|
|
'created_by' => $this->user->id, 'updated_by' => $this->user->id]);
|
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertRedirect()->assertSessionHasNoErrors();
|
|
Http::assertNothingSent();
|
|
$this->assertDatabaseCount('business_courier_agreement_signatures', 1);
|
|
}
|
|
|
|
public function test_personal_signer_cannot_be_assigned_license_stamp(): void
|
|
{
|
|
Http::fake();
|
|
$payload = $this->payload('entity-change');
|
|
$payload['entity_change_old_company_deregistered'] = 1;
|
|
$payload['credential_signer'] = 0;
|
|
$this->withExceptionHandling()->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)
|
|
->assertSessionHasErrors('credential_signer');
|
|
$this->assertDatabaseCount('business_courier_agreement_signatures', 0);
|
|
Http::assertNothingSent();
|
|
}
|
|
|
|
public function test_entity_change_sends_four_files_with_credentials_only_for_new_company(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $this->payload('entity-change'))
|
|
->assertRedirect()->assertSessionHasNoErrors();
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$this->assertCount(3, $signature->credentialDocuments());
|
|
$this->assertCount(1, $signature->credentialDocuments()['license']['originals']);
|
|
if ($qa = getenv('CAIWU_CREDENTIAL_QA')) {
|
|
foreach ($signature->credentialDocuments() as $key => $document) {
|
|
file_put_contents($qa . '/' . $key . '.pdf', Storage::disk('uploads')->get($document['source_path']));
|
|
}
|
|
}
|
|
Http::assertSent(function (ClientRequest $request): bool {
|
|
if (! str_ends_with($request->url(), '/create-by-file')) return false;
|
|
$data = $request->data();
|
|
$this->assertCount(4, $data['docs']);
|
|
$this->assertCount(1, $data['signers'][0]['signFields']);
|
|
$this->assertCount(4, $data['signers'][1]['signFields']);
|
|
$this->assertSame(1, $data['signers'][1]['signerType']);
|
|
return true;
|
|
});
|
|
}
|
|
|
|
public function test_archive_matches_file_ids_even_when_remote_files_are_out_of_order(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $this->payload('authorization'))->assertRedirect();
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$signature->update(['status' => 'completed', 'remote_status' => '2']);
|
|
$this->fakeDownloads($signature);
|
|
$archived = app(CourierAgreementSignatureService::class)->archive($signature);
|
|
$this->assertSame('archived', $archived->status);
|
|
$this->assertSame('%PDF-main', Storage::disk('uploads')->get($archived->signed_pdf_path));
|
|
foreach ($archived->credentialDocuments() as $key => $document) {
|
|
$this->assertSame('%PDF-' . $key, Storage::disk('uploads')->get($document['signed_path']));
|
|
$this->assertSame(hash('sha256', '%PDF-' . $key), $document['signed_sha256']);
|
|
$this->loginAs()->get(route('business.courier-agreement-signatures.download', [$archived, 'credential-' . $key . '-signed']))->assertOk();
|
|
$preview = $this->loginAs()->get(route('business.courier-agreement-signatures.download', [$archived, 'credential-' . $key . '-signed', 'preview' => 1]));
|
|
$preview->assertOk();
|
|
$this->assertStringStartsWith('inline;', $preview->headers->get('Content-Disposition'));
|
|
}
|
|
}
|
|
|
|
public function test_missing_credential_download_prevents_marking_task_archived(): void
|
|
{
|
|
$this->fakeEsign();
|
|
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $this->payload('authorization'))->assertRedirect();
|
|
$signature = CourierAgreementSignature::query()->firstOrFail();
|
|
$signature->update(['status' => 'completed', 'remote_status' => '2']);
|
|
$this->fakeDownloads($signature, true);
|
|
try {
|
|
app(CourierAgreementSignatureService::class)->archive($signature);
|
|
$this->fail('Missing credential must prevent successful archive');
|
|
} catch (RuntimeException $e) {
|
|
$this->assertStringContainsString('全部证件', $e->getMessage());
|
|
}
|
|
$this->assertSame('completed', $signature->fresh()->status);
|
|
$this->assertNull($signature->fresh()->archived_at);
|
|
}
|
|
|
|
private function fakeEsign(): void
|
|
{
|
|
$counter = 0;
|
|
Http::fake(function (ClientRequest $request) use (&$counter) {
|
|
if (str_ends_with($request->url(), '/file-upload-url')) {
|
|
$id = 'file-' . ++$counter;
|
|
return Http::response(['code' => 0, 'data' => ['fileId' => $id, 'fileUploadUrl' => 'https://upload.esign.test/' . $id]]);
|
|
}
|
|
if (str_starts_with($request->url(), 'https://upload.esign.test/')) return Http::response('ok');
|
|
if (preg_match('~/v3/files/file-\d+$~', $request->url())) return Http::response(['code' => 0, 'data' => ['status' => 2]]);
|
|
if (str_ends_with($request->url(), '/create-by-file')) return Http::response(['code' => 0, 'data' => ['signFlowId' => 'flow-credentials']]);
|
|
return Http::response([], 404);
|
|
});
|
|
}
|
|
|
|
private function fakeDownloads(CourierAgreementSignature $signature, bool $missing = false): void
|
|
{
|
|
Http::swap(new \Illuminate\Http\Client\Factory());
|
|
$files = [['fileId' => $signature->esign_file_id, 'downloadUrl' => 'https://download.esign.test/main']];
|
|
$responses = ['https://download.esign.test/main' => Http::response('%PDF-main')];
|
|
foreach ($signature->credentialDocuments() as $key => $document) {
|
|
if (! $missing || $key !== 'bank') $files[] = ['fileId' => $document['file_id'], 'downloadUrl' => 'https://download.esign.test/' . $key];
|
|
$responses['https://download.esign.test/' . $key] = Http::response('%PDF-' . $key);
|
|
}
|
|
$responses['https://smlopenapi.esign.test/v3/sign-flow/flow-credentials/file-download-url'] = Http::response(['code' => 0, 'data' => ['files' => array_reverse($files)]]);
|
|
Http::fake($responses);
|
|
}
|
|
|
|
private function payload(string $type): array
|
|
{
|
|
$images = [];
|
|
foreach (CourierAgreementCredentials::definitions($type) as $definition) {
|
|
foreach ($definition['images'] as $key) $images[$key] = UploadedFile::fake()->image($key . '.png', 900, 550);
|
|
}
|
|
return ['partner_type' => 'courier', 'idempotency_key' => 'credentials-' . $type,
|
|
'courier_name' => '测试快递', 'document_type' => $type, 'signing_subject' => $this->company->name,
|
|
'company_full_name' => $this->company->name, 'signature_date' => now()->toDateString(),
|
|
'signing_deadline' => now()->addDays(7)->toDateString(), 'esign_account' => '13800138000',
|
|
'secondary_esign_account' => '13900139000', 'operator_name' => '测试甲方', 'secondary_operator_name' => '测试乙方',
|
|
'counterparty_signer_type' => 'organization', 'secondary_signer_type' => 'person', 'archive_mode' => 'auto',
|
|
'principal_name' => '测试委托公司', 'principal_id_number' => 'TEST-PRINCIPAL',
|
|
'trustee_name' => '测试乙方', 'trustee_id_number' => 'TEST-TRUSTEE', 'authorization_items' => ['1'],
|
|
'bill_counterparty' => '测试往来公司', 'loan_counterparty' => '测试往来公司', 'payment_counterparty' => '测试往来公司',
|
|
'authorization_start_date' => now()->toDateString(), 'authorization_end_date' => now()->addYear()->toDateString(),
|
|
'former_company_name' => '测试原公司', 'changed_company_name' => '测试新公司',
|
|
'entity_change_date' => now()->toDateString(), 'changed_company_start_date' => now()->toDateString(),
|
|
'credential_required' => 1, 'credential_signer' => $type === 'entity-change' ? 1 : 0, 'credential_images' => $images];
|
|
}
|
|
}
|