diff --git a/README.md b/README.md index 91f0d58..bf8344e 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Financial은 개인 금융 데이터를 서버 렌더링 PHP 화면에서 관리 - `app/lib/auth.php`: 인증, remember token, CSRF, no-store header - `app/lib/db.php`: PDO 연결 - `app/lib/account_service.php`: 계좌 서비스 +- `app/lib/transaction_record_service.php`: 거래 fingerprint, 입력 검증, 공통 거래 row 기록 - `app/lib/transaction_service.php`: 거래 저장과 조회 - `app/lib/installment_service.php`: 할부 청구와 선납 처리 - `app/lib/loan_service.php`: 대출과 상환 처리 @@ -58,9 +59,12 @@ Financial은 개인 금융 데이터를 서버 렌더링 PHP 화면에서 관리 4. 저장 후 redirect와 flash 메시지로 응답합니다. 5. 카테고리 추천 API는 merchant rule 우선순위와 confidence를 반환합니다. +거래 테이블에 실제 row를 추가하는 공통 경로는 `transaction_record_service.php`입니다. 일반 거래 등록/수정은 같은 입력 검증 함수를 쓰고, 대출 실행/상환/중도상환과 할부 선결제도 공통 기록 함수를 통해 `transactions`에 남깁니다. 카드/계좌/할부/대출 기능을 바꿀 때는 먼저 이 공통 기록 규칙과 잔액 재계산 흐름을 확인해야 합니다. + ## 주요 함수/모듈 - `auth.php`: 로그인, 세션, CSRF +- `transaction_record_service.php`: 거래 공통 검증과 row 기록 - `transaction_service.php`: 거래 저장과 조회 - `installment_*`: 할부 청구와 선납 처리 - `merchant_rules.php`: 가맹점 자동분류 규칙 관리 diff --git a/app/lib/installment_service.php b/app/lib/installment_service.php index 0d12004..440415e 100644 --- a/app/lib/installment_service.php +++ b/app/lib/installment_service.php @@ -1,8 +1,9 @@ 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 ?: '할부 선결제/중도상환'; - - $fingerprint = hash('sha256', implode('|', [ - $userId, - $paymentAccountId, - 'installment_prepay', - $installmentId, - $prepayDate, - number_format($totalAmount, 2, '.', ''), - $desc - ])); - - $stmtCat = $pdo->prepare(" - SELECT id + $desc = $description ?: '할부 선결제/중도상환'; + + $stmtCat = $pdo->prepare(" + SELECT id FROM categories WHERE user_id = ? AND category_type = 'expense' @@ -594,22 +562,23 @@ function prepay_installment( $category = $stmtCat->fetch(); if (!$category) { - throw new RuntimeException('선결제 기록용 expense 카테고리(기타지출)를 찾을 수 없습니다.'); - } - - $stmt->execute([ - $userId, - $paymentAccountId, - (int)$category['id'], - $totalAmount, - $totalAmount, - $prepayDate, - $installment['merchant_name'], - '[할부 선결제] ' . $desc, - $fingerprint - ]); - - $pdo->commit(); + throw new RuntimeException('선결제 기록용 expense 카테고리(기타지출)를 찾을 수 없습니다.'); + } + + insert_transaction_record([ + 'user_id' => $userId, + 'account_id' => $paymentAccountId, + 'category_id' => (int)$category['id'], + 'transaction_type' => 'expense', + 'amount' => $totalAmount, + 'transaction_date' => $prepayDate, + 'merchant_name' => $installment['merchant_name'], + 'description' => '[할부 선결제] ' . $installmentId . ' / ' . $desc, + 'related_account_id' => null, + 'installment_prepay_amount' => $totalAmount, + ], true); + + $pdo->commit(); recalculate_account_balance($paymentAccountId); } catch (Throwable $e) { @@ -701,4 +670,4 @@ function rebuild_all_installments_for_user(int $userId): int throw $e; } -} \ No newline at end of file +} diff --git a/app/lib/loan_service.php b/app/lib/loan_service.php index 591b5c5..5794dd9 100644 --- a/app/lib/loan_service.php +++ b/app/lib/loan_service.php @@ -1,7 +1,8 @@ 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 insert_transaction_row(array $data): int +{ + return insert_transaction_record($data, true); +} function create_loan_schedule_only( int $loanId, @@ -928,20 +829,44 @@ function prepay_loan( ) VALUES (?, ?, NULL, ?, ?, ?, ?, ?, ?, 'prepayment', 0, ?) "); - $stmt->execute([ - $userId, - $loanId, - $accountId ?: null, + $stmt->execute([ + $userId, + $loanId, + $accountId ?: null, $paymentDate, $principalAmount, $interestAmount, $feeAmount, $totalAmount, - $description, - ]); - - if ($principalAmount > 0) { - $stmt = $pdo->prepare(" + $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) { + $stmt = $pdo->prepare(" UPDATE loans SET current_principal_balance = GREATEST(current_principal_balance - ?, 0) WHERE id = ? @@ -1151,4 +1076,4 @@ function update_loan_and_rebuild_full_history(int $userId, int $loanId, array $d } throw $e; } -} \ No newline at end of file +} diff --git a/app/lib/transaction_record_service.php b/app/lib/transaction_record_service.php new file mode 100644 index 0000000..539ae7e --- /dev/null +++ b/app/lib/transaction_record_service.php @@ -0,0 +1,274 @@ +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(); +} diff --git a/app/lib/transaction_service.php b/app/lib/transaction_service.php index f58fdf0..93cfd32 100644 --- a/app/lib/transaction_service.php +++ b/app/lib/transaction_service.php @@ -1,56 +1,14 @@ 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 -{ +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(); @@ -83,20 +41,7 @@ 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; $installmentInterestRate = !empty($data['installment_interest_rate']) ? (float)$data['installment_interest_rate'] : 0.0; @@ -146,51 +91,14 @@ function create_transaction(array $data, bool $skipIfDuplicate = false): bool $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(); + $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' && @@ -281,18 +189,11 @@ function update_transaction(int $transactionId, int $userId, array $data): void $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'] - ); - } + $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; @@ -438,4 +339,4 @@ function delete_transaction(int $transactionId, int $userId): void } throw $e; } -} \ No newline at end of file +} diff --git a/public/transaction_create.php b/public/transaction_create.php index fe64d3e..7362582 100644 --- a/public/transaction_create.php +++ b/public/transaction_create.php @@ -118,73 +118,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') { 'save_as_defaults' => !empty($_POST['save_as_defaults']) ? 1 : 0, ]; - $transactionType = $form['transaction_type']; - $accountId = (int)$form['account_id']; - $categoryId = (int)$form['category_id']; - $amount = (float)str_replace(',', '', $form['amount']); - $transactionDate = $form['transaction_date']; - $merchantName = $form['merchant_name']; - $description = $form['description']; - $relatedAccountId = $form['related_account_id'] > 0 ? (int)$form['related_account_id'] : null; - - $isInstallment = (int)$form['is_installment']; - $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, - ]); + $payload = build_transaction_payload_from_input($form, $uid); + $transactionType = $payload['transaction_type']; + $accountId = (int)$payload['account_id']; + $categoryId = (int)$payload['category_id']; + $merchantName = (string)($payload['merchant_name'] ?? ''); + $relatedAccountId = $payload['related_account_id'] ? (int)$payload['related_account_id'] : null; + $isInstallment = (int)$payload['is_installment']; + $installmentMonths = $payload['installment_months']; + $installmentInterestRate = (float)$payload['installment_interest_rate']; + + create_transaction($payload); if ($form['save_as_defaults']) { $save = [ @@ -877,4 +821,4 @@ require __DIR__ . '/../app/views/header.php'; })(); - \ No newline at end of file + diff --git a/public/transaction_edit.php b/public/transaction_edit.php index 2c7b081..a02bbcb 100644 --- a/public/transaction_edit.php +++ b/public/transaction_edit.php @@ -41,55 +41,8 @@ $categories = $stmt->fetchAll(); if ($_SERVER['REQUEST_METHOD'] === 'POST') { try { - $transactionType = $_POST['transaction_type'] ?? ''; - $accountId = (int)($_POST['account_id'] ?? 0); - $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, - ]); + $payload = build_transaction_payload_from_input($_POST, $uid); + update_transaction($id, $uid, $payload); redirect('/transactions.php'); } catch (Throwable $e) {