183 lines
7.0 KiB
PHP
183 lines
7.0 KiB
PHP
<?php
|
|
|
|
namespace Modules\EcommerceFinance\Services;
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
/**
|
|
* 按账户滚动计算每日资金流水,并回写账户当前余额、每日可出金额和店铺余额。
|
|
*
|
|
* 计算口径固定在服务端,页面只读结果:
|
|
* 期末 = 期初 + 当日入账 - 当日出账
|
|
* 账户当前余额 = 最新一条流水期末
|
|
* 每日可出金额 = 当前余额 - 卡住金额
|
|
* 店铺余额 = 店铺所属公司名下账户当前余额合计(待财务确认)
|
|
*/
|
|
class FinanceLedgerCalculator
|
|
{
|
|
public function recalculate(): array
|
|
{
|
|
$connection = $this->connection();
|
|
$openingDate = (string) config('finance_ledger.opening_date', '2026-10-17');
|
|
$now = now();
|
|
|
|
$opening = $connection->table('fin_account_opening')
|
|
->where('opening_date', '<=', $openingDate)
|
|
->orderBy('opening_date')
|
|
->get()
|
|
->groupBy('account_short')
|
|
->map(static fn ($rows) => $rows->last());
|
|
|
|
$accountCompanies = $connection->table('fin_accounts')
|
|
->select(['account_short', 'company_name'])
|
|
->get()
|
|
->groupBy('account_short')
|
|
->map(static fn ($rows) => (string) ($rows->first()->company_name ?? ''));
|
|
|
|
$mapping = $connection->table('fin_account_map')
|
|
->whereNotNull('account_short')
|
|
->where('account_short', '<>', '')
|
|
->get(['source', 'source_value', 'account_short'])
|
|
->keyBy(static fn ($row): string => $row->source . "\0" . $row->source_value);
|
|
|
|
$transactions = $connection->table('fin_transactions')
|
|
->where('txn_date', '>=', $openingDate)
|
|
->orderBy('txn_date')
|
|
->orderBy('id')
|
|
->get(['txn_date', 'account_short', 'source', 'source_value', 'income', 'expense']);
|
|
|
|
$daily = [];
|
|
$balances = [];
|
|
|
|
foreach ($transactions as $transaction) {
|
|
$mapKey = $transaction->source . "\0" . $transaction->source_value;
|
|
$account = (string) ($mapping->get($mapKey)->account_short ?? '');
|
|
if ($account === '') {
|
|
$account = trim((string) $transaction->account_short);
|
|
}
|
|
if ($account === '' || ! $opening->has($account)) {
|
|
continue;
|
|
}
|
|
|
|
$date = (string) $transaction->txn_date;
|
|
$key = $account . "\0" . $date;
|
|
$daily[$key] ??= [
|
|
'account_short' => $account,
|
|
'flow_date' => $date,
|
|
'inflow' => 0.0,
|
|
'outflow' => 0.0,
|
|
];
|
|
$daily[$key]['inflow'] += (float) $transaction->income;
|
|
$daily[$key]['outflow'] += (float) $transaction->expense;
|
|
}
|
|
|
|
$flows = [];
|
|
$latest = [];
|
|
|
|
foreach ($opening as $account => $openingRow) {
|
|
$account = (string) $account;
|
|
$balance = (float) $openingRow->opening_balance;
|
|
$company = $accountCompanies->get($account, '');
|
|
$start = max((string) $openingRow->opening_date, $openingDate);
|
|
|
|
$rows = array_values(array_filter(
|
|
$daily,
|
|
static fn (array $row): bool => $row['account_short'] === $account,
|
|
));
|
|
usort($rows, static fn (array $a, array $b): int => strcmp($a['flow_date'], $b['flow_date']));
|
|
|
|
$openingFlow = [
|
|
'flow_date' => $start,
|
|
'company_name' => $company,
|
|
'account_short' => $account,
|
|
'opening_balance' => round($balance, 2),
|
|
'inflow' => 0.0,
|
|
'outflow' => 0.0,
|
|
'closing_balance' => round($balance, 2),
|
|
'remark' => '期初',
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
];
|
|
$flows[] = $openingFlow;
|
|
$openingIndex = array_key_last($flows);
|
|
|
|
foreach ($rows as $row) {
|
|
if ($row['flow_date'] < $start) {
|
|
continue;
|
|
}
|
|
$inflow = round((float) $row['inflow'], 2);
|
|
$outflow = round((float) $row['outflow'], 2);
|
|
if (abs($inflow) < 0.005 && abs($outflow) < 0.005) {
|
|
continue;
|
|
}
|
|
|
|
if ($row['flow_date'] === $start) {
|
|
// 期初日发生动账时合并到同一行,避免唯一键冲突。
|
|
$balance = round($balance + $inflow - $outflow, 2);
|
|
$flows[$openingIndex]['inflow'] = $inflow;
|
|
$flows[$openingIndex]['outflow'] = $outflow;
|
|
$flows[$openingIndex]['closing_balance'] = $balance;
|
|
$flows[$openingIndex]['remark'] = null;
|
|
continue;
|
|
}
|
|
|
|
$balance = round($balance + $inflow - $outflow, 2);
|
|
$flows[] = [
|
|
'flow_date' => $row['flow_date'],
|
|
'company_name' => $company,
|
|
'account_short' => $account,
|
|
'opening_balance' => round($balance - $inflow + $outflow, 2),
|
|
'inflow' => $inflow,
|
|
'outflow' => $outflow,
|
|
'closing_balance' => $balance,
|
|
'remark' => null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
];
|
|
}
|
|
|
|
$latest[$account] = $balance;
|
|
}
|
|
|
|
$connection->transaction(function () use ($connection, $flows, $latest, $now): void {
|
|
$connection->table('fin_daily_flows')->delete();
|
|
foreach (array_chunk($flows, 500) as $chunk) {
|
|
$connection->table('fin_daily_flows')->insert($chunk);
|
|
}
|
|
|
|
foreach ($latest as $account => $balance) {
|
|
$connection->table('fin_accounts')
|
|
->where('account_short', $account)
|
|
->update([
|
|
'current_balance' => round((float) $balance, 2),
|
|
'daily_available' => $connection->raw('ROUND(current_balance - blocked_amount, 2)'),
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
|
|
$companies = $connection->table('fin_accounts')
|
|
->selectRaw('company_name, SUM(current_balance) AS balance')
|
|
->groupBy('company_name')
|
|
->pluck('balance', 'company_name');
|
|
|
|
foreach ($companies as $company => $balance) {
|
|
$connection->table('fin_shops')
|
|
->where('company_name', (string) $company)
|
|
->update(['shop_balance' => round((float) $balance, 2), 'updated_at' => $now]);
|
|
}
|
|
});
|
|
|
|
return [
|
|
'accounts' => count($latest),
|
|
'flows' => count($flows),
|
|
'opening_date' => $openingDate,
|
|
'balance_sum' => round(array_sum($latest), 2),
|
|
];
|
|
}
|
|
|
|
private function connection()
|
|
{
|
|
return DB::connection((string) config('upstream_mirrors.connection', 'business_mirror'));
|
|
}
|
|
}
|