341 lines
14 KiB
PHP
341 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Business;
|
|
|
|
use App\Models\Auth\Permission;
|
|
use App\Models\Auth\Role;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Modules\EcommerceFinance\Services\FinanceLedgerTransactionWriter;
|
|
use Tests\Feature\FeatureTestCase;
|
|
|
|
class FinanceLedgerManualTransactionTest extends FeatureTestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
$this->user->attachPermission(
|
|
Permission::where('name', 'read-admin-panel')->firstOrFail()
|
|
);
|
|
|
|
DB::connection('business_mirror')->table('fin_accounts')->insert([
|
|
[
|
|
'company_name' => '手工测试公司甲',
|
|
'account_short' => '手工账户甲',
|
|
'account_type' => '公户',
|
|
'current_balance' => 100,
|
|
],
|
|
[
|
|
'company_name' => '手工测试公司乙',
|
|
'account_short' => '手工账户乙',
|
|
'account_type' => '个人卡',
|
|
'current_balance' => 0,
|
|
],
|
|
]);
|
|
DB::connection('business_mirror')->table('fin_account_opening')->insert([
|
|
'account_short' => '手工账户甲',
|
|
'opening_date' => '2026-10-17',
|
|
'opening_balance' => 100,
|
|
]);
|
|
}
|
|
|
|
private function pageUrl(): string
|
|
{
|
|
return route('business.pages.show', [
|
|
'company_id' => $this->company->id,
|
|
'section' => 'financial-operations',
|
|
'page' => 'ledger',
|
|
]) . '?tab=transactions';
|
|
}
|
|
|
|
private function storeUrl(): string
|
|
{
|
|
return route('business.financial-ledger.transactions.manual', [
|
|
'company_id' => $this->company->id,
|
|
]);
|
|
}
|
|
|
|
private function transactionAccountUrl(int $transactionId): string
|
|
{
|
|
return route('business.financial-ledger.transactions.account.update', [
|
|
'company_id' => $this->company->id,
|
|
'id' => $transactionId,
|
|
]);
|
|
}
|
|
|
|
private function payload(array $overrides = []): array
|
|
{
|
|
return array_merge([
|
|
'idempotency_key' => '72901f47-0557-45f8-8519-8a51cc0efb52',
|
|
'txn_date' => '2026-10-18',
|
|
'account_short' => '手工账户甲',
|
|
'detail' => '手工补录测试',
|
|
'income' => 25.50,
|
|
'expense' => 0,
|
|
'approval_no' => '',
|
|
], $overrides);
|
|
}
|
|
|
|
public function test_transaction_page_exposes_account_master_picker_and_inline_add_row(): void
|
|
{
|
|
$this->loginAs()->get($this->pageUrl())
|
|
->assertOk()
|
|
->assertSee('新增手工明细')
|
|
->assertSee('data-ledger-manual-trigger', false)
|
|
->assertSee('data-ledger-manual-draft', false)
|
|
->assertSee('data-ledger-manual-date-picker', false)
|
|
->assertSee('calendar_today', false)
|
|
->assertSee('name="account_short"', false)
|
|
->assertSee('list="ledger-manual-account-options"', false)
|
|
->assertSee('data-ledger-manual-account', false)
|
|
->assertSee('输入账户简称', false)
|
|
->assertSee('手工账户甲 · 手工测试公司甲', false)
|
|
->assertSee('name="income"', false)
|
|
->assertSee('name="expense"', false)
|
|
->assertSee('name="approval_no"', false)
|
|
->assertSee('手工录入');
|
|
}
|
|
|
|
public function test_manual_income_is_saved_and_updates_daily_flow_and_current_balance(): void
|
|
{
|
|
$response = $this->loginAs()->postJson($this->storeUrl(), $this->payload())
|
|
->assertCreated()
|
|
->assertJsonPath('row.source', FinanceLedgerTransactionWriter::MANUAL_SOURCE)
|
|
->assertJsonPath('row.account_short', '手工账户甲')
|
|
->assertJsonPath('daily_flow.opening_balance', 100)
|
|
->assertJsonPath('daily_flow.inflow', 25.5)
|
|
->assertJsonPath('daily_flow.closing_balance', 125.5)
|
|
->assertJsonPath('account.current_balance', 125.5)
|
|
->assertJsonPath('summary.count', 1)
|
|
->assertJsonPath('summary.income', 25.5)
|
|
->assertJsonPath('summary.expense', 0)
|
|
->assertJsonPath('total_transactions', 1);
|
|
|
|
$this->assertNotEmpty($response->json('row.approval_no'));
|
|
$this->assertDatabaseHas('fin_transactions', [
|
|
'account_short' => '手工账户甲',
|
|
'source' => FinanceLedgerTransactionWriter::MANUAL_SOURCE,
|
|
'created_by' => 'Test User',
|
|
'updated_by' => 'Test User',
|
|
], 'business_mirror');
|
|
$this->assertDatabaseHas('fin_daily_flows', [
|
|
'flow_date' => '2026-10-18',
|
|
'account_short' => '手工账户甲',
|
|
'company_name' => '手工测试公司甲',
|
|
'opening_balance' => 100,
|
|
'inflow' => 25.5,
|
|
'outflow' => 0,
|
|
'closing_balance' => 125.5,
|
|
], 'business_mirror');
|
|
$this->assertDatabaseHas('fin_accounts', [
|
|
'account_short' => '手工账户甲',
|
|
'current_balance' => 125.5,
|
|
], 'business_mirror');
|
|
}
|
|
|
|
public function test_account_without_opening_gets_zero_flow_and_can_receive_manual_expense(): void
|
|
{
|
|
$this->loginAs()->postJson($this->storeUrl(), $this->payload([
|
|
'idempotency_key' => '0d1066f0-2b23-49b3-a6a0-91bf1f93d1c1',
|
|
'account_short' => '手工账户乙',
|
|
'detail' => '无期初账户手工支出',
|
|
'income' => 0,
|
|
'expense' => 20,
|
|
]))->assertCreated()->assertJsonPath('daily_flow.closing_balance', -20);
|
|
|
|
$connection = DB::connection('business_mirror');
|
|
$this->assertDatabaseHas('fin_daily_flows', [
|
|
'flow_date' => '2026-10-17',
|
|
'account_short' => '手工账户乙',
|
|
'company_name' => '手工测试公司乙',
|
|
'opening_balance' => 0,
|
|
'closing_balance' => 0,
|
|
], 'business_mirror');
|
|
$this->assertDatabaseHas('fin_daily_flows', [
|
|
'flow_date' => '2026-10-18',
|
|
'account_short' => '手工账户乙',
|
|
'opening_balance' => 0,
|
|
'outflow' => 20,
|
|
'closing_balance' => -20,
|
|
], 'business_mirror');
|
|
$this->assertSame(-20.0, (float) $connection->table('fin_accounts')
|
|
->where('account_short', '手工账户乙')
|
|
->value('current_balance'));
|
|
}
|
|
|
|
public function test_manual_submit_is_idempotent_and_survives_upstream_replace(): void
|
|
{
|
|
$this->loginAs()->postJson($this->storeUrl(), $this->payload())->assertCreated();
|
|
$this->loginAs()->postJson($this->storeUrl(), $this->payload())
|
|
->assertOk()
|
|
->assertJsonPath('created', false);
|
|
|
|
$connection = DB::connection('business_mirror');
|
|
$this->assertSame(1, $connection->table('fin_transactions')
|
|
->where('source', FinanceLedgerTransactionWriter::MANUAL_SOURCE)
|
|
->count());
|
|
|
|
app(FinanceLedgerTransactionWriter::class)->replace([
|
|
[
|
|
'txn_date' => '2026-10-19',
|
|
'account_short' => '同步账户',
|
|
'detail' => '上游同步明细',
|
|
'income' => 10,
|
|
'expense' => 0,
|
|
'approval_no' => 'UPSTREAM-1',
|
|
'source' => 'dingtalk',
|
|
'source_value' => 'UPSTREAM-VALUE-1',
|
|
'process_type' => '报销',
|
|
],
|
|
], false);
|
|
|
|
$this->assertDatabaseHas('fin_transactions', [
|
|
'account_short' => '手工账户甲',
|
|
'source' => FinanceLedgerTransactionWriter::MANUAL_SOURCE,
|
|
], 'business_mirror');
|
|
$this->assertDatabaseHas('fin_transactions', [
|
|
'detail' => '上游同步明细',
|
|
'source' => 'dingtalk',
|
|
], 'business_mirror');
|
|
}
|
|
|
|
public function test_manual_submit_returns_summary_for_current_transaction_filters(): void
|
|
{
|
|
$this->loginAs()->postJson(
|
|
$this->storeUrl() . '?txns_account=' . urlencode('不匹配账户'),
|
|
$this->payload()
|
|
)
|
|
->assertCreated()
|
|
->assertJsonPath('summary.count', 0)
|
|
->assertJsonPath('summary.income', 0)
|
|
->assertJsonPath('summary.expense', 0)
|
|
->assertJsonPath('total_transactions', 1);
|
|
}
|
|
|
|
public function test_transaction_row_can_reassign_the_source_account_mapping(): void
|
|
{
|
|
$connection = DB::connection('business_mirror');
|
|
$transactionId = $connection->table('fin_transactions')->insertGetId([
|
|
'txn_date' => '2026-10-18',
|
|
'account_short' => null,
|
|
'detail' => '待修正来源账户',
|
|
'income' => 12,
|
|
'expense' => 0,
|
|
'approval_no' => 'MAP-ROW-1',
|
|
'source' => 'dingtalk',
|
|
'source_value' => 'SOURCE-MAP-ROW-1',
|
|
'process_type' => '报销',
|
|
'row_key' => 'manual-account-map-row-1',
|
|
]);
|
|
$connection->table('fin_account_map')->insert([
|
|
'source' => 'dingtalk',
|
|
'source_value' => 'SOURCE-MAP-ROW-1',
|
|
'account_short' => null,
|
|
'txn_count' => 1,
|
|
'income_sum' => 12,
|
|
'expense_sum' => 0,
|
|
]);
|
|
|
|
$this->loginAs()->get($this->pageUrl())
|
|
->assertOk()
|
|
->assertSee('data-ledger-transaction-account', false)
|
|
->assertSee($this->transactionAccountUrl($transactionId), false);
|
|
|
|
$this->loginAs()->postJson($this->transactionAccountUrl($transactionId), [
|
|
'account_short' => '手工账户乙',
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('changed', true)
|
|
->assertJsonPath('scope', 'mapping')
|
|
->assertJsonPath('account_short', '手工账户乙')
|
|
->assertJsonPath('daily_flow.closing_balance', 12)
|
|
->assertJsonPath('current_balance', 12);
|
|
|
|
$this->assertDatabaseHas('fin_account_map', [
|
|
'source' => 'dingtalk',
|
|
'source_value' => 'SOURCE-MAP-ROW-1',
|
|
'account_short' => '手工账户乙',
|
|
'updated_by' => 'Test User',
|
|
], 'business_mirror');
|
|
$this->assertDatabaseHas('fin_account_map_events', [
|
|
'source' => 'dingtalk',
|
|
'source_value' => 'SOURCE-MAP-ROW-1',
|
|
'old_account_short' => null,
|
|
'new_account_short' => '手工账户乙',
|
|
'user_name' => 'Test User',
|
|
], 'business_mirror');
|
|
$this->assertDatabaseHas('fin_daily_flows', [
|
|
'flow_date' => '2026-10-18',
|
|
'account_short' => '手工账户乙',
|
|
'inflow' => 12,
|
|
'closing_balance' => 12,
|
|
], 'business_mirror');
|
|
}
|
|
|
|
public function test_manual_transaction_row_can_change_its_own_account(): void
|
|
{
|
|
$this->loginAs()->postJson($this->storeUrl(), $this->payload())->assertCreated();
|
|
$transactionId = (int) DB::connection('business_mirror')->table('fin_transactions')
|
|
->where('source', FinanceLedgerTransactionWriter::MANUAL_SOURCE)
|
|
->value('id');
|
|
|
|
$this->loginAs()->postJson($this->transactionAccountUrl($transactionId), [
|
|
'account_short' => '手工账户乙',
|
|
])
|
|
->assertOk()
|
|
->assertJsonPath('changed', true)
|
|
->assertJsonPath('scope', 'transaction')
|
|
->assertJsonPath('account_short', '手工账户乙')
|
|
->assertJsonPath('daily_flow.closing_balance', 25.5)
|
|
->assertJsonPath('current_balance', 25.5);
|
|
|
|
$this->assertDatabaseHas('fin_transactions', [
|
|
'id' => $transactionId,
|
|
'source' => FinanceLedgerTransactionWriter::MANUAL_SOURCE,
|
|
'account_short' => '手工账户乙',
|
|
'updated_by' => 'Test User',
|
|
], 'business_mirror');
|
|
$this->assertDatabaseHas('fin_daily_flows', [
|
|
'flow_date' => '2026-10-18',
|
|
'account_short' => '手工账户乙',
|
|
'inflow' => 25.5,
|
|
'closing_balance' => 25.5,
|
|
], 'business_mirror');
|
|
}
|
|
|
|
public function test_manual_submit_validates_amount_account_date_and_permission(): void
|
|
{
|
|
$this->withExceptionHandling()->loginAs()->postJson($this->storeUrl(), $this->payload([
|
|
'income' => 10,
|
|
'expense' => 5,
|
|
]))->assertUnprocessable()->assertJsonValidationErrors('income');
|
|
|
|
$this->withExceptionHandling()->loginAs()->postJson($this->storeUrl(), $this->payload([
|
|
'idempotency_key' => '3f25a128-f733-4b99-883c-9d5d6ec2639f',
|
|
'account_short' => '不存在的账户',
|
|
]))->assertStatus(422);
|
|
|
|
$this->withExceptionHandling()->loginAs()->postJson($this->storeUrl(), $this->payload([
|
|
'idempotency_key' => '1b035dae-f7b5-4b01-aac7-12cb4f27ab37',
|
|
'txn_date' => '2026-10-16',
|
|
]))->assertUnprocessable()->assertJsonValidationErrors('txn_date');
|
|
|
|
$viewer = user_model_class()::create([
|
|
'name' => '手工明细只读用户',
|
|
'email' => 'manual-viewer-' . uniqid() . '@example.com',
|
|
'password' => 'local-test-password',
|
|
'locale' => 'zh-CN',
|
|
'enabled' => 1,
|
|
'landing_page' => 'dashboard',
|
|
'created_from' => 'core::test',
|
|
]);
|
|
$viewer->companies()->attach($this->company->id);
|
|
$viewer->roles()->attach(Role::where('name', 'user')->firstOrFail()->id);
|
|
$viewer->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
|
|
|
$this->withExceptionHandling()->loginAs($viewer)->postJson($this->storeUrl(), $this->payload([
|
|
'idempotency_key' => '9b31ee32-9a72-4d8f-a144-e2ca9ca5ecdc',
|
|
]))->assertForbidden();
|
|
}
|
|
}
|