feat: 固定客服服务公司结算比例 (#1090)

客服服务公司结算比例固定为 1.5%,后端同步校验。
This commit is contained in:
DoDo 2026-11-11 17:22:14 +08:00
parent 11c506af89
commit a5c536f9dd
4 changed files with 119 additions and 9 deletions

View File

@ -24,6 +24,10 @@ class ServiceCompanyReconciliationRepository
'settlement_income' => '业绩明细收入(不减退款)',
];
public const FIXED_FEE_RATES_PERCENT = [
'customer_service' => 1.5,
];
private readonly ConnectionInterface $connection;
public function __construct(DatabaseManager $database)
@ -523,6 +527,17 @@ class ServiceCompanyReconciliationRepository
if ($feeRatePercent < 0 || $feeRatePercent > 100) {
throw ValidationException::withMessages(['fee_rate_percent' => '结算比例必须在 0% 到 100% 之间。']);
}
$fixedFeeRatePercent = self::fixedFeeRatePercent((string) $company['service_type_code']);
if ($fixedFeeRatePercent !== null
&& abs($feeRatePercent - $fixedFeeRatePercent) > 0.000001) {
throw ValidationException::withMessages([
'fee_rate_percent' => sprintf(
'%s的结算比例固定为 %s%%。',
self::SERVICE_TYPES[(string) $company['service_type_code']],
rtrim(rtrim(number_format($fixedFeeRatePercent, 4, '.', ''), '0'), '.'),
),
]);
}
$payload = [
'service_company_key' => (string) $company['company_key'],
@ -1039,6 +1054,11 @@ class ServiceCompanyReconciliationRepository
return $text === '' ? null : mb_substr($text, 0, $length);
}
public static function fixedFeeRatePercent(string $serviceTypeCode): ?float
{
return self::FIXED_FEE_RATES_PERCENT[$serviceTypeCode] ?? null;
}
private function serviceTypeCodeFromLabel(string $label): string
{
if (mb_stripos($label, '客服') !== false) {

View File

@ -41,6 +41,7 @@ class ServiceCompanyReconciliation extends Controller
'storeCompanies' => [],
'serviceTypes' => ServiceCompanyReconciliationRepository::SERVICE_TYPES,
'baseMetrics' => ServiceCompanyReconciliationRepository::BASE_METRICS,
'fixedFeeRatePercent' => null,
'companyDirectory' => $this->repository->companyDirectory(),
'companyPlanSyncError' => $companyPlanSyncError,
'canEdit' => FinanceLedgerAccess::canEditMapping(user()),
@ -94,6 +95,9 @@ class ServiceCompanyReconciliation extends Controller
'storeCompanies' => $this->repository->storeCompanies($tenantId),
'serviceTypes' => ServiceCompanyReconciliationRepository::SERVICE_TYPES,
'baseMetrics' => ServiceCompanyReconciliationRepository::BASE_METRICS,
'fixedFeeRatePercent' => ServiceCompanyReconciliationRepository::fixedFeeRatePercent(
(string) $company['service_type_code'],
),
'companyDirectory' => $this->repository->companyDirectory(),
'companyPlanSyncError' => $companyPlanSyncError,
'canEdit' => FinanceLedgerAccess::canEditMapping(user()),

View File

@ -995,7 +995,13 @@
<header class="svc-panel__head">
<div>
<h3 id="svc-relations-title">店铺公司结算关系</h3>
<p>一行只对应一家店铺公司;修改比例会生成新版本。</p>
<p>
@if ($fixedFeeRatePercent !== null)
客服服务公司结算比例固定为 {{ $fixedFeeRatePercent }}%;其他信息修改会生成新版本。
@else
一行只对应一家店铺公司;修改比例会生成新版本。
@endif
</p>
</div>
@if ($canEdit)
<button class="svc-page__button" type="button" data-relation-create>
@ -1314,7 +1320,12 @@
@if ($isDetail)
<dialog class="svc-dialog" id="svc-relation-dialog" aria-labelledby="svc-relation-dialog-title">
<form method="POST" action="{{ route('business.service-company-reconciliation.relations.store', ['serviceCompany' => $serviceCompany['id']]) }}" data-relation-form>
<form
method="POST"
action="{{ route('business.service-company-reconciliation.relations.store', ['serviceCompany' => $serviceCompany['id']]) }}"
data-relation-form
@if ($fixedFeeRatePercent !== null) data-fixed-fee-rate="{{ $fixedFeeRatePercent }}" @endif
>
@csrf
<input type="hidden" name="_method" value="POST" data-relation-method>
<header class="svc-dialog__head">
@ -1346,7 +1357,21 @@
</div>
<div class="svc-field">
<label for="svc-fee-rate">结算比例(%)</label>
<input id="svc-fee-rate" type="number" name="fee_rate_percent" min="0" max="100" step="0.0001" value="1.5" required data-relation-rate>
<input
id="svc-fee-rate"
type="number"
name="fee_rate_percent"
min="0"
max="100"
step="0.0001"
value="{{ $fixedFeeRatePercent ?? 1.5 }}"
required
data-relation-rate
@if ($fixedFeeRatePercent !== null) readonly @endif
>
@if ($fixedFeeRatePercent !== null)
<span class="svc-page__hint-text">客服服务公司固定 1.5%,由系统统一控制。</span>
@endif
</div>
<div class="svc-field">
<label for="svc-effective-from">生效日期</label>
@ -1496,6 +1521,7 @@
const relationForm = relationDialog?.querySelector('[data-relation-form]');
const relationStore = relationDialog?.querySelector('[data-relation-store]');
const fixedFeeRate = relationForm?.dataset.fixedFeeRate || '';
function resetRelationForm() {
if (!relationForm) return;
@ -1503,7 +1529,8 @@
relationForm.action = page.dataset.storeRoute;
relationForm.querySelector('[data-relation-method]').value = 'POST';
relationDialog.querySelector('[data-relation-dialog-title]').textContent = '新增店铺公司关系';
relationForm.querySelector('[data-relation-rate]').value = '1.5';
relationForm.querySelector('[data-relation-rate]').value = fixedFeeRate || '1.5';
relationForm.querySelector('[data-relation-rate]').readOnly = fixedFeeRate !== '';
relationForm.querySelector('[data-relation-from]').value = @json($isDetail ? now()->startOfMonth()->toDateString() : '');
}
@ -1521,7 +1548,7 @@
relationDialog.querySelector('[data-relation-dialog-title]').textContent = '编辑店铺公司关系';
relationForm.querySelector('[data-relation-store]').value = button.dataset.storeKey;
relationForm.querySelector('[data-relation-base]').value = button.dataset.baseMetric;
relationForm.querySelector('[data-relation-rate]').value = button.dataset.feeRate;
relationForm.querySelector('[data-relation-rate]').value = fixedFeeRate || button.dataset.feeRate;
relationForm.querySelector('[data-relation-from]').value = button.dataset.effectiveFrom;
relationForm.querySelector('[data-relation-to]').value = button.dataset.effectiveTo || '';
relationForm.querySelector('[data-relation-contract-no]').value = button.dataset.contractNo || '';

View File

@ -161,10 +161,11 @@ SQL);
$relationId = (int) DB::connection('business_mirror')
->table('svc_service_relations')
->value('id');
$this->patch(route('business.service-company-reconciliation.relations.update', [
$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,
@ -173,7 +174,21 @@ SQL);
'contract_no' => 'HT-TEST-001',
'contract_status' => 'stamped',
'status' => 'active',
])->assertSessionHasNoErrors();
];
$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,
@ -198,10 +213,54 @@ SQL);
->assertSeeText('常州测试客服服务有限公司')
->assertSeeText('上海测试店铺公司有限公司')
->assertSeeText('HT-TEST-001')
->assertSeeText('2.0000%')
->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();