companyPlanPath = storage_path(
'framework/testing/service-company-plan-' . Str::uuid() . '.sqlite',
);
config(['company_plan.database_path' => $this->companyPlanPath]);
}
protected function tearDown(): void
{
@unlink($this->companyPlanPath);
parent::tearDown();
}
public function test_overview_imports_company_plan_rows_whose_type_contains_service_company(): void
{
$pdo = app(CompanyPlanRepository::class)->pdo();
$insert = $pdo->prepare(<<<'SQL'
INSERT INTO company_plan_rows (
company_id, sequence_no, company_name, company_type, taxpayer_type,
business_registration_type, unified_social_credit_code, company_status, created_at, updated_at
) VALUES (
:company_id, :sequence_no, :company_name, :company_type, :taxpayer_type,
:business_registration_type, :credit_code, :company_status, :created_at, :updated_at
)
SQL);
$now = now()->toDateTimeString();
foreach ([
[1, '上海测试科技有限公司', '科技有限公司', '一般纳税人', '查账征收', '91310000TESTTECH01'],
[2, '常州测试仓储服务有限公司', '仓储服务公司', '小规模纳税人', '查账征收', '91320400TESTWAREHOUSE01'],
[3, '常州测试客服服务有限公司', '客服服务公司', '小规模纳税人', '个人独资企业', '91320400TESTCUSTOMER06'],
[4, '常州测试运营服务有限公司', '运营服务公司', '小规模纳税人', '核定', '91320400TESTOPS01'],
] as [$sequence, $name, $type, $taxType, $businessType, $creditCode]) {
$insert->execute([
':company_id' => $this->company->id,
':sequence_no' => $sequence,
':company_name' => $name,
':company_type' => $type,
':taxpayer_type' => $taxType,
':business_registration_type' => $businessType,
':credit_code' => $creditCode,
':company_status' => '正常',
':created_at' => $now,
':updated_at' => $now,
]);
}
$this->loginAs()
->get(route('business.service-company-reconciliation.index'))
->assertOk()
->assertSee('class="svc-table svc-table--companies"', false)
->assertSeeText('常州测试仓储服务有限公司')
->assertSeeText('常州测试客服服务有限公司')
->assertSeeText('常州测试运营服务有限公司')
->assertSeeText('个人独资企业')
->assertSeeText('核定')
->assertSeeInOrder([
'
公司名称 | ',
'类型 | ',
'税务类型 | ',
'工商类型 | ',
], false)
->assertSee('结算比例 | ', false)
->assertDontSee('结算基数 | ', false)
->assertSeeText('1.5%')
->assertDontSeeText('上海测试科技有限公司');
$types = DB::connection('business_mirror')
->table('svc_service_companies')
->where('status', 'active')
->pluck('service_type_code')
->sort()
->values()
->all();
$this->assertSame(['customer_service', 'operations', 'warehouse'], $types);
$this->assertSame(
3,
DB::connection('business_mirror')
->table('svc_service_companies')
->where('source', 'company_plan')
->count(),
);
$pdo->prepare(
"UPDATE company_plan_rows SET company_type = '科技有限公司' WHERE company_name = ?"
)->execute(['常州测试客服服务有限公司']);
$this->get(route('business.service-company-reconciliation.index'))
->assertOk()
->assertDontSeeText('常州测试客服服务有限公司');
$this->assertSame(
'inactive',
DB::connection('business_mirror')
->table('svc_service_companies')
->where('unified_social_credit_code', '91320400TESTCUSTOMER06')
->value('status'),
);
}
public function test_service_company_and_relation_can_be_configured_from_the_real_page(): void
{
Storage::fake('uploads');
$this->seedStoreMirror();
$this->loginAs();
$this->get(route('business.service-company-reconciliation.index'))
->assertOk()
->assertSeeText('服务公司总览')
->assertSeeText('新增服务公司');
$response = $this->post(route('business.service-company-reconciliation.companies.store'), [
'company_name' => '常州测试客服服务有限公司',
'unified_social_credit_code' => '91320400TESTCUSTOMER01',
'service_type_code' => 'customer_service',
'taxpayer_type' => '小规模纳税人',
'business_registration_type' => '查账征收',
'company_status' => '正常',
'planning_target_amount' => 3000000,
'status' => 'active',
'notes' => '测试服务公司',
]);
$serviceCompanyId = (int) DB::connection('business_mirror')
->table('svc_service_companies')
->value('id');
$response->assertRedirect(route('business.service-company-reconciliation.show', [
'serviceCompany' => $serviceCompanyId,
]));
$storeCompanyKey = ServiceCompanyReconciliationRepository::companyKey(
'上海测试店铺公司有限公司',
'91310000TESTSTORE01',
);
$this->post(route('business.service-company-reconciliation.relations.store', [
'serviceCompany' => $serviceCompanyId,
]), [
'store_company_key' => $storeCompanyKey,
'base_metric' => 'settlement_income',
'fee_rate_percent' => 1.5,
'effective_from' => '2025-01-01',
'effective_to' => '',
'contract_no' => 'HT-TEST-001',
'contract_status' => 'stamped',
'status' => 'active',
])->assertSessionHasNoErrors();
$relationId = (int) DB::connection('business_mirror')
->table('svc_service_relations')
->value('id');
$updateRoute = route('business.service-company-reconciliation.relations.update', [
'serviceCompany' => $serviceCompanyId,
'relation' => $relationId,
]);
$relationPayload = [
'store_company_key' => $storeCompanyKey,
'base_metric' => 'settlement_income',
'fee_rate_percent' => 2,
'effective_from' => '2025-01-01',
'effective_to' => '',
'contract_no' => 'HT-TEST-001',
'contract_status' => 'stamped',
'status' => 'active',
];
$this->withExceptionHandling()
->patch($updateRoute, $relationPayload)
->assertSessionHasErrors('fee_rate_percent');
$this->assertSame(
1,
(int) DB::connection('business_mirror')
->table('svc_service_relations')
->where('id', $relationId)
->value('rule_version'),
);
$relationPayload['fee_rate_percent'] = 1.5;
$this->patch($updateRoute, $relationPayload)
->assertSessionHasNoErrors();
$this->assertSame(
2,
(int) DB::connection('business_mirror')
->table('svc_service_relations')
->where('id', $relationId)
->value('rule_version'),
);
$this->assertSame(
2,
DB::connection('business_mirror')
->table('svc_service_relation_versions')
->where('relation_id', $relationId)
->count(),
);
$relationPayload['contract_status'] = 'pending';
$this->patch($updateRoute, $relationPayload)->assertSessionHasNoErrors();
$this->get(route('business.service-company-reconciliation.show', [
'serviceCompany' => $serviceCompanyId,
'tab' => 'relations',
]))
->assertOk()
->assertSeeText('常州测试客服服务有限公司')
->assertSeeText('上海测试店铺公司有限公司')
->assertSeeText('HT-TEST-001')
->assertSeeText('1.5%')
->assertSeeText('客服服务公司结算比例固定为 1.5%')
->assertSeeTextInOrder(['收入', '比例', '结算金额', '开票情况', '资金结算情况', '生效时间'])
->assertSee('收入 | ', false)
->assertDontSeeText('收入口径')
->assertDontSee('data-svc-tab="reconciliation"', false)
->assertSee('name="invoice_completion"', false)
->assertSee('name="payment_completion"', false)
->assertSee('data-relation-income-amount="0.00"', false)
->assertSee('data-relation-settlement-amount="0.00"', false)
->assertSeeText('未生成对账')
->assertSee('data-fixed-fee-rate="1.5"', false)
->assertSeeText('同一店铺公司 + 同一服务类型');
$this->post(route('business.service-company-reconciliation.relations.attachments.store', [
'serviceCompany' => $serviceCompanyId,
'relation' => $relationId,
]), [
'period' => CarbonImmutable::now()->subMonthNoOverflow()->format('Y-m'),
'attachment' => UploadedFile::fake()->create('unsigned-contract.pdf', 32, 'application/pdf'),
])->assertSessionHasErrors('attachment');
$this->assertSame(
0,
DB::connection('business_mirror')->table('svc_service_contract_attachments')->count(),
);
$this->assertSame([], Storage::disk('uploads')->allFiles());
}
public function test_contract_attachment_can_be_uploaded_and_downloaded_from_relation(): void
{
Storage::fake('uploads');
$this->seedStoreMirror();
$this->loginAs();
$serviceCompanyId = $this->createServiceCompany(
'常州测试客服服务有限公司',
'91320400TESTCUSTOMER07',
);
$storeKey = ServiceCompanyReconciliationRepository::companyKey(
'上海测试店铺公司有限公司',
'91310000TESTSTORE01',
);
$this->post(route('business.service-company-reconciliation.relations.store', [
'serviceCompany' => $serviceCompanyId,
]), [
'store_company_key' => $storeKey,
'base_metric' => 'settlement_income',
'fee_rate_percent' => 1.5,
'effective_from' => '2025-01-01',
'effective_to' => '',
'contract_no' => 'HT-CONTRACT-001',
'contract_status' => 'stamped',
'status' => 'active',
])->assertSessionHasNoErrors();
$relationId = (int) DB::connection('business_mirror')
->table('svc_service_relations')
->value('id');
$period = CarbonImmutable::now()->subMonthNoOverflow()->format('Y-m');
$this->post(route('business.service-company-reconciliation.relations.attachments.store', [
'serviceCompany' => $serviceCompanyId,
'relation' => $relationId,
]), [
'period' => $period,
'attachment' => UploadedFile::fake()->create('signed-contract.pdf', 128, 'application/pdf'),
])
->assertRedirect(route('business.service-company-reconciliation.show', [
'serviceCompany' => $serviceCompanyId,
'period' => $period,
'tab' => 'relations',
'invoice_completion' => 'unfinished',
'payment_completion' => 'unfinished',
]))
->assertSessionHasNoErrors();
$attachment = DB::connection('business_mirror')
->table('svc_service_contract_attachments')
->where('relation_id', $relationId)
->first();
$this->assertNotNull($attachment);
$this->assertSame('signed-contract.pdf', $attachment->original_name);
Storage::disk('uploads')->assertExists($attachment->path);
$this->get(route('business.service-company-reconciliation.show', [
'serviceCompany' => $serviceCompanyId,
'period' => $period,
'tab' => 'relations',
]))
->assertOk()
->assertSeeText('合同签约情况')
->assertSeeText('HT-CONTRACT-001')
->assertSee('name="attachment"', false)
->assertSeeText('signed-contract.pdf');
$this->get(route('business.service-company-reconciliation.relations.attachments.show', [
'serviceCompany' => $serviceCompanyId,
'relation' => $relationId,
'attachment' => $attachment->id,
]))
->assertOk()
->assertHeader('Content-Type', 'application/pdf');
}
public function test_relation_config_defaults_to_unfinished_and_filters_completed_states(): void
{
$period = CarbonImmutable::now()->subMonthNoOverflow()->format('Y-m');
$this->seedStoreMirror($period);
$this->loginAs();
$serviceCompanyId = $this->createServiceCompany(
'常州测试客服服务有限公司',
'91320400TESTCUSTOMER08',
);
$storeKey = ServiceCompanyReconciliationRepository::companyKey(
'上海测试店铺公司有限公司',
'91310000TESTSTORE01',
);
$this->post(route('business.service-company-reconciliation.relations.store', [
'serviceCompany' => $serviceCompanyId,
]), [
'store_company_key' => $storeKey,
'base_metric' => 'settlement_income',
'fee_rate_percent' => 1.5,
'effective_from' => '2025-01-01',
'effective_to' => '',
'contract_no' => 'HT-COMPLETION-001',
'contract_status' => 'stamped',
'status' => 'active',
])->assertSessionHasNoErrors();
$relationId = (int) DB::connection('business_mirror')
->table('svc_service_relations')
->value('id');
$this->post(route('business.service-company-reconciliation.settlements.generate', [
'serviceCompany' => $serviceCompanyId,
]), ['period' => $period])->assertRedirect();
$settlement = DB::connection('business_mirror')
->table('svc_service_settlements')
->where('period', $period)
->first();
$this->assertNotNull($settlement);
$this->get(route('business.service-company-reconciliation.show', [
'serviceCompany' => $serviceCompanyId,
'period' => $period,
'tab' => 'relations',
]))
->assertOk()
->assertSee('data-relation-row="' . $relationId . '"', false)
->assertSee('data-relation-income-amount="125000.00"', false)
->assertSee('data-relation-settlement-amount="1875.00"', false)
->assertSeeText('未完成');
$this->patch(route('business.service-company-reconciliation.settlements.update', [
'serviceCompany' => $serviceCompanyId,
'settlement' => $settlement->id,
]), [
'action' => 'save',
'invoice_status' => 'invoiced',
'invoice_amount' => 1875,
'payment_status' => 'pending',
'paid_amount' => 0,
])->assertSessionHasNoErrors();
$this->get(route('business.service-company-reconciliation.show', [
'serviceCompany' => $serviceCompanyId,
'period' => $period,
'tab' => 'relations',
]))
->assertOk()
->assertSee('data-relation-row="' . $relationId . '"', false)
->assertSeeText('默认显示任一未完成');
$this->patch(route('business.service-company-reconciliation.settlements.update', [
'serviceCompany' => $serviceCompanyId,
'settlement' => $settlement->id,
]), [
'action' => 'save',
'invoice_status' => 'invoiced',
'invoice_amount' => 1875,
'payment_status' => 'paid',
'paid_amount' => 1875,
])->assertSessionHasNoErrors();
$this->get(route('business.service-company-reconciliation.show', [
'serviceCompany' => $serviceCompanyId,
'period' => $period,
'tab' => 'relations',
]))
->assertOk()
->assertDontSee('data-relation-row="' . $relationId . '"', false);
$this->get(route('business.service-company-reconciliation.show', [
'serviceCompany' => $serviceCompanyId,
'period' => $period,
'tab' => 'relations',
'invoice_completion' => 'completed',
'payment_completion' => 'completed',
]))
->assertOk()
->assertSee('data-relation-row="' . $relationId . '"', false)
->assertSeeText('已完成');
$this->get(route('business.service-company-reconciliation.show', [
'serviceCompany' => $serviceCompanyId,
'period' => $period,
'tab' => 'relations',
'invoice_completion' => 'completed',
'payment_completion' => 'unfinished',
]))
->assertOk()
->assertDontSee('data-relation-row="' . $relationId . '"', false);
}
public function test_non_customer_service_company_keeps_configurable_settlement_rate(): void
{
$this->seedStoreMirror();
$this->loginAs();
$this->post(route('business.service-company-reconciliation.companies.store'), [
'company_name' => '常州测试运营服务有限公司',
'unified_social_credit_code' => '91320400TESTOPS02',
'service_type_code' => 'operations',
'company_status' => '正常',
'status' => 'active',
])->assertSessionHasNoErrors();
$serviceCompanyId = (int) DB::connection('business_mirror')
->table('svc_service_companies')
->where('unified_social_credit_code', '91320400TESTOPS02')
->value('id');
$storeCompanyKey = ServiceCompanyReconciliationRepository::companyKey(
'上海测试店铺公司有限公司',
'91310000TESTSTORE01',
);
$this->post(route('business.service-company-reconciliation.relations.store', [
'serviceCompany' => $serviceCompanyId,
]), [
'store_company_key' => $storeCompanyKey,
'base_metric' => 'settlement_income',
'fee_rate_percent' => 2,
'effective_from' => '2025-01-01',
'effective_to' => '',
'contract_no' => 'HT-TEST-OPERATIONS',
'contract_status' => 'stamped',
'status' => 'active',
])->assertSessionHasNoErrors();
$this->assertSame(
0.02,
(float) DB::connection('business_mirror')
->table('svc_service_relations')
->where('service_company_id', $serviceCompanyId)
->value('fee_rate'),
);
$this->get(route('business.service-company-reconciliation.index'))
->assertOk()
->assertSeeText('2%')
->assertDontSeeText('结算基数');
}
public function test_customer_service_excel_mapping_is_synced_from_store_company_to_relation(): void
{
$this->seedStoreMirror();
$this->loginAs();
$serviceCompanyId = $this->createServiceCompany(
'常州测试客服服务有限公司',
'91320400TESTCUSTOMER05',
);
$repository = app(ServiceCompanyReconciliationRepository::class);
$rows = [[
'service_company_name' => '常州测试客服服务有限公司',
'store_company_name' => '上海测试店铺公司有限公司',
'fee_rate' => 0.015,
'source_row' => 2,
]];
$first = $repository->syncCustomerServiceMappings(
$this->company->id,
$rows,
'2026-10-01',
'测试导入',
);
$this->assertSame(1, $first['created']);
$this->assertSame([], $first['errors']);
$relation = DB::connection('business_mirror')
->table('svc_service_relations')
->where('service_company_id', $serviceCompanyId)
->first();
$this->assertNotNull($relation);
$this->assertSame('customer_service_mapping', $relation->source);
$this->assertSame('2026-10-01', $relation->effective_from);
$this->assertSame(0.015, (float) $relation->fee_rate);
$this->assertSame(
ServiceCompanyReconciliationRepository::companyKey(
'上海测试店铺公司有限公司',
'91310000TESTSTORE01',
),
$relation->store_company_key,
);
$second = $repository->syncCustomerServiceMappings(
$this->company->id,
$rows,
'2026-10-01',
'测试导入',
);
$this->assertSame(1, $second['unchanged']);
$this->assertSame(1, DB::connection('business_mirror')->table('svc_service_relations')->count());
}
public function test_unassigned_store_with_income_is_auto_balanced_once_and_then_fixed(): void
{
$this->seedStoreMirror('2026-10');
$this->loginAs();
$firstServiceId = $this->createServiceCompany(
'常州测试客服甲公司',
'91320400TESTCUSTOMERA',
);
$secondServiceId = $this->createServiceCompany(
'常州测试客服乙公司',
'91320400TESTCUSTOMERB',
);
$mirror = DB::connection('business_mirror');
$paidGeneration = '11111111-1111-1111-1111-111111111111';
$lifecycleGeneration = '22222222-2222-2222-2222-222222222222';
foreach ([
['上海测试店铺二公司有限公司', '91310000TESTSTORE02', true],
['上海测试店铺三公司有限公司', '91310000TESTSTORE03', false],
] as [$companyName, $creditCode, $hasIncome]) {
$mirror->table('store_report_lifecycle_rows')->insert([
'generation' => $lifecycleGeneration,
'business_key' => hash('sha256', $companyName . '|2025-01-01'),
'erp_store_name' => $companyName . '店铺',
'company_name' => $companyName,
'unified_social_credit_code' => $creditCode,
'effective_at' => '2025-01-01 00:00:00',
'expires_at' => null,
'source_file' => 'test.xlsx',
'row_hash' => hash('sha256', 'lifecycle-' . $creditCode),
'imported_at' => now(),
]);
if ($hasIncome) {
$mirror->table('store_report_paid_sales_rows')->insert([
'generation' => $paidGeneration,
'store_name' => $companyName . '店铺',
'occurred_on' => '2026-10-09',
'paid_amount' => 20000,
'settlement_income' => 20000,
'order_count' => 1,
'updated_at' => now(),
]);
}
}
$this->mock(BusinessInformationMirrorRepository::class, function ($mock): void {
$mock->shouldReceive('allStores')->andReturn([['erp_store_name' => '测试店铺']]);
});
$this->mock(StoreReportRepository::class, function ($mock): void {
$mock->shouldReceive('rows')->andReturn([
[
'entity' => '上海测试店铺公司有限公司',
'previous_month_settlement_income' => 125000,
],
[
'entity' => '上海测试店铺二公司有限公司',
'previous_month_settlement_income' => 20000,
],
[
'entity' => '上海测试店铺三公司有限公司',
'previous_month_settlement_income' => 0,
],
]);
});
$repository = app(ServiceCompanyReconciliationRepository::class);
$assigned = [[
'service_company_name' => '常州测试客服甲公司',
'store_company_name' => '上海测试店铺公司有限公司',
'fee_rate' => 0.015,
'source_row' => 2,
]];
$unassigned = [
['store_company_name' => '上海测试店铺二公司有限公司', 'fee_rate' => 0.015, 'source_row' => 3],
['store_company_name' => '上海测试店铺三公司有限公司', 'fee_rate' => 0.015, 'source_row' => 4],
];
$first = $repository->syncCustomerServiceMappings(
$this->company->id,
$assigned,
'2026-10-01',
'测试导入',
$unassigned,
'2026-10',
);
$this->assertSame(1, $first['auto_assigned']);
$this->assertSame(125000.0, $first['auto_totals']['常州测试客服甲公司']);
$this->assertSame(20000.0, $first['auto_totals']['常州测试客服乙公司']);
$autoRelation = $mirror->table('svc_service_relations')
->where('store_company_name', '上海测试店铺二公司有限公司')
->first();
$this->assertSame($secondServiceId, (int) $autoRelation->service_company_id);
$this->assertSame('customer_service_auto_balance', $autoRelation->source);
$this->assertSame(
0,
$mirror->table('svc_service_relations')
->where('store_company_name', '上海测试店铺三公司有限公司')
->count(),
);
$second = $repository->syncCustomerServiceMappings(
$this->company->id,
$assigned,
'2026-10-01',
'再次导入',
$unassigned,
'2026-10',
);
$this->assertSame(0, $second['auto_assigned']);
$this->assertSame(
$secondServiceId,
(int) $mirror->table('svc_service_relations')
->where('store_company_name', '上海测试店铺二公司有限公司')
->value('service_company_id'),
);
$this->assertSame(
2,
$mirror->table('svc_service_relations')->count(),
);
$conflict = $repository->syncCustomerServiceMappings(
$this->company->id,
[[
'service_company_name' => '常州测试客服乙公司',
'store_company_name' => '上海测试店铺公司有限公司',
'fee_rate' => 0.015,
'source_row' => 2,
]],
'2026-10-01',
'错误改派',
[],
'2026-10',
);
$this->assertNotSame([], $conflict['errors']);
$this->assertSame(
$firstServiceId,
(int) $mirror->table('svc_service_relations')
->where('store_company_name', '上海测试店铺公司有限公司')
->value('service_company_id'),
);
$this->mock(StoreReportRepository::class, function ($mock): void {
$mock->shouldReceive('rows')->andReturn([[
'entity' => '上海测试店铺二公司有限公司',
'previous_month_settlement_income' => 0,
]]);
});
$cleanup = app(ServiceCompanyReconciliationRepository::class)
->deactivateAutoMappingsWithoutReportIncome(
$this->company->id,
'2026-10',
'校正测试',
);
$this->assertSame(1, $cleanup['deactivated']);
$this->assertSame('inactive', $mirror->table('svc_service_relations')
->where('store_company_name', '上海测试店铺二公司有限公司')
->value('status'));
}
public function test_same_store_company_and_service_type_cannot_have_overlapping_service_companies(): void
{
$this->seedStoreMirror();
$this->loginAs();
$firstId = $this->createServiceCompany('第一测试客服有限公司', '91320400TESTCUSTOMER02');
$secondId = $this->createServiceCompany('第二测试客服有限公司', '91320400TESTCUSTOMER03');
$storeKey = ServiceCompanyReconciliationRepository::companyKey(
'上海测试店铺公司有限公司',
'91310000TESTSTORE01',
);
$payload = [
'store_company_key' => $storeKey,
'base_metric' => 'settlement_income',
'fee_rate_percent' => 1.5,
'effective_from' => '2026-01-01',
'effective_to' => '2026-12-31',
'contract_no' => 'HT-TEST-002',
'contract_status' => 'stamped',
'status' => 'active',
];
$this->post(route('business.service-company-reconciliation.relations.store', [
'serviceCompany' => $firstId,
]), $payload)->assertSessionHasNoErrors();
$this->withExceptionHandling()
->post(route('business.service-company-reconciliation.relations.store', [
'serviceCompany' => $secondId,
]), $payload)
->assertSessionHasErrors('effective_from');
$this->assertSame(
1,
DB::connection('business_mirror')->table('svc_service_relations')->count(),
);
}
public function test_monthly_preview_uses_income_amount_and_prevents_confirmed_overwrite(): void
{
$period = CarbonImmutable::now()->subMonthNoOverflow()->format('Y-m');
$this->seedStoreMirror($period);
$this->loginAs();
$serviceCompanyId = $this->createServiceCompany(
'常州测试客服服务有限公司',
'91320400TESTCUSTOMER04',
);
$storeKey = ServiceCompanyReconciliationRepository::companyKey(
'上海测试店铺公司有限公司',
'91310000TESTSTORE01',
);
$this->post(route('business.service-company-reconciliation.relations.store', [
'serviceCompany' => $serviceCompanyId,
]), [
'store_company_key' => $storeKey,
'base_metric' => 'settlement_income',
'fee_rate_percent' => 1.5,
'effective_from' => '2025-01-01',
'effective_to' => '',
'contract_no' => 'HT-TEST-003',
'contract_status' => 'stamped',
'status' => 'active',
])->assertSessionHasNoErrors();
$this->post(route('business.service-company-reconciliation.settlements.generate', [
'serviceCompany' => $serviceCompanyId,
]), ['period' => $period])->assertRedirect();
$settlement = DB::connection('business_mirror')
->table('svc_service_settlements')
->where('period', $period)
->first();
$this->assertNotNull($settlement);
$this->assertSame(125000.0, (float) $settlement->base_amount);
$this->assertSame(1875.0, (float) $settlement->service_fee);
$this->assertSame(
2,
DB::connection('business_mirror')->table('svc_service_settlement_details')->count(),
);
$this->patch(route('business.service-company-reconciliation.settlements.update', [
'serviceCompany' => $serviceCompanyId,
'settlement' => $settlement->id,
]), [
'action' => 'confirm',
'invoice_status' => 'invoiced',
'invoice_amount' => 1875,
'payment_status' => 'paid',
'paid_amount' => 1875,
])->assertSessionHasNoErrors();
$this->assertSame(
'confirmed',
DB::connection('business_mirror')
->table('svc_service_settlements')
->where('id', $settlement->id)
->value('status'),
);
$this->post(route('business.service-company-reconciliation.settlements.generate', [
'serviceCompany' => $serviceCompanyId,
]), ['period' => $period])
->assertRedirect()
->assertSessionHas('error');
$this->assertSame(
1875.0,
(float) DB::connection('business_mirror')
->table('svc_service_settlements')
->where('id', $settlement->id)
->value('service_fee'),
);
}
public function test_people_can_be_assigned_from_the_labor_snapshot(): void
{
$this->seedStoreMirror();
$this->loginAs();
$serviceCompanyId = $this->createServiceCompany(
'常州测试客服服务有限公司',
'91320400TESTCUSTOMER05',
);
DB::table('business_labor_expense_snapshots')->insert([
'company_id' => $this->company->id,
'source_name' => '测试工资.xlsx',
'rows' => json_encode([[
'B' => '小测',
'C' => '测试员工',
'D' => '客服一组',
'E' => '客服',
'F' => '8000',
]], JSON_UNESCAPED_UNICODE),
'row_count' => 1,
'created_at' => now(),
'updated_at' => now(),
]);
$personKey = ServiceCompanyReconciliationRepository::personKey('测试员工', '小测', '客服一组');
$this->post(route('business.service-company-reconciliation.people.store', [
'serviceCompany' => $serviceCompanyId,
]), [
'person_key' => $personKey,
'effective_from' => now()->toDateString(),
])->assertSessionHasNoErrors();
$this->get(route('business.service-company-reconciliation.show', [
'serviceCompany' => $serviceCompanyId,
'tab' => 'people',
]))
->assertOk()
->assertSeeText('测试员工')
->assertSeeText('客服一组')
->assertSeeText('¥8,000.00');
}
public function test_finance_ledger_viewer_can_read_but_cannot_change_configuration(): void
{
$viewer = User::create([
'name' => 'fixture-service-company-viewer',
'email' => 'fixture-service-company-viewer@example.test',
'password' => 'fixture-unused-password',
'enabled' => true,
'locale' => 'zh-CN',
'landing_page' => 'dashboard',
]);
$viewer->roles()->attach(Role::where('name', 'user')->firstOrFail()->id);
$viewer->permissions()->attach(Permission::where('name', 'read-admin-panel')->firstOrFail()->id);
$viewer->companies()->attach($this->company->id);
$subject = 'user_' . $viewer->id;
setting()->set('system.sidebar_permissions', [
'configured' => [$subject],
'subjects' => [$subject => ['financial-operations/ledger']],
]);
$this->loginAs($viewer)
->get(route('business.service-company-reconciliation.index'))
->assertOk()
->assertSeeText('服务公司总览')
->assertDontSee('data-company-create>', false);
$this->withExceptionHandling()
->post(route('business.service-company-reconciliation.companies.store'), [
'company_name' => '不应创建的客服公司',
'service_type_code' => 'customer_service',
'company_status' => '正常',
'status' => 'active',
])
->assertForbidden();
}
private function createServiceCompany(string $name, string $code): int
{
$this->post(route('business.service-company-reconciliation.companies.store'), [
'company_name' => $name,
'unified_social_credit_code' => $code,
'service_type_code' => 'customer_service',
'taxpayer_type' => '小规模纳税人',
'company_status' => '正常',
'planning_target_amount' => 3000000,
'status' => 'active',
])->assertSessionHasNoErrors();
return (int) DB::connection('business_mirror')
->table('svc_service_companies')
->where('unified_social_credit_code', $code)
->value('id');
}
private function seedStoreMirror(?string $period = null): void
{
$period = $period ?: CarbonImmutable::now()->format('Y-m');
$periodStart = CarbonImmutable::createFromFormat('!Y-m', $period)->startOfMonth();
$mirror = DB::connection('business_mirror');
$paidGeneration = '11111111-1111-1111-1111-111111111111';
$lifecycleGeneration = '22222222-2222-2222-2222-222222222222';
$mirror->table('store_report_import_states')->insert([
[
'dataset_key' => 'store-report.paid-sales',
'status' => 'succeeded',
'active_generation' => $paidGeneration,
'row_count' => 2,
'created_at' => now(),
'updated_at' => now(),
],
[
'dataset_key' => 'store-report.store-lifecycles',
'status' => 'succeeded',
'active_generation' => $lifecycleGeneration,
'row_count' => 1,
'created_at' => now(),
'updated_at' => now(),
],
]);
foreach (['测试店铺A', '测试店铺B'] as $storeName) {
$mirror->table('store_report_lifecycle_rows')->insert([
'generation' => $lifecycleGeneration,
'business_key' => hash('sha256', '上海测试店铺公司有限公司|' . $storeName . '|2025-01-01'),
'erp_store_name' => $storeName,
'company_name' => '上海测试店铺公司有限公司',
'unified_social_credit_code' => '91310000TESTSTORE01',
'effective_at' => '2025-01-01 00:00:00',
'expires_at' => null,
'source_file' => 'test.xlsx',
'row_hash' => hash('sha256', 'lifecycle-row-' . $storeName),
'imported_at' => now(),
]);
}
foreach ([['测试店铺A', 100000], ['测试店铺B', 25000]] as [$storeName, $amount]) {
$mirror->table('store_report_paid_sales_rows')->insert([
'generation' => $paidGeneration,
'store_name' => $storeName,
'occurred_on' => $periodStart->addDays(8)->toDateString(),
'paid_amount' => $amount,
'settlement_income' => $amount,
'order_count' => 1,
'updated_at' => now(),
]);
}
}
}