接入公司非店铺支付宝账单、受限批处理导入,并为账户总表及每日资金流水增加管理员手动编辑与审计。 Co-authored-by: 92451 <924518135@qq.com> Co-committed-by: 92451 <924518135@qq.com>
208 lines
7.4 KiB
PHP
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;
|
|
}
|
|
}
|