diff --git a/app/Business/UpstreamBusinessInformation.php b/app/Business/UpstreamBusinessInformation.php index e5da4d50..c52eb352 100644 --- a/app/Business/UpstreamBusinessInformation.php +++ b/app/Business/UpstreamBusinessInformation.php @@ -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 $row @param array $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> $rows @param array $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 === '') { diff --git a/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php b/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php index 8902d8c6..a315204a 100644 --- a/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/BusinessInformationMirrorRepository.php @@ -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, '%_\\') . '%'); } } diff --git a/modules/EcommerceFinance/Repositories/StoreReportRepository.php b/modules/EcommerceFinance/Repositories/StoreReportRepository.php index 54f313c7..8bd278be 100644 --- a/modules/EcommerceFinance/Repositories/StoreReportRepository.php +++ b/modules/EcommerceFinance/Repositories/StoreReportRepository.php @@ -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> $rows @param array $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; } diff --git a/resources/views/business/pages/partials/directory-table.blade.php b/resources/views/business/pages/partials/directory-table.blade.php index abb0c618..fc9a39b5 100644 --- a/resources/views/business/pages/partials/directory-table.blade.php +++ b/resources/views/business/pages/partials/directory-table.blade.php @@ -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 @@ {{ $upstreamError }} + @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)) +
+ + 已从共享业务镜像读取 {{ $directoryTotal }} 条数据,当前页面为只读展示。 +
@endif @else
@@ -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
- 勾选字段 + 显示字段
勾选需要显示的表头,可重复调整。 @foreach ($directoryFields as $field) @@ -664,7 +693,7 @@
@endif - @if (($page['slug'] ?? '') === 'store-report') + @if (($page['slug'] ?? '') === 'store-report' && ! $directoryUsesFieldQuery)
@php $storeFilterTextFields = [ @@ -746,7 +775,7 @@
@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)
@@ -785,10 +814,17 @@ $direction = $isSorted ? request('direction', 'asc') : null; $nextDirection = $isSorted && $direction === 'asc' ? 'desc' : 'asc'; @endphp - +
{{ $field['label'] }} - - +
+ +
+ 升序 + 降序 + +
+
+
@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) => { diff --git a/resources/views/business/pages/partials/prototype-page.blade.php b/resources/views/business/pages/partials/prototype-page.blade.php index 1ff377f0..a40b3fb4 100644 --- a/resources/views/business/pages/partials/prototype-page.blade.php +++ b/resources/views/business/pages/partials/prototype-page.blade.php @@ -72,6 +72,10 @@ 'title' => '内部流转报表', 'description' => '查看内部流转事项及办理状态', 'columns' => [], ], + 'internal-flow-report' => [ + 'title' => '内部流转报表', 'description' => '查看内部流转事项及办理状态', + 'columns' => [], + ], ]; $definition = $definitions[$slug] ?? ['title' => $page['title'] ?? '业务页面', 'description' => '', 'columns' => [], 'rows' => []]; @endphp diff --git a/tests/Feature/Business/ManagementCrudTest.php b/tests/Feature/Business/ManagementCrudTest.php index 8d85e0f5..199c8b2e 100644 --- a/tests/Feature/Business/ManagementCrudTest.php +++ b/tests/Feature/Business/ManagementCrudTest.php @@ -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('
', 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); } diff --git a/tests/Feature/Business/PrototypePageTest.php b/tests/Feature/Business/PrototypePageTest.php index ef71306b..4e2abd1d 100644 --- a/tests/Feature/Business/PrototypePageTest.php +++ b/tests/Feature/Business/PrototypePageTest.php @@ -104,6 +104,7 @@ class PrototypePageTest extends FeatureTestCase ->assertSeeText('测试主体') ->assertSeeText('共 1 条') ->assertSeeText('已从共享业务镜像读取') + ->assertDontSee('
', false) ->assertDontSeeText('经营报表尚未同步。完成店铺主体和销量镜像接入后') ->assertDontSeeText('经营报表共享镜像尚未完成同步。'); } diff --git a/tests/Unit/EcommerceFinance/StoreReportRepositoryTest.php b/tests/Unit/EcommerceFinance/StoreReportRepositoryTest.php index 2eccad9b..528a4a7a 100644 --- a/tests/Unit/EcommerceFinance/StoreReportRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/StoreReportRepositoryTest.php @@ -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