合并财务报表表格查询布局并修正筛选排序 #274

Merged
WuYongJie merged 6 commits from fix/WuYongJie-finance-layout-release into main 2026-09-24 20:23:42 +08:00
8 changed files with 226 additions and 27 deletions

View File

@ -197,16 +197,20 @@ SQL);
*/
private function storeMatchesFilters(array $row, array $filters): bool
{
foreach (['erp_store_name', 'store_name', 'store_id', 'profit_center', 'store_manager', 'department_manager', 'company_type'] as $key) {
foreach (['erp_store_name', 'store_name', 'store_id', 'profit_center', 'store_manager', 'department_manager'] as $key) {
$expected = trim((string) ($filters[$key] ?? ''));
if ($expected === '') {
continue;
}
$actual = trim((string) ($row[$key] ?? ''));
if ($actual !== $expected) {
if (! str_contains(mb_strtolower($actual), mb_strtolower($expected))) {
return false;
}
}
$companyType = trim((string) ($filters['company_type'] ?? ''));
if ($companyType !== '' && trim((string) ($row['company_type'] ?? '')) !== $companyType) {
return false;
}
$statusMap = ['0' => '正常', '3' => '关闭中', '6' => '已关闭'];
$status = trim((string) ($filters['status'] ?? ''));
@ -252,7 +256,8 @@ SQL);
private function subjectOperatingReportPage(int $page, int $perPage): array
{
$rows = $this->storeReportRows();
$filters = $this->pageFilters('company-management', 'subject-operating-report');
$rows = $this->storeReportRows(array_intersect_key($filters, array_flip(['company_type'])));
$entities = [];
foreach ($rows as $row) {
@ -284,20 +289,74 @@ SQL);
}
$rows = array_values($entities);
usort($rows, static fn (array $left, array $right): int => strcmp($left['entity'], $right['entity']));
$offset = ($page - 1) * $perPage;
$pageRows = array_slice($rows, $offset, $perPage);
$details = $this->companyDetails(array_column($pageRows, 'entity'));
foreach ($pageRows as &$row) {
$details = $this->companyDetails(array_column($rows, 'entity'));
foreach ($rows as &$row) {
$row['company_details'] = $details[$row['entity']] ?? [];
$row['city'] = $this->cityFromDetails($row['company_details']);
}
unset($row);
$rows = array_values(array_filter($rows, fn (array $row): bool => $this->subjectMatchesFilters($row, $filters)));
$this->sortSubjectRows($rows, $filters);
$offset = ($page - 1) * $perPage;
$pageRows = array_slice($rows, $offset, $perPage);
return ['data' => $pageRows, 'total' => count($rows)];
}
/** @param array<string,mixed> $row @param array<string,string> $filters */
private function subjectMatchesFilters(array $row, array $filters): bool
{
if (($filters['company_type'] ?? '') !== '' && (string) ($row['company_type'] ?? '') !== (string) $filters['company_type']) {
return false;
}
foreach (['city', 'entity', 'plan', 'new_entity'] as $key) {
$needle = trim((string) ($filters[$key] ?? ''));
if ($needle !== '' && mb_stripos((string) ($row[$key] ?? ''), $needle) === false) {
return false;
}
}
foreach (['rolling_12m_income', ...array_map(static fn (int $month): string => sprintf('income_%02d', $month), range(1, 12))] as $key) {
$needle = str_replace(',', '', trim((string) ($filters[$key] ?? '')));
if ($needle !== '' && (! is_numeric($needle) || ! is_numeric($row[$key] ?? null)
|| abs((float) $row[$key] - (float) $needle) > 0.000001)) {
return false;
}
}
$canChange = trim((string) ($filters['can_change_entity'] ?? ''));
return $canChange === '' || (string) ($row['can_change_entity'] ?? '') === $canChange;
}
/** @param array<int,array<string,mixed>> $rows @param array<string,string> $filters */
private function sortSubjectRows(array &$rows, array $filters): void
{
$key = (string) ($filters['_sort'] ?? 'entity');
$allowed = [
'city', 'entity', 'company_type', 'rolling_12m_income',
...array_map(static fn (int $month): string => sprintf('income_%02d', $month), range(1, 12)),
'can_change_entity', 'plan', 'new_entity',
];
if (! in_array($key, $allowed, true)) {
return;
}
$direction = ($filters['_direction'] ?? 'asc') === 'desc' ? -1 : 1;
usort($rows, static function (array $left, array $right) use ($key, $direction): int {
$leftValue = $left[$key] ?? null;
$rightValue = $right[$key] ?? null;
if (is_numeric($leftValue) && is_numeric($rightValue)) {
$comparison = ((float) $leftValue <=> (float) $rightValue) * $direction;
} else {
$comparison = strcmp((string) ($leftValue ?? ''), (string) ($rightValue ?? '')) * $direction;
}
return $comparison !== 0 ? $comparison : strcmp((string) ($left['entity'] ?? ''), (string) ($right['entity'] ?? ''));
});
}
private function sumNullable(array &$row, string $key, mixed $value): void
{
if ($value === null || $value === '') {

View File

@ -118,7 +118,7 @@ SQL)
] as $key => $column) {
$value = trim((string) ($filters[$key] ?? ''));
if ($value !== '') {
$query->where($column, '=', $value);
$query->where($column, 'like', '%' . addcslashes($value, '%_\\') . '%');
}
}

View File

@ -52,10 +52,27 @@ class StoreReportRepository
foreach (['erp_store_name', 'store_name', 'store_id', 'profit_center', 'store_manager', 'department_manager', 'entity', 'company_type'] as $key) {
$rules[$key] = ['nullable', 'string', 'max:191'];
}
foreach (['period_sales', ...array_map(static fn (int $month): string => sprintf('sales_%02d', $month), range(1, 12))] as $key) {
$rules[$key] = ['nullable', 'string', 'max:64'];
}
$rules['status'] = ['nullable', 'string', 'in:0,3,6'];
$rules['can_change'] = ['nullable', 'string', 'in:是,否'];
$sortFields = [
'erp_store_name', 'store_name', 'store_id', 'profit_center', 'store_manager', 'department_manager',
'opened_at', 'status', 'can_change', 'entity', 'company_type', 'effective_at',
'invalid_at', 'period_sales', ...array_map(static fn (int $month): string => sprintf('sales_%02d', $month), range(1, 12)),
];
$rules['sort'] = ['nullable', 'string', 'in:' . implode(',', $sortFields)];
$rules['direction'] = ['nullable', 'string', 'in:asc,desc'];
return validator(request()->only(array_keys($rules)), $rules)->validate();
$filters = validator(request()->only(array_keys($rules)), $rules)->validate();
if (! empty($filters['sort'])) {
$filters['_sort'] = $filters['sort'];
$filters['_direction'] = ($filters['direction'] ?? 'asc') === 'desc' ? 'desc' : 'asc';
}
unset($filters['sort'], $filters['direction']);
return $filters;
}
/**
@ -146,6 +163,47 @@ class StoreReportRepository
}
}
return $this->sortRows($rows, $filters);
}
/** @param array<int,array<string,mixed>> $rows @param array<string,string> $filters */
private function sortRows(array $rows, array $filters): array
{
$key = (string) ($filters['_sort'] ?? '');
if ($key === '') {
return $rows;
}
$allowed = [
'erp_store_name' => 'erp_store_name', 'store_name' => 'store_name', 'store_id' => 'store_id',
'profit_center' => 'profit_center', 'store_manager' => 'store_manager',
'department_manager' => 'department_manager', 'opened_at' => 'opened_at',
'status' => 'status', 'can_change' => 'can_change', 'entity' => 'entity',
'company_type' => 'company_type', 'effective_at' => 'effective_at',
'invalid_at' => 'invalid_at', 'period_sales' => 'period_sales',
...array_combine(
array_map(static fn (int $month): string => sprintf('sales_%02d', $month), range(1, 12)),
array_map(static fn (int $month): string => sprintf('sales_%02d', $month), range(1, 12)),
),
];
$column = $allowed[$key] ?? null;
if ($column === null) {
return $rows;
}
$direction = ($filters['_direction'] ?? 'asc') === 'desc' ? -1 : 1;
usort($rows, static function (array $left, array $right) use ($column, $direction): int {
$leftValue = $left[$column] ?? null;
$rightValue = $right[$column] ?? null;
if (is_numeric($leftValue) && is_numeric($rightValue)) {
$comparison = ((float) $leftValue <=> (float) $rightValue) * $direction;
} else {
$comparison = strcmp((string) ($leftValue ?? ''), (string) ($rightValue ?? '')) * $direction;
}
return $comparison !== 0 ? $comparison : strcmp((string) ($left['store_id'] ?? ''), (string) ($right['store_id'] ?? ''));
});
return $rows;
}
@ -302,7 +360,7 @@ class StoreReportRepository
}
/**
* 生命周期字段的行级过滤:主体、能否变更、负责人、利润中心精确匹配;生效、失效时间左闭右开。
* 生命周期字段的行级过滤:主体、负责人和利润中心按关键词匹配,能否变更按原值匹配;生效、失效时间左闭右开。
*
* 店铺负责人/部门负责人/利润中心展示店铺账号镜像(PostgreSQL)覆盖后的值,
* 与页面筛选保持一致,因此在这里按行值匹配而不是上游镜像 SQL。
@ -312,12 +370,16 @@ class StoreReportRepository
*/
private function matchesFilters(array $row, string $expiresAt, array $filters): bool
{
foreach (['entity', 'can_change', 'store_manager', 'department_manager', 'profit_center'] as $key) {
foreach (['entity', 'store_manager', 'department_manager', 'profit_center'] as $key) {
$expected = trim((string) ($filters[$key] ?? ''));
if ($expected !== '' && trim((string) ($row[$key] ?? '')) !== $expected) {
if ($expected !== '' && ! str_contains(mb_strtolower(trim((string) ($row[$key] ?? ''))), mb_strtolower($expected))) {
return false;
}
}
$canChange = trim((string) ($filters['can_change'] ?? ''));
if ($canChange !== '' && trim((string) ($row['can_change'] ?? '')) !== $canChange) {
return false;
}
foreach (['effective_at' => (string) $row['effective_at'], 'invalid_at' => $expiresAt] as $key => $value) {
$from = trim((string) ($filters[$key . '_from'] ?? ''));
@ -330,6 +392,14 @@ class StoreReportRepository
}
}
foreach (['period_sales', ...array_map(static fn (int $month): string => sprintf('sales_%02d', $month), range(1, 12))] as $key) {
$needle = str_replace(',', '', trim((string) ($filters[$key] ?? '')));
if ($needle !== '' && (! is_numeric($needle) || ! is_numeric($row[$key] ?? null)
|| abs((float) $row[$key] - (float) $needle) > 0.000001)) {
return false;
}
}
return true;
}

View File

@ -23,18 +23,29 @@
$directoryCurrentPage = $directoryIsPaginated ? $directoryRows->currentPage() : 1;
$directoryLastPage = $directoryIsPaginated ? $directoryRows->lastPage() : 1;
$directoryPerPage = $directoryIsPaginated ? $directoryRows->perPage() : 0;
$directoryPageKey = ($section['slug'] ?? '') . '/' . ($page['slug'] ?? '');
// 页面配置在部分导航上下文中会省略父级 slug,优先使用当前路由参数避免功能开关失配。
$directoryRouteSection = request()->route('section') ?: request()->segment(3) ?: ($section['slug'] ?? '');
$directoryRoutePage = request()->route('page') ?: request()->segment(4) ?: ($page['slug'] ?? '');
$directoryPageKey = (string) $directoryRouteSection . '/' . (string) $directoryRoutePage;
$directoryUsesFieldQuery = in_array($directoryPageKey, [
'company-management/basic-information',
'store-management/basic-information',
'trademark-management/information',
'store-management/store-accounts',
'store-management/store-report',
'company-management/subject-operating-report',
'company-management/company-cost-arrangement',
'company-management/personnel-inventory',
], true);
$directoryQueryPageDescriptions = [
'company-management/basic-information' => '公司主体、状态和登记资料的查询与导出',
'store-management/basic-information' => '店铺主体、平台和经营资料的查询与导出',
'trademark-management/information' => '商标主体、状态和登记资料的查询与导出',
'store-management/store-accounts' => '店铺账号资料的查询与安全状态导出',
'store-management/store-report' => '按店铺、主体和月份销量查询经营数据并导出',
'company-management/subject-operating-report' => '按主体和收入月份查询经营汇总并导出',
'company-management/company-cost-arrangement' => '按公司类型和发票类型查询成本安排字段',
'company-management/personnel-inventory' => '按人员、职位和公司类型查询人员盘点字段',
];
@endphp
@ -372,6 +383,12 @@
.directory-sort-link:hover { color: var(--vip-content-accent-dark, #856632); }
.directory-sort-icon { width: 1rem; height: 1rem; color: var(--vip-content-muted, #595959); font-size: 1rem; line-height: 1; }
.directory-sort-link[aria-current="true"] .directory-sort-icon { color: var(--vip-content-accent-dark, #856632); }
.directory-header-menu { position: relative; display: inline-flex; align-items: center; gap: 0.15rem; }
.directory-header-menu summary { display: inline-flex; align-items: center; cursor: pointer; list-style: none; }
.directory-header-menu summary::-webkit-details-marker { display: none; }
.directory-header-menu__panel { position: absolute; top: calc(100% + 0.35rem); left: 0; z-index: 9; display: grid; min-width: 8rem; gap: 0.2rem; padding: 0.35rem; border: 1px solid var(--vip-border, #d8d2c7); border-radius: 0.375rem; background: #fff; box-shadow: 0 4px 8px rgba(23, 23, 23, 0.10); }
.directory-header-menu__panel a, .directory-header-menu__panel button { display: block; width: 100%; padding: 0.35rem 0.5rem; border: 0; border-radius: 0.25rem; background: transparent; color: var(--vip-content-ink, #171717); font: inherit; font-size: 0.6875rem; text-align: left; text-decoration: none; cursor: pointer; white-space: nowrap; }
.directory-header-menu__panel a:hover, .directory-header-menu__panel button:hover, .directory-header-menu__panel a:focus, .directory-header-menu__panel button:focus { background: var(--vip-content-accent-soft, #f7f0de); outline: 0; }
.directory-table tbody tr:hover {
background: #fbf8f0;
@ -484,6 +501,11 @@
<span class="material-icons-outlined" aria-hidden="true">error_outline</span>
<span>{{ $upstreamError }}</span>
</div>
@elseif ($directoryTotal > 0 && in_array($directoryPageKey, ['store-management/store-accounts', 'store-management/store-report', 'company-management/subject-operating-report', 'company-management/company-cost-arrangement', 'company-management/personnel-inventory'], true))
<div class="directory-table-notice" role="status">
<span class="material-icons-outlined" aria-hidden="true">cloud_done</span>
<span>已从共享业务镜像读取 {{ $directoryTotal }} 条数据,当前页面为只读展示。</span>
</div>
@endif
@else
<header class="directory-table-head">
@ -566,6 +588,10 @@
'store-management/basic-information' => ['platform', 'store_status', 'withdrawal_account'],
'trademark-management/information' => ['trademark_status'],
'store-management/store-accounts' => ['account_type', 'password_configured'],
'store-management/store-report' => ['status', 'can_change', 'company_type'],
'company-management/subject-operating-report' => ['company_type', 'can_change_entity'],
'company-management/company-cost-arrangement' => ['company_type', 'invoice_type'],
'company-management/personnel-inventory' => ['company_type'],
default => [],
};
$directoryColumnNames = collect($directoryFields)->pluck('name')->all();
@ -620,6 +646,9 @@
'taxpayer_type' => ['一般纳税人', '小规模纳税人', '个体工商户', '无税务登记', 'CORP'],
'company_status' => ['正常', '已注销'],
'company_type' => ['个体工商户', '个体户', '二级公司', '人力资源公司', '仓储公司', '其他', '合同公司', '品牌公司', '店铺-国标公司', '店铺-国标商标', '店铺-大店公司', '店铺-普通公司', '母公司', '资产型'],
'status' => ['0' => '开店中', '3' => '关店中', '6' => '关店'],
'can_change', 'can_change_entity' => ['是', '否'],
'invoice_type' => ['普票', '专票'],
default => [],
};
@endphp
@ -635,7 +664,7 @@
@endforeach
<div class="directory-query-actions">
<details class="directory-query-picker" data-directory-query-picker>
<summary>勾选字段</summary>
<summary>显示字段</summary>
<div class="directory-query-picker__panel" role="group" aria-label="勾选展示字段">
<span class="directory-query-picker__hint">勾选需要显示的表头,可重复调整。</span>
@foreach ($directoryFields as $field)
@ -664,7 +693,7 @@
</div>
</div>
@endif
@if (($page['slug'] ?? '') === 'store-report')
@if (($page['slug'] ?? '') === 'store-report' && ! $directoryUsesFieldQuery)
<form method="GET" action="{{ request()->url() }}" class="installation-filter-bar" aria-label="店铺维度报表筛选查询栏">
@php
$storeFilterTextFields = [
@ -746,7 +775,7 @@
</div>
</form>
@endif
@if (!in_array(($section['slug'] ?? '') . '/' . ($page['slug'] ?? ''), ['company-management/basic-information', 'store-management/basic-information', 'trademark-management/information', 'store-management/store-accounts'], true))
@if (! $directoryUsesFieldQuery)
<div class="directory-table-tools" data-directory-tools>
<label class="sr-only" for="directory-search-{{ $page['slug'] ?? 'page' }}">搜索当前表格</label>
<input id="directory-search-{{ $page['slug'] ?? 'page' }}" type="search" placeholder="搜索当前页数据..." data-directory-search>
@ -785,10 +814,17 @@
$direction = $isSorted ? request('direction', 'asc') : null;
$nextDirection = $isSorted && $direction === 'asc' ? 'desc' : 'asc';
@endphp
<a class="directory-sort-link" href="{{ request()->fullUrlWithQuery(['sort' => $field['name'], 'direction' => $nextDirection, 'page' => 1]) }}" aria-label="按{{ $field['label'] }}{{ $nextDirection === 'asc' ? '升序' : '降序' }}排列" aria-current="{{ $isSorted ? 'true' : 'false' }}">
<div class="directory-header-menu" data-directory-header-menu="{{ $field['name'] }}">
<span data-directory-header-label>{{ $field['label'] }}</span>
<span class="material-icons-outlined directory-sort-icon" aria-hidden="true">{{ !$isSorted ? 'unfold_more' : ($direction === 'asc' ? 'arrow_upward' : 'arrow_downward') }}</span>
</a>
<details>
<summary aria-label="{{ $field['label'] }}排序和字段操作"><span class="material-icons-outlined directory-sort-icon" aria-hidden="true">{{ !$isSorted ? 'unfold_more' : ($direction === 'asc' ? 'arrow_upward' : 'arrow_downward') }}</span></summary>
<div class="directory-header-menu__panel">
<a href="{{ request()->fullUrlWithQuery(['sort' => $field['name'], 'direction' => 'asc', 'page' => 1]) }}">升序</a>
<a href="{{ request()->fullUrlWithQuery(['sort' => $field['name'], 'direction' => 'desc', 'page' => 1]) }}">降序</a>
<button type="button" data-directory-header-hide="{{ $field['name'] }}">隐藏字段</button>
</div>
</details>
</div>
@else
{{ $field['label'] }}
@endif
@ -943,11 +979,13 @@
input?.addEventListener('blur', () => window.setTimeout(() => { const list = field.querySelector('[data-directory-query-suggestions]'); if (list) list.hidden = true; }, 150));
});
const columnChecks = [...queryBar.querySelectorAll('[data-directory-query-column]')];
const pickerSummary = queryBar.querySelector('[data-directory-query-picker] summary');
const applyVisibleColumns = () => {
const selected = columnChecks.filter((check) => check.checked).map((check) => check.value);
table.querySelectorAll('[data-column-key]').forEach((cell) => {
cell.classList.toggle('is-column-hidden', !selected.includes(cell.dataset.columnKey));
});
if (pickerSummary) pickerSummary.textContent = `显示字段 ${selected.length}/${columnChecks.length}`;
let hiddenColumns = queryBar.querySelector(`input[form="${CSS.escape(queryBar.querySelector('form[id]')?.id || '')}"][name="columns"]`);
if (!hiddenColumns) {
hiddenColumns = document.createElement('input');
@ -963,6 +1001,14 @@
window.history.replaceState({}, '', url);
};
columnChecks.forEach((check) => check.addEventListener('change', applyVisibleColumns));
section.querySelectorAll('[data-directory-header-hide]').forEach((button) => button.addEventListener('click', () => {
const key = button.dataset.directoryHeaderHide;
const check = columnChecks.find((item) => item.value === key);
if (!check) return;
check.checked = false;
check.dispatchEvent(new Event('change', { bubbles: true }));
button.closest('details')?.removeAttribute('open');
}));
queryBar.querySelector('[data-directory-query-reset]')?.addEventListener('click', () => {
const url = new URL(window.location.href);
fields.forEach((field) => {

View File

@ -72,6 +72,10 @@
'title' => '内部流转报表', 'description' => '查看内部流转事项及办理状态',
'columns' => [],
],
'internal-flow-report' => [
'title' => '内部流转报表', 'description' => '查看内部流转事项及办理状态',
'columns' => [],
],
];
$definition = $definitions[$slug] ?? ['title' => $page['title'] ?? '业务页面', 'description' => '', 'columns' => [], 'rows' => []];
@endphp

View File

@ -311,7 +311,7 @@ class ManagementCrudTest extends FeatureTestCase
->assertSeeText('既有只读上游')
->assertSee('aria-label="公司基础信息查询条件"', false)
->assertSeeText('相关人员')
->assertSeeText('勾选字段')
->assertSeeText('显示字段')
->assertSeeText('导出 Excel')
->assertSee('data-directory-query-export-field', false)
->assertDontSee('<div class="directory-table-tools" data-directory-tools>', false)
@ -560,10 +560,10 @@ class ManagementCrudTest extends FeatureTestCase
'page' => 'store-report',
]))
->assertOk()
->assertSee('data-directory-field-picker', false)
->assertSee('data-directory-export="visible"', false)
->assertSee('data-directory-export-all="all-fields"', false)
->assertSee('data-directory-reset', false)
->assertSee('data-directory-query-picker', false)
->assertSee('data-directory-query-export', false)
->assertSeeText('显示字段')
->assertSeeText('导出 Excel')
->assertSee('position: sticky', false)
->assertSee('overflow: auto', false);
}

View File

@ -104,6 +104,7 @@ class PrototypePageTest extends FeatureTestCase
->assertSeeText('测试主体')
->assertSeeText('共 1 条')
->assertSeeText('已从共享业务镜像读取')
->assertDontSee('<div class="directory-table-tools" data-directory-tools>', false)
->assertDontSeeText('经营报表尚未同步。完成店铺主体和销量镜像接入后')
->assertDontSeeText('经营报表共享镜像尚未完成同步。');
}

View File

@ -112,6 +112,20 @@ class StoreReportRepositoryTest extends TestCase
$this->assertSame('公司表主体', $rows[0]['entity']);
$this->assertSame('2026-07-01', $rows[0]['effective_at']);
$this->assertSame('存续中', $rows[0]['invalid_at']);
$this->assertCount(1, $this->repository->rows([['erp_store_name' => '店铺A']], 2026, ['sales_07' => '30.00']));
$this->assertCount(0, $this->repository->rows([['erp_store_name' => '店铺A']], 2026, ['sales_07' => '130']));
}
public function test_store_name_sort_is_accepted_and_applied(): void
{
request()->merge(['sort' => 'store_name', 'direction' => 'asc']);
$filters = StoreReportRepository::requestFilters();
$rows = $this->repository->rows([
['erp_store_name' => '店铺A', 'store_name' => 'Z店'],
['erp_store_name' => '店铺B', 'store_name' => 'A店'],
], 2026, $filters);
$this->assertSame(['A店', 'Z店'], array_column($rows, 'store_name'));
}
public function test_lifecycle_effective_date_before_default_is_preserved(): void
@ -178,7 +192,7 @@ class StoreReportRepositoryTest extends TestCase
$this->insertBill('店铺B', '2026-08-11 10:00:00', 40);
$mirror = Mockery::mock(BusinessInformationMirrorRepository::class);
$mirror->shouldReceive('ready')->with('store-information.read')->andReturnTrue();
$mirror->shouldReceive('allStores')->once()->andReturn([
$mirror->shouldReceive('allStores')->times(3)->andReturn([
['erp_store_name' => '店铺A', 'company_name' => '主体甲', 'store_status' => '0'],
['erp_store_name' => '店铺B', 'company_name' => '主体甲', 'store_status' => '0'],
]);
@ -202,6 +216,11 @@ class StoreReportRepositoryTest extends TestCase
$this->assertSame(10.0, $rows[0]->income_06);
$this->assertSame(20.0, $rows[0]->income_07);
$this->assertSame(70.0, $rows[0]->income_08);
request()->merge(['income_08' => '70.00']);
$this->assertSame(1, $service->paginate('company-management', 'subject-operating-report', 50)->total());
request()->merge(['income_08' => '71.00']);
$this->assertSame(0, $service->paginate('company-management', 'subject-operating-report', 50)->total());
}
private function insertBill(string $store, string $occurredAt, float $income): void