CaiWuXiTong/tests/Feature/Business/WithdrawalManagementImportExportTest.php
92451 0b69f30714 feat: 补录财务账单店铺余额并标记来源
聚水潭缺失店铺按财务账单补录;页面和导出标记数据来源。

PR #552
Co-authored-by: 92451 <924518135@qq.com>
Co-committed-by: 92451 <924518135@qq.com>
2026-10-25 17:06:19 +08:00

749 lines
33 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use App\Models\Business\StoreReceivingAccount;
use App\Business\WithdrawalManagementReport;
use Illuminate\Http\UploadedFile;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
use Mockery;
use Modules\EcommerceFinance\Repositories\JushuitanShopBalanceRepository;
use PhpOffice\PhpSpreadsheet\Cell\DataType;
use PhpOffice\PhpSpreadsheet\IOFactory;
use PhpOffice\PhpSpreadsheet\Spreadsheet;
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
use Tests\Feature\FeatureTestCase;
class WithdrawalManagementImportExportTest extends FeatureTestCase
{
private const HEADERS = [
'店铺平台',
'ERP店铺名称',
'平台店铺ID',
'店铺主体',
'提现路径',
'提现账户名称',
'银行卡号(后四位)',
];
private const IMPORT_HEADERS = [...self::HEADERS, '提现金额'];
private const WITHDRAWAL_TEMPLATE_HEADERS = [
'店铺平台',
'ERP店铺名称',
'平台店铺ID',
'余额日期',
'店铺余额',
'预留标准',
'提现金额',
'提现后余额',
'是否达标',
];
/** @var list<string> */
private array $temporaryFiles = [];
protected function setUp(): void
{
parent::setUp();
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail(),
);
$this->withoutMiddleware(ThrottleRequests::class);
}
public function test_page_exposes_import_and_filtered_export_actions(): void
{
StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '天猫',
'erp_store_name' => '操作按钮测试店',
'store_entity_name' => '按钮测试主体',
]);
$this->loginAs()->get(route('business.pages.show', [
'section' => 'fund-management',
'page' => 'withdrawal-management',
'platform' => '天猫',
]))
->assertOk()
->assertSeeText('导入')
->assertSeeText('导出')
->assertSee('data-withdrawal-import-open', false)
->assertSee('class="withdrawal-management__actions"', false)
->assertSee('data-withdrawal-column-trigger', false)
->assertSee('data-withdrawal-column-toggle', false)
->assertSee('data-withdrawal-sort-asc', false)
->assertSee('data-withdrawal-sort-desc', false)
->assertSee(route('business.withdrawal-management.export', ['platform' => '天猫']), false)
->assertSee(route('business.withdrawal-management.template'), false)
->assertSee(route('business.withdrawal-management.template.withdrawals', ['platform' => '天猫']), false)
->assertSeeText('下载提现金额模板')
->assertSeeText('按大笔提现处理');
}
public function test_page_sorts_all_filtered_rows_before_pagination(): void
{
foreach (['B排序测试店', 'A排序测试店'] as $name) {
StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '排序测试平台',
'erp_store_name' => $name,
'store_entity_name' => '排序测试主体',
]);
}
$ascending = $this->loginAs()->get(route('business.pages.show', [
'section' => 'fund-management',
'page' => 'withdrawal-management',
'platform' => '排序测试平台',
'sort' => 'erp_store_name',
'direction' => 'asc',
]));
$descending = $this->get(route('business.pages.show', [
'section' => 'fund-management',
'page' => 'withdrawal-management',
'platform' => '排序测试平台',
'sort' => 'erp_store_name',
'direction' => 'desc',
]));
$ascending
->assertOk()
->assertSeeInOrder(['A排序测试店', 'B排序测试店'])
->assertSee('name="sort" value="erp_store_name"', false)
->assertSee('name="direction" value="asc"', false)
->assertSee('aria-sort="ascending"', false);
$descending
->assertOk()
->assertSeeInOrder(['B排序测试店', 'A排序测试店'])
->assertSee('aria-sort="descending"', false);
}
public function test_export_contains_all_filtered_rows_and_excludes_passwords(): void
{
StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '天猫',
'erp_store_name' => '导出目标店',
'store_id' => '00001234',
'store_entity_name' => '导出主体',
'login_password' => 'never-export-this-password',
'withdrawal_path' => '支付宝',
'withdrawal_account_name' => '目标账户',
'bank_card_last_four' => '0098',
]);
StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '京东',
'erp_store_name' => '不应导出的店铺',
'store_entity_name' => '其他主体',
]);
$response = $this->loginAs()->get(route('business.withdrawal-management.export', [
'platform' => '天猫',
]));
$response->assertOk()->assertDownload();
$path = $this->temporaryPath('withdrawal-export-');
file_put_contents($path, $response->streamedContent());
$spreadsheet = IOFactory::load($path);
try {
$sheet = $spreadsheet->getActiveSheet();
$this->assertSame([
...self::HEADERS,
now()->subDays(3)->format('n月j日') . '店铺余额',
now()->subDays(2)->format('n月j日') . '店铺余额',
now()->subDay()->format('n月j日') . '店铺余额',
'余额来源',
'余额预警',
'预留标准',
'提现金额',
'是否达标',
], $sheet->rangeToArray('A1:O1')[0]);
$this->assertSame(2, $sheet->getHighestDataRow());
$this->assertSame('导出目标店', $sheet->getCell('B2')->getValue());
$this->assertSame('00001234', $sheet->getCell('C2')->getValue());
$this->assertSame(DataType::TYPE_STRING, $sheet->getCell('C2')->getDataType());
$this->assertSame('0098', $sheet->getCell('G2')->getValue());
$this->assertSame(DataType::TYPE_STRING, $sheet->getCell('G2')->getDataType());
$values = array_merge(...$sheet->toArray(null, false, false, false));
$this->assertNotContains('不应导出的店铺', $values);
$this->assertNotContains('never-export-this-password', $values);
} finally {
$spreadsheet->disconnectWorksheets();
}
}
public function test_template_has_import_headers_and_instructions(): void
{
$response = $this->loginAs()->get(route('business.withdrawal-management.template'));
$response->assertOk()->assertDownload('withdrawal_management_import_template.xlsx');
$path = $this->temporaryPath('withdrawal-template-');
file_put_contents($path, $response->streamedContent());
$spreadsheet = IOFactory::load($path);
try {
$this->assertSame(self::IMPORT_HEADERS, $spreadsheet->getSheet(0)->rangeToArray('A1:H1')[0]);
$this->assertSame('填写说明', $spreadsheet->getSheet(1)->getTitle());
$this->assertStringContainsString('[清空]', (string) $spreadsheet->getSheet(1)->getCell('A4')->getValue());
} finally {
$spreadsheet->disconnectWorksheets();
}
}
public function test_withdrawal_amount_template_prefills_current_filtered_store_and_balance(): void
{
Carbon::setTestNow('2026-09-24 09:00:00');
try {
StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '天猫',
'erp_store_name' => '金额模板目标店',
'store_id' => 'TM-AMOUNT-TEMPLATE',
'store_entity_name' => '模板测试主体',
]);
$this->insertBalanceSnapshot('2026-09-23', '2026-09-24 08:00:00', '金额模板目标店', 25000);
$response = $this->loginAs()->get(route('business.withdrawal-management.template.withdrawals', [
'platform' => '天猫',
'keyword' => '金额模板目标店',
]));
$response->assertOk()->assertDownload();
$path = $this->temporaryPath('withdrawal-amount-template-');
file_put_contents($path, $response->streamedContent());
$spreadsheet = IOFactory::load($path);
try {
$sheet = $spreadsheet->getSheet(0);
$this->assertSame('提现金额', $sheet->getTitle());
$this->assertSame(self::WITHDRAWAL_TEMPLATE_HEADERS, $sheet->rangeToArray('A1:I1')[0]);
$this->assertSame('天猫', $sheet->getCell('A2')->getValue());
$this->assertSame('金额模板目标店', $sheet->getCell('B2')->getValue());
$this->assertSame('TM-AMOUNT-TEMPLATE', $sheet->getCell('C2')->getValue());
$this->assertSame('2026-09-23', $sheet->getCell('D2')->getValue());
$this->assertSame(25000.0, (float) $sheet->getCell('E2')->getValue());
$this->assertSame(20000.0, (float) $sheet->getCell('F2')->getValue());
$this->assertNull($sheet->getCell('G2')->getValue());
$this->assertStringContainsString('E2-G2', (string) $sheet->getCell('H2')->getValue());
$this->assertStringContainsString('H2<=F2', (string) $sheet->getCell('I2')->getValue());
$this->assertSame(2, $sheet->getHighestDataRow());
$this->assertStringContainsString('大笔提现', (string) $spreadsheet->getSheetByName('填写说明')?->getCell('A5')->getValue());
} finally {
$spreadsheet->disconnectWorksheets();
}
} finally {
Carbon::setTestNow();
}
}
public function test_withdrawal_amount_template_imports_only_filled_amounts_and_previews_compliance(): void
{
Carbon::setTestNow('2026-09-24 09:00:00');
try {
$qualified = StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '天猫',
'erp_store_name' => '大笔达标店',
'store_id' => 'TM-LARGE-YES',
'store_entity_name' => '模板测试主体',
]);
$unqualified = StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '天猫',
'erp_store_name' => '大笔未达标店',
'store_id' => 'TM-LARGE-NO',
'store_entity_name' => '模板测试主体',
]);
$this->insertBalanceSnapshot('2026-09-23', '2026-09-24 08:00:00', '大笔达标店', 25000);
$this->insertBalanceSnapshot('2026-09-23', '2026-09-24 08:01:00', '大笔未达标店', 25000);
$this->loginAs();
$previewResponse = $this->post(route('business.withdrawal-management.import.preview'), [
'import' => $this->withdrawalWorkbook([
['天猫', '大笔达标店', 'TM-LARGE-YES', '2026-09-23', 25000, 20000, '5000.00'],
['天猫', '大笔未达标店', 'TM-LARGE-NO', '2026-09-23', 25000, 20000, '4999.99'],
]),
]);
$previewResponse->assertSessionHas('withdrawalImportPreview', function (array $preview): bool {
return $preview['total'] === 2
&& $preview['changed'] === 2
&& $preview['preview'][0]['withdrawal_amount'] === 5000.0
&& $preview['preview'][0]['post_withdrawal_balance'] === 20000.0
&& $preview['preview'][0]['compliance'] === 'yes'
&& $preview['preview'][1]['withdrawal_amount'] === 4999.99
&& $preview['preview'][1]['post_withdrawal_balance'] === 20000.01
&& $preview['preview'][1]['compliance'] === 'no';
});
$preview = $previewResponse->getSession()->get('withdrawalImportPreview');
$this->get(route('business.pages.show', [
'section' => 'fund-management',
'page' => 'withdrawal-management',
]))
->assertOk()
->assertSeeText('提现后余额')
->assertSeeText('是否达标(大笔)')
->assertSeeText('是(大笔)');
$this->post(route('business.withdrawal-management.import.commit'), [
'preview_token' => $preview['token'],
])->assertSessionHasNoErrors();
$this->assertSame('5000.00', $qualified->fresh()->withdrawal_amount);
$this->assertSame('2026-09-23', $qualified->fresh()->withdrawal_balance_date);
$this->assertSame('4999.99', $unqualified->fresh()->withdrawal_amount);
$this->assertSame('2026-09-23', $unqualified->fresh()->withdrawal_balance_date);
} finally {
Carbon::setTestNow();
}
}
public function test_withdrawal_amount_template_rejects_a_stale_balance_date(): void
{
Carbon::setTestNow('2026-09-24 09:00:00');
try {
$store = StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '京东',
'erp_store_name' => '过期金额模板店',
'store_id' => 'JD-STALE-AMOUNT',
'store_entity_name' => '模板测试主体',
]);
$this->loginAs();
$this->withExceptionHandling()
->post(route('business.withdrawal-management.import.preview'), [
'import' => $this->withdrawalWorkbook([
['京东', '过期金额模板店', 'JD-STALE-AMOUNT', '2026-09-22', 10000, 5000, '5000.00'],
]),
])
->assertSessionHasErrors('import');
$this->assertNull($store->fresh()->withdrawal_amount);
} finally {
Carbon::setTestNow();
}
}
public function test_valid_import_is_previewed_before_allowed_fields_are_committed(): void
{
$store = StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '天猫',
'erp_store_name' => '导入更新店',
'store_id' => 'TM-IMPORT-1',
'store_entity_name' => '原主体',
'login_password' => 'password-remains-private',
'withdrawal_path' => '银行卡',
'withdrawal_account_name' => '原账户',
'bank_card_last_four' => '1111',
]);
$this->loginAs();
$file = $this->workbook([[
'天猫', '导入更新店', 'TM-IMPORT-1', '新主体', '支付宝', '新账户', '0022',
]]);
$previewResponse = $this->post(route('business.withdrawal-management.import.preview', [
'platform' => '天猫',
]), ['import' => $file]);
$previewResponse
->assertRedirect(route('business.pages.show', [
'section' => 'fund-management',
'page' => 'withdrawal-management',
'platform' => '天猫',
]))
->assertSessionHas('withdrawalImportPreview', function (array $preview): bool {
return $preview['total'] === 1
&& $preview['changed'] === 1
&& $preview['unchanged'] === 0
&& $preview['preview'][0]['changes'] === '店铺主体、提现路径、提现账户名称、银行卡号后四位';
});
$this->assertSame('原主体', $store->fresh()->store_entity_name);
$preview = $previewResponse->getSession()->get('withdrawalImportPreview');
$this->post(route('business.withdrawal-management.import.commit', [
'platform' => '天猫',
]), ['preview_token' => $preview['token']])
->assertRedirect()
->assertSessionHas('success', '提现配置导入完成,共更新 1 家店铺。');
$store = $store->fresh();
$this->assertSame('新主体', $store->store_entity_name);
$this->assertSame('支付宝', $store->withdrawal_path);
$this->assertSame('新账户', $store->withdrawal_account_name);
$this->assertSame('0022', $store->bank_card_last_four);
$this->assertSame('password-remains-private', $store->login_password);
}
public function test_invalid_bank_card_rejects_the_entire_preview_without_writes(): void
{
$store = StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '京东',
'erp_store_name' => '非法卡号测试店',
'store_id' => 'JD-INVALID-1',
'store_entity_name' => '原主体',
'withdrawal_path' => '银行卡',
]);
$this->loginAs();
$this->withExceptionHandling()
->post(route('business.withdrawal-management.import.preview'), ['import' => $this->workbook([[
'京东', '非法卡号测试店', 'JD-INVALID-1', '不应写入的主体', '', '', '12ab',
]])])
->assertRedirect()
->assertSessionHasErrors('import');
$this->assertSame('原主体', $store->fresh()->store_entity_name);
}
public function test_clear_marker_clears_nullable_and_required_configuration_fields(): void
{
$store = StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '拼多多',
'erp_store_name' => '清空配置测试店',
'store_id' => 'PDD-CLEAR-1',
'store_entity_name' => '待清空主体',
'withdrawal_path' => '银行卡',
'withdrawal_account_name' => '待清空账户',
'bank_card_last_four' => '8899',
]);
$this->loginAs();
$previewResponse = $this->post(route('business.withdrawal-management.import.preview'), [
'import' => $this->workbook([[
'拼多多', '清空配置测试店', 'PDD-CLEAR-1', '[清空]', '[清空]', '[清空]', '[清空]',
]]),
]);
$preview = $previewResponse->getSession()->get('withdrawalImportPreview');
$this->post(route('business.withdrawal-management.import.commit'), [
'preview_token' => $preview['token'],
])->assertRedirect()->assertSessionHasNoErrors();
$store = $store->fresh();
$this->assertSame('', $store->getRawOriginal('store_entity_name'));
$this->assertSame('', $store->getRawOriginal('receiving_method'));
$this->assertNull($store->withdrawal_account_name);
$this->assertNull($store->bank_card_last_four);
}
public function test_broken_workbook_is_reported_as_an_import_error(): void
{
$this->loginAs();
$this->withExceptionHandling()
->post(route('business.withdrawal-management.import.preview'), [
'import' => UploadedFile::fake()->createWithContent('损坏文件.xlsx', "PK\x03\x04broken"),
])
->assertRedirect()
->assertSessionHasErrors([
'import' => 'Excel 文件无法读取,请确认文件未损坏并使用最新模板。',
]);
}
public function test_commit_rejects_a_preview_when_store_changed_after_preview(): void
{
$store = StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '抖音',
'erp_store_name' => '并发修改测试店',
'store_id' => 'DY-STALE-1',
'store_entity_name' => '预览前主体',
]);
$this->loginAs();
$previewResponse = $this->post(route('business.withdrawal-management.import.preview'), [
'import' => $this->workbook([[
'抖音', '并发修改测试店', 'DY-STALE-1', '导入主体', '', '', '',
]]),
]);
$preview = $previewResponse->getSession()->get('withdrawalImportPreview');
DB::table('business_store_receiving_accounts')->where('id', $store->id)->update([
'store_entity_name' => '他人已修改主体',
'updated_at' => now()->addMinute(),
]);
$this->withExceptionHandling()
->post(route('business.withdrawal-management.import.commit'), [
'preview_token' => $preview['token'],
])
->assertRedirect()
->assertSessionHasErrors('import');
$this->assertSame('他人已修改主体', $store->fresh()->store_entity_name);
}
public function test_compliance_uses_balance_minus_current_date_withdrawal_and_matches_export(): void
{
Carbon::setTestNow('2026-09-24 09:00:00');
try {
$stores = [];
foreach ([
['达标测试店', '5000.00', '2026-09-23'],
['未达标测试店', '4999.99', '2026-09-23'],
['过期金额测试店', '5000.00', '2026-09-22'],
['缺少金额测试店', null, null],
['缺少余额测试店', '5000.00', '2026-09-23'],
] as [$name, $amount, $date]) {
$stores[$name] = StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '京东',
'erp_store_name' => $name,
'store_entity_name' => '测试主体',
'withdrawal_amount' => $amount,
'withdrawal_balance_date' => $date,
]);
}
$stores['缺少标准测试店'] = StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '唯品会',
'erp_store_name' => '缺少标准测试店',
'store_entity_name' => '测试主体',
'withdrawal_amount' => '5000.00',
'withdrawal_balance_date' => '2026-09-23',
]);
$balances = [];
foreach ($stores as $store) {
$balances[(int) $store->id] = [
'status' => $store->erp_store_name === '缺少余额测试店' ? 'unmatched' : 'matched',
'balance' => $store->erp_store_name === '缺少余额测试店' ? null : 10000.00,
'source_store_name' => $store->erp_store_name,
'source_platform' => '京东',
];
}
$mirror = Mockery::mock(JushuitanShopBalanceRepository::class);
$mirror->shouldReceive('latestBalanceDate')->andReturn('2026-09-23');
$mirror->shouldReceive('availableDates')->andReturn(['2026-09-23', '2026-09-22']);
$historyDates = ['2026-09-21', '2026-09-22', '2026-09-23'];
$balancesByDate = [];
foreach (array_keys($balances) as $storeId) {
foreach ($historyDates as $date) {
$balancesByDate[$storeId][$date] = $balances[$storeId];
}
}
$mirror->shouldReceive('matchesForStoresOnDates')->andReturn($balancesByDate);
$mirror->shouldReceive('countForDate')->andReturn(6);
$this->app->instance(JushuitanShopBalanceRepository::class, $mirror);
$report = $this->app->make(WithdrawalManagementReport::class)->results(request());
$this->assertSame('yes', $report['compliance'][(int) $stores['达标测试店']->id]);
$this->assertSame('no', $report['compliance'][(int) $stores['未达标测试店']->id]);
$this->assertNull($report['compliance'][(int) $stores['过期金额测试店']->id]);
$this->assertNull($report['compliance'][(int) $stores['缺少金额测试店']->id]);
$this->assertNull($report['compliance'][(int) $stores['缺少余额测试店']->id]);
$this->assertNull($report['compliance'][(int) $stores['缺少标准测试店']->id]);
$page = $this->loginAs()->get(route('business.pages.show', [
'section' => 'fund-management',
'page' => 'withdrawal-management',
'platform' => '京东',
]));
$page->assertOk()
->assertSeeText('待核验');
$this->assertMatchesRegularExpression('/data-withdrawal-column="compliance">\s*<span class="biz-badge biz-badge-yes">是<\/span>/', $page->getContent());
$this->assertMatchesRegularExpression('/data-withdrawal-column="compliance">\s*<span class="biz-badge biz-badge-no">否<\/span>/', $page->getContent());
$response = $this->get(route('business.withdrawal-management.export', ['platform' => '京东']));
$path = $this->temporaryPath('withdrawal-compliance-export-');
file_put_contents($path, $response->streamedContent());
$spreadsheet = IOFactory::load($path);
try {
$sheet = $spreadsheet->getActiveSheet();
$values = [];
for ($row = 2; $row <= $sheet->getHighestDataRow(); $row++) {
$values[(string) $sheet->getCell("B{$row}")->getValue()] = [
$sheet->getCell("N{$row}")->getValue(),
$sheet->getCell("O{$row}")->getValue(),
];
}
$this->assertSame([5000.0, '是'], $values['达标测试店']);
$this->assertSame([4999.99, '否'], $values['未达标测试店']);
$this->assertSame(['待接入', '待核验'], $values['过期金额测试店']);
$this->assertSame(['待接入', '待核验'], $values['缺少金额测试店']);
$this->assertSame([5000.0, '待核验'], $values['缺少余额测试店']);
} finally {
$spreadsheet->disconnectWorksheets();
}
} finally {
Carbon::setTestNow();
}
}
public function test_withdrawal_amount_import_is_validated_and_bound_to_current_balance_date(): void
{
Carbon::setTestNow('2026-09-24 09:00:00');
try {
$store = StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '京东',
'erp_store_name' => '金额导入测试店',
'store_entity_name' => '测试主体',
]);
$this->loginAs();
$this->withExceptionHandling()
->post(route('business.withdrawal-management.import.preview'), [
'import' => $this->workbook([['京东', '金额导入测试店', '', '', '', '', '', '-1']]),
])
->assertSessionHasErrors('import');
$this->assertNull($store->fresh()->withdrawal_amount);
$previewResponse = $this->post(route('business.withdrawal-management.import.preview'), [
'import' => $this->workbook([['京东', '金额导入测试店', '', '', '', '', '', '5000.00']]),
]);
$previewResponse->assertSessionHas('withdrawalImportPreview', function (array $preview): bool {
return $preview['preview'][0]['changes'] === '提现金额';
});
$preview = $previewResponse->getSession()->get('withdrawalImportPreview');
$this->post(route('business.withdrawal-management.import.commit'), [
'preview_token' => $preview['token'],
])->assertSessionHasNoErrors();
$this->assertSame('5000.00', $store->fresh()->withdrawal_amount);
$this->assertSame('2026-09-23', $store->fresh()->withdrawal_balance_date);
} finally {
Carbon::setTestNow();
}
}
public function test_page_defaults_to_the_latest_balance_and_supports_a_date_filter(): void
{
Carbon::setTestNow('2026-10-20 10:00:00');
try {
StoreReceivingAccount::query()->create([
'company_id' => $this->company->id,
'store_platform' => '天猫',
'erp_store_name' => '五天保留测试店',
'shop_name' => '五天保留测试店',
'store_entity_name' => '五天保留测试主体',
]);
$this->insertBalanceSnapshot('2026-10-15', '2026-10-16 18:31:00', '天猫-五天保留测试店', 12345.67);
$this->insertBalanceSnapshot('2026-10-14', '2026-10-15 18:31:00', '天猫-五天保留测试店', 99999.99);
$response = $this->loginAs()->get(route('business.pages.show', [
'section' => 'fund-management',
'page' => 'withdrawal-management',
'platform' => '天猫',
]));
$historyResponse = $this->loginAs()->get(route('business.pages.show', [
'section' => 'fund-management',
'page' => 'withdrawal-management',
'platform' => '天猫',
'balance_date' => '2026-10-14',
]));
$response->assertOk()
->assertSeeText('10月14日店铺余额')
->assertSeeText('10月15日店铺余额')
->assertSeeText('12,345.67')
->assertSeeText('99,999.99')
->assertSee('余额日期:2026-10-15', false);
$historyResponse->assertOk()
->assertSeeText('10月14日店铺余额')
->assertSeeText('99,999.99')
->assertDontSeeText('12,345.67')
->assertSee('余额日期:2026-10-14', false);
} finally {
Carbon::setTestNow();
}
}
private function insertBalanceSnapshot(
string $balanceDate,
string $importedAt,
string $storeName,
float $balance,
): void {
DB::connection('business_mirror')->table(JushuitanShopBalanceRepository::TABLE)->insert([
'balance_date' => $balanceDate,
'source_scope' => 'all',
'source_platform' => '天猫',
'store_name' => $storeName,
'normalized_store_name' => JushuitanShopBalanceRepository::normalizeStoreName($storeName),
'platform_account_id' => 'feature-' . $balanceDate,
'balance' => $balance,
'source_row_key' => hash('sha256', $balanceDate . '|' . $storeName),
'source_file_name' => 'feature-fixture.json',
'source_sha256' => str_repeat('d', 64),
'imported_at' => Carbon::parse($importedAt),
'created_at' => Carbon::parse($importedAt),
'updated_at' => Carbon::parse($importedAt),
]);
}
/** @param list<list<mixed>> $rows */
private function workbook(array $rows): UploadedFile
{
$path = $this->temporaryPath('withdrawal-import-');
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('提现管理');
$sheet->fromArray(self::IMPORT_HEADERS, null, 'A1');
if ($rows !== []) {
$sheet->fromArray($rows, null, 'A2');
}
(new Xlsx($spreadsheet))->save($path);
$spreadsheet->disconnectWorksheets();
return new UploadedFile(
$path,
'提现管理.xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
null,
true,
);
}
/** @param list<list<mixed>> $rows */
private function withdrawalWorkbook(array $rows): UploadedFile
{
$path = $this->temporaryPath('withdrawal-amount-import-');
$spreadsheet = new Spreadsheet();
$sheet = $spreadsheet->getActiveSheet();
$sheet->setTitle('提现金额');
$sheet->fromArray(self::WITHDRAWAL_TEMPLATE_HEADERS, null, 'A1');
if ($rows !== []) {
$sheet->fromArray($rows, null, 'A2');
}
(new Xlsx($spreadsheet))->save($path);
$spreadsheet->disconnectWorksheets();
return new UploadedFile(
$path,
'提现金额.xlsx',
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
null,
true,
);
}
private function temporaryPath(string $prefix): string
{
$path = tempnam(sys_get_temp_dir(), $prefix);
$this->temporaryFiles[] = $path;
return $path;
}
protected function tearDown(): void
{
foreach ($this->temporaryFiles as $path) {
@unlink($path);
}
parent::tearDown();
}
}