Initial financial project import
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user