user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail(), ); $this->accountShort = '汇总表格测试账户-' . strtoupper(substr(md5(uniqid('', true)), 0, 8)); $connection = DB::connection('business_mirror'); $connection->table('fin_accounts')->insert([ 'company_name' => '汇总表格测试公司', 'account_short' => $this->accountShort, 'account_type' => '公户', 'current_balance' => 100, 'daily_available' => 100, 'blocked_amount' => 0, ]); $connection->table('fin_account_opening')->insert([ 'account_short' => $this->accountShort, 'opening_date' => '2026-11-01', 'opening_balance' => 100, ]); } protected function tearDown(): void { Carbon::setTestNow(); parent::tearDown(); } public function test_summary_exposes_excel_style_add_row_and_add_column_controls(): void { $html = $this->loginAs()->get($this->pageUrl())->assertOk()->getContent(); $this->assertStringContainsString('data-ledger-flow-summary-add-row', $html); $this->assertStringContainsString('data-ledger-flow-summary-add-column', $html); $this->assertStringContainsString('data-ledger-flow-summary-column-dialog', $html); $this->assertStringContainsString('>新增行', $html); $this->assertStringContainsString('>新增列', $html); } public function test_summary_custom_column_values_can_be_saved_and_column_can_be_deleted(): void { $create = $this->loginAs()->postJson($this->columnStoreUrl(), [ 'field_names' => ['核对状态'], 'position' => 'end', ])->assertCreated() ->assertJsonPath('fields.0.field_name', '核对状态'); $fieldId = (int) $create->json('fields.0.id'); $this->assertDatabaseHas('fin_flow_summary_custom_fields', [ 'id' => $fieldId, 'field_name' => '核对状态', ], 'business_mirror'); $this->loginAs()->postJson($this->valueUrl(), [ 'field_id' => $fieldId, 'account_short' => $this->accountShort, 'value' => '已核对', ])->assertOk() ->assertJsonPath('value.value', '已核对'); $html = $this->loginAs()->get($this->pageUrl())->assertOk()->getContent(); $this->assertStringContainsString('title="核对状态">核对状态', $html); $this->assertStringContainsString('data-ledger-flow-summary-custom-value', $html); $this->assertStringContainsString('data-account-short="' . $this->accountShort . '"', $html); $this->assertStringContainsString('value="已核对"', $html); $this->loginAs()->deleteJson($this->columnDeleteUrl($fieldId)) ->assertOk() ->assertJsonPath('deleted', true); $this->assertDatabaseMissing('fin_flow_summary_custom_fields', [ 'id' => $fieldId, ], 'business_mirror'); $this->assertDatabaseMissing('fin_flow_summary_custom_values', [ 'custom_field_id' => $fieldId, ], 'business_mirror'); } public function test_summary_keeps_every_account_master_row_and_follows_account_edits(): void { $connection = DB::connection('business_mirror'); $originalId = (int) $connection->table('fin_accounts') ->where('account_short', $this->accountShort) ->value('id'); $duplicateId = (int) $connection->table('fin_accounts')->insertGetId([ 'company_name' => '重名账户测试公司', 'account_short' => $this->accountShort, 'account_type' => '公户', 'current_balance' => 20, 'daily_available' => 20, 'blocked_amount' => 0, ]); $html = $this->loginAs()->get($this->pageUrl())->assertOk()->getContent(); $this->assertStringContainsString('data-flow-summary-account-id="' . $originalId . '"', $html); $this->assertStringContainsString('data-flow-summary-account-id="' . $duplicateId . '"', $html); $this->assertStringContainsString('data-flow-summary-account-display="' . $this->accountShort . '1"', $html); $this->assertStringContainsString('data-flow-summary-account-display="' . $this->accountShort . '2"', $html); $this->assertStringContainsString('>' . $this->accountShort . '1', $html); $this->assertStringContainsString('>' . $this->accountShort . '2', $html); $this->assertStringContainsString('汇总表格测试公司', $html); $this->assertStringContainsString('重名账户测试公司', $html); $accountDirectoryHtml = $this->loginAs()->get($this->accountDirectoryUrlForAccount($this->accountShort)) ->assertOk() ->getContent(); $this->assertStringContainsString('data-ledger-account-display="' . $this->accountShort . '1"', $accountDirectoryHtml); $this->assertStringContainsString('data-ledger-account-display="' . $this->accountShort . '2"', $accountDirectoryHtml); $renamedShort = $this->accountShort . '-已同步'; $this->loginAs()->postJson(route('business.financial-ledger.accounts.update', [ 'company_id' => $this->company->id, 'id' => $duplicateId, ]), [ 'company_name' => '账户编辑后公司', 'account_short' => $renamedShort, 'status' => 'normal', ])->assertOk()->assertJsonPath('account.company_name', '账户编辑后公司'); $renamedHtml = $this->loginAs()->get($this->pageUrlForAccount($renamedShort)) ->assertOk() ->getContent(); $this->assertStringContainsString('data-flow-summary-account-id="' . $duplicateId . '"', $renamedHtml); $this->assertStringContainsString('账户编辑后公司', $renamedHtml); $this->assertStringNotContainsString('重名账户测试公司', $renamedHtml); } public function test_data_source_is_after_company_and_can_be_edited_inline(): void { $html = $this->loginAs()->get($this->pageUrl())->assertOk()->getContent(); $flowsStart = strpos($html, 'id="ledger-panel-flows"'); $transactionsStart = strpos($html, 'id="ledger-panel-transactions"'); $flowsHtml = substr($html, (int) $flowsStart, (int) $transactionsStart - (int) $flowsStart); $accountHeader = strpos($flowsHtml, 'data-ledger-header-field="account_short"'); $companyHeader = strpos($flowsHtml, 'data-ledger-header-field="company_name"'); $sourceHeader = strpos($flowsHtml, 'data-ledger-header-field="data_source"'); $openingHeader = strpos($flowsHtml, 'data-ledger-header-field="opening_balance"'); $this->assertIsInt($accountHeader); $this->assertIsInt($companyHeader); $this->assertIsInt($sourceHeader); $this->assertIsInt($openingHeader); $this->assertTrue($accountHeader < $companyHeader); $this->assertTrue($companyHeader < $sourceHeader); $this->assertTrue($sourceHeader < $openingHeader); $this->assertStringContainsString('data-ledger-flow-summary-source', $flowsHtml); $this->assertStringContainsString('placeholder="填写数据来源"', $flowsHtml); $this->loginAs()->postJson($this->sourceUrl(), [ 'account_short' => $this->accountShort, 'value' => '银行流水', ])->assertOk() ->assertJsonPath('value.data_source', '银行流水'); $this->assertDatabaseHas('fin_flow_summary_account_sources', [ 'account_short' => $this->accountShort, 'data_source' => '银行流水', ], 'business_mirror'); $accountId = (int) DB::connection('business_mirror')->table('fin_accounts') ->where('account_short', $this->accountShort) ->value('id'); $renamedShort = $this->accountShort . '-来源同步'; $this->loginAs()->postJson(route('business.financial-ledger.accounts.update', [ 'company_id' => $this->company->id, 'id' => $accountId, ]), [ 'company_name' => '汇总表格测试公司', 'account_short' => $renamedShort, 'status' => 'normal', ])->assertOk(); $this->assertDatabaseHas('fin_flow_summary_account_sources', [ 'account_short' => $renamedShort, 'data_source' => '银行流水', ], 'business_mirror'); $this->assertDatabaseHas('fin_account_opening', [ 'account_short' => $renamedShort, ], 'business_mirror'); $this->assertDatabaseMissing('fin_account_opening', [ 'account_short' => $this->accountShort, ], 'business_mirror'); $renamedHtml = $this->loginAs()->get($this->pageUrlForAccount($renamedShort)) ->assertOk() ->getContent(); $this->assertStringContainsString('value="银行流水"', $renamedHtml); } public function test_data_source_column_name_is_reserved_for_the_fixed_column(): void { $this->withExceptionHandling()->loginAs()->postJson($this->columnStoreUrl(), [ 'field_names' => ['数据来源'], 'position' => 'end', ])->assertUnprocessable() ->assertJsonPath('errors.field_names.0', '“数据来源”是账户资金汇总固定列,不能重复新增。'); } public function test_summary_spreadsheet_endpoints_reject_account_viewers(): void { $viewer = user_model_class()::create([ 'name' => '汇总只读用户', 'email' => 'flow-summary-viewer-' . uniqid() . '@example.com', 'password' => 'local-test-password', 'locale' => 'zh-CN', 'enabled' => 1, 'landing_page' => 'dashboard', 'created_from' => 'core::test', ]); $viewer->companies()->attach($this->company->id); $viewer->roles()->attach(Role::where('name', 'user')->firstOrFail()->id); $viewer->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); $this->withExceptionHandling()->loginAs($viewer)->postJson($this->columnStoreUrl(), [ 'field_names' => ['无权限列'], ])->assertForbidden(); $this->withExceptionHandling()->loginAs($viewer)->postJson($this->sourceUrl(), [ 'account_short' => $this->accountShort, 'value' => '无权限来源', ])->assertForbidden(); } private function pageUrl(): string { return $this->pageUrlForAccount($this->accountShort); } private function pageUrlForAccount(string $accountShort): string { return route('business.pages.show', [ 'company_id' => $this->company->id, 'section' => 'financial-operations', 'page' => 'ledger', ]) . '?' . http_build_query([ 'tab' => 'flows', 'flows_view' => 'summary', 'flows_from' => '2026-11-01', 'flows_to' => '2026-11-30', 'flows_account' => $accountShort, ]); } private function accountDirectoryUrlForAccount(string $accountShort): string { return route('business.pages.show', [ 'company_id' => $this->company->id, 'section' => 'financial-operations', 'page' => 'ledger', ]) . '?' . http_build_query([ 'tab' => 'accounts', 'accounts_view' => 'all', 'accounts_short' => $accountShort, ]); } private function columnStoreUrl(): string { return route('business.financial-ledger.flows.summary.columns.store', [ 'company_id' => $this->company->id, ]); } private function columnDeleteUrl(int $fieldId): string { return route('business.financial-ledger.flows.summary.columns.delete', [ 'company_id' => $this->company->id, 'id' => $fieldId, ]); } private function valueUrl(): string { return route('business.financial-ledger.flows.summary.values.update', [ 'company_id' => $this->company->id, ]); } private function sourceUrl(): string { return route('business.financial-ledger.flows.summary.source.update', [ 'company_id' => $this->company->id, ]); } }