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(); }