feat: 合作合同乙方签约地支持编辑 (#343)

This commit is contained in:
limeng 2026-10-15 18:13:24 +08:00
parent 5c20defc79
commit d1b14fc10d
6 changed files with 56 additions and 3 deletions

View File

@ -523,6 +523,7 @@ class CourierAgreementSignatures extends Controller
'cooperation_party_a_phone' => ['required_if:document_type,cooperation-contract', 'string', 'max:64'],
'cooperation_party_b_name' => ['required_if:document_type,cooperation-contract', 'string', 'max:255'],
'cooperation_party_b_phone' => ['required_if:document_type,cooperation-contract', 'string', 'max:64'],
'cooperation_party_b_signing_place' => ['nullable', 'string', 'max:100'],
'cooperation_contract_number' => ['nullable', 'string', 'max:100'],
'cooperation_party_b_legal_representative' => ['nullable', 'string', 'max:100'],
'cooperation_start_date' => ['required_if:document_type,cooperation-contract', 'date'],
@ -548,6 +549,7 @@ class CourierAgreementSignatures extends Controller
unset($payload['esign_account'], $payload['idempotency_key'], $payload['operator_name'], $payload['credential_images']);
$payload['authorization_items'] = array_values(array_map('strval', $data['authorization_items'] ?? []));
if (($data['document_type'] ?? '') === 'cooperation-contract' && ($data['partner_type'] ?? 'courier') === 'courier') {
$payload['cooperation_party_b_signing_place'] = trim((string) ($data['cooperation_party_b_signing_place'] ?? '')) ?: '常州市';
$payload['cooperation_contract_number'] = trim((string) ($data['cooperation_contract_number'] ?? ''));
$payload['cooperation_party_b_legal_representative'] = ($data['counterparty_signer_type'] ?? 'organization') === 'person'
? null : trim((string) ($data['cooperation_party_b_legal_representative'] ?? ''));

View File

@ -1866,6 +1866,11 @@
<label for="agreement-cooperation-end-date">协议结束日期 <span class="agreement-required" aria-hidden="true">*</span></label>
<input class="agreement-control" id="agreement-cooperation-end-date" name="cooperation_end_date" type="date" value="{{ now()->addYear()->toDateString() }}" required>
</div>
<div class="agreement-field" data-courier-contract-metadata>
<label for="agreement-cooperation-party-b-signing-place">乙方签约地</label>
<input class="agreement-control" id="agreement-cooperation-party-b-signing-place" name="cooperation_party_b_signing_place" type="text" maxlength="100" value="{{ old('cooperation_party_b_signing_place', '常州市') }}" placeholder="填写乙方实际签约地点">
<p class="agreement-help">默认常州市,可按实际签约地点修改;留空时仍使用常州市。</p>
</div>
<div class="agreement-field" data-courier-contract-metadata>
<label for="agreement-cooperation-contract-number">合同编号(选填)</label>
<input class="agreement-control" id="agreement-cooperation-contract-number" name="cooperation_contract_number" type="text" maxlength="100" placeholder="填写正式合同编号,不填写则合同管理显示未填写">
@ -3627,6 +3632,7 @@
setPreviewValue('cooperationPartyAPhone', fieldValue('[name="cooperation_party_a_phone"]', '待填写'));
setPreviewValue('cooperationPartyBName', fieldValue('[name="cooperation_party_b_name"]', courier === '快递公司' ? '待填写' : courier));
setPreviewValue('cooperationPartyBPhone', fieldValue('[name="cooperation_party_b_phone"]', '待填写'));
setPreviewValue('cooperationPartyBSigningPlace', fieldValue('[name="cooperation_party_b_signing_place"]', '常州市'));
setPreviewValue('cooperationStartDate', formatDateField('[name="cooperation_start_date"]', '待填写'));
setPreviewValue('cooperationEndDate', formatDateField('[name="cooperation_end_date"]', '待填写'));
if (supplierContract) {

View File

@ -144,7 +144,7 @@
<p>代表人:</p>
<div class="agreement-preview-handwriting-blank"></div>
<p>日 期:<span data-preview-bind="signatureDate">待填写</span></p>
<p>签约地:常州市</p>
<p>签约地:<span class="agreement-preview-value" data-preview-bind="cooperationPartyBSigningPlace">常州市</span></p>
</div>
</div>
@else
@ -166,7 +166,7 @@
<p class="contract-signatures__line">代表人:</p>
<div class="contract-signatures__handwriting"></div>
<p class="contract-signatures__line">日 期:{!! $mixedText($signatureDate) !!}</p>
<p class="contract-signatures__line">签约地:常州市</p>
<p class="contract-signatures__line">签约地:{!! $mixedText(trim((string) ($payload['cooperation_party_b_signing_place'] ?? '')) ?: '常州市') !!}</p>
</td>
</tr>
</table>

View File

@ -507,6 +507,18 @@ class AgreementSigningPageTest extends FeatureTestCase
->assertSeeText('委托有效期:从');
}
public function test_cooperation_contract_has_an_editable_party_b_signing_place_and_preview(): void
{
$this->loginAs()->get(route('business.pages.show', [
'section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'create',
]))
->assertOk()
->assertSeeText('乙方签约地')
->assertSee('name="cooperation_party_b_signing_place"', false)
->assertSee('data-preview-bind="cooperationPartyBSigningPlace"', false)
->assertSeeText('默认常州市,可按实际签约地点修改;留空时仍使用常州市。');
}
public function testAgreementSigningPageRejectsUsersWithoutItsSidebarPermission(): void
{
$subject = 'user_' . $this->user->id;

View File

@ -545,6 +545,7 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
$payload = $this->cooperationContractPayload('cooperation-001');
$payload['cooperation_contract_number'] = ' KD-2026-009 ';
$payload['cooperation_party_b_legal_representative'] = ' 王法人 ';
$payload['cooperation_party_b_signing_place'] = ' 南京市鼓楼区 ';
$this->loginAs()
->post(route('business.courier-agreement-signatures.store'), $payload)
@ -559,6 +560,7 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
$this->assertSame('2026-12-31', $signature->payload['cooperation_end_date']);
$this->assertSame('KD-2026-009', $signature->payload['cooperation_contract_number']);
$this->assertSame('王法人', $signature->payload['cooperation_party_b_legal_representative']);
$this->assertSame('南京市鼓楼区', $signature->payload['cooperation_party_b_signing_place']);
Http::assertSent(function (ClientRequest $request): bool {
if ($request->url() !== 'https://smlopenapi.esign.test/v3/sign-flow/create-by-file') {
@ -585,8 +587,9 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
$payload = $this->cooperationContractPayload('contract-metadata-too-long');
$payload['cooperation_contract_number'] = str_repeat('X', 101);
$payload['cooperation_party_b_legal_representative'] = str_repeat('法', 101);
$payload['cooperation_party_b_signing_place'] = str_repeat('地', 101);
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)
->assertSessionHasErrors(['cooperation_contract_number', 'cooperation_party_b_legal_representative']);
->assertSessionHasErrors(['cooperation_contract_number', 'cooperation_party_b_legal_representative', 'cooperation_party_b_signing_place']);
$this->assertSame(0, CourierAgreementSignature::query()->count());
Http::assertNothingSent();
}
@ -595,6 +598,7 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
{
$payload = $this->cooperationContractPayload('cooperation-font');
$payload['signature_date'] = '2026-10-10';
$payload['cooperation_party_b_signing_place'] = '南京市 <script>签约点</script>';
$signature = new CourierAgreementSignature([
'business_no' => 'AGR-FONT-REGRESSION',
'template_code' => 'TPL-COOPERATION-CONTRACT-V1',
@ -635,11 +639,39 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
$this->assertStringContainsString('95%', $text);
$this->assertStringContainsString('5000元/次', $text);
$xpath = new DOMXPath($document);
$parties = $xpath->query('//table[@class="contract-signatures__table"]//td');
$this->assertStringContainsString('签约地:常州市', $parties->item(0)->textContent);
$this->assertStringContainsString('签约地:南京市 <script>签约点</script>', $parties->item(1)->textContent);
$this->assertSame(0, $xpath->query('//body//script')->length);
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);
}
}
/** @dataProvider missingOrBlankSigningPlaces */
public function test_cooperation_contract_defaults_missing_or_blank_party_b_signing_place(?string $place): void
{
$this->fakeEsign();
$payload = $this->cooperationContractPayload('signing-place-default');
if ($place !== null) {
$payload['cooperation_party_b_signing_place'] = $place;
}
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)->assertRedirect();
$signature = CourierAgreementSignature::query()->firstOrFail();
$this->assertSame('常州市', $signature->payload['cooperation_party_b_signing_place']);
// Old saved payloads also retain the previous location when rendered.
$html = view('business.partials.cooperation-contract', [
'mode' => 'pdf', 'payload' => [], 'signatureDate' => '待填写', 'mixedText' => fn ($value) => e($value),
])->render();
$this->assertSame(2, substr_count($html, '签约地:常州市'));
}
public static function missingOrBlankSigningPlaces(): array
{
return ['missing' => [null], 'empty' => [''], 'whitespace' => [' ']];
}
public function test_cooperation_contract_uses_the_persons_real_name_instead_of_the_courier_name(): void
{
$this->fakeEsign();

View File

@ -69,6 +69,7 @@ class CourierAgreementSignatureLayoutTest extends TestCase
'contract_partner_name' => '示例合作公司', 'shared_courier_names' => ['示例快递'],
'cooperation_party_a_name' => '示例甲方公司', 'cooperation_party_a_phone' => '00000000000',
'cooperation_party_b_name' => '示例乙方公司', 'cooperation_party_b_phone' => '00000000000',
'cooperation_party_b_signing_place' => '江苏省南京市鼓楼区',
'cooperation_start_date' => '2026-10-11', 'cooperation_end_date' => '2027-10-10',
'principal_name' => '示例委托方', 'principal_id_number' => 'EXAMPLE-ID',
'trustee_name' => '示例受托方', 'trustee_id_number' => 'EXAMPLE-ID',