修复收款账户协议签署主体识别 #231

Merged
limeng merged 5 commits from fix/limeng-agreement-signing-payee-separation into main 2026-09-21 17:01:32 +08:00
5 changed files with 128 additions and 26 deletions

View File

@ -91,7 +91,7 @@ class CourierAgreementSignatures extends Controller
// signing_subject identifies our initiating company. The E-sign
// organization must be the counterparty that receives this request.
$data['organization_name'] = $this->counterpartyOrganizationName($data);
$data['operator_name'] = $data['operator_name'] ?? $data['courier_name'];
$data['operator_name'] = trim((string) ($data['operator_name'] ?? '')) ?: null;
$data['operator_account'] = $data['esign_account'];
$data['payload'] = $this->payload($data);
@ -282,7 +282,8 @@ class CourierAgreementSignatures extends Controller
'signing_deadline' => $date,
'esign_account' => ['required', 'string', 'max:255'],
'secondary_esign_account' => ['required_if:document_type,entity-change,cooperation-contract,authorization', 'nullable', 'string', 'max:255'],
'operator_name' => ['nullable', 'string', 'max:100'],
'operator_name' => ['required_if:document_type,account-proof,account-change', 'nullable', 'string', 'min:2', 'max:100'],
'counterparty_organization_name' => ['required_if:document_type,account-proof,account-change', 'nullable', 'string', 'min:2', 'max:255'],
'archive_mode' => ['required', 'in:auto,manual'],
'account_proof_effective_date' => ['required_if:document_type,account-proof', 'date'],
'account_proof_account_name' => ['required_if:document_type,account-proof', 'string', 'max:255'],
@ -369,8 +370,7 @@ class CourierAgreementSignatures extends Controller
private function counterpartyOrganizationName(array $data): string
{
return match ($data['document_type']) {
'account-proof' => trim((string) $data['account_proof_account_name']),
'account-change' => trim((string) $data['new_account_name']),
'account-proof', 'account-change' => trim((string) $data['counterparty_organization_name']),
'shared-contract' => trim((string) (($data['shared_courier_names'][0] ?? null) ?: ($data['shared_courier_name'] ?? $data['courier_name']))),
'entity-change' => trim((string) ($data['former_company_name'] ?? $data['courier_name'])),
'authorization' => trim((string) ($data['principal_name'] ?? $data['courier_name'])),

View File

@ -285,15 +285,10 @@ final class CourierAgreementSignatureService
$payload = $signature->payload ?? [];
if (in_array($signature->document_type, ['account-proof', 'account-change'], true)) {
$name = $signature->organization_name;
$isOrganization = $this->looksLikeOrganization($name);
return [[
'signer_type' => $isOrganization ? 1 : 0,
'organization_name' => $isOrganization ? $name : null,
'operator_name' => $isOrganization
? ($signature->operator_name ?: $signature->courier_name)
: $name,
'signer_type' => 1,
'organization_name' => $signature->organization_name,
'operator_name' => $signature->operator_name,
'operator_account' => $signature->operator_account,
]];
}
@ -352,11 +347,6 @@ final class CourierAgreementSignatureService
return [$primary];
}
private function looksLikeOrganization(string $name): bool
{
return preg_match('/公司|有限公司|有限责任|企业|集团|合作社|事务所|中心|厂/u', $name) === 1;
}
private function safeTitle(string $title): string
{
$title = preg_replace('/[^\p{Han}\p{L}\p{N}_ -]/u', '-', $title) ?: '协议签署';

View File

@ -1427,9 +1427,15 @@
<input class="agreement-control" id="agreement-esign-account" name="esign_account" type="text" value="" data-esign-account placeholder="手机号或邮箱" autocomplete="email" required>
<p class="agreement-help">选择快递公司后自动带出工作平台的“电子签章经办人”(dzqzjbr);如资料未维护,可手工填写对方在 E 签宝实名绑定的手机号或邮箱。</p>
</div>
<div class="agreement-field" data-account-organization-field>
<label for="agreement-counterparty-organization">对方签署企业全称 <span class="agreement-required" aria-hidden="true">*</span></label>
<input class="agreement-control" id="agreement-counterparty-organization" name="counterparty_organization_name" type="text" placeholder="填写对方在 E 签宝认证的企业全称" autocomplete="organization" data-account-organization-name required>
<p class="agreement-help">必须与对方 E 签宝企业认证名称一致;这是签署主体,不要求与银行卡持卡人姓名一致。</p>
</div>
<div class="agreement-field">
<label for="agreement-operator-name">对方签署人姓名</label>
<input class="agreement-control" id="agreement-operator-name" name="operator_name" type="text" placeholder="可选,默认使用对方签署主体名称" autocomplete="name">
<label for="agreement-operator-name">对方 E 签宝实名认证姓名 <span class="agreement-required" data-account-operator-required aria-hidden="true">*</span></label>
<input class="agreement-control" id="agreement-operator-name" name="operator_name" type="text" placeholder="填写与 E 签宝账号匹配的实名姓名" autocomplete="name" data-account-operator-name required>
<p class="agreement-help">仅用于 E 签宝身份核验,不是收款人姓名;收款人姓名请填写在下方账户资料中。</p>
</div>
<div class="agreement-field" data-quote-confirmation-field hidden>
<label for="agreement-quote-version">报价版本 <span class="agreement-required" aria-hidden="true">*</span></label>
@ -1904,6 +1910,10 @@
var subjectCodeInput = root.querySelector('[data-subject-code]');
var courierSelect = root.querySelector('[data-courier-select]');
var esignAccountInput = root.querySelector('input[name="esign_account"]');
var accountOrganizationField = root.querySelector('[data-account-organization-field]');
var accountOrganizationInput = root.querySelector('[data-account-organization-name]');
var operatorNameInput = root.querySelector('[data-account-operator-name]');
var operatorRequiredMark = root.querySelector('[data-account-operator-required]');
var sharedCourierList = root.querySelector('[data-shared-courier-list]');
var sharedCourierAdd = root.querySelector('[data-shared-courier-add]');
var quoteVersionField = root.querySelector('[data-quote-confirmation-field]');
@ -2412,6 +2422,20 @@
var templateType = documentOption.value;
var courier = fieldValue('[name="courier_name"]', '快递公司');
var companyName = fieldValue('[name="company_full_name"]', '待填写公司全称');
var accountAgreement = templateType === 'account-proof' || templateType === 'account-change';
if (accountOrganizationField) {
accountOrganizationField.hidden = !accountAgreement;
}
if (accountOrganizationInput) {
accountOrganizationInput.disabled = !accountAgreement;
accountOrganizationInput.required = accountAgreement;
}
if (operatorNameInput) {
operatorNameInput.required = accountAgreement;
}
if (operatorRequiredMark) {
operatorRequiredMark.hidden = !accountAgreement;
}
activateTemplate(templateType);
refreshSignerAccountLabels(templateType);
var secondary = root.querySelector('[data-secondary-signer-fields]');
@ -2452,8 +2476,7 @@
setPreviewValue('proofAccountName', fieldValue('[name="account_proof_account_name"]', '待填写'));
setPreviewValue('proofBankName', fieldValue('[name="account_proof_bank_name"]', '待填写'));
setPreviewValue('proofBankAccount', fieldValue('[name="account_proof_bank_account"]', '待填写'));
var proofName = fieldValue('[name="account_proof_account_name"]', '待填写');
setPreviewValue('proofSigner', proofName + (/(公司|有限公司|有限责任|企业|集团|合作社|事务所|中心|厂)/.test(proofName) ? ' 公司盖章' : ' 签字'));
setPreviewValue('proofSigner', '公司盖章');
}
if (templateType === 'account-change') {
@ -2465,8 +2488,7 @@
setPreviewValue('newAccountName', fieldValue('[name="new_account_name"]', '待填写'));
setPreviewValue('newBankName', fieldValue('[name="new_bank_name"]', '待填写'));
setPreviewValue('newBankAccount', fieldValue('[name="new_bank_account"]', '待填写'));
var changeName = fieldValue('[name="new_account_name"]', '待填写');
setPreviewValue('changeSigner', changeName + (/(公司|有限公司|有限责任|企业|集团|合作社|事务所|中心|厂)/.test(changeName) ? ' 公司盖章' : ' 签字'));
setPreviewValue('changeSigner', '公司盖章');
}
if (templateType === 'entity-change') {

View File

@ -77,6 +77,12 @@ class AgreementSigningPageTest extends FeatureTestCase
->assertSee("labels.secondary = '我方 E 签宝账号'", false)
->assertSee("labels.primary = '委托方 E 签宝账号'", false)
->assertSee("labels.secondary = '受托方 E 签宝账号'", false)
->assertSeeText('对方签署企业全称')
->assertSeeText('必须与对方 E 签宝企业认证名称一致')
->assertSeeText('对方 E 签宝实名认证姓名')
->assertSeeText('仅用于 E 签宝身份核验,不是收款人姓名')
->assertSee("setPreviewValue('proofSigner', '公司盖章')", false)
->assertSee("setPreviewValue('changeSigner', '公司盖章')", false)
->assertSeeText('合作合同统一使用我方账号 18052733960')
->assertSeeText('填写变量')
->assertSeeText('签署完成后自动归档')

View File

@ -87,12 +87,70 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
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;
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_cooperation_contract_maps_party_variables_and_creates_two_signers(): void
{
$this->fakeEsign();
@ -622,6 +680,7 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
'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' => '新北中通快递有限公司',
@ -630,6 +689,31 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
];
}
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([