CaiWuXiTong/modules/EcommerceFinance/Services/FinanceLedgerFlowEditor.php
92451 040d0d08ae feat: 接入公司支付宝账单与台账手动编辑 (#797)
接入公司非店铺支付宝账单、受限批处理导入,并为账户总表及每日资金流水增加管理员手动编辑与审计。
Co-authored-by: 92451 <924518135@qq.com>
Co-committed-by: 92451 <924518135@qq.com>
2026-10-30 20:03:04 +08:00

208 lines
7.4 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Carbon\CarbonImmutable;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\DatabaseManager;
use Illuminate\Validation\ValidationException;
class FinanceLedgerFlowEditor
{
private readonly ConnectionInterface $connection;
public function __construct(
DatabaseManager $databases,
private readonly FinanceLedgerCalculator $calculator,
) {
$this->connection = $databases->connection(
(string) config('upstream_mirrors.connection', 'business_mirror'),
);
}
/**
* @param array<string,mixed> $data
* @return array<string,mixed>
*/
public function update(array $data): array
{
$date = $this->date($data['flow_date'] ?? null);
$accountShort = $this->text($data['account_short'] ?? '');
$openingCents = $this->cents($data['opening_balance'] ?? 0);
$inflowCents = $this->cents($data['inflow'] ?? 0);
$outflowCents = $this->cents($data['outflow'] ?? 0);
$closingCents = $this->cents($data['closing_balance'] ?? 0);
$opening = $openingCents / 100;
$inflow = $inflowCents / 100;
$outflow = $outflowCents / 100;
$closing = $closingCents / 100;
$remark = $this->nullableText($data['remark'] ?? null);
if ($openingCents + $inflowCents - $outflowCents !== $closingCents) {
throw ValidationException::withMessages([
'closing_balance' => '期末余额必须等于期初余额 + 当日入账 - 当日出账。',
]);
}
$this->connection->transaction(function () use (
$date,
$accountShort,
$opening,
$inflow,
$outflow,
$closing,
$remark,
): void {
$account = $this->connection->table('fin_accounts')
->where('account_short', $accountShort)
->lockForUpdate()
->first();
if ($account === null) {
throw ValidationException::withMessages([
'account_short' => '账户简称不存在,请从账户总表中选择。',
]);
}
$before = $this->existingFlow($date, $accountShort);
$values = [
'opening_balance' => $opening,
'inflow' => $inflow,
'outflow' => $outflow,
'closing_balance' => $closing,
'remark' => $remark,
'updated_by' => (string) (user()?->name ?? ''),
'updated_at' => now(),
];
$existing = $this->connection->table('fin_daily_flow_overrides')
->where('flow_date', $date)
->where('account_short', $accountShort)
->first();
if ($existing === null) {
$this->connection->table('fin_daily_flow_overrides')->insert($values + [
'flow_date' => $date,
'account_short' => $accountShort,
'created_at' => now(),
]);
} else {
$this->connection->table('fin_daily_flow_overrides')
->where('id', $existing->id)
->update($values);
}
$this->connection->table('fin_ledger_edit_events')->insert([
'entity_type' => 'daily_flow',
'entity_key' => $date . '|' . $accountShort,
'changes' => json_encode([
'before' => $before,
'after' => [
'flow_date' => $date,
'account_short' => $accountShort,
'opening_balance' => $opening,
'inflow' => $inflow,
'outflow' => $outflow,
'closing_balance' => $closing,
'remark' => $remark,
],
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
'user_id' => user()?->id,
'user_name' => (string) (user()?->name ?? ''),
'created_at' => now(),
]);
$this->calculator->recalculate();
});
return $this->existingFlow($date, $accountShort) ?? [];
}
/**
* @return array<string,mixed>|null
*/
public function reset(string $flowDate, string $accountShort): ?array
{
$date = $this->date($flowDate);
$accountShort = $this->text($accountShort);
$this->connection->transaction(function () use ($date, $accountShort): void {
$before = $this->existingFlow($date, $accountShort);
$deleted = $this->connection->table('fin_daily_flow_overrides')
->where('flow_date', $date)
->where('account_short', $accountShort)
->delete();
if ($deleted > 0) {
$this->connection->table('fin_ledger_edit_events')->insert([
'entity_type' => 'daily_flow_reset',
'entity_key' => $date . '|' . $accountShort,
'changes' => json_encode([
'before' => $before,
'after' => null,
], JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
'user_id' => user()?->id,
'user_name' => (string) (user()?->name ?? ''),
'created_at' => now(),
]);
}
$this->calculator->recalculate();
});
return $this->existingFlow($date, $accountShort);
}
/** @return array<string,mixed>|null */
public function existingFlow(string $date, string $accountShort): ?array
{
$row = $this->connection->table('fin_daily_flows')
->whereDate('flow_date', $date)
->where('account_short', $accountShort)
->first();
if ($row === null) {
return null;
}
return [
'id' => (int) $row->id,
'flow_date' => substr((string) $row->flow_date, 0, 10),
'company_name' => $row->company_name,
'account_short' => $row->account_short,
'opening_balance' => round((float) $row->opening_balance, 2),
'inflow' => round((float) $row->inflow, 2),
'outflow' => round((float) $row->outflow, 2),
'closing_balance' => round((float) $row->closing_balance, 2),
'remark' => $row->remark,
'manual_override' => (bool) ($row->manual_override ?? false),
'updated_by' => $row->updated_by ?? null,
];
}
private function date(mixed $value): string
{
$text = trim((string) $value);
$date = CarbonImmutable::createFromFormat('!Y-m-d', $text);
if (! $date instanceof CarbonImmutable || $date->format('Y-m-d') !== $text) {
throw ValidationException::withMessages([
'flow_date' => '流水日期必须使用 YYYY-MM-DD 格式。',
]);
}
return $text;
}
private function cents(mixed $value): int
{
return (int) round((float) $value * 100);
}
private function text(mixed $value): string
{
return preg_replace('/\s+/u', ' ', trim((string) $value)) ?? trim((string) $value);
}
private function nullableText(mixed $value): ?string
{
$text = $this->text($value);
return $text !== '' ? $text : null;
}
}