修复收款账户协议签署主体识别 #231
@ -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'])),
|
||||
|
||||
@ -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) ?: '协议签署';
|
||||
|
||||
@ -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') {
|
||||
|
||||
@ -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('签署完成后自动归档')
|
||||
|
||||
@ -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([
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user