511 lines
23 KiB
PHP
511 lines
23 KiB
PHP
<?php
|
|
|
|
namespace Tests\Unit\EcommerceFinance;
|
|
|
|
use Illuminate\Database\Schema\Blueprint;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Modules\EcommerceFinance\View\ManagementExpensesPageData;
|
|
use Tests\TestCase;
|
|
|
|
class ManagementExpensesPageDataTest extends TestCase
|
|
{
|
|
private ManagementExpensesPageData $pageData;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
Carbon::setTestNow('2026-11-02 12:00:00');
|
|
config([
|
|
'database.connections.business_mirror' => [
|
|
'driver' => 'sqlite',
|
|
'database' => ':memory:',
|
|
'prefix' => '',
|
|
],
|
|
'upstream_mirrors.connection' => 'business_mirror',
|
|
]);
|
|
DB::purge('business_mirror');
|
|
$this->createTables();
|
|
$this->seedRows();
|
|
$this->pageData = app(ManagementExpensesPageData::class);
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
Carbon::setTestNow();
|
|
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function test_page_groups_all_paid_oa_rows_by_second_level_and_fee_ratio(): void
|
|
{
|
|
$data = $this->pageData->page(Request::create(
|
|
'/1/workspace/financial-operations/management-expenses',
|
|
'GET',
|
|
));
|
|
|
|
$this->assertSame(5, $data['rows']->total());
|
|
$this->assertSame(670.0, $data['summary']['amount_total']);
|
|
$this->assertSame(500.0, $data['summary']['in_fee_ratio_amount']);
|
|
$this->assertSame(120.0, $data['summary']['non_fee_ratio_amount']);
|
|
$this->assertSame(50.0, $data['summary']['unmapped_amount']);
|
|
$this->assertSame(5, $data['summary']['row_count']);
|
|
$this->assertSame(5, $data['summary']['approval_count']);
|
|
$this->assertSame('2026-10', $data['filters']['paid_month']);
|
|
$this->assertSame('2026年11月', $data['monthOptions']['2026-11']);
|
|
$this->assertSame('2026年1月', $data['monthOptions']['2026-01']);
|
|
|
|
$groups = collect($data['secondLevelSummary'])->keyBy(
|
|
static fn (array $row): string => $row['expense_category'] . '|' . $row['second_level'],
|
|
);
|
|
$this->assertSame(120.0, $groups['管理费用|办公费']['amount_total']);
|
|
$this->assertSame(2, $groups['管理费用|办公费']['approval_count']);
|
|
$this->assertSame(500.0, $groups['销售费用|技术服务费']['in_fee_ratio_amount']);
|
|
$this->assertSame(0.0, $groups['销售费用|技术服务费']['non_fee_ratio_amount']);
|
|
$this->assertSame(500.0, $groups['销售费用|技术服务费']['amount_total']);
|
|
$this->assertSame(2, $groups['销售费用|技术服务费']['approval_count']);
|
|
$this->assertSame(50.0, $groups['未映射科目|未映射科目']['unmapped_amount']);
|
|
$this->assertCount(3, $data['secondLevelSummary']);
|
|
}
|
|
|
|
public function test_explicitly_cleared_paid_month_shows_all_months(): void
|
|
{
|
|
$data = $this->pageData->page(Request::create(
|
|
'/1/workspace/financial-operations/management-expenses',
|
|
'GET',
|
|
['paid_month' => ''],
|
|
));
|
|
|
|
$this->assertSame('', $data['filters']['paid_month']);
|
|
$this->assertSame(6, $data['rows']->total());
|
|
$this->assertSame(700.0, $data['summary']['amount_total']);
|
|
}
|
|
|
|
public function test_detail_template_filters_and_paginates_approved_paid_rows(): void
|
|
{
|
|
$data = $this->pageData->details(Request::create(
|
|
'/1/workspace/financial-operations/management-expenses/details',
|
|
'GET',
|
|
[
|
|
'expense_category' => '管理费用',
|
|
'second_level' => '办公费',
|
|
'per_page' => 50,
|
|
],
|
|
));
|
|
|
|
$this->assertSame(2, $data['pagination']['total']);
|
|
$this->assertSame(120.0, $data['summary']['amount_total']);
|
|
$this->assertSame(
|
|
['审批编号', '部门', '店铺', '用途', '分类', '二级', '三级', '金额', '是否进费比', '是否分摊'],
|
|
array_values(ManagementExpensesPageData::DETAIL_COLUMNS),
|
|
);
|
|
$this->assertSame(
|
|
['202610011200000000001', '202609251200000000008'],
|
|
array_column($data['rows'], 'business_id'),
|
|
);
|
|
$this->assertSame('不进费比', $data['rows'][0]['fee_ratio']);
|
|
$this->assertSame('分摊', $data['rows'][0]['allocation']);
|
|
$this->assertSame('天猫-办公店', $data['rows'][0]['store_name']);
|
|
}
|
|
|
|
public function test_store_summary_groups_current_paid_filter_by_store(): void
|
|
{
|
|
$data = $this->pageData->storeSummary(Request::create(
|
|
'/1/workspace/financial-operations/management-expenses/store-summary',
|
|
'GET',
|
|
[
|
|
'expense_category' => '管理费用',
|
|
'second_level' => '办公费',
|
|
'paid_month' => '2026-10',
|
|
],
|
|
));
|
|
|
|
$this->assertSame(120.0, $data['total']);
|
|
$this->assertSame(
|
|
[
|
|
['store_name' => '天猫-办公店', 'amount' => 100.0, 'row_count' => 1, 'approval_count' => 1],
|
|
['store_name' => '京东-办公店', 'amount' => 20.0, 'row_count' => 1, 'approval_count' => 1],
|
|
],
|
|
$data['rows'],
|
|
);
|
|
|
|
$filtered = $this->pageData->storeSummary(Request::create(
|
|
'/1/workspace/financial-operations/management-expenses/store-summary',
|
|
'GET',
|
|
[
|
|
'expense_category' => '管理费用',
|
|
'second_level' => '办公费',
|
|
'paid_month' => '2026-10',
|
|
'store_name' => '京东-办公店',
|
|
],
|
|
));
|
|
$this->assertSame(20.0, $filtered['total']);
|
|
$this->assertSame('京东-办公店', $filtered['rows'][0]['store_name']);
|
|
}
|
|
|
|
public function test_unmapped_account_is_visible_instead_of_being_silently_dropped(): void
|
|
{
|
|
$data = $this->pageData->details(Request::create(
|
|
'/1/workspace/financial-operations/management-expenses/details',
|
|
'GET',
|
|
['fee_ratio' => 'unmapped'],
|
|
));
|
|
|
|
$this->assertSame(1, $data['pagination']['total']);
|
|
$this->assertSame('未映射科目', $data['rows'][0]['expense_category']);
|
|
$this->assertSame(50.0, $data['rows'][0]['amount']);
|
|
}
|
|
|
|
public function test_paid_month_includes_all_lines_from_an_unfinished_paid_approval(): void
|
|
{
|
|
$businessId = '202610201200000000001';
|
|
$first = $this->row(
|
|
$businessId,
|
|
'5602.04',
|
|
1,
|
|
10,
|
|
'审批中',
|
|
'pending_sync',
|
|
'present',
|
|
'2026-10-20 10:00:00',
|
|
'2026-10-20 00:00',
|
|
);
|
|
$second = $this->row(
|
|
$businessId,
|
|
'5602.04',
|
|
2,
|
|
15,
|
|
'审批中',
|
|
'pending_sync',
|
|
'present',
|
|
'2026-10-20 10:00:00',
|
|
null,
|
|
);
|
|
DB::connection('business_mirror')
|
|
->table('ding_process_detail_mirror')
|
|
->insert([$first, $second]);
|
|
DB::connection('business_mirror')
|
|
->table('ding_process_payment_mirror')
|
|
->insert([
|
|
'business_id' => $businessId,
|
|
'process_instance_id' => 'process-' . $businessId,
|
|
'payment_no' => 1,
|
|
'sequence_spec' => '1-2',
|
|
'amount' => 25,
|
|
'paid_at' => '2026-10-20 00:00',
|
|
'source_scope' => 'personal_permission',
|
|
'synced_at' => now(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
DB::connection('business_mirror')
|
|
->table('ding_process_payment_line_mirror')
|
|
->insert([
|
|
$this->paymentLine($businessId, 1, 10, '2026-10-20', 1),
|
|
$this->paymentLine($businessId, 2, 15, '2026-10-20', 1),
|
|
]);
|
|
|
|
$data = $this->pageData->details(Request::create(
|
|
'/1/workspace/financial-operations/management-expenses/details',
|
|
'GET',
|
|
['business_id' => $businessId],
|
|
));
|
|
|
|
$this->assertSame(2, $data['pagination']['total']);
|
|
$this->assertSame(2, $data['summary']['row_count']);
|
|
$this->assertSame(1, $data['summary']['approval_count']);
|
|
$this->assertSame(25.0, $data['summary']['amount_total']);
|
|
$this->assertSame(
|
|
[$businessId, $businessId],
|
|
array_column($data['rows'], 'business_id'),
|
|
);
|
|
}
|
|
|
|
public function test_cross_month_payments_split_the_same_detail_line_by_paid_amount(): void
|
|
{
|
|
$businessId = '202609151200000000010';
|
|
DB::connection('business_mirror')
|
|
->table('ding_process_detail_mirror')
|
|
->insert($this->row(
|
|
$businessId,
|
|
'5602.04',
|
|
1,
|
|
100,
|
|
'审批中',
|
|
'pending_sync',
|
|
'present',
|
|
'2026-09-15 10:00:00',
|
|
null,
|
|
));
|
|
DB::connection('business_mirror')
|
|
->table('ding_process_payment_line_mirror')
|
|
->insert([
|
|
$this->paymentLine($businessId, 1, 40, '2026-09-28', 1),
|
|
$this->paymentLine($businessId, 1, 60, '2026-10-02', 2),
|
|
]);
|
|
|
|
foreach (['2026-09' => 40.0, '2026-10' => 60.0] as $month => $amount) {
|
|
$data = $this->pageData->details(Request::create(
|
|
'/1/workspace/financial-operations/management-expenses/details',
|
|
'GET',
|
|
['business_id' => $businessId, 'paid_month' => $month],
|
|
));
|
|
|
|
$this->assertSame(1, $data['pagination']['total']);
|
|
$this->assertSame($amount, $data['summary']['amount_total']);
|
|
$this->assertSame($amount, $data['rows'][0]['amount']);
|
|
}
|
|
}
|
|
|
|
public function test_workbook_mapping_snapshot_is_complete_and_unique(): void
|
|
{
|
|
$mapping = json_decode(
|
|
(string) file_get_contents(base_path('resources/data/oa_process_account_mapping.json')),
|
|
true,
|
|
512,
|
|
JSON_THROW_ON_ERROR,
|
|
);
|
|
|
|
$this->assertCount(196, $mapping);
|
|
$this->assertCount(196, array_unique(array_column($mapping, 'account_code')));
|
|
$this->assertSame(37, count(array_filter($mapping, static fn (array $row): bool => $row['in_fee_ratio'])));
|
|
$this->assertSame(128, count(array_filter($mapping, static fn (array $row): bool => $row['allocation'] !== null)));
|
|
$mappingByCode = array_column($mapping, null, 'account_code');
|
|
$this->assertSame('往来', $mappingByCode['2202.01']['expense_category']);
|
|
$this->assertSame('往来', $mappingByCode['2202.01']['second_level']);
|
|
$this->assertSame('供应商', $mappingByCode['2202.01']['third_level']);
|
|
$this->assertTrue($mappingByCode['1122.01']['in_fee_ratio']);
|
|
$this->assertSame('销售费用', $mappingByCode['5601.11.01']['expense_category']);
|
|
$this->assertSame('货代费', $mappingByCode['5601.11.01']['second_level']);
|
|
$this->assertNull($mappingByCode['5601.11.01']['third_level']);
|
|
$this->assertTrue($mappingByCode['5601.11.01']['in_fee_ratio']);
|
|
$this->assertSame('销售费用', $mappingByCode['1221.07']['expense_category']);
|
|
$this->assertSame('推广费用', $mappingByCode['1221.07']['second_level']);
|
|
$this->assertSame('摊销', $mappingByCode['1123.09.99']['expense_category']);
|
|
$this->assertSame('摊销', $mappingByCode['1123.09.99']['second_level']);
|
|
$this->assertSame('租赁', $mappingByCode['1123.09.99']['third_level']);
|
|
$this->assertSame('摊销', $mappingByCode['1123.11']['expense_category']);
|
|
$this->assertSame('摊销', $mappingByCode['1123.11']['second_level']);
|
|
$this->assertSame('平台使用费', $mappingByCode['1123.11']['third_level']);
|
|
$this->assertSame('空包费', $mappingByCode['1221.07']['third_level']);
|
|
$this->assertTrue($mappingByCode['1221.07']['in_fee_ratio']);
|
|
$this->assertSame('管理费用', $mappingByCode['5602.11.03.19']['expense_category']);
|
|
$this->assertSame('技术服务费', $mappingByCode['5602.11.03.19']['second_level']);
|
|
$this->assertSame('AI算力软件', $mappingByCode['5602.11.03.19']['third_level']);
|
|
$this->assertFalse($mappingByCode['5602.11.03.19']['in_fee_ratio']);
|
|
$this->assertSame('分摊', $mappingByCode['5602.11.03.19']['allocation']);
|
|
$this->assertSame('往来', $mappingByCode['1221.03.02']['expense_category']);
|
|
$this->assertSame('往来', $mappingByCode['1221.03.02']['second_level']);
|
|
$this->assertSame('押金', $mappingByCode['1221.03.02']['third_level']);
|
|
$this->assertFalse($mappingByCode['1221.03.02']['in_fee_ratio']);
|
|
$this->assertSame('分摊', $mappingByCode['1221.03.02']['allocation']);
|
|
$this->assertNotContains('报销', array_column($mapping, 'expense_category'));
|
|
$this->assertNotContains('订单退款', array_column($mapping, 'expense_category'));
|
|
$this->assertContains('分摊', array_column($mapping, 'allocation'));
|
|
$this->assertContains('核实进店铺', array_column($mapping, 'allocation'));
|
|
$this->assertContains('进店铺', array_column($mapping, 'allocation'));
|
|
}
|
|
|
|
private function createTables(): void
|
|
{
|
|
Schema::connection('business_mirror')->create(
|
|
'oa_process_account_mapping',
|
|
function (Blueprint $table): void {
|
|
$table->string('account_code', 64)->primary();
|
|
$table->string('account_name', 255);
|
|
$table->string('expense_category', 100);
|
|
$table->string('second_level', 100);
|
|
$table->string('third_level', 100);
|
|
$table->boolean('in_fee_ratio')->default(false);
|
|
$table->string('allocation', 100)->nullable();
|
|
$table->timestamps();
|
|
},
|
|
);
|
|
Schema::connection('business_mirror')->create(
|
|
'ding_process_detail_mirror',
|
|
function (Blueprint $table): void {
|
|
$table->bigIncrements('id');
|
|
$table->string('business_id', 32);
|
|
$table->string('process_instance_id', 100)->nullable();
|
|
$table->unsignedInteger('line_no');
|
|
$table->string('account_code', 64)->nullable();
|
|
$table->string('account_name', 255)->nullable();
|
|
$table->string('store_name', 500)->nullable();
|
|
$table->string('originator_department', 255)->nullable();
|
|
$table->string('originator_name', 100)->nullable();
|
|
$table->string('status', 20)->nullable();
|
|
$table->string('payment_source_state', 24)->nullable();
|
|
$table->string('cashier_status', 32)->nullable();
|
|
$table->text('purpose')->nullable();
|
|
$table->decimal('amount', 18, 2)->nullable();
|
|
$table->string('paid_at', 50)->nullable();
|
|
$table->dateTime('approval_finished_at')->nullable();
|
|
$table->string('source_scope', 40)->default('personal_permission');
|
|
$table->timestamps();
|
|
},
|
|
);
|
|
Schema::connection('business_mirror')->create(
|
|
'ding_process_payment_mirror',
|
|
function (Blueprint $table): void {
|
|
$table->bigIncrements('id');
|
|
$table->string('business_id', 32);
|
|
$table->string('process_instance_id', 100);
|
|
$table->unsignedInteger('payment_no');
|
|
$table->string('sequence_spec', 255)->nullable();
|
|
$table->decimal('amount', 18, 2);
|
|
$table->string('paid_at', 50)->nullable();
|
|
$table->string('source_scope', 40)->default('personal_permission');
|
|
$table->dateTime('synced_at', 6);
|
|
$table->timestamps();
|
|
},
|
|
);
|
|
Schema::connection('business_mirror')->create(
|
|
'ding_process_payment_line_mirror',
|
|
function (Blueprint $table): void {
|
|
$table->bigIncrements('id');
|
|
$table->string('source_scope', 40)->default('personal_permission');
|
|
$table->string('business_id', 32);
|
|
$table->string('process_instance_id', 100)->nullable();
|
|
$table->unsignedInteger('payment_no');
|
|
$table->unsignedInteger('line_no');
|
|
$table->string('paid_month', 7)->default('');
|
|
$table->string('paid_at', 50)->nullable();
|
|
$table->decimal('amount', 18, 2);
|
|
$table->timestamps();
|
|
},
|
|
);
|
|
}
|
|
|
|
private function seedRows(): void
|
|
{
|
|
$now = now();
|
|
DB::connection('business_mirror')->table('oa_process_account_mapping')->insert([
|
|
[
|
|
'account_code' => '5602.04',
|
|
'account_name' => '管理费用_办公费',
|
|
'expense_category' => '管理费用',
|
|
'second_level' => '办公费',
|
|
'third_level' => '办公费用品',
|
|
'in_fee_ratio' => false,
|
|
'allocation' => '分摊',
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
],
|
|
[
|
|
'account_code' => '5601.05.03',
|
|
'account_name' => '销售费用_营销费用_软件服务费',
|
|
'expense_category' => '销售费用',
|
|
'second_level' => '技术服务费',
|
|
'third_level' => '软件服务费',
|
|
'in_fee_ratio' => true,
|
|
'allocation' => null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
],
|
|
]);
|
|
DB::connection('business_mirror')->table('ding_process_detail_mirror')->insert([
|
|
$this->row('202610011200000000001', '5602.04', 1, 100, '已完成', 'completed', 'present', '2026-10-01 10:00:00', '2026-10-10', '天猫-办公店'),
|
|
$this->row('202610021200000000002', '5601.05.03', 1, 200, '已完成', 'completed', 'present', '2026-10-02 10:00:00', '2026-10-11'),
|
|
$this->row('202610031200000000003', '5601.05.03', 1, 300, '审批中', 'completed', 'present', '2026-10-03 10:00:00', '2026-10-12'),
|
|
$this->row('202610041200000000004', '5602.04', 1, 400, '审批中', 'pending', 'present', '2026-10-04 10:00:00', '2026-10-13'),
|
|
$this->row('202610051200000000005', '9999.99', 1, 50, '已完成', 'completed', 'present', '2026-10-05 10:00:00', '2026-10-14'),
|
|
$this->row('202610061200000000006', '5601.05.03', 1, 70, '已完成', 'not_applicable', 'not_applicable', '2026-10-06 10:00:00', '2026-10-15'),
|
|
$this->row('202610071200000000007', '5602.04', 1, 25, '已完成', null, 'present', '2026-10-07 10:00:00', '2026-10-16'),
|
|
$this->row('202609251200000000008', '5602.04', 1, 20, '已完成', 'completed', 'present', '2026-09-25 10:00:00', '2026-10-20', '京东-办公店'),
|
|
$this->row('202610281200000000009', '5602.04', 1, 30, '已完成', 'completed', 'present', '2026-10-28 10:00:00', '2026-09-30'),
|
|
]);
|
|
DB::connection('business_mirror')->table('ding_process_payment_mirror')->insert([
|
|
$this->payment('202610011200000000001', 100, '2026-10-10 00:00'),
|
|
$this->payment('202610021200000000002', 200, '2026-10-11 00:00'),
|
|
$this->payment('202610031200000000003', 300, '2026-10-12 00:00'),
|
|
$this->payment('202610051200000000005', 50, '2026-10-14 00:00'),
|
|
$this->payment('202609251200000000008', 20, '2026-10-20 00:00'),
|
|
$this->payment('202610281200000000009', 30, '2026-09-30 00:00'),
|
|
]);
|
|
DB::connection('business_mirror')->table('ding_process_payment_line_mirror')->insert([
|
|
$this->paymentLine('202610011200000000001', 1, 100, '2026-10-10', 1),
|
|
$this->paymentLine('202610021200000000002', 1, 200, '2026-10-11', 1),
|
|
$this->paymentLine('202610031200000000003', 1, 300, '2026-10-12', 1),
|
|
$this->paymentLine('202610051200000000005', 1, 50, '2026-10-14', 1),
|
|
$this->paymentLine('202609251200000000008', 1, 20, '2026-10-20', 1),
|
|
$this->paymentLine('202610281200000000009', 1, 30, '2026-09-30', 1),
|
|
]);
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
private function row(
|
|
string $businessId,
|
|
string $accountCode,
|
|
int $lineNo,
|
|
float $amount,
|
|
string $status,
|
|
?string $cashierStatus,
|
|
string $paymentSourceState,
|
|
string $approvalFinishedAt,
|
|
?string $paidAt,
|
|
string $storeName = '测试店铺',
|
|
): array {
|
|
return [
|
|
'business_id' => $businessId,
|
|
'process_instance_id' => 'process-' . $businessId,
|
|
'line_no' => $lineNo,
|
|
'account_code' => $accountCode,
|
|
'account_name' => '流程科目',
|
|
'store_name' => $storeName,
|
|
'originator_department' => '财务中心',
|
|
'originator_name' => '测试申请人',
|
|
'status' => $status,
|
|
'payment_source_state' => $paymentSourceState,
|
|
'cashier_status' => $cashierStatus,
|
|
'purpose' => '测试用途',
|
|
'amount' => $amount,
|
|
'paid_at' => $paidAt,
|
|
'approval_finished_at' => $approvalFinishedAt,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
];
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
private function payment(string $businessId, float $amount, string $paidAt): array
|
|
{
|
|
return [
|
|
'business_id' => $businessId,
|
|
'process_instance_id' => 'process-' . $businessId,
|
|
'payment_no' => 1,
|
|
'sequence_spec' => '1',
|
|
'amount' => $amount,
|
|
'paid_at' => $paidAt,
|
|
'source_scope' => 'personal_permission',
|
|
'synced_at' => now(),
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
];
|
|
}
|
|
|
|
/** @return array<string,mixed> */
|
|
private function paymentLine(
|
|
string $businessId,
|
|
int $lineNo,
|
|
float $amount,
|
|
string $paidAt,
|
|
int $paymentNo,
|
|
): array {
|
|
return [
|
|
'source_scope' => 'personal_permission',
|
|
'business_id' => $businessId,
|
|
'process_instance_id' => 'process-' . $businessId,
|
|
'payment_no' => $paymentNo,
|
|
'line_no' => $lineNo,
|
|
'paid_month' => substr($paidAt, 0, 7),
|
|
'paid_at' => $paidAt,
|
|
'amount' => $amount,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
];
|
|
}
|
|
}
|