201 lines
6.6 KiB
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;
|
|
}
|
|
}
|