修正供应商易签宝账号并接入签署选择 #300

Merged
limeng merged 3 commits from fix/limeng-supplier-signing-account into main 2026-10-11 13:39:04 +08:00
9 changed files with 211 additions and 33 deletions

View File

@ -40,9 +40,13 @@ class CourierAgreementSignatures extends Controller
$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' => '供应商资料尚未接入,暂不能发起供应商签署。请在资料接入后选择供应商。',
]);
$request->validate(['supplier_id' => ['required', 'integer', 'min:1'],
'document_type' => ['required', 'in:account-proof,account-change,entity-change,cooperation-contract,authorization']]);
$supplier = app(\Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository::class)
->signingSupplier((int) $request->input('supplier_id'));
if (! $supplier) throw ValidationException::withMessages(['supplier_id' => '供应商资料暂不可用,请重新选择供应商。']);
$request->merge(['courier_name' => trim((string) $supplier->scmname)]);
if (! $request->filled('esign_account')) $request->merge(['esign_account' => trim((string) $supplier->accountbankno)]);
}
abort_unless(EsignClient::configured(), 422, 'E 签宝沙箱尚未配置 App ID 或 AppSecret。');
$requestedType = (string) $request->input('document_type');
@ -413,6 +417,7 @@ class CourierAgreementSignatures extends Controller
return [
'partner_type' => ['required', 'in:courier,supplier'],
'supplier_id' => ['required_if:partner_type,supplier', 'nullable', 'integer', 'min:1'],
'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))],

View File

@ -1633,6 +1633,7 @@ class Pages extends Controller
$receiptStats = null;
$agreementCourierNames = $this->agreementCourierNames($page['slug'] ?? null);
$agreementCourierAccounts = $this->agreementCourierAccounts($page['slug'] ?? null, $agreementCourierNames);
$agreementSupplierOptions = $this->agreementSupplierOptions($page['slug'] ?? null);
// 内部流转报表读取共享业务镜像中由工作簿导入的数据集,不走上游只读查询。
if ($page && $content === null && $this->isInternalFlowReportPage($section, $page)) {
@ -1710,6 +1711,7 @@ class Pages extends Controller
'agreementSubjects' => $this->agreementSubjects($page['slug']),
'agreementCourierNames' => $agreementCourierNames,
'agreementCourierAccounts' => $agreementCourierAccounts,
'agreementSupplierOptions' => $agreementSupplierOptions,
'agreementQuoteVersions' => $this->agreementQuoteVersions($page['slug']),
'inventoryWarehouseMappings' => $inventoryWarehouseMappings,
'receiptStats' => $receiptStats,
@ -1770,6 +1772,7 @@ class Pages extends Controller
'agreementSubjects' => $this->agreementSubjects($page['slug'] ?? null),
'agreementCourierNames' => $agreementCourierNames,
'agreementCourierAccounts' => $agreementCourierAccounts,
'agreementSupplierOptions' => $agreementSupplierOptions,
'agreementQuoteVersions' => $this->agreementQuoteVersions($page['slug'] ?? null),
'inventoryWarehouseMappings' => $inventoryWarehouseMappings,
'receiptStats' => $receiptStats,
@ -2173,6 +2176,17 @@ class Pages extends Controller
}
}
protected function agreementSupplierOptions(?string $page): array
{
if ($page !== 'agreement-signing') return [];
try {
return app(\Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository::class)->signingOptions();
} catch (Throwable $exception) {
report($exception);
return [];
}
}
/** @return array<string,string> */
protected function agreementCourierAccounts(?string $page, ?Collection $courierNames = null): array
{

View File

@ -210,7 +210,7 @@ return [
'target' => 'js_supplier_management', 'keys' => ['Id'],
'full_reconcile_hours' => 1,
'columns' => ['Id', 'guid', 'sourcefrom', 'coopersgn', 'scmcode', 'scmname', 'scmtype',
'tradebody', 'company', 'enterpriseType', 'creditcode', 'handlePerson', 'legalPerson',
'tradebody', 'company', 'enterpriseType', 'creditcode', 'handlePerson', 'accountbankno', 'legalPerson',
'buyer', 'tradebtime', 'tradeetime', 'cooperbtime', 'cooperetime', 'memo',
'updator', 'local_created', 'local_updated'],
'indexes' => [['scmcode', 'Id'], ['coopersgn', 'Id']],

View File

@ -66,7 +66,7 @@
| 店铺收款账单旧来源(已停用) | `dbo.alipay_Bill_Detail` |
| 营销账户 | `dbo.tmall_spread_bills`、`dbo.pdd_spread_bills`、`dbo.alibaba_bill`、`dbo.jd_spreadBills`、`dbo.jd_costBills` |
| 供应商应付 | `dbo.finance_purchaseReturn`、`dbo.finance_invoiceDeduction`、`dbo.fys_supplierPaymentSummary`、`dbo.finance_advanceLoans`、`dbo.pm_unOrderDetails`、`dbo.finance_sampleDeduction`、`dbo.fys_express_courier_settlement` |
| 供应商管理(简尚版) | `jiansun.dbo.js_supplier_View`(`Id` 稳定键,镜像 `js_supplier_management`)、`dbo.js_supplier_contract_View`(`id` 稳定键,镜像 `js_supplier_contracts`)、`dbo.js_supplier_linkers`(镜像 `js_supplier_contacts`)与 `dbo.js_supplier_payinfo`(镜像 `js_supplier_payment_accounts`)。联系人、打款资料按 `Id` / `local_updated` 增量并每 24 小时全量核对;主视图及合同视图无可靠更新时间,按稳定键追加并每小时全量校准。复用现有轮转调度、游标与代次,未完成校准继续全量模式。页面按供应商名称、采购人员及合作状态筛选;合同同时匹配合同主体和签订公司,联系人及账户按供应商名称关联。未完成首次同步时明确提示,不在页面请求中直连上游;操作日志关联未确认时不展示其他模块日志。 |
| 供应商管理(简尚版) | `jiansun.dbo.js_supplier_View`(`Id` 稳定键,镜像 `js_supplier_management`)、`dbo.js_supplier_contract_View`(`id` 稳定键,镜像 `js_supplier_contracts`)、`dbo.js_supplier_linkers`(镜像 `js_supplier_contacts`)与 `dbo.js_supplier_payinfo`(镜像 `js_supplier_payment_accounts`)。联系人、打款资料按 `Id` / `local_updated` 增量并每 24 小时全量核对;主视图及合同视图无可靠更新时间,按稳定键追加并每小时全量校准。复用现有轮转调度、游标与代次,未完成校准继续全量模式。页面按供应商名称、采购人员及合作状态筛选;合同同时匹配合同主体和签订公司,联系人及账户按供应商名称关联。`accountbankno` 对应上游“电子合同经办人”账号,`handlePerson` 是经办人实名姓名,两者分别使用;发起签署按正式供应商 `Id` 选择并从该镜像带入账号,支持简称、合同主体全称及编码搜索,账号未维护时保留空值供手工核对,不用联系人电话替代。未完成首次同步时明确提示,不在页面请求中直连上游;操作日志关联未确认时不展示其他模块日志。 |
| 快递运费对账明细 | `dbo.jst_order_express_details`、`dbo.Jst_SendData_View_extension`、`dbo.jst_salesOutboundInfo` |
| 工作平台退货取件管理 | `dbo.ekf_ReturnPickupManagement`、`dbo.ekf_ReturnPickupManagement_Item`、`dbo.ekf_ReturnPickupManagement_TruthReExpressNumber`、发货仓名称 `dbo.wp_warehouse`、`dbo.wp_warehouseConfig` |
| 聚水潭进销存 | `dbo.jst_inoutWater`、`dbo.jst_inoutWaterItem`、`dbo.jst_inventoryInfo`(全部仓库、`io_date >= 2026-08-01`;专用稳定游标与完整代次镜像。三方仓页面仍固定 `wms_co_id = 15045364`,商品价格管理的进销存报表读取全仓库) |

View File

@ -21,7 +21,7 @@ class SupplierManagementMirrorRepository
'sourcefrom' => '数据来源', 'coopersgn' => '是否合作', 'scmcode' => '供应商代码',
'scmname' => '供应商名称', 'scmtype' => '供应商类型', 'tradebody' => '合同主体',
'company' => '签订公司', 'enterpriseType' => '企业类型', 'creditcode' => '社会信用代码',
'handlePerson' => '电子合同经办人', 'buyer' => '采购人员',
'accountbankno' => '电子合同经办人', 'buyer' => '采购人员',
];
public function __construct(
@ -40,6 +40,33 @@ class SupplierManagementMirrorRepository
return $this->mirror->state($this->datasets->get(self::DATASET));
}
/** 正式供应商签署资料;accountbankno 是上游电子合同账号,handlePerson 是实名姓名。 */
public function signingOptions(): array
{
if (! $this->signingReady()) return [];
return $this->mirror->connection()->table($this->datasets->get(self::DATASET)->targetTable())
->orderBy('scmname')->orderBy('Id')
->get(['Id', 'scmname', 'scmcode', 'tradebody', 'accountbankno', 'handlePerson'])
->map(static fn (object $row): array => [
'id' => (int) $row->Id, 'value' => trim((string) $row->scmname),
'code' => trim((string) $row->scmcode), 'company' => trim((string) $row->tradebody),
'account' => trim((string) $row->accountbankno), 'operator' => trim((string) $row->handlePerson),
])->all();
}
public function signingSupplier(int $id): ?object
{
if (! $this->signingReady()) return null;
return $this->mirror->connection()->table($this->datasets->get(self::DATASET)->targetTable())
->where('Id', $id)->first(['Id', 'scmname', 'tradebody', 'accountbankno', 'handlePerson']);
}
private function signingReady(): bool
{
return $this->ready() && $this->mirror->connection()->getSchemaBuilder()
->hasColumn($this->datasets->get(self::DATASET)->targetTable(), 'accountbankno');
}
public function paginate(array $filters, int $page, int $limit): array
{
$query = $this->query($filters);

View File

@ -3,6 +3,7 @@
@php($agreementSubjects = $agreementSubjects ?? collect())
@php($agreementCourierNames = collect($agreementCourierNames ?? []))
@php($agreementCourierAccounts = $agreementCourierAccounts ?? [])
@php($agreementSupplierOptions = $agreementSupplierOptions ?? [])
@php($agreementQuoteVersions = $agreementQuoteVersions ?? collect())
@php($agreementCooperationSecondaryEsignAccount = '18052733960')
@php($agreementDefaultSubject = $agreementSubjects->firstWhere('is_default', true) ?: $agreementSubjects->first())
@ -1587,10 +1588,8 @@
<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>
<input type="hidden" name="supplier_id" data-signing-supplier-id disabled>
<p class="agreement-help" data-supplier-control hidden aria-live="polite">按供应商简称、企业全称或编码搜索,选择后自动带入易签宝账号。</p>
</div>
<div class="agreement-field">
<label for="agreement-document-type">文件类型 <span class="agreement-required" aria-hidden="true">*</span></label>
@ -2249,6 +2248,8 @@
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 supplierIdInput = root.querySelector('[data-signing-supplier-id]');
var supplierOptions = @json($agreementSupplierOptions);
var documentSelect = form.querySelector('[data-document-type]');
var documentOptions = Array.prototype.slice.call(documentSelect.options);
var currentPartnerType = 'courier';
@ -2259,6 +2260,8 @@
}).map(function (option) {
return { value: option.value, account: option.dataset.esignAccount || '' };
}) : [];
var originalCourierOptions = courierOptions;
var selectedSupplierId = '';
var visibleCourierIndexes = [];
var activeCourierIndex = -1;
var esignAccountInput = root.querySelector('input[name="esign_account"]');
@ -2759,16 +2762,29 @@
secondaryEsignAccountInput.value = '';
secondaryOperatorInput.value = '';
root.querySelector('[data-request-error]').textContent = '';
supplierIdInput.value = '';
['former_company_name', 'cooperation_party_b_name', 'principal_name'].forEach(function (name) {
var input = form.querySelector('[name="' + name + '"]');
if (input) input.value = '';
});
}
currentPartnerType = type;
courierControl.hidden = supplier;
courierSelect.disabled = supplier;
courierSearch.disabled = supplier;
courierOptions = supplier ? supplierOptions : originalCourierOptions;
courierSelect.replaceChildren(new Option('请选择', ''));
courierOptions.forEach(function (item) {
var option = new Option(item.value, item.value);
option.dataset.esignAccount = item.account;
courierSelect.appendChild(option);
});
supplierIdInput.disabled = !supplier;
courierSearch.placeholder = supplier ? '输入供应商简称、全称或编码搜索' : '输入快递名称搜索';
courierOptionsList.setAttribute('aria-label', supplier ? '供应商资料' : '快递资料');
supplierControl.hidden = !supplier;
courierLabel.htmlFor = supplier ? 'agreement-supplier-search' : courierSearch.id;
supplierControl.textContent = supplierOptions.length ? '按供应商简称、企业全称或编码搜索,选择后自动带入易签宝账号。' : '供应商资料暂不可用,请稍后重试。';
courierLabel.htmlFor = 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;
@ -2782,9 +2798,10 @@
} else {
documentSelect.value = 'account-proof';
}
sendButton.disabled = supplier || !esignAvailable;
sendButton.title = supplier ? '供应商资料尚未接入,暂不能发起签署' : (esignAvailable ? '推送至E签宝' : 'E签宝沙箱应用尚未配置');
sendButton.querySelector('span:last-child').textContent = supplier ? '供应商资料待接入' : '推送至E签宝';
sendButton.disabled = (supplier && !supplierOptions.length) || !esignAvailable;
sendButton.title = supplier && !supplierOptions.length ? '供应商资料暂不可用' : (esignAvailable ? '推送至E签宝' : 'E签宝尚未配置');
sendButton.querySelector('span:last-child').textContent = '推送至E签宝';
syncCourierSelection();
}
function fieldValue(selector, fallback) {
@ -2851,11 +2868,30 @@
var option = courierSelect.options[courierSelect.selectedIndex];
var account = option && option.dataset.esignAccount ? option.dataset.esignAccount.trim() : '';
if (currentPartnerType === 'supplier') {
var supplier = courierOptions.find(function (item) { return String(item.id) === supplierIdInput.value; });
if (selectedSupplierId !== supplierIdInput.value) {
selectedSupplierId = supplierIdInput.value;
esignAccountInput.value = '';
delete esignAccountInput.dataset.autoFilled;
accountOrganizationInput.value = supplier ? supplier.company : '';
operatorNameInput.value = supplier ? supplier.operator : '';
['former_company_name', 'cooperation_party_b_name', 'principal_name'].forEach(function (name) {
var input = form.querySelector('[name="' + name + '"]');
if (input) input.value = supplier ? supplier.company : '';
});
}
supplierControl.textContent = supplier
? supplier.value + ' · ' + supplier.code + (supplier.company ? ' · ' + supplier.company : '') + (supplier.account ? '' : ';未维护易签宝账号,请手工填写并核对。')
: (supplierOptions.length ? '请从搜索结果选择供应商。' : '供应商资料暂不可用,请稍后重试。');
} else {
selectedSupplierId = '';
}
if (account !== '') {
esignAccountInput.value = account;
esignAccountInput.dataset.autoFilled = 'true';
announce('已带出该快递的电子签章经办人账号');
announce(currentPartnerType === 'supplier' ? '已带出该供应商的易签宝账号' : '已带出该快递的电子签章经办人账号');
} else if (esignAccountInput.dataset.autoFilled === 'true') {
esignAccountInput.value = '';
delete esignAccountInput.dataset.autoFilled;
@ -2882,7 +2918,8 @@
var matches = [];
courierOptions.forEach(function (option, index) {
if (normalizedQuery === '' || option.value.toLocaleLowerCase().indexOf(normalizedQuery) !== -1) {
var searchText = [option.value, option.company || '', option.code || ''].join(' ').toLocaleLowerCase();
if (normalizedQuery === '' || searchText.indexOf(normalizedQuery) !== -1) {
matches.push(index);
}
});
@ -2894,7 +2931,9 @@
var empty = document.createElement('div');
empty.className = 'agreement-courier-options-status';
empty.setAttribute('role', 'status');
empty.textContent = normalizedQuery === '' ? '暂无可选快递资料' : '没有匹配的快递名称';
empty.textContent = currentPartnerType === 'supplier'
? (normalizedQuery === '' ? '暂无可选供应商资料' : '没有匹配的供应商')
: (normalizedQuery === '' ? '暂无可选快递资料' : '没有匹配的快递名称');
courierOptionsList.appendChild(empty);
} else {
visibleCourierIndexes.forEach(function (optionIndex) {
@ -2905,7 +2944,7 @@
item.setAttribute('role', 'option');
item.setAttribute('aria-selected', courierSelect.value === option.value ? 'true' : 'false');
item.dataset.courierOption = String(optionIndex);
item.textContent = option.value;
item.textContent = currentPartnerType === 'supplier' ? option.value + ' · ' + option.code + (option.company ? ' · ' + option.company : '') : option.value;
courierOptionsList.appendChild(item);
});
@ -2948,6 +2987,7 @@
var option = courierOptions.find(function (item) {
return item.value.toLocaleLowerCase() === enteredName.toLocaleLowerCase();
});
supplierIdInput.value = currentPartnerType === 'supplier' && option ? option.id : '';
if (option) {
courierSearch.value = option.value;
@ -2955,7 +2995,7 @@
courierSearch.setCustomValidity('');
} else {
courierSelect.value = '';
courierSearch.setCustomValidity(enteredName === '' ? '请选择快递公司' : '请从快递资料中选择快递公司');
courierSearch.setCustomValidity(currentPartnerType === 'supplier' ? '请从供应商资料中选择供应商' : (enteredName === '' ? '请选择快递公司' : '请从快递资料中选择快递公司'));
}
}
@ -3237,7 +3277,7 @@
function refreshPreview() {
var documentOption = selectedDocument();
var templateType = documentOption.value;
var courier = currentPartnerType === 'supplier' ? '供应商' : fieldValue('[name="courier_name"]', '快递公司');
var courier = fieldValue('[name="courier_name"]', currentPartnerType === 'supplier' ? '供应商' : '快递公司');
var companyName = fieldValue('[name="company_full_name"]', '待填写公司全称');
var accountAgreement = templateType === 'account-proof' || templateType === 'account-change';
var entityChange = templateType === 'entity-change';
@ -3659,7 +3699,7 @@
input.addEventListener('change', function () {
syncPartnerType();
refreshPreview();
announce(currentPartnerType === 'supplier' ? '已切换到供应商,资料尚未接入' : '已切换到快递公司');
announce(currentPartnerType === 'supplier' ? '已切换到供应商' : '已切换到快递公司');
});
});
@ -3669,11 +3709,6 @@
});
form.addEventListener('submit', function (event) {
if (currentPartnerType === 'supplier') {
event.preventDefault();
root.querySelector('[data-request-error]').textContent = '供应商资料尚未接入,暂不能发起供应商签署。';
return;
}
refreshPreview();
if (!form.checkValidity()) {
event.preventDefault();

View File

@ -11,6 +11,7 @@ use App\Services\Esign\EsignClient;
use Illuminate\Support\Facades\DB;
use Mockery;
use Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository;
use Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository;
use Symfony\Component\HttpKernel\Exception\HttpException;
use Tests\Feature\FeatureTestCase;
@ -65,7 +66,8 @@ class AgreementSigningPageTest extends FeatureTestCase
->assertSeeText('合作方类型')
->assertSee('name="partner_type" value="courier" checked', false)
->assertSee('name="partner_type" value="supplier"', false)
->assertSeeText('供应商资料尚未接入,暂不能选择或发起签署。')
->assertSee('data-signing-supplier-id', false)
->assertSeeText('按供应商简称、企业全称或编码搜索,选择后自动带入易签宝账号。')
->assertSeeText('快递公司')
->assertSee('data-courier-search', false)
->assertSee('placeholder="输入快递名称搜索"', false)
@ -107,6 +109,21 @@ class AgreementSigningPageTest extends FeatureTestCase
->assertSeeText('收款帐户资料证明');
}
public function test_signing_page_exposes_supplier_account_and_search_identity_separately(): void
{
$this->grantWorkspaceAccess();
$repository = Mockery::mock(SupplierManagementMirrorRepository::class);
$repository->shouldReceive('signingOptions')->once()->andReturn([[
'id' => 7, 'value' => '测试供应商', 'code' => 'T007', 'company' => '测试供应商有限公司',
'account' => '13800138000', 'operator' => '张经理',
]]);
$this->app->instance(SupplierManagementMirrorRepository::class, $repository);
$response = $this->loginAs()->get(route('business.pages.show', ['section' => 'courier-payables',
'page' => 'agreement-signing', 'agreement_view' => 'create']))->assertOk();
$this->assertSame('13800138000', $response->viewData('agreementSupplierOptions')[0]['account']);
$this->assertSame('张经理', $response->viewData('agreementSupplierOptions')[0]['operator']);
}
public function testAgreementSigningPageUsesCourierNamesFromCourierInformation(): void
{
$this->grantWorkspaceAccess();

View File

@ -15,6 +15,8 @@ use Illuminate\Http\Client\Request as ClientRequest;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\Facades\Storage;
use Mockery;
use Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository;
use Tests\Feature\FeatureTestCase;
class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
@ -73,7 +75,7 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
$this->assertSame(1, CourierAgreementSignature::query()->count());
}
public function test_supplier_requests_are_rejected_before_creating_or_sending_any_document(): void
public function test_supplier_requests_require_a_selected_supplier_before_sending(): void
{
Http::preventStrayRequests();
$this->withExceptionHandling();
@ -81,12 +83,71 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
$payload['partner_type'] = 'supplier';
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)
->assertSessionHasErrors(['partner_type' => '供应商资料尚未接入,暂不能发起供应商签署。请在资料接入后选择供应商。']);
->assertSessionHasErrors('supplier_id');
$this->assertSame(0, CourierAgreementSignature::query()->count());
Http::assertNothingSent();
}
public function test_supplier_request_uses_selected_supplier_and_its_signing_account(): void
{
$this->fakeEsign();
$repository = Mockery::mock(SupplierManagementMirrorRepository::class);
$repository->shouldReceive('signingSupplier')->with(7)->once()->andReturn((object) [
'Id' => 7, 'scmname' => '测试供应商', 'accountbankno' => '13900139000',
]);
$this->app->instance(SupplierManagementMirrorRepository::class, $repository);
$payload = $this->accountProofPayload('supplier-signing');
$payload['partner_type'] = 'supplier';
$payload['supplier_id'] = 7;
$payload['courier_name'] = '伪造名称';
unset($payload['esign_account']);
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)->assertRedirect();
$signature = CourierAgreementSignature::query()->firstOrFail();
$this->assertSame('测试供应商', $signature->courier_name);
$this->assertSame('13900139000', $signature->operator_account);
$this->assertSame('supplier', $signature->payload['partner_type']);
$this->assertSame(7, $signature->payload['supplier_id']);
$this->assertSame(CourierAgreementSignature::STATUS_SIGNING, $signature->status);
}
public function test_invalid_supplier_and_courier_only_templates_never_send(): void
{
Http::preventStrayRequests();
$this->withExceptionHandling();
$repository = Mockery::mock(SupplierManagementMirrorRepository::class);
$repository->shouldReceive('signingSupplier')->with(999)->once()->andReturnNull();
$this->app->instance(SupplierManagementMirrorRepository::class, $repository);
$payload = $this->accountProofPayload('supplier-invalid');
$payload['partner_type'] = 'supplier';
$payload['supplier_id'] = 999;
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)->assertSessionHasErrors('supplier_id');
$payload['document_type'] = 'quote-confirmation';
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)->assertSessionHasErrors('document_type');
$this->assertSame(0, CourierAgreementSignature::query()->count());
Http::assertNothingSent();
}
public function test_supplier_cooperation_contract_keeps_both_signing_parties(): void
{
$this->fakeEsign();
$repository = Mockery::mock(SupplierManagementMirrorRepository::class);
$repository->shouldReceive('signingSupplier')->with(7)->once()->andReturn((object) [
'Id' => 7, 'scmname' => '测试供应商', 'accountbankno' => '13900139000',
]);
$this->app->instance(SupplierManagementMirrorRepository::class, $repository);
$payload = $this->cooperationContractPayload('supplier-cooperation');
$payload['partner_type'] = 'supplier';
$payload['supplier_id'] = 7;
unset($payload['esign_account']);
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $payload)->assertRedirect();
$signature = CourierAgreementSignature::query()->firstOrFail();
$this->assertSame('测试供应商', $signature->courier_name);
$this->assertSame('13900139000', $signature->operator_account);
$this->assertSame('18052733960', $signature->payload['secondary_esign_account']);
$this->assertSame(CourierAgreementSignature::STATUS_SIGNING, $signature->status);
}
public function test_unknown_partner_type_is_rejected_before_sending(): void
{
Http::preventStrayRequests();

View File

@ -96,10 +96,29 @@ class SupplierManagementMirrorRepositoryTest extends TestCase
{
$this->table('js_supplier_management')->insert(['Id' => 1]);
$this->assertFalse($this->repository->ready());
$this->assertSame([], $this->repository->signingOptions());
$this->assertNull($this->repository->signingSupplier(1));
$this->expectException(\RuntimeException::class);
$this->repository->paginate([], 1, 20);
}
public function test_signing_accounts_use_the_account_field_and_keep_real_names_separate(): void
{
$this->ready(SupplierManagementMirrorRepository::DATASET);
$this->table('js_supplier_management')->insert([
['Id' => 7, 'scmname' => '测试供应商', 'scmcode' => 'T007', 'tradebody' => '测试供应商有限公司',
'accountbankno' => '13800138000', 'handlePerson' => '张经理'],
['Id' => 8, 'scmname' => '缺账号供应商', 'scmcode' => 'T008', 'tradebody' => '',
'accountbankno' => null, 'handlePerson' => '李经理'],
]);
$options = collect($this->repository->signingOptions())->keyBy('id');
$this->assertSame('13800138000', $options[7]['account']);
$this->assertSame('张经理', $options[7]['operator']);
$this->assertSame('', $options[8]['account']);
$this->assertSame('13800138000', $this->repository->signingSupplier(7)->accountbankno);
$this->assertNull($this->repository->signingSupplier(999));
}
private function ready(string $key): void
{
$this->table('business_mirror_table_states')->insert(['dataset_key' => $key, 'initial_completed_at' => '2026-10-11 00:00:00']);