CaiWuXiTong/modules/EcommerceFinance/Services/InternalFlowProjection.php
DoDo 0174a056ac feat: 联动内部流转预计销量并支持供应商收票
预计销量自动读取主体维度经营报表上月业绩;开票与进项成本按 20%/25%/80% 计算;供应商收票支持人工覆盖并跨导入保留。
2026-11-09 12:38:19 +08:00

103 lines
3.4 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Cache;
use Modules\EcommerceFinance\Repositories\BusinessInformationMirrorRepository;
use Modules\EcommerceFinance\Repositories\StoreReportRepository;
use Throwable;
/**
* 内部流转预计销量取自主体维度经营报表的上月业绩。
*/
class InternalFlowProjection
{
public const BUYER_INVOICE_RATE = 0.20;
public const INTERNAL_INVOICE_RATE = 0.25;
public const INPUT_COST_RATE = 0.80;
public function __construct(
private readonly BusinessInformationMirrorRepository $businessInformation,
private readonly StoreReportRepository $storeReports,
) {}
/**
* @return array{
* forecast_month:string,
* forecast_month_label:string,
* source_month:string,
* source_month_label:string,
* sales_by_company:array<string,float|null>
* }
*/
public function forMonth(?CarbonImmutable $asOf = null): array
{
$forecastMonth = ($asOf ?? CarbonImmutable::now())->startOfMonth();
$sourceMonth = $forecastMonth->subMonthNoOverflow();
$cacheKey = 'internal-flow-projection:v1:' . $sourceMonth->format('Y-m');
$resolve = fn (): array => $this->build($forecastMonth, $sourceMonth);
try {
return Cache::remember($cacheKey, now()->addMinutes(10), $resolve);
} catch (Throwable $exception) {
report($exception);
return $resolve();
}
}
/**
* @return array{
* forecast_month:string,
* forecast_month_label:string,
* source_month:string,
* source_month_label:string,
* sales_by_company:array<string,float|null>
* }
*/
private function build(CarbonImmutable $forecastMonth, CarbonImmutable $sourceMonth): array
{
$stores = $this->businessInformation->receivingAccountStores();
foreach ($stores as &$store) {
// 标记系统主体已知,避免 StoreReportPeriodBuilder 再次按店铺批量查询主体。
$store['system_company_name'] = $store['company_name'] ?? null;
}
unset($store);
$rows = $this->storeReports->rows($stores, $sourceMonth->year, []);
$incomeKey = sprintf('income_%02d', $sourceMonth->month);
$totals = [];
foreach ($rows as $row) {
$entity = trim((string) ($row['entity'] ?? ''));
if ($entity === '') {
continue;
}
$totals[$entity] ??= ['has_value' => false, 'amount' => 0.0];
$value = $row[$incomeKey] ?? null;
if ($value === null || $value === '') {
continue;
}
$totals[$entity]['has_value'] = true;
$totals[$entity]['amount'] += (float) $value;
}
return [
'forecast_month' => $forecastMonth->format('Y-m'),
'forecast_month_label' => $forecastMonth->format('n') . '月',
'source_month' => $sourceMonth->format('Y-m'),
'source_month_label' => $sourceMonth->format('n') . '月',
'sales_by_company' => array_map(
static fn (array $total): ?float => $total['has_value']
? round((float) $total['amount'], 6)
: null,
$totals,
),
];
}
}