212 lines
7.9 KiB
PHP
212 lines
7.9 KiB
PHP
<?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('科目映射表')
|
||
// 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」。');
|
||
}
|
||
}
|