CaiWuXiTong/tests/Feature/Business/ShopFlowPageTest.php
qiuxiang 78115f38d2 feat: 新增店铺收支明细页
新增店铺收支明细页,支持店铺/账户筛选、余额自检、整卡关联店铺与流式 CSV 导出。
2026-11-03 16:07:13 +08:00

326 lines
13 KiB
PHP

<?php
namespace Tests\Feature\Business;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\View\ShopFlowQuery;
use Tests\Feature\FeatureTestCase;
class ShopFlowPageTest extends FeatureTestCase
{
protected function setUp(): void
{
parent::setUp();
config(['upstream_mirrors.connection' => 'sqlite']);
app('url')->defaults(['company_id' => $this->company->id]);
$this->createFinanceTables();
$this->seedFinanceData();
}
public function test_selected_shop_locks_account_and_builds_balanced_flow_summary(): void
{
$data = app(ShopFlowQuery::class)->pageData(new Request([
'company' => '测试主体',
'shop' => '测试店铺A',
'account' => '其他账户',
'start' => '2026-11-01',
'end' => '2026-11-02',
'direction' => 'all',
'source_type' => 'all',
]));
$this->assertSame('测试账户', $data['shopFlowSelectedAccountShort']);
$this->assertSame('测试店铺A', $data['shopFlowSelectedShop']['shop_name']);
$this->assertCount(2, $data['shopFlowRelatedShops']);
$this->assertSame(4, $data['shopFlowSummary']['income_count']);
$this->assertSame(60.0, $data['shopFlowSummary']['income']);
$this->assertSame(3, $data['shopFlowSummary']['withdraw_count']);
$this->assertSame(1, $data['shopFlowSummary']['pending_count']);
$this->assertSame(1, $data['shopFlowSummary']['expense_count']);
$this->assertSame(25.0, $data['shopFlowSummary']['expense']);
$this->assertSame('matched', $data['shopFlowBalance']['status']);
$this->assertSame(135.0, $data['shopFlowBalance']['closing']);
$this->assertCount(5, $data['shopFlowRows']->items());
$this->assertSame(135.0, $data['shopFlowRows']->items()[0]['balance']);
$this->assertStringContainsString(
'account=' . rawurlencode('测试账户'),
$data['shopFlowUnlockUrl'],
);
$shopRows = collect($data['shopFlowRows']->items())
->where('source', 'shop')
->values();
$matched = $shopRows->firstWhere('match_status', '已匹配提现单');
$pending = $shopRows->firstWhere('match_status', '待匹配提现单');
$this->assertSame('测试店铺A', $matched['shop_name']);
$this->assertSame('', $pending['shop_name']);
}
public function test_detail_filters_do_not_change_period_summary_or_balance_check(): void
{
$data = app(ShopFlowQuery::class)->pageData(new Request([
'account' => '测试账户',
'start' => '2026-11-01',
'end' => '2026-11-02',
'direction' => 'expense',
'source_type' => 'dingtalk',
]));
$this->assertNull($data['shopFlowSelectedShop']);
$this->assertCount(2, $data['shopFlowRelatedShops']);
$this->assertSame(60.0, $data['shopFlowSummary']['income']);
$this->assertSame(25.0, $data['shopFlowSummary']['expense']);
$this->assertCount(1, $data['shopFlowRows']->items());
$this->assertSame('expense_dingtalk', $data['shopFlowRows']->items()[0]['type_key']);
$this->assertSame('matched', $data['shopFlowBalance']['status']);
}
public function test_page_renders_with_export_and_ledger_table_classes(): void
{
$this->loginAs()
->get(route('business.pages.show', [
'section' => 'financial-operations',
'page' => 'shop-flows',
]) . '?shop=' . rawurlencode('测试店铺A')
. '&start=2026-11-01&end=2026-11-02')
->assertOk()
->assertSeeText('店铺收支明细')
->assertSeeText('测试店铺A')
->assertSeeText('¥60.00')
->assertSeeText('查看整张卡')
->assertSee('data-shop-flow-page', false)
->assertSee('data-shop-flow-export', false)
->assertSee('ledger-table', false)
->assertSee('/workspace/financial-operations/shop-flows/shop-flows/export', false);
}
public function test_csv_export_streams_filtered_rows_with_utf8_bom(): void
{
$url = route('business.pages.export', [
'section' => 'financial-operations',
'page' => 'shop-flows',
'tab' => 'shop-flows',
]) . '?' . http_build_query([
'shop' => '测试店铺A',
'start' => '2026-11-01',
'end' => '2026-11-02',
'direction' => 'income',
'source_type' => 'shop',
]);
$request = Request::create($url);
$this->assertSame('测试店铺A', $request->query('shop'));
$this->assertSame(
'测试账户',
app(ShopFlowQuery::class)->pageData($request)['shopFlowSelectedAccountShort'],
);
$directResponse = app(ShopFlowQuery::class)->export($request);
ob_start();
$directResponse->sendContent();
$directContent = ob_get_clean();
$this->assertStringContainsString('测试店铺A', $directContent);
$response = $this->loginAs()->get($url)->assertOk();
$response->assertHeader('content-type', 'text/csv; charset=UTF-8');
$content = $response->streamedContent();
$this->assertStringStartsWith("\xEF\xBB\xBF", $content);
$this->assertStringContainsString('测试店铺A', $content);
$this->assertStringContainsString('已匹配提现单', $content);
$this->assertStringContainsString('待匹配提现单', $content);
$this->assertStringNotContainsString('供应商退款', $content);
$this->assertStringNotContainsString('付款审批', $content);
}
private function createFinanceTables(): void
{
Schema::connection('sqlite')->create('fin_companies', function ($table): void {
$table->increments('id');
$table->string('name');
$table->string('tax_type')->nullable();
$table->string('basic_bank')->nullable();
});
Schema::connection('sqlite')->create('fin_accounts', function ($table): void {
$table->increments('id');
$table->string('company_name');
$table->string('account_short');
$table->string('account_no')->nullable();
$table->string('bank_name')->nullable();
$table->string('account_last4')->nullable();
$table->decimal('current_balance', 18, 2)->default(0);
$table->string('owner')->nullable();
});
Schema::connection('sqlite')->create('fin_shops', function ($table): void {
$table->increments('id');
$table->string('shop_name');
$table->string('platform')->nullable();
$table->string('company_name')->nullable();
$table->string('account_short')->nullable();
$table->string('status')->nullable();
$table->string('payout_account')->nullable();
});
Schema::connection('sqlite')->create('fin_transactions', function ($table): void {
$table->increments('id');
$table->date('txn_date');
$table->string('account_short')->nullable();
$table->text('detail')->nullable();
$table->decimal('income', 18, 2)->default(0);
$table->decimal('expense', 18, 2)->default(0);
$table->string('approval_no')->nullable();
$table->string('source');
$table->string('source_value')->nullable();
$table->string('process_type')->nullable();
$table->string('row_key');
});
Schema::connection('sqlite')->create('fin_daily_flows', function ($table): void {
$table->increments('id');
$table->date('flow_date');
$table->string('account_short');
$table->decimal('closing_balance', 18, 2)->default(0);
});
Schema::connection('sqlite')->create('fin_account_opening', function ($table): void {
$table->increments('id');
$table->string('account_short');
$table->date('opening_date');
$table->decimal('opening_balance', 18, 2)->default(0);
});
Schema::connection('sqlite')->create('fin_account_map', function ($table): void {
$table->increments('id');
$table->string('source');
$table->string('source_value');
$table->string('account_short')->nullable();
});
}
private function seedFinanceData(): void
{
DB::table('fin_companies')->insert([
'name' => '测试主体',
'tax_type' => '一般纳税人',
'basic_bank' => '测试银行',
]);
DB::table('fin_accounts')->insert([
'company_name' => '测试主体',
'account_short' => '测试账户',
'account_no' => '6222000012345678',
'bank_name' => '测试银行',
'account_last4' => '5678',
'current_balance' => 120,
'owner' => '测试户名',
]);
DB::table('fin_shops')->insert([
[
'shop_name' => '测试店铺A',
'platform' => '京东',
'company_name' => '测试主体',
'account_short' => '测试账户',
'status' => '在营',
'payout_account' => '测试收款账号',
],
[
'shop_name' => '测试店铺B',
'platform' => '京东',
'company_name' => '测试主体',
'account_short' => '测试账户',
'status' => '在营',
'payout_account' => '测试收款账号',
],
]);
DB::table('fin_daily_flows')->insert([
[
'flow_date' => '2026-10-31',
'account_short' => '测试账户',
'closing_balance' => 100,
],
[
'flow_date' => '2026-11-02',
'account_short' => '测试账户',
'closing_balance' => 135,
],
]);
DB::table('fin_account_opening')->insert([
'account_short' => '测试账户',
'opening_date' => '2026-10-01',
'opening_balance' => 80,
]);
DB::table('fin_transactions')->insert([
[
'txn_date' => '2026-11-01',
'account_short' => '测试账户',
'detail' => '京东账单提现 · 测试店铺A',
'income' => 30,
'expense' => 0,
'approval_no' => 'JD:W001',
'source' => 'shop',
'source_value' => '测试店铺A',
'process_type' => '店铺提现',
'row_key' => 'shop-match',
],
[
'txn_date' => '2026-11-01',
'account_short' => '测试账户',
'detail' => '供应商退款',
'income' => 10,
'expense' => 0,
'approval_no' => 'PAY-001',
'source' => 'payable',
'source_value' => '供应商甲',
'process_type' => '供应商退款',
'row_key' => 'other-income',
],
[
'txn_date' => '2026-11-01',
'account_short' => '测试账户',
'detail' => '店铺提现待匹配',
'income' => 5,
'expense' => 0,
'approval_no' => 'SHOP-UNMATCHED',
'source' => 'shop',
'source_value' => null,
'process_type' => '店铺提现',
'row_key' => 'shop-pending',
],
[
'txn_date' => '2026-11-02',
'account_short' => '',
'detail' => '付款审批',
'income' => 0,
'expense' => 25,
'approval_no' => 'DING-001',
'source' => 'dingtalk',
'source_value' => '测试账户',
'process_type' => '付款审批',
'row_key' => 'ding-expense',
],
[
'txn_date' => '2026-11-01',
'account_short' => '测试账户',
'detail' => '店铺账单提现 · 测试店铺B',
'income' => 15,
'expense' => 0,
'approval_no' => 'BILL-001',
'source' => 'shop_bill',
'source_value' => '测试店铺B',
'process_type' => '店铺提现',
'row_key' => 'shop-bill-match',
],
]);
DB::table('fin_account_map')->insert([
[
'source' => 'payable',
'source_value' => '供应商甲',
'account_short' => '测试账户',
],
[
'source' => 'dingtalk',
'source_value' => '测试账户',
'account_short' => '测试账户',
],
]);
}
}