CaiWuXiTong/modules/EcommerceFinance/Services/FinanceLedgerManualTransactionEditor.php

201 lines
6.6 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Carbon\CarbonImmutable;
use Illuminate\Database\ConnectionInterface;
use Illuminate\Database\DatabaseManager;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class FinanceLedgerManualTransactionEditor
{
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(int $id, array $data): array
{
$date = $this->date($data['txn_date'] ?? null);
$accountShort = $this->text($data['account_short'] ?? '');
$detail = $this->text($data['detail'] ?? '');
$incomeCents = $this->cents($data['income'] ?? 0);
$expenseCents = $this->cents($data['expense'] ?? 0);
$approvalNo = $this->nullableText($data['approval_no'] ?? null);
if ($detail === '') {
throw ValidationException::withMessages([
'detail' => '请填写明细。',
]);
}
if (($incomeCents > 0) === ($expenseCents > 0)) {
throw ValidationException::withMessages([
'income' => '收入和支出只能填写一项。',
]);
}
return $this->connection->transaction(function () use (
$id,
$date,
$accountShort,
$detail,
$incomeCents,
$expenseCents,
$approvalNo,
): array {
$before = $this->manualRow($id, true);
$account = $this->connection->table('fin_accounts')
->where('account_short', $accountShort)
->orderBy('id')
->first(['account_short', 'company_name']);
if ($account === null) {
throw ValidationException::withMessages([
'account_short' => '账户简称不存在,请从账户总表中选择。',
]);
}
$now = now();
$income = $incomeCents / 100;
$expense = $expenseCents / 100;
$after = [
'txn_date' => $date,
'account_short' => (string) $account->account_short,
'company_name' => (string) $account->company_name,
'detail' => $detail,
'income' => $income,
'expense' => $expense,
'approval_no' => $approvalNo,
];
$this->connection->table('fin_transactions')->where('id', $id)->update([
'txn_date' => $date,
'account_short' => $accountShort,
'detail' => $detail,
'income' => $income,
'expense' => $expense,
'approval_no' => $approvalNo,
'updated_by' => (string) (user()?->name ?? ''),
'updated_at' => $now,
]);
$this->audit(
'manual_transaction_update',
(string) $before['row_key'],
['before' => $before, 'after' => $after],
);
$this->calculator->recalculate();
return $after + [
'id' => $id,
'source' => FinanceLedgerTransactionWriter::MANUAL_SOURCE,
];
});
}
/**
* @return array<string,mixed>
*/
public function delete(int $id): array
{
return $this->connection->transaction(function () use ($id): array {
$before = $this->manualRow($id, true);
$this->connection->table('fin_transactions')->where('id', $id)->delete();
$this->audit(
'manual_transaction_delete',
(string) $before['row_key'],
['before' => $before, 'after' => null],
);
$this->calculator->recalculate();
return $before;
});
}
/**
* @return array<string,mixed>
*/
private function manualRow(int $id, bool $lock = false): array
{
$query = $this->connection->table('fin_transactions')
->where('id', $id)
->where('source', FinanceLedgerTransactionWriter::MANUAL_SOURCE);
if ($lock) {
$query->lockForUpdate();
}
$row = $query->first();
if ($row === null) {
throw new NotFoundHttpException('只能编辑或删除手工录入的动账明细。');
}
return [
'id' => (int) $row->id,
'row_key' => (string) $row->row_key,
'txn_date' => substr((string) $row->txn_date, 0, 10),
'account_short' => (string) ($row->account_short ?? ''),
'detail' => (string) ($row->detail ?? ''),
'income' => round((float) $row->income, 2),
'expense' => round((float) $row->expense, 2),
'approval_no' => $row->approval_no !== null ? (string) $row->approval_no : null,
];
}
/**
* @param array<string,mixed> $changes
*/
private function audit(string $entityType, string $rowKey, array $changes): void
{
$this->connection->table('fin_ledger_edit_events')->insert([
'entity_type' => $entityType,
'entity_key' => 'manual_transaction|' . $rowKey,
'changes' => json_encode($changes, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR),
'user_id' => user()?->id,
'user_name' => (string) (user()?->name ?? ''),
'created_at' => now(),
]);
}
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([
'txn_date' => '动账日期必须使用 YYYY-MM-DD 格式。',
]);
}
return $text;
}
private function cents(mixed $value): int
{
return (int) round((float) $value * 100);
}
private function text(mixed $value): string
{
$text = trim((string) $value);
return preg_replace('/\s+/u', ' ', $text) ?? $text;
}
private function nullableText(mixed $value): ?string
{
$text = $this->text($value);
return $text !== '' ? mb_substr($text, 0, 100) : null;
}
}