Files
sam-api/app/Services/LoanService.php

584 lines
20 KiB
PHP
Raw Normal View History

<?php
namespace App\Services;
use App\Models\Tenants\Loan;
use App\Models\Tenants\Withdrawal;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class LoanService extends Service
{
// =========================================================================
// 가지급금 목록/상세
// =========================================================================
/**
* 가지급금 목록
*/
public function index(array $params): LengthAwarePaginator
{
$tenantId = $this->tenantId();
$query = Loan::query()
->where('tenant_id', $tenantId)
->with(['user:id,name,email', 'creator:id,name']);
// 사용자 필터
if (! empty($params['user_id'])) {
$query->where('user_id', $params['user_id']);
}
// 상태 필터
if (! empty($params['status'])) {
$query->where('status', $params['status']);
}
// 날짜 범위 필터
if (! empty($params['start_date'])) {
$query->where('loan_date', '>=', $params['start_date']);
}
if (! empty($params['end_date'])) {
$query->where('loan_date', '<=', $params['end_date']);
}
// 검색 (사용자명, 목적)
if (! empty($params['search'])) {
$search = $params['search'];
$query->where(function ($q) use ($search) {
$q->whereHas('user', function ($userQ) use ($search) {
$userQ->where('name', 'like', "%{$search}%");
})->orWhere('purpose', 'like', "%{$search}%");
});
}
// 정렬
$sortBy = $params['sort_by'] ?? 'loan_date';
$sortDir = $params['sort_dir'] ?? 'desc';
$query->orderBy($sortBy, $sortDir);
$perPage = $params['per_page'] ?? 20;
return $query->paginate($perPage);
}
/**
* 가지급금 상세
*/
public function show(int $id): Loan
{
$tenantId = $this->tenantId();
return Loan::query()
->where('tenant_id', $tenantId)
->with([
'user:id,name,email',
'withdrawal',
'creator:id,name',
'updater:id,name',
])
->findOrFail($id);
}
/**
* 가지급금 요약 (특정 사용자 또는 전체)
*/
public function summary(?int $userId = null): array
{
$tenantId = $this->tenantId();
$query = Loan::query()
->where('tenant_id', $tenantId);
if ($userId) {
$query->where('user_id', $userId);
}
$stats = $query->selectRaw('
COUNT(*) as total_count,
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as outstanding_count,
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as settled_count,
SUM(CASE WHEN status = ? THEN 1 ELSE 0 END) as partial_count,
SUM(amount) as total_amount,
SUM(COALESCE(settlement_amount, 0)) as total_settled,
SUM(amount - COALESCE(settlement_amount, 0)) as total_outstanding
', [Loan::STATUS_OUTSTANDING, Loan::STATUS_SETTLED, Loan::STATUS_PARTIAL])
->first();
return [
'total_count' => (int) $stats->total_count,
'outstanding_count' => (int) $stats->outstanding_count,
'settled_count' => (int) $stats->settled_count,
'partial_count' => (int) $stats->partial_count,
'total_amount' => (float) $stats->total_amount,
'total_settled' => (float) $stats->total_settled,
'total_outstanding' => (float) $stats->total_outstanding,
];
}
// =========================================================================
// 가지급금 생성/수정/삭제
// =========================================================================
/**
* 가지급금 생성
*/
public function store(array $data): Loan
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($data, $tenantId, $userId) {
// 출금 내역 연결 검증
$withdrawalId = null;
if (! empty($data['withdrawal_id'])) {
$withdrawal = Withdrawal::query()
->where('tenant_id', $tenantId)
->where('id', $data['withdrawal_id'])
->first();
if (! $withdrawal) {
throw new BadRequestHttpException(__('error.loan.invalid_withdrawal'));
}
$withdrawalId = $withdrawal->id;
}
return Loan::create([
'tenant_id' => $tenantId,
'user_id' => $data['user_id'],
'loan_date' => $data['loan_date'],
'amount' => $data['amount'],
'purpose' => $data['purpose'] ?? null,
'status' => Loan::STATUS_OUTSTANDING,
'withdrawal_id' => $withdrawalId,
'created_by' => $userId,
'updated_by' => $userId,
]);
});
}
/**
* 가지급금 수정
*/
public function update(int $id, array $data): Loan
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$loan = Loan::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
if (! $loan->isEditable()) {
throw new BadRequestHttpException(__('error.loan.not_editable'));
}
// 출금 내역 연결 검증
if (isset($data['withdrawal_id']) && $data['withdrawal_id']) {
$withdrawal = Withdrawal::query()
->where('tenant_id', $tenantId)
->where('id', $data['withdrawal_id'])
->first();
if (! $withdrawal) {
throw new BadRequestHttpException(__('error.loan.invalid_withdrawal'));
}
}
$loan->fill([
'user_id' => $data['user_id'] ?? $loan->user_id,
'loan_date' => $data['loan_date'] ?? $loan->loan_date,
'amount' => $data['amount'] ?? $loan->amount,
'purpose' => $data['purpose'] ?? $loan->purpose,
'withdrawal_id' => $data['withdrawal_id'] ?? $loan->withdrawal_id,
'updated_by' => $userId,
]);
$loan->save();
return $loan->fresh(['user:id,name,email', 'creator:id,name']);
}
/**
* 가지급금 삭제
*/
public function destroy(int $id): bool
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$loan = Loan::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
if (! $loan->isDeletable()) {
throw new BadRequestHttpException(__('error.loan.not_deletable'));
}
$loan->deleted_by = $userId;
$loan->save();
$loan->delete();
return true;
}
// =========================================================================
// 정산 처리
// =========================================================================
/**
* 가지급금 정산
*/
public function settle(int $id, array $data): Loan
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
$loan = Loan::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
if (! $loan->isSettleable()) {
throw new BadRequestHttpException(__('error.loan.not_settleable'));
}
$settlementAmount = (float) $data['settlement_amount'];
$currentSettled = (float) ($loan->settlement_amount ?? 0);
$totalSettled = $currentSettled + $settlementAmount;
$loanAmount = (float) $loan->amount;
// 정산 금액이 가지급금액을 초과하는지 확인
if ($totalSettled > $loanAmount) {
throw new BadRequestHttpException(__('error.loan.settlement_exceeds'));
}
// 상태 결정
$status = Loan::STATUS_PARTIAL;
if (abs($totalSettled - $loanAmount) < 0.01) { // 부동소수점 비교
$status = Loan::STATUS_SETTLED;
}
$loan->settlement_date = $data['settlement_date'];
$loan->settlement_amount = $totalSettled;
$loan->status = $status;
$loan->updated_by = $userId;
$loan->save();
return $loan->fresh(['user:id,name,email']);
});
}
// =========================================================================
// 인정이자 계산
// =========================================================================
/**
* 인정이자 일괄 계산
*
* @param int $year 계산 연도
* @param int|null $userId 특정 사용자 (미지정시 전체)
*/
public function calculateInterest(int $year, ?int $userId = null): array
{
$tenantId = $this->tenantId();
$query = Loan::query()
->where('tenant_id', $tenantId)
->whereIn('status', [Loan::STATUS_OUTSTANDING, Loan::STATUS_PARTIAL]);
if ($userId) {
$query->where('user_id', $userId);
}
$loans = $query->with('user:id,name,email')->get();
$interestRate = Loan::getInterestRate($year);
$baseDate = now()->endOfYear()->year === $year
? now()
: now()->setYear($year)->endOfYear();
$results = [];
$totalBalance = 0;
$totalInterest = 0;
$totalCorporateTax = 0;
$totalIncomeTax = 0;
$totalLocalTax = 0;
foreach ($loans as $loan) {
// 연도 내 경과일수 계산
$startOfYear = now()->setYear($year)->startOfYear();
$effectiveStartDate = $loan->loan_date->greaterThan($startOfYear)
? $loan->loan_date
: $startOfYear;
$elapsedDays = $effectiveStartDate->diffInDays($baseDate);
$balance = $loan->outstanding_amount;
$interest = $loan->calculateRecognizedInterest($elapsedDays, $year);
$taxes = $loan->calculateTaxes($interest);
$results[] = [
'loan_id' => $loan->id,
'user' => [
'id' => $loan->user->id,
'name' => $loan->user->name,
'email' => $loan->user->email,
],
'loan_date' => $loan->loan_date->toDateString(),
'amount' => (float) $loan->amount,
'settlement_amount' => (float) ($loan->settlement_amount ?? 0),
'outstanding_amount' => $balance,
'elapsed_days' => $elapsedDays,
'interest_rate' => $interestRate,
'recognized_interest' => $taxes['recognized_interest'],
'corporate_tax' => $taxes['corporate_tax'],
'income_tax' => $taxes['income_tax'],
'local_tax' => $taxes['local_tax'],
'total_tax' => $taxes['total_tax'],
];
$totalBalance += $balance;
$totalInterest += $taxes['recognized_interest'];
$totalCorporateTax += $taxes['corporate_tax'];
$totalIncomeTax += $taxes['income_tax'];
$totalLocalTax += $taxes['local_tax'];
}
return [
'year' => $year,
'interest_rate' => $interestRate,
'base_date' => $baseDate->toDateString(),
'summary' => [
'total_balance' => round($totalBalance, 2),
'total_recognized_interest' => round($totalInterest, 2),
'total_corporate_tax' => round($totalCorporateTax, 2),
'total_income_tax' => round($totalIncomeTax, 2),
'total_local_tax' => round($totalLocalTax, 2),
'total_tax' => round($totalCorporateTax + $totalIncomeTax + $totalLocalTax, 2),
],
'details' => $results,
];
}
/**
* 가지급금 대시보드 데이터
*
* CEO 대시보드 카드/가지급금 관리 섹션(cm2) 모달용 데이터 제공
*
* @return array{
* summary: array{
* total_outstanding: float,
* recognized_interest: float,
* outstanding_count: int
* },
* loans: array
* }
*/
public function dashboard(): array
{
$tenantId = $this->tenantId();
$currentYear = now()->year;
// 1. Summary 데이터
$summaryData = $this->summary();
// 2. 인정이자 계산 (현재 연도 기준)
$interestData = $this->calculateInterest($currentYear);
$recognizedInterest = $interestData['summary']['total_recognized_interest'] ?? 0;
// 3. 가지급금 목록 (최근 10건, 미정산 우선)
$loans = Loan::query()
->where('tenant_id', $tenantId)
->with(['user:id,name,email', 'withdrawal'])
->orderByRaw('CASE WHEN status = ? THEN 0 WHEN status = ? THEN 1 ELSE 2 END', [
Loan::STATUS_OUTSTANDING,
Loan::STATUS_PARTIAL,
])
->orderByDesc('loan_date')
->limit(10)
->get()
->map(function ($loan) {
return [
'id' => $loan->id,
'loan_date' => $loan->loan_date->format('Y-m-d'),
'user_name' => $loan->user?->name ?? '미지정',
'category' => $loan->withdrawal_id ? '카드' : '계좌',
'amount' => (float) $loan->amount,
'status' => $loan->status,
'content' => $loan->purpose ?? '',
];
})
->toArray();
return [
'summary' => [
'total_outstanding' => (float) $summaryData['total_outstanding'],
'recognized_interest' => (float) $recognizedInterest,
'outstanding_count' => (int) $summaryData['outstanding_count'],
],
'loans' => $loans,
];
}
/**
* 세금 시뮬레이션 데이터
*
* CEO 대시보드 카드/가지급금 관리 섹션(cm2) 세금 비교 분석용 데이터 제공
*
* @param int $year 시뮬레이션 연도
* @return array{
* year: int,
* loan_summary: array{
* total_outstanding: float,
* recognized_interest: float,
* interest_rate: float
* },
* corporate_tax: array{
* without_loan: array{taxable_income: float, tax_amount: float},
* with_loan: array{taxable_income: float, tax_amount: float},
* difference: float,
* rate_info: string
* },
* income_tax: array{
* without_loan: array{taxable_income: float, tax_rate: string, tax_amount: float},
* with_loan: array{taxable_income: float, tax_rate: string, tax_amount: float},
* difference: float,
* breakdown: array{income_tax: float, local_tax: float, insurance: float}
* }
* }
*/
public function taxSimulation(int $year): array
{
// 1. 가지급금 요약 데이터
$summaryData = $this->summary();
$totalOutstanding = (float) $summaryData['total_outstanding'];
// 2. 인정이자 계산
$interestData = $this->calculateInterest($year);
$recognizedInterest = $interestData['summary']['total_recognized_interest'] ?? 0;
$interestRate = Loan::getInterestRate($year);
// 3. 법인세 비교 계산
// - 가지급금이 없을 때: 인정이자가 비용으로 처리되지 않음
// - 가지급금이 있을 때: 인정이자만큼 추가 과세
$corporateTaxRate = Loan::CORPORATE_TAX_RATE;
$corporateTaxWithout = [
'taxable_income' => 0.0,
'tax_amount' => 0.0,
];
$corporateTaxWith = [
'taxable_income' => $recognizedInterest,
'tax_amount' => round($recognizedInterest * $corporateTaxRate, 2),
];
$corporateTaxDifference = $corporateTaxWith['tax_amount'] - $corporateTaxWithout['tax_amount'];
// 4. 소득세 비교 계산 (대표이사 상여처분 시)
$incomeTaxRate = Loan::INCOME_TAX_RATE;
$localTaxRate = Loan::LOCAL_TAX_RATE;
$insuranceRate = 0.09; // 4대보험 약 9%
$incomeTaxWithout = [
'taxable_income' => 0.0,
'tax_rate' => '0%',
'tax_amount' => 0.0,
];
$incomeTaxAmount = round($recognizedInterest * $incomeTaxRate, 2);
$localTaxAmount = round($incomeTaxAmount * $localTaxRate, 2);
$insuranceAmount = round($recognizedInterest * $insuranceRate, 2);
$incomeTaxWith = [
'taxable_income' => $recognizedInterest,
'tax_rate' => ($incomeTaxRate * 100).'%',
'tax_amount' => $incomeTaxAmount + $localTaxAmount,
];
$incomeTaxDifference = $incomeTaxWith['tax_amount'] - $incomeTaxWithout['tax_amount'];
return [
'year' => $year,
'loan_summary' => [
'total_outstanding' => $totalOutstanding,
'recognized_interest' => (float) $recognizedInterest,
'interest_rate' => $interestRate,
],
'corporate_tax' => [
'without_loan' => $corporateTaxWithout,
'with_loan' => $corporateTaxWith,
'difference' => round($corporateTaxDifference, 2),
'rate_info' => '법인세 '.($corporateTaxRate * 100).'% 적용',
],
'income_tax' => [
'without_loan' => $incomeTaxWithout,
'with_loan' => $incomeTaxWith,
'difference' => round($incomeTaxDifference, 2),
'breakdown' => [
'income_tax' => $incomeTaxAmount,
'local_tax' => $localTaxAmount,
'insurance' => $insuranceAmount,
],
],
];
}
/**
* 인정이자 리포트 (연도별 요약)
*/
public function interestReport(int $year): array
{
$tenantId = $this->tenantId();
// 사용자별 가지급금 집계
$userLoans = Loan::query()
->where('tenant_id', $tenantId)
->whereYear('loan_date', '<=', $year)
->whereIn('status', [Loan::STATUS_OUTSTANDING, Loan::STATUS_PARTIAL])
->select('user_id')
->selectRaw('SUM(amount) as total_amount')
->selectRaw('SUM(COALESCE(settlement_amount, 0)) as total_settled')
->selectRaw('SUM(amount - COALESCE(settlement_amount, 0)) as total_outstanding')
->selectRaw('COUNT(*) as loan_count')
->groupBy('user_id')
->with('user:id,name,email')
->get();
$interestRate = Loan::getInterestRate($year);
$results = [];
foreach ($userLoans as $userLoan) {
$userInterest = $this->calculateInterest($year, $userLoan->user_id);
$results[] = [
'user' => [
'id' => $userLoan->user_id,
'name' => $userLoan->user?->name ?? 'Unknown',
'email' => $userLoan->user?->email ?? '',
],
'loan_count' => $userLoan->loan_count,
'total_amount' => (float) $userLoan->total_amount,
'total_settled' => (float) $userLoan->total_settled,
'total_outstanding' => (float) $userLoan->total_outstanding,
'recognized_interest' => $userInterest['summary']['total_recognized_interest'],
'total_tax' => $userInterest['summary']['total_tax'],
];
}
// 전체 합계
$grandTotal = [
'total_amount' => array_sum(array_column($results, 'total_amount')),
'total_outstanding' => array_sum(array_column($results, 'total_outstanding')),
'recognized_interest' => array_sum(array_column($results, 'recognized_interest')),
'total_tax' => array_sum(array_column($results, 'total_tax')),
];
return [
'year' => $year,
'interest_rate' => $interestRate,
'users' => $results,
'grand_total' => $grandTotal,
];
}
}