Files
financial/app/lib/transaction_service.php

343 lines
11 KiB
PHP

<?php
require_once __DIR__ . '/db.php';
require_once __DIR__ . '/account_service.php';
require_once __DIR__ . '/installment_service.php';
require_once __DIR__ . '/helpers.php';
require_once __DIR__ . '/card_billing_service.php';
require_once __DIR__ . '/transaction_record_service.php';
function create_transaction(array $data, bool $skipIfDuplicate = false): bool
{
$pdo = db();
$pdo->beginTransaction();
try {
$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
);
if ($skipIfDuplicate) {
$stmt = $pdo->prepare("
SELECT id
FROM transactions
WHERE user_id = ?
AND fingerprint = ?
LIMIT 1
");
$stmt->execute([$data['user_id'], $fingerprint]);
if ($stmt->fetch()) {
$pdo->commit();
return false;
}
}
$isInstallment = !empty($data['is_installment']) ? 1 : 0;
$installmentMonths = !empty($data['installment_months']) ? (int)$data['installment_months'] : null;
$installmentInterestRate = !empty($data['installment_interest_rate']) ? (float)$data['installment_interest_rate'] : 0.0;
$installmentInterestTotal =
isset($data['installment_interest_total']) &&
$data['installment_interest_total'] !== null &&
$data['installment_interest_total'] !== ''
? (float)$data['installment_interest_total']
: null;
$installmentTotalBilled =
isset($data['installment_total_billed']) &&
$data['installment_total_billed'] !== null &&
$data['installment_total_billed'] !== ''
? (float)$data['installment_total_billed']
: null;
if ($isInstallment && ($installmentMonths === null || $installmentMonths <= 1)) {
throw new RuntimeException('할부 개월 수가 올바르지 않습니다.');
}
if ($isInstallment) {
if ($installmentInterestTotal === null) {
$installmentInterestTotal = calculate_installment_interest_total(
(float)$data['amount'],
$installmentMonths,
$installmentInterestRate
);
}
$expectedTotal = round(
(float)$data['amount'] + (float)$installmentInterestTotal,
2
);
if ($installmentTotalBilled === null) {
$installmentTotalBilled = $expectedTotal;
}
if (round($installmentTotalBilled, 2) !== $expectedTotal) {
throw new RuntimeException('총 청구금액은 원금 + 할부이자와 같아야 합니다.');
}
} else {
$installmentMonths = null;
$installmentInterestRate = 0.0;
$installmentInterestTotal = 0.0;
$installmentTotalBilled = null;
}
$transactionId = insert_transaction_record(array_merge($data, [
'is_installment' => $isInstallment,
'installment_months' => $installmentMonths,
'installment_interest_rate' => $installmentInterestRate,
'installment_interest_total' => $installmentInterestTotal,
'installment_total_billed' => $installmentTotalBilled,
]), false);
$pdo->commit();
if (
$data['transaction_type'] === 'expense' &&
$isInstallment === 1 &&
$installmentMonths !== null &&
$installmentMonths > 1
) {
create_installment_schedule(
(int)$data['user_id'],
$transactionId,
(int)$data['account_id'],
(float)$data['amount'],
$installmentMonths,
(string)$data['transaction_date'],
$installmentInterestRate,
$installmentInterestTotal,
$installmentTotalBilled,
$installmentInterestRate > 0 ? 'fixed_total' : 'none'
);
}
if (
$data['transaction_type'] === 'card_payment' &&
!empty($data['related_account_id'])
) {
$yearMonth = date('Y-m', strtotime((string)$data['transaction_date']));
$processedCount = mark_installment_month_billed_for_card_payment(
(int)$data['user_id'],
(int)$data['related_account_id'],
$yearMonth
);
if ($processedCount > 0) {
set_flash_message(
'success',
$yearMonth . ' 할부 청구 회차 ' . intvalf($processedCount) . '건 자동 처리'
);
}
}
recalculate_account_balance((int)$data['account_id']);
if (!empty($data['related_account_id'])) {
recalculate_account_balance((int)$data['related_account_id']);
}
return true;
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
}
function update_transaction(int $transactionId, int $userId, array $data): void
{
$pdo = db();
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("
SELECT account_id, related_account_id
FROM transactions
WHERE id = ?
AND user_id = ?
LIMIT 1
");
$stmt->execute([$transactionId, $userId]);
$old = $stmt->fetch();
if (!$old) {
throw new RuntimeException('거래를 찾을 수 없습니다.');
}
$fingerprint = build_transaction_fingerprint(
$userId,
(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
);
$billingYearMonth = get_transaction_billing_year_month(
$userId,
(int)$data['account_id'],
(string)$data['transaction_date']
);
$isInstallment = !empty($data['is_installment']) ? 1 : 0;
$installmentMonths = !empty($data['installment_months']) ? (int)$data['installment_months'] : null;
$installmentInterestRate = !empty($data['installment_interest_rate']) ? (float)$data['installment_interest_rate'] : 0.0;
$stmt = $pdo->prepare("
UPDATE transactions
SET
account_id = ?,
category_id = ?,
transaction_type = ?,
amount = ?,
is_installment = ?,
installment_months = ?,
installment_interest_rate = ?,
installment_interest_total = ?,
installment_total_billed = ?,
transaction_date = ?,
billing_year_month = ?,
merchant_name = ?,
description = ?,
related_account_id = ?,
fingerprint = ?
WHERE id = ?
AND user_id = ?
");
$stmt->execute([
$data['account_id'],
$data['category_id'],
$data['transaction_type'],
$data['amount'],
$isInstallment,
$installmentMonths,
$installmentInterestRate,
$data['installment_interest_total'] ?? 0,
$data['installment_total_billed'] ?? null,
$data['transaction_date'],
$billingYearMonth,
$data['merchant_name'],
$data['description'],
$data['related_account_id'],
$fingerprint,
$transactionId,
$userId
]);
delete_installment_by_transaction_id($transactionId);
if (
$data['transaction_type'] === 'expense' &&
$isInstallment === 1 &&
$installmentMonths !== null &&
$installmentMonths > 1
) {
$interestTotal = calculate_installment_interest_total(
(float)$data['amount'],
$installmentMonths,
$installmentInterestRate
);
$totalBilled = round(
(float)$data['amount'] + (float)$interestTotal,
2
);
create_installment_schedule(
$userId,
$transactionId,
(int)$data['account_id'],
(float)$data['amount'],
$installmentMonths,
(string)$data['transaction_date'],
$installmentInterestRate,
$interestTotal,
$totalBilled,
$installmentInterestRate > 0 ? 'fixed_total' : 'none'
);
}
$pdo->commit();
$recalcIds = array_unique(array_filter([
(int)$old['account_id'],
!empty($old['related_account_id']) ? (int)$old['related_account_id'] : null,
(int)$data['account_id'],
!empty($data['related_account_id']) ? (int)$data['related_account_id'] : null,
]));
foreach ($recalcIds as $id) {
recalculate_account_balance((int)$id);
}
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
}
function delete_transaction(int $transactionId, int $userId): void
{
$pdo = db();
$pdo->beginTransaction();
try {
$stmt = $pdo->prepare("
SELECT account_id, related_account_id
FROM transactions
WHERE id = ?
AND user_id = ?
LIMIT 1
");
$stmt->execute([$transactionId, $userId]);
$row = $stmt->fetch();
if (!$row) {
throw new RuntimeException('거래를 찾을 수 없습니다.');
}
delete_installment_by_transaction_id($transactionId);
$stmt = $pdo->prepare("
DELETE FROM transactions
WHERE id = ?
AND user_id = ?
");
$stmt->execute([$transactionId, $userId]);
$pdo->commit();
recalculate_account_balance((int)$row['account_id']);
if (!empty($row['related_account_id'])) {
recalculate_account_balance((int)$row['related_account_id']);
}
} catch (Throwable $e) {
if ($pdo->inTransaction()) {
$pdo->rollBack();
}
throw $e;
}
}