488 lines
22 KiB
PHP
488 lines
22 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Business;
|
||
|
||
use App\Models\Auth\Permission;
|
||
use App\Models\Business\CourierAgreementSignature;
|
||
use App\Models\Business\CourierQuoteChange;
|
||
use App\Models\Business\CourierQuoteSignature;
|
||
use App\Models\Business\CourierQuoteVersion;
|
||
use App\Services\Esign\EsignClient;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Mockery;
|
||
use Modules\EcommerceFinance\Repositories\CourierInformationMirrorRepository;
|
||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||
use Tests\Feature\FeatureTestCase;
|
||
|
||
class AgreementSigningPageTest extends FeatureTestCase
|
||
{
|
||
protected function grantWorkspaceAccess(): void
|
||
{
|
||
$this->user->attachPermission(
|
||
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
||
);
|
||
}
|
||
|
||
public function testAgreementSigningPageRendersTheSigningWorkspace(): void
|
||
{
|
||
$this->grantWorkspaceAccess();
|
||
|
||
$this->loginAs()
|
||
->get(route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'agreement-signing',
|
||
]))
|
||
->assertOk()
|
||
->assertSeeText('协议签署')
|
||
->assertSeeText('签署任务')
|
||
->assertSeeText('发起签章')
|
||
->assertSeeText('模板管理')
|
||
->assertSeeText(EsignClient::configured() ? 'E签宝已连接' : 'E签宝未连接')
|
||
->assertSeeText('还没有协议签署任务')
|
||
->assertSeeText('收款账户变更证明.doc')
|
||
->assertSeeText('收款账户证明文件.doc')
|
||
->assertSeeText('公司主体变更证明.docx')
|
||
->assertSeeText('共用合同证明.docx')
|
||
->assertSeeText('授权委托书(快递).docx');
|
||
}
|
||
|
||
public function testAgreementSigningPageIncludesSinglePageRequestFormAndLivePreview(): void
|
||
{
|
||
$this->grantWorkspaceAccess();
|
||
|
||
$this->loginAs()
|
||
->get(route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'agreement-signing',
|
||
'agreement_view' => 'create',
|
||
]))
|
||
->assertOk()
|
||
->assertSee('data-signing-request-form', false)
|
||
->assertSee('data-agreement-preview', false)
|
||
->assertSee('data-sign-request-send', false)
|
||
->assertDontSee('data-agreement-step-panel', false)
|
||
->assertSeeText('发起签章请求')
|
||
->assertSeeText('快递公司')
|
||
->assertSee('data-courier-search', false)
|
||
->assertSee('placeholder="输入快递名称搜索"', false)
|
||
->assertSee('aria-haspopup="listbox"', false)
|
||
->assertSee('data-courier-options', false)
|
||
->assertSeeText('文件类型')
|
||
->assertSeeText('收款账户证明')
|
||
->assertSeeText('TPL-ACCOUNT-CERT-V2')
|
||
->assertSeeText('我方签署主体')
|
||
->assertSeeText('公司全称')
|
||
->assertSee('name="company_full_name"', false)
|
||
->assertSee('data-company-name', false)
|
||
->assertSee('data-subject-linked-name', false)
|
||
->assertSeeText('签署截止日期')
|
||
->assertSeeText('对方 E 签宝账号')
|
||
->assertSeeText('对方签署主体类型')
|
||
->assertSee('对方签署个人姓名', false)
|
||
->assertSee('name="counterparty_signer_type"', false)
|
||
->assertSee('data-primary-esign-label', false)
|
||
->assertSee('data-secondary-esign-label', false)
|
||
->assertSee("labels.primary = '快递方 E 签宝账号'", false)
|
||
->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('我方 E 签宝实名认证姓名')
|
||
->assertSee('name="secondary_operator_name"', false)
|
||
->assertSeeText('填写变量')
|
||
->assertSeeText('签署完成后自动归档')
|
||
->assertSeeText('人工确认后归档')
|
||
->assertSeeText('刷新预览')
|
||
->assertSeeText('推送至E签宝')
|
||
->assertSeeText('收款帐户资料证明');
|
||
}
|
||
|
||
public function testAgreementSigningPageUsesCourierNamesFromCourierInformation(): void
|
||
{
|
||
$this->grantWorkspaceAccess();
|
||
|
||
$courierInformation = Mockery::mock(CourierInformationMirrorRepository::class);
|
||
$courierInformation->shouldReceive('names')
|
||
->once()
|
||
->andReturn(['顺丰', '韵达']);
|
||
$courierInformation->shouldReceive('signingAccountsByName')
|
||
->once()
|
||
->with(['顺丰', '韵达'])
|
||
->andReturn(['顺丰' => '13800000001']);
|
||
$this->app->instance(CourierInformationMirrorRepository::class, $courierInformation);
|
||
|
||
$this->loginAs()
|
||
->get(route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'agreement-signing',
|
||
'agreement_view' => 'create',
|
||
]))
|
||
->assertOk()
|
||
->assertSee('data-courier-search', false)
|
||
->assertSee('data-courier-select required', false)
|
||
->assertSee('<option value="顺丰" data-esign-account="13800000001">顺丰</option>', false)
|
||
->assertSee('<option value="韵达" data-esign-account="">韵达</option>', false)
|
||
->assertDontSee('<option value="新北中通"', false);
|
||
}
|
||
|
||
public function testAgreementSigningPageShowsOurCompanyAsInitiator(): void
|
||
{
|
||
$this->grantWorkspaceAccess();
|
||
|
||
CourierAgreementSignature::create([
|
||
'company_id' => $this->company->id,
|
||
'business_no' => 'AGR-INITIATOR-001',
|
||
'idempotency_key' => 'initiator-001',
|
||
'document_type' => 'account-proof',
|
||
'template_code' => 'TPL-ACCOUNT-CERT-V2',
|
||
'template_version' => '2',
|
||
'source_file_name' => '收款账户证明文件.doc',
|
||
'status' => CourierAgreementSignature::STATUS_SIGNING,
|
||
'courier_name' => '新北中通',
|
||
'company_full_name' => $this->company->name,
|
||
'organization_name' => '新北中通',
|
||
'operator_name' => '张经理',
|
||
'operator_account' => '13800138000',
|
||
'payload' => ['signing_subject' => $this->company->name],
|
||
'environment' => 'sandbox',
|
||
'source_pdf_path' => 'courier-agreements/initiator/source.pdf',
|
||
'source_pdf_sha256' => hash('sha256', '%PDF-source'),
|
||
'esign_sign_flow_id' => 'flow-initiator-001',
|
||
'created_by' => $this->user->id,
|
||
'updated_by' => $this->user->id,
|
||
]);
|
||
|
||
$this->loginAs()
|
||
->get(route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'agreement-signing',
|
||
]))
|
||
->assertOk()
|
||
->assertSee('<td>' . e($this->company->name) . '</td>', false)
|
||
->assertDontSee('<td>张经理</td>', false);
|
||
}
|
||
|
||
public function testAgreementSigningPageFiltersAgreementTasksBySearchStatusAndType(): void
|
||
{
|
||
$this->grantWorkspaceAccess();
|
||
|
||
$this->createAgreementTask([
|
||
'business_no' => 'AGR-FILTER-MATCH',
|
||
'document_type' => 'account-proof',
|
||
'status' => CourierAgreementSignature::STATUS_ARCHIVED,
|
||
'courier_name' => '筛选快递',
|
||
'operator_name' => '筛选签署人',
|
||
]);
|
||
$this->createAgreementTask([
|
||
'business_no' => 'AGR-FILTER-OTHER',
|
||
'document_type' => 'account-change',
|
||
'status' => CourierAgreementSignature::STATUS_SIGNING,
|
||
'courier_name' => '另一家快递',
|
||
'operator_name' => '另一位签署人',
|
||
]);
|
||
|
||
$this->loginAs()
|
||
->get(route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'agreement-signing',
|
||
'agreement_search' => 'AGR-FILTER-MATCH',
|
||
'agreement_status' => CourierAgreementSignature::STATUS_ARCHIVED,
|
||
'agreement_type' => 'account-proof',
|
||
]))
|
||
->assertOk()
|
||
->assertSeeText('AGR-FILTER-MATCH')
|
||
->assertDontSeeText('AGR-FILTER-OTHER')
|
||
->assertSee('name="agreement_search"', false)
|
||
->assertSee('value="AGR-FILTER-MATCH"', false)
|
||
->assertSee('value="archived" selected', false)
|
||
->assertSee('value="account-proof" selected', false);
|
||
}
|
||
|
||
public function testAgreementSigningPageFiltersQuoteConfirmationsByVersionCarrierAndStatus(): void
|
||
{
|
||
$this->grantWorkspaceAccess();
|
||
|
||
$this->createQuoteSignature('QB-FILTER-MATCH', CourierQuoteSignature::STATUS_ARCHIVED, '筛选快递');
|
||
$this->createQuoteSignature('QB-FILTER-OTHER', CourierQuoteSignature::STATUS_SIGNING, '另一家快递');
|
||
|
||
$this->loginAs()
|
||
->get(route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'agreement-signing',
|
||
'agreement_search' => 'QB-FILTER-MATCH',
|
||
'agreement_status' => CourierQuoteSignature::STATUS_ARCHIVED,
|
||
'agreement_type' => 'quote-confirmation',
|
||
]))
|
||
->assertOk()
|
||
->assertSeeText('QB-FILTER-MATCH')
|
||
->assertDontSee('<td>QB-FILTER-OTHER</td>', false)
|
||
->assertDontSeeText('没有匹配的签署任务');
|
||
}
|
||
|
||
public function testAgreementSigningPageDistinguishesDefinitiveFailureFromUnknownResult(): void
|
||
{
|
||
$this->grantWorkspaceAccess();
|
||
|
||
foreach ([
|
||
[
|
||
'business_no' => 'AGR-DEFINITIVE-FAILURE',
|
||
'last_error' => 'E 签宝接口返回业务错误(1435002):签署账号 18810726376 与身份信息不一致',
|
||
],
|
||
[
|
||
'business_no' => 'AGR-UNKNOWN-RESULT',
|
||
'last_error' => '创建签署流程时连接超时,无法确认远端结果。',
|
||
],
|
||
] as $index => $error) {
|
||
CourierAgreementSignature::create([
|
||
'company_id' => $this->company->id,
|
||
'business_no' => $error['business_no'],
|
||
'idempotency_key' => 'status-display-' . $index,
|
||
'document_type' => 'authorization',
|
||
'template_code' => 'TPL-AUTHORIZATION-V1',
|
||
'template_version' => '1',
|
||
'source_file_name' => '授权委托书(快递).docx',
|
||
'status' => CourierAgreementSignature::STATUS_UNKNOWN,
|
||
'courier_name' => '测试快递',
|
||
'company_full_name' => $this->company->name,
|
||
'organization_name' => '测试快递有限公司',
|
||
'operator_name' => '测试人员',
|
||
'operator_account' => '18810726376',
|
||
'payload' => ['signing_subject' => $this->company->name],
|
||
'environment' => 'production',
|
||
'source_pdf_path' => "courier-agreements/status-{$index}/source.pdf",
|
||
'source_pdf_sha256' => hash('sha256', '%PDF-source-' . $index),
|
||
'last_error' => $error['last_error'],
|
||
'created_by' => $this->user->id,
|
||
'updated_by' => $this->user->id,
|
||
]);
|
||
}
|
||
|
||
$this->loginAs()
|
||
->get(route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'agreement-signing',
|
||
]))
|
||
->assertOk()
|
||
->assertSeeText('处理失败')
|
||
->assertSeeText('状态未知,请人工核对')
|
||
->assertSeeText('E 签宝接口返回业务错误(1435002):签署账号 188****6376 与身份信息不一致')
|
||
->assertDontSee('签署账号 18810726376', false);
|
||
}
|
||
|
||
public function testAgreementSigningPageKeepsRenderingWhenLegacyPayloadCannotBeDecrypted(): void
|
||
{
|
||
$this->grantWorkspaceAccess();
|
||
|
||
DB::table('business_courier_agreement_signatures')->insert([
|
||
'company_id' => $this->company->id,
|
||
'business_no' => 'AGR-LEGACY-001',
|
||
'idempotency_key' => 'legacy-payload-001',
|
||
'document_type' => 'account-proof',
|
||
'template_code' => 'TPL-ACCOUNT-CERT-V2',
|
||
'template_version' => '2',
|
||
'source_file_name' => '收款账户证明文件.doc',
|
||
'status' => CourierAgreementSignature::STATUS_COMPLETED,
|
||
'courier_name' => '新北中通',
|
||
'company_full_name' => $this->company->name,
|
||
'organization_name' => '新北中通',
|
||
'operator_name' => '张经理',
|
||
'operator_account' => '13800138000',
|
||
// This value is intentionally not decryptable by the current APP_KEY.
|
||
'payload' => 'legacy-ciphertext',
|
||
'environment' => 'sandbox',
|
||
'source_pdf_path' => 'courier-agreements/legacy/source.pdf',
|
||
'source_pdf_sha256' => hash('sha256', '%PDF-source'),
|
||
'created_by' => $this->user->id,
|
||
'updated_by' => $this->user->id,
|
||
'created_at' => now(),
|
||
'updated_at' => now(),
|
||
]);
|
||
|
||
$this->loginAs()
|
||
->get(route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'agreement-signing',
|
||
]))
|
||
->assertOk()
|
||
->assertSeeText('AGR-LEGACY-001')
|
||
->assertSee('<td>' . e($this->company->name) . '</td>', false);
|
||
}
|
||
|
||
public function testAgreementSigningPageMapsEverySourceDocumentToItsOwnFieldsAndPreview(): void
|
||
{
|
||
$this->grantWorkspaceAccess();
|
||
|
||
$response = $this->loginAs()
|
||
->get(route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'agreement-signing',
|
||
'agreement_view' => 'create',
|
||
]))
|
||
->assertOk();
|
||
|
||
$response
|
||
->assertSee('data-template-fields="account-proof"', false)
|
||
->assertSee('data-template-preview="account-proof"', false)
|
||
->assertSeeText('生效日期')
|
||
->assertSeeText('银行帐户')
|
||
->assertSeeText('开户名称(持卡人姓名)')
|
||
->assertSeeText('填写实际收款银行卡对应的持卡人姓名,无需与营业执照或公司全称一致。')
|
||
->assertSeeText('我司同意自')
|
||
->assertSee('data-template-fields="account-change"', false)
|
||
->assertSee('data-template-preview="account-change"', false)
|
||
->assertSeeText('原收款账户')
|
||
->assertSeeText('原账户停止使用日期')
|
||
->assertSeeText('新账户启用日期')
|
||
->assertSeeText('之前相应的账户')
|
||
->assertSee('data-template-fields="entity-change"', false)
|
||
->assertSee('data-template-preview="entity-change"', false)
|
||
->assertSeeText('变更前公司名称')
|
||
->assertSeeText('变更后公司名称')
|
||
->assertSeeText('变更前公司已注销,使用原法人个人签署')
|
||
->assertSeeText('变更后主体 E 签宝实名认证姓名')
|
||
->assertSeeText('原公司法人个人签字')
|
||
->assertSeeText('受托方签署类型')
|
||
->assertSeeText('个人签署')
|
||
->assertSeeText('变更后,原公司的各项事宜由变更后的公司继续执行')
|
||
->assertSee('data-template-fields="shared-contract"', false)
|
||
->assertSee('data-template-preview="shared-contract"', false)
|
||
->assertSeeText('合同合作方')
|
||
->assertSeeText('合同主体(企业全称)')
|
||
->assertSeeText('共用合同物流商(快递名称,可多个)')
|
||
->assertSeeText('填写合同主体经办人的真实姓名,须与上方 E 签宝账号实名认证一致')
|
||
->assertSee('name="shared_courier_names[]"', false)
|
||
->assertSee('data-shared-courier-add', false)
|
||
->assertSeeText('添加共用公司')
|
||
->assertSeeText('备注')
|
||
->assertSeeText('共同用一份合同,打款资料为同一份资料')
|
||
->assertSee('data-template-fields="cooperation-contract"', false)
|
||
->assertSee('data-template-preview="cooperation-contract"', false)
|
||
->assertSee('data-template-preview="quote-confirmation"', false)
|
||
->assertSee('data-quote-version', false)
|
||
->assertSeeText('报价确认单')
|
||
->assertSeeText('绑定已审核报价版本')
|
||
->assertSeeText('甲方名称')
|
||
->assertSee('name="cooperation_party_a_name"', false)
|
||
->assertSee('data-subject-linked-name', false)
|
||
->assertSeeText('乙方电话')
|
||
->assertSeeText('协议开始日期')
|
||
->assertSeeText('协议结束日期')
|
||
->assertSeeText('甲、乙双方根据')
|
||
->assertSee('data-template-fields="authorization"', false)
|
||
->assertSee('data-template-preview="authorization"', false)
|
||
->assertSeeText('委托方(个人或公司)')
|
||
->assertSeeText('授权事项(可多选)')
|
||
->assertSeeText('代签汇款账号变更')
|
||
->assertSeeText('委托有效期:从');
|
||
}
|
||
|
||
public function testAgreementSigningPageRejectsUsersWithoutItsSidebarPermission(): void
|
||
{
|
||
$subject = 'user_' . $this->user->id;
|
||
|
||
$this->user->roles()->sync([\App\Models\Auth\Role::where('name', 'admin')->firstOrFail()->id]);
|
||
setting()->set('system.sidebar_permissions', json_encode([
|
||
'configured' => [$subject],
|
||
'subjects' => [
|
||
$subject => ['courier-payables/freight-reconciliation'],
|
||
],
|
||
], JSON_UNESCAPED_UNICODE));
|
||
|
||
try {
|
||
$this->loginAs()->get(route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'agreement-signing',
|
||
]));
|
||
$this->fail('没有协议签署页面权限的用户不应访问该页面。');
|
||
} catch (HttpException $exception) {
|
||
$this->assertSame(404, $exception->getStatusCode());
|
||
}
|
||
}
|
||
|
||
private function createAgreementTask(array $overrides = []): CourierAgreementSignature
|
||
{
|
||
return CourierAgreementSignature::create(array_merge([
|
||
'company_id' => $this->company->id,
|
||
'business_no' => 'AGR-FILTER-' . uniqid(),
|
||
'idempotency_key' => 'filter-' . uniqid(),
|
||
'document_type' => 'account-proof',
|
||
'template_code' => 'TPL-ACCOUNT-CERT-V2',
|
||
'template_version' => '2',
|
||
'source_file_name' => '收款账户证明文件.doc',
|
||
'status' => CourierAgreementSignature::STATUS_SIGNING,
|
||
'courier_name' => '新北中通',
|
||
'company_full_name' => $this->company->name,
|
||
'organization_name' => '新北中通',
|
||
'operator_name' => '张经理',
|
||
'operator_account' => '13800138000',
|
||
'payload' => ['signing_subject' => $this->company->name],
|
||
'environment' => 'sandbox',
|
||
'source_pdf_path' => 'courier-agreements/filter/source.pdf',
|
||
'source_pdf_sha256' => str_repeat('a', 64),
|
||
'created_by' => $this->user->id,
|
||
'updated_by' => $this->user->id,
|
||
], $overrides));
|
||
}
|
||
|
||
private function createQuoteSignature(string $versionCode, string $status, string $carrierName): CourierQuoteSignature
|
||
{
|
||
$change = CourierQuoteChange::create([
|
||
'company_id' => $this->company->id,
|
||
'number' => 'BG-' . $versionCode,
|
||
'source_quote_id' => 'QB-SOURCE-' . $versionCode,
|
||
'source_sheet_name' => $carrierName . '报价',
|
||
'carrier_name' => $carrierName,
|
||
'quote_type' => '标准报价',
|
||
'change_type' => '面单调整',
|
||
'status' => CourierQuoteChange::STATUS_APPROVED,
|
||
'effective_date' => now()->toDateString(),
|
||
'is_retrospective' => false,
|
||
'recalculation_status' => CourierQuoteChange::RECALCULATION_NOT_REQUIRED,
|
||
'summary' => '筛选测试报价。',
|
||
'created_by' => $this->user->id,
|
||
'approved_by' => $this->user->id,
|
||
'approved_at' => now(),
|
||
]);
|
||
|
||
$version = CourierQuoteVersion::create([
|
||
'company_id' => $this->company->id,
|
||
'version_code' => $versionCode,
|
||
'source_quote_id' => 'QB-SOURCE-' . $versionCode,
|
||
'source_change_id' => $change->id,
|
||
'sheet_name' => $carrierName . '报价',
|
||
'company_name' => $this->company->name,
|
||
'carrier_name' => $carrierName,
|
||
'quote_type' => '标准报价',
|
||
'effective_date' => now()->toDateString(),
|
||
'source_range' => 'A1:B2',
|
||
'values_json' => json_encode([['地区', '面单'], ['广东省', 6.5]], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
|
||
'formulas_json' => json_encode([[], []], JSON_THROW_ON_ERROR),
|
||
'status' => 'approved',
|
||
'signature_status' => '未签署',
|
||
'created_by' => $this->user->id,
|
||
'approved_by' => $this->user->id,
|
||
]);
|
||
|
||
return CourierQuoteSignature::create([
|
||
'company_id' => $this->company->id,
|
||
'quote_version_id' => $version->id,
|
||
'provider' => 'esign',
|
||
'environment' => 'sandbox',
|
||
'status' => $status,
|
||
'organization_name' => $carrierName,
|
||
'operator_name' => '筛选签署人',
|
||
'source_pdf_path' => 'courier-quotes/filter/' . $versionCode . '.pdf',
|
||
'source_pdf_sha256' => str_repeat('b', 64),
|
||
'source_pdf_generated_at' => now(),
|
||
'created_by' => $this->user->id,
|
||
'updated_by' => $this->user->id,
|
||
]);
|
||
}
|
||
}
|