user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); $connection = DB::connection('business_mirror'); $connection->table('fin_accounts')->insert([ [ 'company_name' => '甲公司', 'account_short' => '账户甲', 'bank_name' => '招商银行', 'account_type' => '公户', 'region' => '上海', 'company_plan' => '保留', 'account_should_keep' => '是', 'current_balance' => 100, 'daily_available' => 80, 'blocked_amount' => 20, ], [ 'company_name' => '乙公司', 'account_short' => '账户乙', 'bank_name' => '支付宝', 'account_type' => '支付宝', 'region' => '杭州', 'company_plan' => '待注销', 'account_should_keep' => '否', 'current_balance' => 300, 'daily_available' => 300, 'blocked_amount' => 0, ], ]); $connection->table('fin_shops')->insert([ [ 'shop_name' => '天猫店铺', 'platform' => '天猫', 'company_name' => '甲公司', 'account_short' => '账户甲', 'status' => '在营', 'shop_balance' => 100, ], [ 'shop_name' => '拼多多店铺', 'platform' => '拼多多', 'company_name' => '乙公司', 'account_short' => '账户乙', 'status' => '停业', 'shop_balance' => 300, ], ]); $connection->table('fin_daily_flows')->insert([ [ 'flow_date' => '2026-10-17', 'company_name' => '甲公司', 'account_short' => '账户甲', 'opening_balance' => 100, 'inflow' => 10, 'outflow' => 0, 'closing_balance' => 110, ], [ 'flow_date' => '2026-10-18', 'company_name' => '乙公司', 'account_short' => '账户乙', 'opening_balance' => 300, 'inflow' => 0, 'outflow' => 20, 'closing_balance' => 280, ], ]); $connection->table('fin_transactions')->insert([ [ 'txn_date' => '2026-10-17', 'account_short' => '账户甲', 'detail' => '甲笔流水', 'income' => 10, 'expense' => 0, 'approval_no' => 'APR-甲', 'source' => 'dingtalk', 'source_value' => 'VAL-甲', 'process_type' => '报销', 'row_key' => 'header-filter-row-a', ], [ 'txn_date' => '2026-10-18', 'account_short' => '账户乙', 'detail' => '乙笔流水', 'income' => 0, 'expense' => 20, 'approval_no' => 'APR-乙', 'source' => 'shop', 'source_value' => 'VAL-乙', 'process_type' => '提现', 'row_key' => 'header-filter-row-b', ], ]); $connection->table('fin_account_map')->insert([ [ 'source' => 'dingtalk', 'source_value' => 'VAL-甲', 'account_short' => '账户甲', 'txn_count' => 1, 'income_sum' => 10, 'expense_sum' => 0, ], [ 'source' => 'shop', 'source_value' => 'VAL-乙', 'account_short' => null, 'txn_count' => 1, 'income_sum' => 0, 'expense_sum' => 20, ], ]); } protected function tearDown(): void { Carbon::setTestNow(); parent::tearDown(); } public function test_every_financial_ledger_table_has_a_header_menu(): void { $response = $this->loginAs()->get($this->pageUrl()); $html = $response->assertOk()->getContent(); $this->assertSame(72, preg_match_all('/]*data-ledger-header-details/', $html)); $this->assertSame(72, preg_match_all('/]*data-ledger-header-label/', $html)); $this->assertSame(2, preg_match_all('/]*>/', $html, $dateInput)); $this->assertStringNotContainsString('min=', $dateInput[0]); $allHtml = $this->loginAs()->get($this->pageUrl([ 'txns_from' => '2026-10-01', 'txns_to' => '2026-10-31', ]))->assertOk()->getContent(); $this->assertStringNotContainsString('独立账单首笔', $allHtml); $this->assertStringNotContainsString('独立账单末笔', $allHtml); $this->assertSame(1, preg_match('/]*>/', $allHtml, $allDateInput)); $this->assertStringContainsString('min="2026-10-01"', $allDateInput[0]); $nextMonthHtml = $this->loginAs()->get($this->pageUrl([ 'txns_view' => 'zhu-junxiang-alipay', 'txns_from' => '2026-11-01', 'txns_to' => '2026-11-30', ]))->assertOk()->getContent(); $this->assertSame(1, preg_match('/(
.*?)(?=
)/s', $nextMonthHtml, $nextMatches)); $this->assertStringContainsString('title="90.00 元" data-ledger-transaction-summary-previous-closing>0.01', $nextMatches[1]); $this->assertStringContainsString('title="90.00 元" data-ledger-transaction-summary-current-balance>0.01', $nextMatches[1]); $this->assertStringContainsString('data-ledger-transaction-summary-income>0.00', $nextMatches[1]); $this->assertStringContainsString('data-ledger-transaction-summary-expense>0.00', $nextMatches[1]); } public function test_independent_statement_accounts_are_hidden_from_shared_ledger_views(): void { $connection = DB::connection('business_mirror'); $connection->table('fin_accounts')->insert([ 'company_name' => '朱俊祥', 'account_short' => '朱俊祥支付宝', 'account_type' => '支付宝', 'current_balance' => 999999, 'daily_available' => 999999, ]); $connection->table('fin_transactions')->insert([ 'txn_date' => '2026-10-18', 'account_short' => '朱俊祥支付宝', 'detail' => '共享明细不应出现', 'income' => 10, 'expense' => 0, 'balance' => 100, 'approval_no' => 'INDEPENDENT-HIDDEN', 'source' => 'alipay-company', 'source_value' => 'ALI-INDEPENDENT', 'row_key' => 'independent-hidden-row', ]); $html = $this->loginAs()->get($this->pageUrl() . '?tab=accounts')->assertOk()->getContent(); $this->assertSame(1, preg_match('/.*?<\/table>/s', $html, $matches)); $this->assertStringNotContainsString('朱俊祥支付宝', $matches[0]); $this->assertStringNotContainsString('共享明细不应出现', $html); $this->assertStringNotContainsString('999999.00', $html); } public function test_shop_ledger_header_labels_and_column_order(): void { $html = $this->loginAs()->get($this->pageUrl())->assertOk()->getContent(); $panelStart = strpos($html, 'id="ledger-panel-shops"'); $theadStart = strpos($html, '', $panelStart); $theadEnd = strpos($html, '', $theadStart); $this->assertNotFalse($panelStart); $this->assertNotFalse($theadStart); $this->assertNotFalse($theadEnd); $headerHtml = substr($html, $theadStart, $theadEnd - $theadStart); $platformPosition = strpos($headerHtml, 'data-ledger-header-field="platform"'); $shopNamePosition = strpos($headerHtml, 'data-ledger-header-field="shop_name"'); $this->assertNotFalse($platformPosition); $this->assertNotFalse($shopNamePosition); $this->assertLessThan($shopNamePosition, $platformPosition); $this->assertStringContainsString('data-ledger-header-label>平台', $headerHtml); $this->assertStringContainsString('data-ledger-header-label>ERP系统店铺名称', $headerHtml); $this->assertStringContainsString('data-ledger-header-label>店铺主体', $headerHtml); $this->assertStringContainsString('data-ledger-header-label>店铺负责人', $headerHtml); $this->assertStringContainsString('data-ledger-header-label>部门负责人', $headerHtml); $this->assertStringContainsString('data-ledger-header-label>余额来源', $headerHtml); $this->assertStringContainsString('data-ledger-header-label>预留标准', $headerHtml); $this->assertStringContainsString('data-ledger-header-label>余额预警', $headerHtml); $this->assertStringNotContainsString('data-ledger-header-label>店铺名称', $headerHtml); $this->assertStringNotContainsString('data-ledger-header-label>所属公司', $headerHtml); } public function test_transaction_account_filter_provides_typeahead_suggestions(): void { $html = $this->loginAs()->get($this->pageUrl()) ->assertOk() ->assertSee('data-ledger-account-filter', false) ->assertSee('data-limit="60"', false) ->assertSee('id="txns-account"', false) ->assertSee('role="combobox"', false) ->assertSee('aria-autocomplete="list"', false) ->assertSee('aria-controls="txns-account-menu"', false) ->assertSee('aria-haspopup="listbox"', false) ->assertSee('data-ledger-account-filter-toggle', false) ->assertSee('data-ledger-account-filter-menu', false) ->assertSee('max-height:min(18rem, 50vh)', false) ->assertSee("input.addEventListener('input'", false) ->assertDontSee('id="txns-account" name="txns_account" list=', false) ->getContent(); $this->assertStringContainsString('.*?<\/tr>/s', $summaryHtml, $accountRow)); $this->assertStringContainsString('', $accountRow[0]); $this->assertStringContainsString('>所属银行', $summaryHtml); $this->assertStringContainsString('', $accountRow[0]); $this->assertSame(1, preg_match('/.*?<\/tr>/s', $summaryHtml, $emptyAccountRow)); $this->assertStringContainsString('', $emptyAccountRow[0]); $this->assertStringContainsString('', $emptyAccountRow[0]); $this->assertStringContainsString('', $emptyAccountRow[0]); $this->assertStringContainsString('>账户汇总', $summaryHtml); $this->assertStringContainsString('>按日流水', $summaryHtml); $dailyHtml = $this->loginAs()->get($this->pageUrl([ 'flows_view' => 'daily', 'flows_from' => '2026-10-01', 'flows_to' => '2026-10-31', ]))->assertOk()->getContent(); $this->assertStringContainsString('>当日入账', $dailyHtml); $this->assertStringContainsString('>2026-10-17', $dailyHtml); } public function test_account_bank_affiliation_uses_only_the_account_short_suffix(): void { $affiliations = [ '农行' => '农业银行', '招行' => '招商银行', '工行' => '工商银行', '兴业' => '兴业银行', '中行' => '中国银行', '浦发' => '浦发银行', '华夏' => '华夏银行', '支付宝' => '支付宝', '网商' => '网商银行', '建行' => '建设银行', '农商' => '江南农商行', '北京' => '北京银行', '中信' => '中信银行', '杭州' => '杭州银行', ]; $rows = []; foreach (array_keys($affiliations) as $suffix) { $rows[] = [ 'company_name' => '映射公司' . $suffix, 'account_short' => '映射账户' . $suffix, 'bank_name' => '不应采用的银行名称', 'account_type' => '公户', 'current_balance' => 0, 'daily_available' => 0, 'blocked_amount' => 0, ]; } DB::connection('business_mirror')->table('fin_accounts')->insert($rows); $html = $this->loginAs()->get($this->pageUrl([ 'flows_from' => '2026-10-01', 'flows_to' => '2026-10-31', ]))->assertOk()->getContent(); foreach ($affiliations as $suffix => $expected) { $this->assertStringContainsString( '', $html, ); } $this->assertSame(1, preg_match('/
账户甲—甲公司100.0010.00110.001
账户丙—丙公司—尚未起账映射账户' . $suffix . '' . $expected . '映射公司' . $suffix . '
.*?<\/tr><\/thead>/s', $html, $headerRow)); $accountPosition = strpos($headerRow[0], 'data-ledger-header-field="account_short"'); $affiliationPosition = strpos($headerRow[0], 'data-ledger-header-field="bank_affiliation"'); $companyPosition = strpos($headerRow[0], 'data-ledger-header-field="company_name"'); $this->assertLessThan($affiliationPosition, $accountPosition); $this->assertLessThan($companyPosition, $affiliationPosition); } public function test_shop_transaction_mapping_and_source_headers_filter_their_tables(): void { $this->loginAs()->get($this->pageUrl([ 'shops_filter_column' => 'platform', 'shops_filter_value' => '天猫', ])) ->assertOk() ->assertSee('>天猫店铺', false) ->assertDontSee('>拼多多店铺', false); $this->loginAs()->get($this->pageUrl([ 'transactions_filter_column' => 'approval_no', 'transactions_filter_value' => 'APR-乙', 'transactions_sort' => 'expense', 'transactions_direction' => 'desc', ])) ->assertOk() ->assertSee('APR-乙') ->assertDontSee('APR-甲'); $this->loginAs()->get($this->pageUrl([ 'transactions_filter_column' => 'effective_account_short', 'transactions_filter_value' => '乙', ])) ->assertOk() ->assertSee('乙笔流水') ->assertDontSee('甲笔流水'); $this->loginAs()->get($this->pageUrl([ 'mapping_filter_column' => 'mapping_status', 'mapping_filter_value' => 'unmapped', ])) ->assertOk() ->assertSee('VAL-乙') ->assertDontSee('VAL-甲'); $this->loginAs()->get($this->pageUrl([ 'sources_filter_column' => 'source', 'sources_filter_value' => 'shop', ])) ->assertOk() ->assertSee('>提现', false) ->assertDontSee('>报销', false); } public function test_ledger_pages_default_to_the_current_month_and_allow_history_selection(): void { Carbon::setTestNow('2026-11-05 12:00:00'); $connection = DB::connection('business_mirror'); $connection->table('fin_daily_flows')->insert([ 'flow_date' => '2026-11-02', 'company_name' => '甲公司', 'account_short' => '账户甲', 'opening_balance' => 100, 'inflow' => 30, 'outflow' => 0, 'closing_balance' => 130, 'remark' => '11月流水行', ]); $connection->table('fin_transactions')->insert([ 'txn_date' => '2026-11-02', 'account_short' => '账户甲', 'detail' => '11月动账明细', 'income' => 30, 'expense' => 0, 'approval_no' => 'APR-11', 'source' => 'dingtalk', 'source_value' => 'VAL-11', 'process_type' => '11月报销', 'row_key' => 'current-month-row-11', ]); $connection->table('fin_account_map')->insert([ 'source' => 'dingtalk', 'source_value' => 'VAL-11', 'account_short' => '账户甲', 'txn_count' => 1, 'income_sum' => 30, 'expense_sum' => 0, ]); $this->loginAs()->get($this->pageUrl(['flows_view' => 'daily'])) ->assertOk() ->assertSee('name="flows_from" value="2026-11-01"', false) ->assertSee('name="flows_to" value="2026-11-30"', false) ->assertSee('name="txns_from" value="2026-11-01"', false) ->assertSee('name="txns_to" value="2026-11-30"', false) ->assertSee('name="mapping_from" value="2026-11-01"', false) ->assertSee('name="mapping_to" value="2026-11-30"', false) ->assertSee('name="sources_from" value="2026-11-01"', false) ->assertSee('name="sources_to" value="2026-11-30"', false) ->assertSee('11月流水行') ->assertSee('11月动账明细') ->assertSee('VAL-11') ->assertSee('11月报销') ->assertDontSee('甲笔流水') ->assertDontSee('VAL-甲') ->assertDontSee('>报销', false); $this->loginAs()->get($this->pageUrl([ 'flows_view' => 'daily', 'flows_from' => '2026-10-01', 'flows_to' => '2026-10-31', 'txns_from' => '2026-10-01', 'txns_to' => '2026-10-31', 'mapping_from' => '2026-10-01', 'mapping_to' => '2026-10-31', 'sources_from' => '2026-10-01', 'sources_to' => '2026-10-31', ])) ->assertOk() ->assertSee('甲笔流水') ->assertSee('VAL-甲') ->assertSee('>报销', false) ->assertDontSee('11月动账明细') ->assertDontSee('VAL-11'); } public function test_flow_summary_uses_period_opening_and_closing_instead_of_summing_daily_balances(): void { $connection = DB::connection('business_mirror'); $connection->table('fin_daily_flows')->insert([ 'flow_date' => '2026-10-19', 'company_name' => '甲公司', 'account_short' => '账户甲', 'opening_balance' => 110, 'inflow' => 50000, 'outflow' => 0, 'closing_balance' => 50110, ]); $html = $this->loginAs()->get($this->pageUrl([ 'flows_from' => '2026-10-01', 'flows_to' => '2026-10-31', ]))->assertOk()->getContent(); $this->assertStringContainsString( '>期初余额合计(万元)0.04', $html, ); $this->assertStringContainsString( '>当期入账(万元)5.00', $html, ); $this->assertStringContainsString( '>当期出账(万元)0.00', $html, ); $this->assertStringContainsString( '>期末余额合计(万元)5.04', $html, ); } private function pageUrl(array $parameters = []): string { return route('business.pages.show', array_merge([ 'company_id' => $this->company->id, 'section' => 'financial-operations', 'page' => 'ledger', ], $parameters)); } }