diff --git a/app/Http/Controllers/Business/CourierAgreementSignatures.php b/app/Http/Controllers/Business/CourierAgreementSignatures.php index 054150b9..41c30f6d 100644 --- a/app/Http/Controllers/Business/CourierAgreementSignatures.php +++ b/app/Http/Controllers/Business/CourierAgreementSignatures.php @@ -37,6 +37,13 @@ class CourierAgreementSignatures extends Controller public function store(Request $request): RedirectResponse { $this->ensureCanManage(); + $request->merge(['partner_type' => $request->input('partner_type', 'courier')]); + $request->validate(['partner_type' => ['required', 'in:courier,supplier']]); + if ($request->input('partner_type') === 'supplier') { + throw ValidationException::withMessages([ + 'partner_type' => '供应商资料尚未接入,暂不能发起供应商签署。请在资料接入后选择供应商。', + ]); + } abort_unless(EsignClient::configured(), 422, 'E 签宝沙箱尚未配置 App ID 或 AppSecret。'); $requestedType = (string) $request->input('document_type'); if ($requestedType === 'quote-confirmation') { @@ -405,6 +412,7 @@ class CourierAgreementSignatures extends Controller $date = ['required', 'date', 'after_or_equal:today', 'before_or_equal:' . now()->addDays(90)->toDateString()]; return [ + 'partner_type' => ['required', 'in:courier,supplier'], 'idempotency_key' => ['required', 'string', 'max:80', 'regex:/^[A-Za-z0-9_-]+$/'], 'courier_name' => ['required', 'string', 'max:255'], 'document_type' => ['required', 'in:' . implode(',', array_keys(CourierAgreementSignature::DOCUMENTS))], diff --git a/app/Models/Business/CourierAgreementSignature.php b/app/Models/Business/CourierAgreementSignature.php index ce9b63a8..b99823fe 100644 --- a/app/Models/Business/CourierAgreementSignature.php +++ b/app/Models/Business/CourierAgreementSignature.php @@ -4,6 +4,7 @@ namespace App\Models\Business; use App\Abstracts\Model; use App\Support\EsignErrorMessage; +use Illuminate\Contracts\Encryption\DecryptException; class CourierAgreementSignature extends Model { @@ -86,6 +87,19 @@ class CourierAgreementSignature extends Model 'operator_account' => 'encrypted', ]; + public function partnerTypeLabel(): string + { + // Records created before partner types were introduced are courier tasks. + try { + $partnerType = $this->payload['partner_type'] ?? 'courier'; + } catch (DecryptException) { + // Keep legacy rows with unreadable encrypted payloads renderable. + $partnerType = 'courier'; + } + + return $partnerType === 'supplier' ? '供应商' : '快递公司'; + } + public function statusLabel(): string { return match ($this->status) { diff --git a/resources/views/business/pages/courier-payables/agreement-signing.blade.php b/resources/views/business/pages/courier-payables/agreement-signing.blade.php index 20d2449b..d9a6bf50 100644 --- a/resources/views/business/pages/courier-payables/agreement-signing.blade.php +++ b/resources/views/business/pages/courier-payables/agreement-signing.blade.php @@ -937,6 +937,17 @@ font-size: 17px; } + .agreement-partner-types { + margin: 0; + padding: 0; + border: 0; + min-width: 0; + } + + .agreement-partner-types legend { + margin-bottom: 0.5rem; + } + .agreement-archive-options { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); @@ -1334,7 +1345,7 @@
@@ -1385,7 +1396,8 @@ 任务编号 - 快递物流商 + 合作方类型 + 合作方名称 业务类型 签署状态 发起人 @@ -1399,6 +1411,7 @@ @php($quoteError = $quoteSignature->lastErrorSummary()) {{ $quoteVersion?->version_code ?? ('报价版本 #' . $quoteSignature->quote_version_id) }} + 快递公司 {{ $quoteVersion?->carrier_name ?? $quoteSignature->organization_name }} 报价确认单 @@ -1443,6 +1456,7 @@ @php($taskFailed = $task->status === 'failed' || ($task->status === 'unknown' && $task->hasDefinitiveFailure())) {{ $task->business_no }} + {{ $task->partnerTypeLabel() }} {{ $task->courier_name }} {{ $task->documentLabel() }} @@ -1492,7 +1506,7 @@ @endforeach @if ($agreementTasks->isEmpty() && $agreementQuoteSignatures->isEmpty()) - +
@@ -1549,8 +1563,15 @@

基础信息

+
+ 合作方类型 +
+ + +
+
- +
+

供应商资料尚未接入,暂不能选择或发起签署。接入后显示供应商简称,并在下方展示企业全称和编码供核对。

+
-

选择快递公司后自动带出工作平台的“电子签章经办人”(dzqzjbr);如资料未维护,可手工填写对方在 E 签宝实名绑定的手机号或邮箱。

+

选择快递公司后自动带出工作平台的“电子签章经办人”(dzqzjbr);如资料未维护,可手工填写对方在 E 签宝实名绑定的手机号或邮箱。

对方签署主体类型 @@ -2222,6 +2247,13 @@ var courierOptionsList = root.querySelector('[data-courier-options]'); var courierControl = root.querySelector('[data-courier-control]'); var courierLabel = root.querySelector('label[for="agreement-courier"]'); + var partnerTypeInputs = Array.prototype.slice.call(root.querySelectorAll('[data-partner-type]')); + var supplierControl = root.querySelector('[data-supplier-control]'); + var documentSelect = form.querySelector('[data-document-type]'); + var documentOptions = Array.prototype.slice.call(documentSelect.options); + var currentPartnerType = 'courier'; + var sendButton = root.querySelector('[data-sign-request-send]'); + var esignAvailable = @json($esignConfigured); var courierOptions = courierSelect ? Array.prototype.filter.call(courierSelect.options, function (option) { return option.value !== '' && !option.disabled; }).map(function (option) { @@ -2712,6 +2744,49 @@ return select.options[select.selectedIndex]; } + function syncPartnerType() { + var selected = partnerTypeInputs.find(function (input) { return input.checked; }); + var type = selected ? selected.value : 'courier'; + var supplier = type === 'supplier'; + if (type !== currentPartnerType) { + closeCourierOptions(); + courierSelect.value = ''; + courierSearch.value = ''; + esignAccountInput.value = ''; + delete esignAccountInput.dataset.autoFilled; + accountOrganizationInput.value = ''; + operatorNameInput.value = ''; + secondaryEsignAccountInput.value = ''; + secondaryOperatorInput.value = ''; + root.querySelector('[data-request-error]').textContent = ''; + } + currentPartnerType = type; + courierControl.hidden = supplier; + courierSelect.disabled = supplier; + courierSearch.disabled = supplier; + supplierControl.hidden = !supplier; + courierLabel.htmlFor = supplier ? 'agreement-supplier-search' : courierSearch.id; + root.querySelector('[data-partner-name-label]').textContent = supplier ? '供应商名称' : '快递公司'; + root.querySelector('[data-partner-account-help]').textContent = supplier + ? '供应商资料接入后可带出签署账号和经办人;发送前需核对对方 E 签宝实名信息。' + : '选择快递公司后自动带出工作平台的“电子签章经办人”(dzqzjbr);如资料未维护,可手工填写对方在 E 签宝实名绑定的手机号或邮箱。'; + // Rebuild the small template list so unavailable options are absent on every browser. + var previousDocument = documentSelect.value; + documentSelect.replaceChildren(); + documentOptions.forEach(function (option) { + var types = (option.dataset.partnerTypes || 'courier').split(' '); + if (types.indexOf(type) !== -1) documentSelect.appendChild(option); + }); + if (Array.prototype.some.call(documentSelect.options, function (option) { return option.value === previousDocument; })) { + documentSelect.value = previousDocument; + } else { + documentSelect.value = 'account-proof'; + } + sendButton.disabled = supplier || !esignAvailable; + sendButton.title = supplier ? '供应商资料尚未接入,暂不能发起签署' : (esignAvailable ? '推送至E签宝' : 'E签宝沙箱应用尚未配置'); + sendButton.querySelector('span:last-child').textContent = supplier ? '供应商资料待接入' : '推送至E签宝'; + } + function fieldValue(selector, fallback) { var field = form.querySelector(selector); return field && field.value.trim() ? field.value.trim() : fallback; @@ -3159,7 +3234,7 @@ function refreshPreview() { var documentOption = selectedDocument(); var templateType = documentOption.value; - var courier = fieldValue('[name="courier_name"]', '快递公司'); + var courier = currentPartnerType === 'supplier' ? '供应商' : fieldValue('[name="courier_name"]', '快递公司'); var companyName = fieldValue('[name="company_full_name"]', '待填写公司全称'); var accountAgreement = templateType === 'account-proof' || templateType === 'account-change'; var entityChange = templateType === 'entity-change'; @@ -3575,12 +3650,25 @@ announce('协议预览已刷新'); }); + partnerTypeInputs.forEach(function (input) { + input.addEventListener('change', function () { + syncPartnerType(); + refreshPreview(); + announce(currentPartnerType === 'supplier' ? '已切换到供应商,资料尚未接入' : '已切换到快递公司'); + }); + }); + form.querySelectorAll('input, select, textarea').forEach(function (control) { control.addEventListener('input', refreshPreview); control.addEventListener('change', refreshPreview); }); form.addEventListener('submit', function (event) { + if (currentPartnerType === 'supplier') { + event.preventDefault(); + root.querySelector('[data-request-error]').textContent = '供应商资料尚未接入,暂不能发起供应商签署。'; + return; + } refreshPreview(); if (!form.checkValidity()) { event.preventDefault(); @@ -3652,6 +3740,7 @@ var initialUrl = new URL(window.location.href); var initialView = initialUrl.searchParams.get('agreement_view') || 'tasks'; initializeCourierSearch(); + syncPartnerType(); renderSubjectSelect(); renderSubjectList(); showTab(initialView, { updateUrl: false }); diff --git a/tests/Feature/Business/AgreementSigningPageTest.php b/tests/Feature/Business/AgreementSigningPageTest.php index bb5b12e7..1e391090 100644 --- a/tests/Feature/Business/AgreementSigningPageTest.php +++ b/tests/Feature/Business/AgreementSigningPageTest.php @@ -62,6 +62,10 @@ class AgreementSigningPageTest extends FeatureTestCase ->assertSee('data-sign-request-send', false) ->assertDontSee('data-agreement-step-panel', false) ->assertSeeText('发起签章请求') + ->assertSeeText('合作方类型') + ->assertSee('name="partner_type" value="courier" checked', false) + ->assertSee('name="partner_type" value="supplier"', false) + ->assertSeeText('供应商资料尚未接入,暂不能选择或发起签署。') ->assertSeeText('快递公司') ->assertSee('data-courier-search', false) ->assertSee('placeholder="输入快递名称搜索"', false) @@ -165,6 +169,9 @@ class AgreementSigningPageTest extends FeatureTestCase ])) ->assertOk() ->assertSee('' . e($this->company->name) . '', false) + ->assertSeeText('合作方类型') + ->assertSeeText('合作方名称') + ->assertSee('快递公司', false) ->assertDontSee('张经理', false); } diff --git a/tests/Feature/Business/CourierAgreementSignatureWorkflowTest.php b/tests/Feature/Business/CourierAgreementSignatureWorkflowTest.php index e906b06a..9ec73219 100644 --- a/tests/Feature/Business/CourierAgreementSignatureWorkflowTest.php +++ b/tests/Feature/Business/CourierAgreementSignatureWorkflowTest.php @@ -53,6 +53,8 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase $this->assertSame('flow-agreement-001', $signature->esign_sign_flow_id); $this->assertSame('新北中通快递有限公司', $signature->organization_name); $this->assertSame('2026-09-21', $signature->payload['signature_date']); + $this->assertSame('courier', $signature->payload['partner_type']); + $this->assertSame('快递公司', $signature->partnerTypeLabel()); Storage::disk('uploads')->assertExists($signature->source_pdf_path); $this->assertSame('新北中通快递有限公司', $signature->payload['account_proof_account_name']); @@ -71,6 +73,34 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase $this->assertSame(1, CourierAgreementSignature::query()->count()); } + public function test_supplier_requests_are_rejected_before_creating_or_sending_any_document(): void + { + Http::preventStrayRequests(); + $this->withExceptionHandling(); + $payload = $this->accountProofPayload('supplier-not-connected'); + $payload['partner_type'] = 'supplier'; + + $this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload) + ->assertSessionHasErrors(['partner_type' => '供应商资料尚未接入,暂不能发起供应商签署。请在资料接入后选择供应商。']); + + $this->assertSame(0, CourierAgreementSignature::query()->count()); + Http::assertNothingSent(); + } + + public function test_unknown_partner_type_is_rejected_before_sending(): void + { + Http::preventStrayRequests(); + $this->withExceptionHandling(); + $payload = $this->accountProofPayload('unknown-partner-type'); + $payload['partner_type'] = 'unknown'; + + $this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload) + ->assertSessionHasErrors('partner_type'); + + $this->assertSame(0, CourierAgreementSignature::query()->count()); + Http::assertNothingSent(); + } + public function test_account_proof_accepts_private_account_holder_name_different_from_company_name(): void { $this->fakeEsign();