금융 거래 기록 흐름 공통화

This commit is contained in:
seo
2026-07-20 18:27:41 +09:00
parent 8a64d993c3
commit c86e5a1481
7 changed files with 383 additions and 413 deletions
+274
View File
@@ -0,0 +1,274 @@
<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/account_service.php';
require_once __DIR__ . '/card_billing_service.php';
function build_transaction_fingerprint(
int $userId,
int $accountId,
?int $relatedAccountId,
int $categoryId,
string $transactionType,
float $amount,
string $transactionDate,
?string $merchantName,
?string $description,
?int $sourceLoanId = null
): string {
$parts = [
$userId,
$accountId,
$relatedAccountId ?? 0,
$categoryId,
$transactionType,
number_format($amount, 2, '.', ''),
$transactionDate,
trim((string)$merchantName),
trim((string)$description),
];
if ($sourceLoanId !== null) {
$parts[] = $sourceLoanId;
}
return hash('sha256', implode('|', $parts));
}
function get_account_for_transaction(int $userId, int $accountId): ?array
{
$pdo = db();
$stmt = $pdo->prepare("
SELECT *
FROM accounts
WHERE id = ?
AND user_id = ?
LIMIT 1
");
$stmt->execute([$accountId, $userId]);
$row = $stmt->fetch();
return $row ?: null;
}
function get_transaction_billing_year_month(int $userId, int $accountId, string $transactionDate): ?string
{
$account = get_account_for_transaction($userId, $accountId);
return $account ? get_card_billing_year_month($account, $transactionDate) : null;
}
function transaction_category_type(string $transactionType): string
{
if ($transactionType === 'income') {
return 'income';
}
if ($transactionType === 'expense') {
return 'expense';
}
return 'transfer';
}
function parse_money_input($value): float
{
return (float)str_replace(',', '', trim((string)$value));
}
function build_transaction_payload_from_input(array $input, int $userId): array
{
$pdo = db();
$transactionType = (string)($input['transaction_type'] ?? 'expense');
$accountId = (int)($input['account_id'] ?? 0);
$categoryId = (int)($input['category_id'] ?? 0);
$amount = parse_money_input($input['amount'] ?? 0);
$transactionDate = (string)($input['transaction_date'] ?? date('Y-m-d'));
$merchantName = trim((string)($input['merchant_name'] ?? ''));
$description = trim((string)($input['description'] ?? ''));
$relatedAccountId = !empty($input['related_account_id']) ? (int)$input['related_account_id'] : null;
$isInstallment = (int)($input['is_installment'] ?? 0);
$installmentMonths = trim((string)($input['installment_months'] ?? '')) !== ''
? (int)$input['installment_months']
: null;
$installmentInterestRate = trim((string)($input['installment_interest_rate'] ?? '0')) !== ''
? (float)$input['installment_interest_rate']
: 0.0;
$installmentInterestTotal = trim((string)($input['installment_interest_total'] ?? '')) !== ''
? parse_money_input($input['installment_interest_total'])
: null;
$installmentTotalBilled = trim((string)($input['installment_total_billed'] ?? '')) !== ''
? parse_money_input($input['installment_total_billed'])
: null;
if ($amount <= 0) {
throw new RuntimeException('금액은 0보다 커야 합니다.');
}
if (!in_array($transactionType, ['income', 'expense', 'transfer', 'card_payment'], true)) {
throw new RuntimeException('거래 유형이 올바르지 않습니다.');
}
if ($accountId <= 0) {
throw new RuntimeException('주 계좌/카드를 선택하세요.');
}
$stmt = $pdo->prepare("SELECT id FROM accounts WHERE id = ? AND user_id = ? AND is_active = 1 LIMIT 1");
$stmt->execute([$accountId, $userId]);
if (!$stmt->fetch()) {
throw new RuntimeException('선택한 주 계좌/카드를 사용할 수 없습니다.');
}
if ($categoryId <= 0) {
throw new RuntimeException('카테고리를 선택하세요.');
}
$stmt = $pdo->prepare("
SELECT category_type
FROM categories
WHERE id = ?
AND user_id = ?
AND is_active = 1
LIMIT 1
");
$stmt->execute([$categoryId, $userId]);
$category = $stmt->fetch();
if (!$category) {
throw new RuntimeException('선택한 카테고리를 사용할 수 없습니다.');
}
if ((string)$category['category_type'] !== transaction_category_type($transactionType)) {
throw new RuntimeException('거래 유형과 카테고리 유형이 일치하지 않습니다.');
}
if (in_array($transactionType, ['transfer', 'card_payment'], true) && !$relatedAccountId) {
throw new RuntimeException('관련 계좌를 선택해야 합니다.');
}
if ($relatedAccountId) {
if ($relatedAccountId === $accountId) {
throw new RuntimeException('주 계좌와 관련 계좌는 같을 수 없습니다.');
}
$stmt = $pdo->prepare("SELECT id FROM accounts WHERE id = ? AND user_id = ? AND is_active = 1 LIMIT 1");
$stmt->execute([$relatedAccountId, $userId]);
if (!$stmt->fetch()) {
throw new RuntimeException('선택한 관련 계좌를 사용할 수 없습니다.');
}
}
if ($transactionType !== 'expense') {
$isInstallment = 0;
$installmentMonths = null;
$installmentInterestRate = 0.0;
$installmentInterestTotal = null;
$installmentTotalBilled = null;
}
return [
'user_id' => $userId,
'account_id' => $accountId,
'category_id' => $categoryId,
'transaction_type' => $transactionType,
'amount' => $amount,
'transaction_date' => $transactionDate,
'merchant_name' => $merchantName !== '' ? $merchantName : null,
'description' => $description !== '' ? $description : null,
'related_account_id' => $relatedAccountId,
'is_installment' => $isInstallment,
'installment_months' => $installmentMonths,
'installment_interest_rate' => $installmentInterestRate,
'installment_interest_total' => $installmentInterestTotal,
'installment_total_billed' => $installmentTotalBilled,
];
}
function insert_transaction_record(array $data, bool $skipIfDuplicate = true): int
{
$pdo = db();
$sourceLoanId = !empty($data['source_loan_id']) ? (int)$data['source_loan_id'] : null;
$fingerprint = build_transaction_fingerprint(
(int)$data['user_id'],
(int)$data['account_id'],
!empty($data['related_account_id']) ? (int)$data['related_account_id'] : null,
(int)$data['category_id'],
(string)$data['transaction_type'],
(float)$data['amount'],
(string)$data['transaction_date'],
$data['merchant_name'] ?? null,
$data['description'] ?? null,
$sourceLoanId
);
if ($skipIfDuplicate) {
$stmt = $pdo->prepare("
SELECT id
FROM transactions
WHERE user_id = ?
AND fingerprint = ?
LIMIT 1
");
$stmt->execute([$data['user_id'], $fingerprint]);
$exists = $stmt->fetch();
if ($exists) {
return (int)$exists['id'];
}
}
$billingYearMonth = get_transaction_billing_year_month(
(int)$data['user_id'],
(int)$data['account_id'],
(string)$data['transaction_date']
);
$stmt = $pdo->prepare("
INSERT INTO transactions
(
user_id,
account_id,
category_id,
transaction_type,
amount,
is_installment,
installment_months,
installment_interest_rate,
installment_interest_total,
installment_total_billed,
installment_prepay_amount,
transaction_date,
billing_year_month,
merchant_name,
description,
related_account_id,
source_loan_id,
fingerprint
)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
");
$stmt->execute([
$data['user_id'],
$data['account_id'],
$data['category_id'],
$data['transaction_type'],
$data['amount'],
!empty($data['is_installment']) ? 1 : 0,
$data['installment_months'] ?? null,
$data['installment_interest_rate'] ?? 0,
$data['installment_interest_total'] ?? 0,
$data['installment_total_billed'] ?? null,
$data['installment_prepay_amount'] ?? 0,
$data['transaction_date'],
$billingYearMonth,
$data['merchant_name'] ?? null,
$data['description'] ?? null,
$data['related_account_id'] ?? null,
$sourceLoanId,
$fingerprint,
]);
return (int)$pdo->lastInsertId();
}