202 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace Modules\EcommerceFinance\Services;
use Illuminate\Support\Facades\DB;
use Throwable;
/**
* 「税金预测表逻辑配置」的可维护参数与规则目录。
*
* 税金预测表页面只展示预测结果;税率、免税额度、减半开关、核定默认值
* 等运行参数由负责人在这里维护,读取时设置值优先、代码默认值兜底。
*/
class TaxForecastLogic
{
private const PREFIX = 'business_tax_forecast';
/** 扁平键:设置组件会按点号展开存储,不能嵌套数组。 */
public const KEYS = [
'vat_small_rate' => self::PREFIX . '.vat_small_rate',
'vat_free_threshold' => self::PREFIX . '.vat_free_threshold',
'vat_general_enabled' => self::PREFIX . '.vat_general_enabled',
'surtax_city_rate' => self::PREFIX . '.surtax_city_rate',
'surtax_education_rate' => self::PREFIX . '.surtax_education_rate',
'surtax_local_education_rate' => self::PREFIX . '.surtax_local_education_rate',
'surtax_halved' => self::PREFIX . '.surtax_halved',
'stamp_duty_rate' => self::PREFIX . '.stamp_duty_rate',
'stamp_duty_halved' => self::PREFIX . '.stamp_duty_halved',
'cit_effective_rate' => self::PREFIX . '.cit_effective_rate',
'cit_threshold' => self::PREFIX . '.cit_threshold',
'individual_halved_threshold' => self::PREFIX . '.individual_halved_threshold',
'assessed_income_rate' => self::PREFIX . '.assessed_income_rate',
'income_forecast_source' => self::PREFIX . '.income_forecast_source',
];
public const INCOME_FORECAST_SOURCES = [
'last_month_actual' => '上月实际收入(bills.income)',
'cost_arrangement' => '公司成本安排的当月收入预测',
'manual' => '手工录入',
];
/** @return array<string,string> */
public static function defaults(): array
{
return [
'vat_small_rate' => '1',
'vat_free_threshold' => '300000',
'vat_general_enabled' => '1',
'surtax_city_rate' => '7',
'surtax_education_rate' => '3',
'surtax_local_education_rate' => '2',
'surtax_halved' => '1',
'stamp_duty_rate' => '0.03',
'stamp_duty_halved' => '1',
'cit_effective_rate' => '5',
'cit_threshold' => '3000000',
'individual_halved_threshold' => '2000000',
'assessed_income_rate' => '10',
'income_forecast_source' => 'last_month_actual',
];
}
/** @return array<string,string> */
public static function current(): array
{
$values = [];
foreach (self::KEYS as $key => $settingKey) {
$value = setting($settingKey, null);
$values[$key] = is_array($value) ? implode('', $value) : (string) ($value ?? '');
}
return array_merge(self::defaults(), array_filter($values, static fn (string $value): bool => $value !== ''));
}
public static function value(string $key, mixed $default = null): mixed
{
$current = self::current();
return $current[$key] ?? ($default ?? (self::defaults()[$key] ?? null));
}
public static function rate(string $key): float
{
return max((float) self::value($key, 0), 0);
}
public static function amount(string $key): float
{
return max((float) self::value($key, 0), 0);
}
public static function toggled(string $key): bool
{
return (string) self::value($key, '1') === '1';
}
/** @param array<string,mixed> $values */
public static function save(array $values): void
{
$payload = [];
foreach (self::defaults() as $key => $default) {
if (! array_key_exists($key, $values) || ! isset(self::KEYS[$key])) {
continue;
}
$payload[self::KEYS[$key]] = mb_substr(trim((string) $values[$key]), 0, 191);
}
if ($payload !== []) {
setting($payload)->save();
}
}
/**
* 规则目录:页面上的规则清单以这里为准,和取数公式文档
* docs/税金预测表需求说明.md 保持一致。
*
* @return array<string,array<int,array{code:string,object:string,logic:string,source:string,priority:string}>>
*/
public static function rules(): array
{
return [
'主体识别' => [
['code' => 'R1', 'object' => '公司名称、公司税务类型', 'logic' => '读取公司基础信息镜像的营业执照名称和纳税类别及类型,作为增值税口径判断依据。', 'source' => 'rpt.fys_companyInfo(只读镜像)', 'priority' => '税务类型空值时按“无税务登记”处理并提示补充。'],
['code' => 'R2', 'object' => '工商类型', 'logic' => '区分有限责任公司、个人独资企业、合伙企业、个体工商户,决定所得税种。', 'source' => '待接入:工商登记信息或人工维护', 'priority' => '缺失时所得税列留空,不按企业所得税估算。'],
['code' => 'R3', 'object' => '征收方式、核定应税所得率', 'logic' => '查账征收按利润计算;核定征收按 收入 × 核定应税所得率 计算;定期定额按核定定额折算。', 'source' => '待接入:电子税务局数据或人工维护', 'priority' => '缺失时按查账征收展示,并在备注列标记待确认。'],
],
'增值税' => [
['code' => 'R4', 'object' => '小规模纳税人免税判断', 'logic' => '季度不含税销售额不超过免税额度时,预计增值税为 0。', 'source' => 'bills.income 按主体期间归集', 'priority' => '免税额度按配置参数执行,默认 30 万元/季。'],
['code' => 'R5', 'object' => '小规模纳税人征收率', 'logic' => '超过免税额度时,预计增值税 = 不含税销售额 × 征收率。', 'source' => '配置参数', 'priority' => '默认 1%(3% 减按 1%)。'],
['code' => 'R6', 'object' => '一般纳税人计税', 'logic' => '预计增值税 = 销项税额 − 进项税额;进项缺失时按销项全额提示。', 'source' => 'bills.income 与进项数据', 'priority' => '默认开启,可在配置中关闭改为简易口径。'],
['code' => 'R7', 'object' => '个体工商户、个人独资企业、合伙企业', 'logic' => '增值税按小规模纳税人口径计算。', 'source' => '同上', 'priority' => '不因工商类型改为一般纳税人口径。'],
],
'附加税费' => [
['code' => 'R8', 'object' => '城建税', 'logic' => '城建税 = 预计增值税 × 城建税税率。', 'source' => '配置参数', 'priority' => '默认市区 7%,县城/镇 5%,其他 1%。'],
['code' => 'R9', 'object' => '教育费附加、地方教育附加', 'logic' => '教育费附加 = 增值税 × 3%;地方教育附加 = 增值税 × 2%。', 'source' => '配置参数', 'priority' => '税率可调。'],
['code' => 'R10', 'object' => '六税两费减半', 'logic' => '小规模纳税人、小型微利企业、个体工商户的附加税费减半征收。', 'source' => '配置开关', 'priority' => '默认开启;关闭后按全额计算。'],
],
'印花税' => [
['code' => 'R11', 'object' => '购销合同印花税', 'logic' => '印花税 = 购销合同金额 × 印花税税率(减半开关生效时再乘 50%)。', 'source' => '配置参数与成本安排金额', 'priority' => '默认 0.03% 并减半。'],
],
'所得税' => [
['code' => 'R12', 'object' => '小型微利企业所得税', 'logic' => '应纳税所得额 × 25% × 20%,年不超过上限部分实际税负 5%。', 'source' => '配置参数 + 预计利润', 'priority' => '默认上限 300 万元,超过部分提示分段计算。'],
['code' => 'R13', 'object' => '经营所得个人所得税', 'logic' => '个人独资企业、合伙企业、个体工商户按经营所得五级超额累进计算;核定征收按 收入 × 核定应税所得率 计算。', 'source' => '配置参数 + 核定通知书', 'priority' => '核定应税所得率默认 10%。'],
['code' => 'R14', 'object' => '个体工商户减半', 'logic' => '年应纳税所得额不超过上限部分,经营所得个人所得税减半征收。', 'source' => '配置参数', 'priority' => '默认上限 200 万元。'],
],
'数据来源' => [
['code' => 'R15', 'object' => '收入预测', 'logic' => '按主体生效/失效期间把 bills.income 归集到预测月份;未出账时按配置的收入预测口径取值。', 'source' => 'runtime/bills.sqlite', 'priority' => '口径可在配置中切换。'],
['code' => 'R16', 'object' => '成本费用预测', 'logic' => '供应商成本安排 + 快递成本安排 + 人工成本预测 + 其他费用预测。', 'source' => '公司成本安排页', 'priority' => '成本安排未接入时所得税列留空。'],
],
];
}
/**
* 数据源现状:只读共享业务镜像,不访问上游。
*
* @return array<int,array{name:string,object:string,status:string,detail:string}>
*/
public static function health(): array
{
$health = [
[
'name' => '公司基础信息(含税务类型)',
'object' => 'ops_fys_company_info',
'status' => '尚未同步',
'detail' => '',
],
[
'name' => '工商类型',
'object' => '待接入',
'status' => '未接入',
'detail' => '现有镜像无法区分个人独资企业、合伙企业;确认数据来源后接入。',
],
[
'name' => '征收方式与核定率',
'object' => '待接入',
'status' => '未接入',
'detail' => '查账/核定、核定应税所得率需电子税务局数据或人工维护。',
],
];
try {
$connection = DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
if ($connection->getSchemaBuilder()->hasTable('ops_fys_company_info')) {
$row = $connection->table('ops_fys_company_info')
->selectRaw("COUNT(*) AS total, SUM(CASE WHEN taxCategoryAndType = '小规模纳税人' THEN 1 ELSE 0 END) AS small, SUM(CASE WHEN taxCategoryAndType = '一般纳税人' THEN 1 ELSE 0 END) AS general")
->first();
$health[0]['status'] = '已同步';
$health[0]['detail'] = sprintf(
'主体 %d 家:小规模 %d 家、一般纳税人 %d 家',
(int) ($row->total ?? 0),
(int) ($row->small ?? 0),
(int) ($row->general ?? 0),
);
}
} catch (Throwable) {
$health[0]['detail'] = '读取共享镜像失败,请检查连接与同步状态。';
}
return $health;
}
}