금융 거래 기록 흐름 공통화
This commit is contained in:
@@ -32,6 +32,7 @@ Financial은 개인 금융 데이터를 서버 렌더링 PHP 화면에서 관리
|
|||||||
- `app/lib/auth.php`: 인증, remember token, CSRF, no-store header
|
- `app/lib/auth.php`: 인증, remember token, CSRF, no-store header
|
||||||
- `app/lib/db.php`: PDO 연결
|
- `app/lib/db.php`: PDO 연결
|
||||||
- `app/lib/account_service.php`: 계좌 서비스
|
- `app/lib/account_service.php`: 계좌 서비스
|
||||||
|
- `app/lib/transaction_record_service.php`: 거래 fingerprint, 입력 검증, 공통 거래 row 기록
|
||||||
- `app/lib/transaction_service.php`: 거래 저장과 조회
|
- `app/lib/transaction_service.php`: 거래 저장과 조회
|
||||||
- `app/lib/installment_service.php`: 할부 청구와 선납 처리
|
- `app/lib/installment_service.php`: 할부 청구와 선납 처리
|
||||||
- `app/lib/loan_service.php`: 대출과 상환 처리
|
- `app/lib/loan_service.php`: 대출과 상환 처리
|
||||||
@@ -58,9 +59,12 @@ Financial은 개인 금융 데이터를 서버 렌더링 PHP 화면에서 관리
|
|||||||
4. 저장 후 redirect와 flash 메시지로 응답합니다.
|
4. 저장 후 redirect와 flash 메시지로 응답합니다.
|
||||||
5. 카테고리 추천 API는 merchant rule 우선순위와 confidence를 반환합니다.
|
5. 카테고리 추천 API는 merchant rule 우선순위와 confidence를 반환합니다.
|
||||||
|
|
||||||
|
거래 테이블에 실제 row를 추가하는 공통 경로는 `transaction_record_service.php`입니다. 일반 거래 등록/수정은 같은 입력 검증 함수를 쓰고, 대출 실행/상환/중도상환과 할부 선결제도 공통 기록 함수를 통해 `transactions`에 남깁니다. 카드/계좌/할부/대출 기능을 바꿀 때는 먼저 이 공통 기록 규칙과 잔액 재계산 흐름을 확인해야 합니다.
|
||||||
|
|
||||||
## 주요 함수/모듈
|
## 주요 함수/모듈
|
||||||
|
|
||||||
- `auth.php`: 로그인, 세션, CSRF
|
- `auth.php`: 로그인, 세션, CSRF
|
||||||
|
- `transaction_record_service.php`: 거래 공통 검증과 row 기록
|
||||||
- `transaction_service.php`: 거래 저장과 조회
|
- `transaction_service.php`: 거래 저장과 조회
|
||||||
- `installment_*`: 할부 청구와 선납 처리
|
- `installment_*`: 할부 청구와 선납 처리
|
||||||
- `merchant_rules.php`: 가맹점 자동분류 규칙 관리
|
- `merchant_rules.php`: 가맹점 자동분류 규칙 관리
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
require_once __DIR__ . '/db.php';
|
require_once __DIR__ . '/db.php';
|
||||||
require_once __DIR__ . '/account_service.php';
|
require_once __DIR__ . '/account_service.php';
|
||||||
require_once __DIR__ . '/card_billing_service.php';
|
require_once __DIR__ . '/card_billing_service.php';
|
||||||
|
require_once __DIR__ . '/transaction_record_service.php';
|
||||||
|
|
||||||
function split_amount_evenly(float $amount, int $months): array
|
function split_amount_evenly(float $amount, int $months): array
|
||||||
{
|
{
|
||||||
@@ -547,41 +548,8 @@ function prepay_installment(
|
|||||||
|
|
||||||
recalculate_installment_status($installmentId);
|
recalculate_installment_status($installmentId);
|
||||||
|
|
||||||
$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,
|
|
||||||
merchant_name,
|
|
||||||
description,
|
|
||||||
related_account_id,
|
|
||||||
fingerprint
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, 'expense', ?, 0, NULL, 0, 0, NULL, ?, ?, ?, ?, NULL, ?)
|
|
||||||
");
|
|
||||||
|
|
||||||
$desc = $description ?: '할부 선결제/중도상환';
|
$desc = $description ?: '할부 선결제/중도상환';
|
||||||
|
|
||||||
$fingerprint = hash('sha256', implode('|', [
|
|
||||||
$userId,
|
|
||||||
$paymentAccountId,
|
|
||||||
'installment_prepay',
|
|
||||||
$installmentId,
|
|
||||||
$prepayDate,
|
|
||||||
number_format($totalAmount, 2, '.', ''),
|
|
||||||
$desc
|
|
||||||
]));
|
|
||||||
|
|
||||||
$stmtCat = $pdo->prepare("
|
$stmtCat = $pdo->prepare("
|
||||||
SELECT id
|
SELECT id
|
||||||
FROM categories
|
FROM categories
|
||||||
@@ -597,17 +565,18 @@ function prepay_installment(
|
|||||||
throw new RuntimeException('선결제 기록용 expense 카테고리(기타지출)를 찾을 수 없습니다.');
|
throw new RuntimeException('선결제 기록용 expense 카테고리(기타지출)를 찾을 수 없습니다.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt->execute([
|
insert_transaction_record([
|
||||||
$userId,
|
'user_id' => $userId,
|
||||||
$paymentAccountId,
|
'account_id' => $paymentAccountId,
|
||||||
(int)$category['id'],
|
'category_id' => (int)$category['id'],
|
||||||
$totalAmount,
|
'transaction_type' => 'expense',
|
||||||
$totalAmount,
|
'amount' => $totalAmount,
|
||||||
$prepayDate,
|
'transaction_date' => $prepayDate,
|
||||||
$installment['merchant_name'],
|
'merchant_name' => $installment['merchant_name'],
|
||||||
'[할부 선결제] ' . $desc,
|
'description' => '[할부 선결제] ' . $installmentId . ' / ' . $desc,
|
||||||
$fingerprint
|
'related_account_id' => null,
|
||||||
]);
|
'installment_prepay_amount' => $totalAmount,
|
||||||
|
], true);
|
||||||
|
|
||||||
$pdo->commit();
|
$pdo->commit();
|
||||||
|
|
||||||
|
|||||||
+26
-101
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
require_once __DIR__ . '/db.php';
|
require_once __DIR__ . '/db.php';
|
||||||
require_once __DIR__ . '/account_service.php';
|
require_once __DIR__ . '/account_service.php';
|
||||||
|
require_once __DIR__ . '/transaction_record_service.php';
|
||||||
|
|
||||||
function add_months_keep_day(string $date, int $months, int $paymentDay): string
|
function add_months_keep_day(string $date, int $months, int $paymentDay): string
|
||||||
{
|
{
|
||||||
@@ -176,109 +177,9 @@ function get_category_id_by_name(int $userId, string $categoryType, string $name
|
|||||||
return $row ? (int)$row['id'] : null;
|
return $row ? (int)$row['id'] : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function build_tx_fingerprint(
|
|
||||||
int $userId,
|
|
||||||
int $accountId,
|
|
||||||
?int $relatedAccountId,
|
|
||||||
int $categoryId,
|
|
||||||
string $transactionType,
|
|
||||||
float $amount,
|
|
||||||
string $transactionDate,
|
|
||||||
?string $merchantName,
|
|
||||||
?string $description,
|
|
||||||
?int $sourceLoanId = null
|
|
||||||
): string {
|
|
||||||
$raw = implode('|', [
|
|
||||||
$userId,
|
|
||||||
$accountId,
|
|
||||||
$relatedAccountId ?? 0,
|
|
||||||
$categoryId,
|
|
||||||
$transactionType,
|
|
||||||
number_format($amount, 2, '.', ''),
|
|
||||||
$transactionDate,
|
|
||||||
trim((string)$merchantName),
|
|
||||||
trim((string)$description),
|
|
||||||
$sourceLoanId ?? 0,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return hash('sha256', $raw);
|
|
||||||
}
|
|
||||||
|
|
||||||
function insert_transaction_row(array $data): int
|
function insert_transaction_row(array $data): int
|
||||||
{
|
{
|
||||||
$pdo = db();
|
return insert_transaction_record($data, true);
|
||||||
|
|
||||||
$sourceLoanId = !empty($data['source_loan_id']) ? (int)$data['source_loan_id'] : null;
|
|
||||||
|
|
||||||
$fingerprint = build_tx_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
|
|
||||||
);
|
|
||||||
|
|
||||||
$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'];
|
|
||||||
}
|
|
||||||
|
|
||||||
$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,
|
|
||||||
merchant_name,
|
|
||||||
description,
|
|
||||||
related_account_id,
|
|
||||||
source_loan_id,
|
|
||||||
fingerprint
|
|
||||||
)
|
|
||||||
VALUES (?, ?, ?, ?, ?, 0, NULL, 0, 0, NULL, 0, ?, ?, ?, ?, ?, ?)
|
|
||||||
");
|
|
||||||
|
|
||||||
$stmt->execute([
|
|
||||||
$data['user_id'],
|
|
||||||
$data['account_id'],
|
|
||||||
$data['category_id'],
|
|
||||||
$data['transaction_type'],
|
|
||||||
$data['amount'],
|
|
||||||
$data['transaction_date'],
|
|
||||||
$data['merchant_name'],
|
|
||||||
$data['description'],
|
|
||||||
$data['related_account_id'],
|
|
||||||
$sourceLoanId,
|
|
||||||
$fingerprint,
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (int)$pdo->lastInsertId();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function create_loan_schedule_only(
|
function create_loan_schedule_only(
|
||||||
@@ -940,6 +841,30 @@ function prepay_loan(
|
|||||||
$description,
|
$description,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if (!empty($accountId) && $totalAmount > 0) {
|
||||||
|
$expenseCategoryId = get_category_id_by_name($userId, 'expense', '대출상환');
|
||||||
|
|
||||||
|
if ($expenseCategoryId) {
|
||||||
|
insert_transaction_row([
|
||||||
|
'user_id' => $userId,
|
||||||
|
'account_id' => $accountId,
|
||||||
|
'category_id' => $expenseCategoryId,
|
||||||
|
'transaction_type' => 'expense',
|
||||||
|
'amount' => $totalAmount,
|
||||||
|
'transaction_date' => $paymentDate,
|
||||||
|
'merchant_name' => $loan['loan_name'],
|
||||||
|
'description' => '[대출중도상환] 원금 ' .
|
||||||
|
number_format($principalAmount, 0) .
|
||||||
|
' / 이자 ' .
|
||||||
|
number_format($interestAmount, 0) .
|
||||||
|
' / 수수료 ' .
|
||||||
|
number_format($feeAmount, 0),
|
||||||
|
'related_account_id' => null,
|
||||||
|
'source_loan_id' => $loanId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($principalAmount > 0) {
|
if ($principalAmount > 0) {
|
||||||
$stmt = $pdo->prepare("
|
$stmt = $pdo->prepare("
|
||||||
UPDATE loans
|
UPDATE loans
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
+11
-110
@@ -5,49 +5,7 @@ require_once __DIR__ . '/account_service.php';
|
|||||||
require_once __DIR__ . '/installment_service.php';
|
require_once __DIR__ . '/installment_service.php';
|
||||||
require_once __DIR__ . '/helpers.php';
|
require_once __DIR__ . '/helpers.php';
|
||||||
require_once __DIR__ . '/card_billing_service.php';
|
require_once __DIR__ . '/card_billing_service.php';
|
||||||
|
require_once __DIR__ . '/transaction_record_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
|
function create_transaction(array $data, bool $skipIfDuplicate = false): bool
|
||||||
{
|
{
|
||||||
@@ -83,19 +41,6 @@ function create_transaction(array $data, bool $skipIfDuplicate = false): bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$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;
|
$isInstallment = !empty($data['is_installment']) ? 1 : 0;
|
||||||
$installmentMonths = !empty($data['installment_months']) ? (int)$data['installment_months'] : null;
|
$installmentMonths = !empty($data['installment_months']) ? (int)$data['installment_months'] : null;
|
||||||
$installmentInterestRate = !empty($data['installment_interest_rate']) ? (float)$data['installment_interest_rate'] : 0.0;
|
$installmentInterestRate = !empty($data['installment_interest_rate']) ? (float)$data['installment_interest_rate'] : 0.0;
|
||||||
@@ -146,50 +91,13 @@ function create_transaction(array $data, bool $skipIfDuplicate = false): bool
|
|||||||
$installmentTotalBilled = null;
|
$installmentTotalBilled = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$stmt = $pdo->prepare("
|
$transactionId = insert_transaction_record(array_merge($data, [
|
||||||
INSERT INTO transactions
|
'is_installment' => $isInstallment,
|
||||||
(
|
'installment_months' => $installmentMonths,
|
||||||
user_id,
|
'installment_interest_rate' => $installmentInterestRate,
|
||||||
account_id,
|
'installment_interest_total' => $installmentInterestTotal,
|
||||||
category_id,
|
'installment_total_billed' => $installmentTotalBilled,
|
||||||
transaction_type,
|
]), false);
|
||||||
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();
|
$pdo->commit();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -281,19 +189,12 @@ function update_transaction(int $transactionId, int $userId, array $data): void
|
|||||||
$data['description'] ?? null
|
$data['description'] ?? null
|
||||||
);
|
);
|
||||||
|
|
||||||
$account = get_account_for_transaction(
|
$billingYearMonth = get_transaction_billing_year_month(
|
||||||
$userId,
|
$userId,
|
||||||
(int)$data['account_id']
|
(int)$data['account_id'],
|
||||||
|
(string)$data['transaction_date']
|
||||||
);
|
);
|
||||||
|
|
||||||
$billingYearMonth = null;
|
|
||||||
if ($account) {
|
|
||||||
$billingYearMonth = get_card_billing_year_month(
|
|
||||||
$account,
|
|
||||||
(string)$data['transaction_date']
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$isInstallment = !empty($data['is_installment']) ? 1 : 0;
|
$isInstallment = !empty($data['is_installment']) ? 1 : 0;
|
||||||
$installmentMonths = !empty($data['installment_months']) ? (int)$data['installment_months'] : null;
|
$installmentMonths = !empty($data['installment_months']) ? (int)$data['installment_months'] : null;
|
||||||
$installmentInterestRate = !empty($data['installment_interest_rate']) ? (float)$data['installment_interest_rate'] : 0.0;
|
$installmentInterestRate = !empty($data['installment_interest_rate']) ? (float)$data['installment_interest_rate'] : 0.0;
|
||||||
|
|||||||
@@ -118,73 +118,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||||||
'save_as_defaults' => !empty($_POST['save_as_defaults']) ? 1 : 0,
|
'save_as_defaults' => !empty($_POST['save_as_defaults']) ? 1 : 0,
|
||||||
];
|
];
|
||||||
|
|
||||||
$transactionType = $form['transaction_type'];
|
$payload = build_transaction_payload_from_input($form, $uid);
|
||||||
$accountId = (int)$form['account_id'];
|
$transactionType = $payload['transaction_type'];
|
||||||
$categoryId = (int)$form['category_id'];
|
$accountId = (int)$payload['account_id'];
|
||||||
$amount = (float)str_replace(',', '', $form['amount']);
|
$categoryId = (int)$payload['category_id'];
|
||||||
$transactionDate = $form['transaction_date'];
|
$merchantName = (string)($payload['merchant_name'] ?? '');
|
||||||
$merchantName = $form['merchant_name'];
|
$relatedAccountId = $payload['related_account_id'] ? (int)$payload['related_account_id'] : null;
|
||||||
$description = $form['description'];
|
$isInstallment = (int)$payload['is_installment'];
|
||||||
$relatedAccountId = $form['related_account_id'] > 0 ? (int)$form['related_account_id'] : null;
|
$installmentMonths = $payload['installment_months'];
|
||||||
|
$installmentInterestRate = (float)$payload['installment_interest_rate'];
|
||||||
|
|
||||||
$isInstallment = (int)$form['is_installment'];
|
create_transaction($payload);
|
||||||
$installmentMonths = $form['installment_months'] !== '' ? (int)$form['installment_months'] : null;
|
|
||||||
$installmentInterestRate = $form['installment_interest_rate'] !== '' ? (float)$form['installment_interest_rate'] : 0.0;
|
|
||||||
$installmentInterestTotal = $form['installment_interest_total'] !== ''
|
|
||||||
? (float)str_replace(',', '', $form['installment_interest_total'])
|
|
||||||
: null;
|
|
||||||
$installmentTotalBilled = $form['installment_total_billed'] !== ''
|
|
||||||
? (float)str_replace(',', '', $form['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('주 계좌/카드를 선택하세요.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($categoryId <= 0) {
|
|
||||||
throw new RuntimeException('카테고리를 선택하세요.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (in_array($transactionType, ['transfer', 'card_payment'], true) && !$relatedAccountId) {
|
|
||||||
throw new RuntimeException('관련 계좌를 선택해야 합니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($relatedAccountId && $relatedAccountId === $accountId) {
|
|
||||||
throw new RuntimeException('주 계좌와 관련 계좌는 같을 수 없습니다.');
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($transactionType !== 'expense') {
|
|
||||||
$isInstallment = 0;
|
|
||||||
$installmentMonths = null;
|
|
||||||
$installmentInterestRate = 0.0;
|
|
||||||
$installmentInterestTotal = null;
|
|
||||||
$installmentTotalBilled = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
create_transaction([
|
|
||||||
'user_id' => $uid,
|
|
||||||
'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,
|
|
||||||
]);
|
|
||||||
|
|
||||||
if ($form['save_as_defaults']) {
|
if ($form['save_as_defaults']) {
|
||||||
$save = [
|
$save = [
|
||||||
|
|||||||
@@ -41,55 +41,8 @@ $categories = $stmt->fetchAll();
|
|||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||||
try {
|
try {
|
||||||
$transactionType = $_POST['transaction_type'] ?? '';
|
$payload = build_transaction_payload_from_input($_POST, $uid);
|
||||||
$accountId = (int)($_POST['account_id'] ?? 0);
|
update_transaction($id, $uid, $payload);
|
||||||
$categoryId = (int)($_POST['category_id'] ?? 0);
|
|
||||||
$amount = (float)str_replace(',', '', (string)($_POST['amount'] ?? 0));
|
|
||||||
$transactionDate = $_POST['transaction_date'] ?? date('Y-m-d');
|
|
||||||
$merchantName = trim($_POST['merchant_name'] ?? '');
|
|
||||||
$description = trim($_POST['description'] ?? '');
|
|
||||||
$relatedAccountId = !empty($_POST['related_account_id']) ? (int)$_POST['related_account_id'] : null;
|
|
||||||
|
|
||||||
$isInstallment = (int)($_POST['is_installment'] ?? 0);
|
|
||||||
$installmentMonths = !empty($_POST['installment_months']) ? (int)$_POST['installment_months'] : null;
|
|
||||||
$installmentInterestRate = !empty($_POST['installment_interest_rate']) ? (float)$_POST['installment_interest_rate'] : 0.0;
|
|
||||||
$installmentInterestTotal = isset($_POST['installment_interest_total']) && $_POST['installment_interest_total'] !== ''
|
|
||||||
? (float)str_replace(',', '', (string)$_POST['installment_interest_total'])
|
|
||||||
: null;
|
|
||||||
$installmentTotalBilled = isset($_POST['installment_total_billed']) && $_POST['installment_total_billed'] !== ''
|
|
||||||
? (float)str_replace(',', '', (string)$_POST['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('주 계좌/카드를 선택하세요.');
|
|
||||||
if ($categoryId <= 0) throw new RuntimeException('카테고리를 선택하세요.');
|
|
||||||
if (in_array($transactionType, ['transfer', 'card_payment'], true) && !$relatedAccountId) throw new RuntimeException('관련 계좌를 선택해야 합니다.');
|
|
||||||
if ($relatedAccountId && $relatedAccountId === $accountId) throw new RuntimeException('주 계좌와 관련 계좌는 같을 수 없습니다.');
|
|
||||||
|
|
||||||
if ($transactionType !== 'expense') {
|
|
||||||
$isInstallment = 0;
|
|
||||||
$installmentMonths = null;
|
|
||||||
$installmentInterestRate = 0.0;
|
|
||||||
$installmentInterestTotal = null;
|
|
||||||
$installmentTotalBilled = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
update_transaction($id, $uid, [
|
|
||||||
'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,
|
|
||||||
]);
|
|
||||||
|
|
||||||
redirect('/transactions.php');
|
redirect('/transactions.php');
|
||||||
} catch (Throwable $e) {
|
} catch (Throwable $e) {
|
||||||
|
|||||||
Reference in New Issue
Block a user