协议签署:增加合作方类型与供应商待接入展示
合作方类型与供应商待接入展示;53 项测试、825 条断言通过。
This commit is contained in:
parent
48553c001d
commit
ad6d70a0a2
@ -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))],
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 @@
|
||||
<label for="agreement-task-search">搜索任务</label>
|
||||
<div class="agreement-search">
|
||||
<span class="material-icons-outlined" aria-hidden="true">search</span>
|
||||
<input class="agreement-control" id="agreement-task-search" name="agreement_search" type="search" value="{{ $agreementSearch }}" placeholder="任务编号、快递公司或签署人" autocomplete="off" data-task-filter>
|
||||
<input class="agreement-control" id="agreement-task-search" name="agreement_search" type="search" value="{{ $agreementSearch }}" placeholder="任务编号、合作方名称或签署人" autocomplete="off" data-task-filter>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agreement-field">
|
||||
@ -1385,7 +1396,8 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">任务编号</th>
|
||||
<th scope="col">快递物流商</th>
|
||||
<th scope="col">合作方类型</th>
|
||||
<th scope="col">合作方名称</th>
|
||||
<th scope="col">业务类型</th>
|
||||
<th scope="col">签署状态</th>
|
||||
<th scope="col">发起人</th>
|
||||
@ -1399,6 +1411,7 @@
|
||||
@php($quoteError = $quoteSignature->lastErrorSummary())
|
||||
<tr data-quote-task-row>
|
||||
<td>{{ $quoteVersion?->version_code ?? ('报价版本 #' . $quoteSignature->quote_version_id) }}</td>
|
||||
<td>快递公司</td>
|
||||
<td>{{ $quoteVersion?->carrier_name ?? $quoteSignature->organization_name }}</td>
|
||||
<td>报价确认单</td>
|
||||
<td class="agreement-status-cell">
|
||||
@ -1443,6 +1456,7 @@
|
||||
@php($taskFailed = $task->status === 'failed' || ($task->status === 'unknown' && $task->hasDefinitiveFailure()))
|
||||
<tr data-agreement-task-row>
|
||||
<td>{{ $task->business_no }}</td>
|
||||
<td>{{ $task->partnerTypeLabel() }}</td>
|
||||
<td>{{ $task->courier_name }}</td>
|
||||
<td>{{ $task->documentLabel() }}</td>
|
||||
<td class="agreement-status-cell">
|
||||
@ -1492,7 +1506,7 @@
|
||||
@endforeach
|
||||
@if ($agreementTasks->isEmpty() && $agreementQuoteSignatures->isEmpty())
|
||||
<tr class="agreement-empty-row">
|
||||
<td colspan="7">
|
||||
<td colspan="8">
|
||||
<div class="agreement-empty">
|
||||
<span class="agreement-empty__icon">
|
||||
<span class="material-icons-outlined" aria-hidden="true">assignment_late</span>
|
||||
@ -1549,8 +1563,15 @@
|
||||
<section class="agreement-form-section" aria-labelledby="agreement-base-heading">
|
||||
<h3 class="agreement-section-heading" id="agreement-base-heading">基础信息</h3>
|
||||
<div class="agreement-form-grid">
|
||||
<fieldset class="agreement-field agreement-field--full agreement-partner-types">
|
||||
<legend class="agreement-field-label">合作方类型 <span class="agreement-required" aria-hidden="true">*</span></legend>
|
||||
<div class="agreement-archive-options">
|
||||
<label class="agreement-archive-choice"><input type="radio" name="partner_type" value="courier" checked data-partner-type><span><strong>快递公司</strong><span>选择快递资料,发起快递合作相关文件</span></span></label>
|
||||
<label class="agreement-archive-choice"><input type="radio" name="partner_type" value="supplier" data-partner-type><span><strong>供应商</strong><span>按供应商简称、企业全称或编码查找</span></span></label>
|
||||
</div>
|
||||
</fieldset>
|
||||
<div class="agreement-field">
|
||||
<label for="agreement-courier">快递公司 <span class="agreement-required" aria-hidden="true">*</span></label>
|
||||
<label for="agreement-courier"><span data-partner-name-label>快递公司</span> <span class="agreement-required" aria-hidden="true">*</span></label>
|
||||
<div class="agreement-courier-search-wrap" data-courier-control>
|
||||
<select class="agreement-control" id="agreement-courier" name="courier_name" data-courier-select required>
|
||||
<option value="">请选择</option>
|
||||
@ -1566,13 +1587,17 @@
|
||||
<div class="agreement-courier-options" id="agreement-courier-options" role="listbox" aria-label="快递资料" hidden data-courier-options></div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-supplier-control hidden>
|
||||
<input class="agreement-control" id="agreement-supplier-search" type="search" placeholder="输入供应商简称、全称或编码搜索" aria-describedby="agreement-supplier-help" disabled>
|
||||
<p class="agreement-help" id="agreement-supplier-help">供应商资料尚未接入,暂不能选择或发起签署。接入后显示供应商简称,并在下方展示企业全称和编码供核对。</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agreement-field">
|
||||
<label for="agreement-document-type">文件类型 <span class="agreement-required" aria-hidden="true">*</span></label>
|
||||
<select class="agreement-control" id="agreement-document-type" name="document_type" data-document-type required>
|
||||
<option value="account-proof" data-title="收款帐户资料证明" data-name="收款账户证明" data-code="TPL-ACCOUNT-CERT-V2" data-file="收款账户证明文件.doc" data-description="原文件:收款账户证明文件.doc;快递公司盖章" selected>收款账户证明</option>
|
||||
<option value="account-change" data-title="收款帐户资料变更证明" data-name="收款账户变更" data-code="TPL-ACCOUNT-CHANGE-V1" data-file="收款账户变更证明.doc" data-description="原文件:收款账户变更证明.doc;快递公司盖章">收款账户变更</option>
|
||||
<option value="entity-change" data-title="公司主体变更证明" data-name="公司主体变更" data-code="TPL-ENTITY-CHANGE-V1" data-file="公司主体变更证明.docx" data-description="原文件:公司主体变更证明.docx;变更前、变更后公司分别签字盖章">公司主体变更</option>
|
||||
<option value="account-proof" data-partner-types="courier supplier" data-title="收款帐户资料证明" data-name="收款账户证明" data-code="TPL-ACCOUNT-CERT-V2" data-file="收款账户证明文件.doc" data-description="原文件:收款账户证明文件.doc;对方企业盖章或个人签字" selected>收款账户证明</option>
|
||||
<option value="account-change" data-partner-types="courier supplier" data-title="收款帐户资料变更证明" data-name="收款账户变更" data-code="TPL-ACCOUNT-CHANGE-V1" data-file="收款账户变更证明.doc" data-description="原文件:收款账户变更证明.doc;对方企业盖章或个人签字">收款账户变更</option>
|
||||
<option value="entity-change" data-partner-types="courier supplier" data-title="公司主体变更证明" data-name="公司主体变更" data-code="TPL-ENTITY-CHANGE-V1" data-file="公司主体变更证明.docx" data-description="原文件:公司主体变更证明.docx;变更前、变更后公司分别签字盖章">公司主体变更</option>
|
||||
<option value="shared-contract" data-title="证明" data-name="共用合同证明" data-code="TPL-SHARED-CONTRACT-V1" data-file="共用合同证明.docx" data-description="物流商归属企业盖章确认共用合同及结算主体;也可选择个人实名签署">共用合同证明</option>
|
||||
<option value="cooperation-contract" data-title="服务协议" data-name="合作合同" data-code="TPL-COOPERATION-CONTRACT-V1" data-file="保丟不保损-合同发快递2024.8.doc" data-description="原文件:保丟不保损-合同发快递2024.8.doc;甲乙双方签署盖章">合作合同</option>
|
||||
<option value="authorization" data-title="授权委托书" data-name="授权委托书" data-code="TPL-AUTHORIZATION-V1" data-file="授权委托书(快递).docx" data-description="原文件:授权委托书(快递).docx;委托方和受托方签署">授权委托书</option>
|
||||
@ -1582,7 +1607,7 @@
|
||||
|
||||
<div class="agreement-template-note" role="status">
|
||||
<span class="material-icons-outlined" aria-hidden="true">description</span>
|
||||
<div><strong><span data-template-name>收款账户证明</span> <span data-template-code>TPL-ACCOUNT-CERT-V2</span></strong><p data-template-description>原文件:收款账户证明文件.doc;快递公司盖章</p></div>
|
||||
<div><strong><span data-template-name>收款账户证明</span> <span data-template-code>TPL-ACCOUNT-CERT-V2</span></strong><p data-template-description>原文件:收款账户证明文件.doc;对方企业盖章或个人签字</p></div>
|
||||
</div>
|
||||
|
||||
<div class="agreement-field">
|
||||
@ -1618,7 +1643,7 @@
|
||||
<div class="agreement-field">
|
||||
<label for="agreement-esign-account"><span data-primary-esign-label>对方 E 签宝账号</span></label>
|
||||
<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>
|
||||
<p class="agreement-help" data-partner-account-help>选择快递公司后自动带出工作平台的“电子签章经办人”(dzqzjbr);如资料未维护,可手工填写对方在 E 签宝实名绑定的手机号或邮箱。</p>
|
||||
</div>
|
||||
<div class="agreement-field agreement-field--full" data-account-signer-type-field>
|
||||
<span class="agreement-field-label" data-account-signer-type-label>对方签署主体类型 <span class="agreement-required" aria-hidden="true">*</span></span>
|
||||
@ -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 });
|
||||
|
||||
@ -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('<td>' . e($this->company->name) . '</td>', false)
|
||||
->assertSeeText('合作方类型')
|
||||
->assertSeeText('合作方名称')
|
||||
->assertSee('<td>快递公司</td>', false)
|
||||
->assertDontSee('<td>张经理</td>', false);
|
||||
}
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user