202 lines
11 KiB
PHP
202 lines
11 KiB
PHP
<?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;
|
||
}
|
||
}
|