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 */ 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 */ 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 $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> */ 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 */ 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; } }