CaiWuXiTong/tests/Feature/Business/AccountMappingPageTest.php

214 lines
8.1 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 Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\File;
use Modules\EcommerceFinance\Services\AccountMappingStore;
use Modules\EcommerceFinance\Services\AccountMappingValidator;
use Tests\Feature\FeatureTestCase;
/**
* 财务规则 · 科目映射表页面。
*
* 注意:这张表的数据落在 storage_path('app/account-mapping'),
* 所以每个用例都把 storage 指到临时目录,绝不污染真实 storage。
* 种子仍取仓库里的 resources/data/account-mapping.json。
*/
class AccountMappingPageTest extends FeatureTestCase
{
private const SEED_TOTAL = 112;
private string $storageRoot = '';
protected function setUp(): void
{
parent::setUp();
$this->storageRoot = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'account-mapping-' . uniqid();
File::makeDirectory($this->storageRoot . '/app', 0777, true, true);
// 只影响 storage_path(),视图编译目录已在启动时解析,不受影响
$this->app->useStoragePath($this->storageRoot);
}
protected function tearDown(): void
{
if ($this->storageRoot !== '' && File::isDirectory($this->storageRoot)) {
File::deleteDirectory($this->storageRoot);
}
parent::tearDown();
}
private function grantAdminPermission(): void
{
$this->user->attachPermission(
Permission::where('name', 'read-admin-panel')->firstOrFail()
);
}
private function pageUrl(): string
{
return route('business.pages.show', [
'company_id' => $this->company->id,
'section' => 'financial-rules',
'page' => 'account-mapping',
]);
}
/** .../actions */
private function actionsBase(): string
{
// 取 rules 路由再砍掉尾巴,等价于控制器里给前端下发端点的方式
return (string) preg_replace('#/rules$#', '', route(
'business.financial-rules.account-mapping.actions.rules',
['company_id' => $this->company->id]
));
}
/** .../actions/rules[/{id}] */
private function rulesUrl(?int $id = null): string
{
return $this->actionsBase() . '/rules' . ($id === null ? '' : '/' . $id);
}
/** .../actions/{preview|import} */
private function actionUrl(string $action): string
{
return $this->actionsBase() . '/' . $action;
}
/**
* 页面必须把种子规则和校验汇总**渲染进 HTML**。
* 只断言 200 是不够的:注入数据为空时页面照样 200,之前就踩过这个坑。
*/
public function test_page_renders_seeded_rules_and_validation_summary(): void
{
$this->grantAdminPermission();
$this->loginAs()->get($this->pageUrl())
->assertOk()
->assertSee('data-account-mapping="true"', false)
->assertSee('共 <b id="amTotal">' . self::SEED_TOTAL . '</b> 条规则', false)
->assertSee('var ROWS = [', false)
->assertSee('"platform":"得物"', false)
->assertSeeText('科目映射表')
// 序号列表头固定为「序号」且宽度 3.5rem:2.5rem 时三位数会被省略号截成「1…」
->assertSee('class="am-index" style="width:3.5rem;">序号</th>', false)
// ENDPOINT 经 @json 输出,斜杠会被转义成 \/,所以只断言变量名
->assertSee('var ENDPOINT =', false);
}
public function test_rules_endpoint_returns_seed_rules_and_summary(): void
{
$this->grantAdminPermission();
$this->loginAs()->getJson($this->rulesUrl())
->assertOk()
->assertJsonCount(self::SEED_TOTAL, 'rules')
->assertJsonPath('summary.total', self::SEED_TOTAL)
->assertJsonStructure(['rules', 'summary', 'options' => ['platforms', 'categories', 'basis', 'invoices']]);
}
public function test_can_create_update_and_delete_a_rule(): void
{
$this->grantAdminPermission();
$payload = ['values' => [
'platform' => '自测平台',
'item' => '自测账单项目',
'memo' => '自测备注',
'invoice' => '不开票',
'category' => '费用',
'dAcct' => '销售费用',
'dSub' => '运费',
'cAcct' => '其他货币资金',
'cSub' => '',
'basis' => '支出基准',
]];
$created = $this->loginAs()->postJson($this->rulesUrl(), $payload);
$created->assertOk()->assertJsonPath('summary.total', self::SEED_TOTAL + 1);
$id = collect($created->json('rules'))->firstWhere('platform', '自测平台')['id'];
$this->assertIsInt($id);
$this->loginAs()->patchJson($this->rulesUrl($id), [
'values' => array_merge($payload['values'], ['memo' => '改过的备注']),
])->assertOk()->assertJsonPath('summary.total', self::SEED_TOTAL + 1);
$store = app(AccountMappingStore::class);
$this->assertSame('改过的备注', $store->find($id)['memo']);
$this->loginAs()->deleteJson($this->rulesUrl($id))
->assertOk()
->assertJsonPath('summary.total', self::SEED_TOTAL);
$this->assertNull(app(AccountMappingStore::class)->find($id));
}
/** 账套里查不到的一级科目必须是「错误」,否则自动生成分录时会静默失败 */
public function test_validator_flags_account_missing_from_chart_of_accounts(): void
{
$store = app(AccountMappingStore::class);
$validator = app(AccountMappingValidator::class);
$bad = $store->create([
'platform' => '自测平台', 'item' => '坏数据',
'dAcct' => '不存在的科目', 'cAcct' => '其他货币资金',
'category' => '费用', 'basis' => '支出基准', 'invoice' => '-',
]);
$issues = $validator->validate($store->all())['issues'][$bad['id']] ?? [];
$this->assertNotEmpty($issues);
$this->assertContains('错误', array_column($issues, 'level'));
}
/** 导入:先预览(不落盘)再确认;两行都是新增 */
public function test_import_preview_and_confirm_adds_new_rules(): void
{
$this->grantAdminPermission();
$csv = "平台,账单项目,财务备注,票据口径,大类,借方科目,借方二级,贷方科目,贷方二级,方向基准\n"
. "自测平台,自测项目甲,备注甲,未开票,费用,销售费用,运费,其他货币资金,,支出基准\n"
. "自测平台,自测项目乙,备注乙,未开票,收入,其他货币资金,,主营业务收入,,收入基准\n";
$preview = $this->loginAs()->post($this->actionUrl('preview'), [
'import' => UploadedFile::fake()->createWithContent('rules.csv', $csv),
], ['Accept' => 'application/json']);
$preview->assertOk()
->assertJsonPath('total', 2)
->assertJsonPath('can_create', 2)
->assertJsonPath('can_overwrite', 0);
// 预览不落盘
$this->assertCount(self::SEED_TOTAL, app(AccountMappingStore::class)->all());
$this->loginAs()->post($this->actionUrl('import'), [
'import' => UploadedFile::fake()->createWithContent('rules.csv', $csv),
], ['Accept' => 'application/json'])
->assertOk()
->assertJsonPath('result.created', 2)
->assertJsonPath('summary.total', self::SEED_TOTAL + 2);
}
/** 非表格扩展名要被挡下,并给出人话提示 */
public function test_import_rejects_non_spreadsheet_extension(): void
{
$this->grantAdminPermission();
// FeatureTestCase 默认关掉了异常处理,这里要验 422 就必须把它打开
$this->withExceptionHandling();
$this->loginAs()->post($this->actionUrl('preview'), [
'import' => UploadedFile::fake()->createWithContent('rules.txt', "a,b\n1,2\n"),
], ['Accept' => 'application/json'])
->assertStatus(422)
->assertJsonPath('errors.import.0', '只支持 xlsx、xls、csv 三种格式,当前是「txt」。');
}
}