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, ]; } /** * 인정이자 리포트 (연도별 요약) */ 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, ]; } }