441 lines
14 KiB
PHP
441 lines
14 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';
|
|
|
|
function build_transaction_fingerprint(
|
|
int $userId,
|
|
int $accountId,
|
|
?int $relatedAccountId,
|
|
int $categoryId,
|
|
string $transactionType,
|
|
float $amount,
|
|
string $transactionDate,
|
|
?string $merchantName,
|
|
?string $description
|
|
): string {
|
|
$raw = implode('|', [
|
|
$userId,
|
|
$accountId,
|
|
$relatedAccountId ?? 0,
|
|
$categoryId,
|
|
$transactionType,
|
|
number_format($amount, 2, '.', ''),
|
|
$transactionDate,
|
|
trim((string)$merchantName),
|
|
trim((string)$description),
|
|
]);
|
|
|
|
return hash('sha256', $raw);
|
|
}
|
|
|
|
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 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;
|
|
}
|
|
}
|
|
|
|
$account = get_account_for_transaction(
|
|
(int)$data['user_id'],
|
|
(int)$data['account_id']
|
|
);
|
|
|
|
$billingYearMonth = null;
|
|
if ($account) {
|
|
$billingYearMonth = get_card_billing_year_month(
|
|
$account,
|
|
(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;
|
|
|
|
$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;
|
|
}
|
|
|
|
$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,
|
|
fingerprint
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0, ?, ?, ?, ?, ?, ?)
|
|
");
|
|
|
|
$stmt->execute([
|
|
$data['user_id'],
|
|
$data['account_id'],
|
|
$data['category_id'],
|
|
$data['transaction_type'],
|
|
$data['amount'],
|
|
$isInstallment,
|
|
$installmentMonths,
|
|
$installmentInterestRate,
|
|
$installmentInterestTotal,
|
|
$installmentTotalBilled,
|
|
$data['transaction_date'],
|
|
$billingYearMonth,
|
|
$data['merchant_name'],
|
|
$data['description'],
|
|
$data['related_account_id'],
|
|
$fingerprint,
|
|
]);
|
|
|
|
$transactionId = (int)$pdo->lastInsertId();
|
|
$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
|
|
);
|
|
|
|
$account = get_account_for_transaction(
|
|
$userId,
|
|
(int)$data['account_id']
|
|
);
|
|
|
|
$billingYearMonth = null;
|
|
if ($account) {
|
|
$billingYearMonth = get_card_billing_year_month(
|
|
$account,
|
|
(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;
|
|
}
|
|
} |