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() ->assertSeeText('常州测试仓储服务有限公司') ->assertSeeText('常州测试客服服务有限公司') ->assertSeeText('常州测试运营服务有限公司') ->assertSeeText('个人独资企业') ->assertSeeText('核定') ->assertSeeInOrder([ '公司名称', '类型', '税务类型', '工商类型', ], false) ->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 { $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(), ); $this->get(route('business.service-company-reconciliation.show', [ 'serviceCompany' => $serviceCompanyId, 'tab' => 'relations', ])) ->assertOk() ->assertSeeText('常州测试客服服务有限公司') ->assertSeeText('上海测试店铺公司有限公司') ->assertSeeText('HT-TEST-001') ->assertSeeText('1.5000%') ->assertSeeText('客服服务公司结算比例固定为 1.5%') ->assertSee('data-fixed-fee-rate="1.5"', false) ->assertSeeText('同一店铺公司 + 同一服务类型'); } 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'), ); } 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(), ]); } } }