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('顺丰', false) ->assertSee('韵达', false) ->assertDontSee('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('' . e($this->company->name) . '', false) ->assertDontSee('张经理', 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('QB-FILTER-OTHER', false) ->assertDontSeeText('没有匹配的签署任务'); } public function testAgreementSigningPageSeparatesActiveAndArchivedTasksForBothDocumentSources(): void { $this->grantWorkspaceAccess(); $this->createAgreementTask(['business_no' => 'AGR-ACTIVE', 'status' => CourierAgreementSignature::STATUS_COMPLETED]); $this->createAgreementTask(['business_no' => 'AGR-ARCHIVED', 'status' => CourierAgreementSignature::STATUS_ARCHIVED]); $this->createQuoteSignature('QB-ACTIVE', CourierQuoteSignature::STATUS_SIGNING, '测试快递'); $this->createQuoteSignature('QB-ARCHIVED', CourierQuoteSignature::STATUS_ARCHIVED, '测试快递'); $this->loginAs(); $url = route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing']); $this->get($url)->assertOk() ->assertSeeText('AGR-ACTIVE')->assertSeeText('QB-ACTIVE') ->assertDontSeeText('AGR-ARCHIVED')->assertDontSee('QB-ARCHIVED', false); $this->get($url . '?agreement_scope=archived')->assertOk() ->assertSeeText('AGR-ARCHIVED')->assertSeeText('QB-ARCHIVED') ->assertDontSeeText('AGR-ACTIVE')->assertDontSee('QB-ACTIVE', false); $this->get($url . '?agreement_scope=all')->assertOk() ->assertSeeText('AGR-ACTIVE')->assertSeeText('QB-ACTIVE') ->assertSeeText('AGR-ARCHIVED')->assertSeeText('QB-ARCHIVED'); // An explicit scope remains authoritative even if a stale URL has a // conflicting status filter; archived rows never enter the inbox. $this->get($url . '?agreement_scope=active&agreement_status=archived')->assertOk() ->assertDontSeeText('AGR-ARCHIVED')->assertDontSee('QB-ARCHIVED', false); $this->get($url . '?agreement_scope=archived&agreement_status=signing')->assertOk() ->assertDontSeeText('AGR-ACTIVE')->assertDontSee('QB-ACTIVE', false); } public function testArchivedTaskShowsMetadataAndAllAvailableFilesWithoutReadingLegacyPayload(): void { $this->grantWorkspaceAccess(); $archived = $this->createAgreementTask([ 'business_no' => 'AGR-ARCHIVE-META', 'status' => CourierAgreementSignature::STATUS_ARCHIVED, 'archived_at' => '2026-10-10 12:34:00', 'payload' => ['archive_reason' => '财务已确认:核对完成'], 'signed_pdf_path' => 'history/signed.pdf', 'certificate_path' => 'history/certificate.pdf', ]); $legacy = $this->createAgreementTask(['business_no' => 'AGR-ARCHIVE-LEGACY', 'status' => CourierAgreementSignature::STATUS_ARCHIVED]); DB::table('business_courier_agreement_signatures')->where('id', $legacy->id)->update(['payload' => 'unreadable-legacy-payload']); $this->loginAs()->get(route('business.pages.show', [ 'section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_scope' => 'archived', ]))->assertOk() ->assertSeeText('2026-10-10 12:34')->assertSeeText('财务已确认:核对完成') ->assertSeeText('AGR-ARCHIVE-LEGACY')->assertSeeText('未记录说明') ->assertSee(route('business.courier-agreement-signatures.download', [$archived, 'source']), false) ->assertSee(route('business.courier-agreement-signatures.download', [$archived, 'signed']), false) ->assertSee(route('business.courier-agreement-signatures.download', [$archived, 'certificate']), false) ->assertDontSee('data-archive-open title=', false); } public function testArchivedScopeRetainsSearchAndPaginationForMoreThanOnePage(): void { $this->grantWorkspaceAccess(); for ($i = 1; $i <= 55; $i++) { $this->createAgreementTask([ 'business_no' => 'AGR-PAGED-' . str_pad((string) $i, 3, '0', STR_PAD_LEFT), 'status' => CourierAgreementSignature::STATUS_ARCHIVED, ]); } $response = $this->loginAs()->get(route('business.pages.show', [ 'section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_scope' => 'archived', 'agreement_search' => 'AGR-PAGED-', 'agreement_task_page' => 2, ]))->assertOk()->assertSeeText('AGR-PAGED-001')->assertDontSeeText('AGR-PAGED-055'); $this->assertSame(55, $response->viewData('agreementTasks')->total()); $this->assertCount(5, $response->viewData('agreementTasks')->items()); $this->assertStringContainsString('agreement_scope=archived', $response->viewData('agreementTasks')->url(1)); $this->assertStringContainsString('agreement_search=AGR-PAGED-', $response->viewData('agreementTasks')->url(1)); } 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('' . e($this->company->name) . '', 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('原法人以个人身份签字,变更后公司仍须法人签字并盖章。') ->assertSeeText('变更后主体 E 签宝实名认证姓名') ->assertSee('data-preview-bind="changedLegalRepresentative"', false) ->assertSeeText('原公司法人个人签字') ->assertSeeText('受托方签署类型') ->assertSeeText('个人签署') ->assertSeeText('变更后,原公司的各项事宜由变更后的公司继续执行') ->assertSee('data-template-fields="shared-contract"', false) ->assertSee('data-template-preview="shared-contract"', false) ->assertSeeText('我方合作公司') ->assertSeeText('物流商归属主体(企业全称)') ->assertSee('物流商归属主体(个人实名)', false) ->assertSee('data-preview-bind="sharedSignerLabel"', false) ->assertSee('data-shared-organization-signature', false) ->assertSeeText('共用合同物流商(快递名称,可多个)') ->assertSeeText('填写物流商归属企业经办人的真实姓名,须与上方 E 签宝账号实名认证一致') ->assertSee('name="shared_courier_names[]"', false) ->assertSee('data-shared-courier-add', false) ->assertSeeText('添加共用公司') ->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, ]); } }