Files
sam-api/app/Services/ReceivablesService.php
kent 8a5c7b5298 feat(API): Service 로직 개선
- EstimateService, ItemService 기능 추가
- OrderService 공정 연동 개선
- SalaryService, ReceivablesService 수정
- HandoverReportService, SiteBriefingService 추가
- Pricing 서비스 추가

Co-Authored-By: Claude <noreply@anthropic.com>
2026-01-13 19:49:06 +09:00

453 lines
14 KiB
PHP

<?php
namespace App\Services;
use App\Models\Orders\Client;
use App\Models\Tenants\Bill;
use App\Models\Tenants\Deposit;
use App\Models\Tenants\Sale;
use Carbon\Carbon;
/**
* 채권 현황 서비스
* 거래처별 월별 매출, 입금, 어음, 미수금 현황 조회
* - 동적 월 표시 지원 (최근 1년: 동적 12개월)
* - 이월잔액 + 누적 미수금 계산
*/
class ReceivablesService extends Service
{
/**
* 채권 현황 목록 조회
*/
public function index(array $params): array
{
$tenantId = $this->tenantId();
$recentYear = $params['recent_year'] ?? false;
$year = $params['year'] ?? date('Y');
$search = $params['search'] ?? null;
// 월 기간 생성 (동적 월 지원)
$periods = $this->generateMonthPeriods($recentYear, $year);
$monthLabels = array_map(fn ($p) => $p['label'], $periods);
// 이월잔액 기준일 (첫번째 월의 시작일 전날)
$carryForwardDate = Carbon::parse($periods[0]['start'])->subDay()->format('Y-m-d');
// 거래처 목록 조회
$clientsQuery = Client::where('tenant_id', $tenantId)
->where('is_active', true);
if ($search) {
$clientsQuery->where('name', 'like', "%{$search}%");
}
$clients = $clientsQuery->orderBy('name')->get();
$result = [];
foreach ($clients as $client) {
// 이월잔액 계산 (기준일 이전까지의 누적 미수금)
$carryForwardBalance = $this->getCarryForwardBalance($tenantId, $client->id, $carryForwardDate);
// 월별 데이터 수집 (년-월 키 기반)
$salesByPeriod = $this->getSalesByPeriods($tenantId, $client->id, $periods);
$depositsByPeriod = $this->getDepositsByPeriods($tenantId, $client->id, $periods);
$billsByPeriod = $this->getBillsByPeriods($tenantId, $client->id, $periods);
// 누적 미수금 계산
$receivablesByPeriod = $this->calculateCumulativeReceivables(
$carryForwardBalance,
$salesByPeriod,
$depositsByPeriod,
$billsByPeriod,
count($periods)
);
// 카테고리별 데이터 생성 (배열 형태)
$categories = [
[
'category' => 'sales',
'amounts' => $this->formatPeriodAmounts($salesByPeriod, count($periods)),
],
[
'category' => 'deposit',
'amounts' => $this->formatPeriodAmounts($depositsByPeriod, count($periods)),
],
[
'category' => 'bill',
'amounts' => $this->formatPeriodAmounts($billsByPeriod, count($periods)),
],
[
'category' => 'receivable',
'amounts' => $this->formatReceivableAmounts($receivablesByPeriod),
],
];
// 연체 여부: 최종 미수금이 양수인 경우
$finalReceivable = end($receivablesByPeriod);
$isOverdue = $client->is_overdue ?? ($finalReceivable > 0);
$result[] = [
'id' => (string) $client->id,
'vendor_id' => $client->id,
'vendor_name' => $client->name,
'is_overdue' => $isOverdue,
'memo' => $client->memo ?? '',
'carry_forward_balance' => $carryForwardBalance,
'month_labels' => $monthLabels,
'categories' => $categories,
];
}
// 미수금이 있는 거래처만 필터링 (선택적)
if (! empty($params['has_receivable'])) {
$result = array_filter($result, function ($item) {
$receivableCat = collect($item['categories'])->firstWhere('category', 'receivable');
return $receivableCat && $receivableCat['amounts']['total'] > 0;
});
$result = array_values($result);
}
// 공통 월 레이블 추가 (프론트엔드에서 헤더로 사용)
return [
'month_labels' => $monthLabels,
'items' => $result,
];
}
/**
* 요약 통계 조회
*/
public function summary(array $params): array
{
$tenantId = $this->tenantId();
$recentYear = $params['recent_year'] ?? false;
$year = $params['year'] ?? date('Y');
// 월 기간 생성
$periods = $this->generateMonthPeriods($recentYear, $year);
$startDate = $periods[0]['start'];
$endDate = end($periods)['end'];
// 이월잔액 기준일
$carryForwardDate = Carbon::parse($startDate)->subDay()->format('Y-m-d');
// 전체 이월잔액 (모든 거래처)
$totalCarryForward = $this->getTotalCarryForwardBalance($tenantId, $carryForwardDate);
// 기간 내 총 매출
$totalSales = Sale::where('tenant_id', $tenantId)
->whereNotNull('client_id')
->whereBetween('sale_date', [$startDate, $endDate])
->sum('total_amount');
// 기간 내 총 입금
$totalDeposits = Deposit::where('tenant_id', $tenantId)
->whereNotNull('client_id')
->whereBetween('deposit_date', [$startDate, $endDate])
->sum('amount');
// 기간 내 총 어음
$totalBills = Bill::where('tenant_id', $tenantId)
->whereNotNull('client_id')
->where('bill_type', 'received')
->whereBetween('issue_date', [$startDate, $endDate])
->sum('amount');
// 총 미수금 (이월잔액 + 매출 - 입금 - 어음)
$totalReceivables = $totalCarryForward + $totalSales - $totalDeposits - $totalBills;
// 거래처 수
$vendorCount = Client::where('tenant_id', $tenantId)
->where('is_active', true)
->count();
// 연체 거래처 수 (미수금이 양수인 거래처)
$overdueVendorCount = Client::where('tenant_id', $tenantId)
->where('is_active', true)
->where('is_overdue', true)
->count();
return [
'total_carry_forward' => (float) $totalCarryForward,
'total_sales' => (float) $totalSales,
'total_deposits' => (float) $totalDeposits,
'total_bills' => (float) $totalBills,
'total_receivables' => (float) $totalReceivables,
'vendor_count' => $vendorCount,
'overdue_vendor_count' => $overdueVendorCount,
];
}
/**
* 월 기간 배열 생성
*
* @return array [['start' => 'Y-m-d', 'end' => 'Y-m-d', 'label' => 'YY.MM', 'year' => Y, 'month' => M], ...]
*/
private function generateMonthPeriods(bool $recentYear, string $year): array
{
$periods = [];
if ($recentYear) {
// 최근 1년: 현재 월 기준으로 12개월 전부터
$current = Carbon::now()->startOfMonth();
$start = $current->copy()->subMonths(11);
for ($i = 0; $i < 12; $i++) {
$month = $start->copy()->addMonths($i);
$periods[] = [
'start' => $month->format('Y-m-01'),
'end' => $month->endOfMonth()->format('Y-m-d'),
'label' => $month->format('y.m'),
'year' => (int) $month->format('Y'),
'month' => (int) $month->format('n'),
];
}
} else {
// 특정 연도: 1월~12월
for ($month = 1; $month <= 12; $month++) {
$date = Carbon::createFromDate($year, $month, 1);
$periods[] = [
'start' => $date->format('Y-m-01'),
'end' => $date->endOfMonth()->format('Y-m-d'),
'label' => "{$month}",
'year' => (int) $year,
'month' => $month,
];
}
}
return $periods;
}
/**
* 이월잔액 계산 (기준일 이전까지의 누적 미수금)
*/
private function getCarryForwardBalance(int $tenantId, int $clientId, string $beforeDate): float
{
// 기준일 이전 총 매출
$totalSales = Sale::where('tenant_id', $tenantId)
->where('client_id', $clientId)
->where('sale_date', '<=', $beforeDate)
->sum('total_amount');
// 기준일 이전 총 입금
$totalDeposits = Deposit::where('tenant_id', $tenantId)
->where('client_id', $clientId)
->where('deposit_date', '<=', $beforeDate)
->sum('amount');
// 기준일 이전 총 어음
$totalBills = Bill::where('tenant_id', $tenantId)
->where('client_id', $clientId)
->where('bill_type', 'received')
->where('issue_date', '<=', $beforeDate)
->sum('amount');
return (float) ($totalSales - $totalDeposits - $totalBills);
}
/**
* 전체 거래처 이월잔액 합계
*/
private function getTotalCarryForwardBalance(int $tenantId, string $beforeDate): float
{
$totalSales = Sale::where('tenant_id', $tenantId)
->whereNotNull('client_id')
->where('sale_date', '<=', $beforeDate)
->sum('total_amount');
$totalDeposits = Deposit::where('tenant_id', $tenantId)
->whereNotNull('client_id')
->where('deposit_date', '<=', $beforeDate)
->sum('amount');
$totalBills = Bill::where('tenant_id', $tenantId)
->whereNotNull('client_id')
->where('bill_type', 'received')
->where('issue_date', '<=', $beforeDate)
->sum('amount');
return (float) ($totalSales - $totalDeposits - $totalBills);
}
/**
* 기간별 매출 조회
*/
private function getSalesByPeriods(int $tenantId, int $clientId, array $periods): array
{
$result = [];
foreach ($periods as $index => $period) {
$total = Sale::where('tenant_id', $tenantId)
->where('client_id', $clientId)
->whereBetween('sale_date', [$period['start'], $period['end']])
->sum('total_amount');
$result[$index] = (float) $total;
}
return $result;
}
/**
* 기간별 입금 조회
*/
private function getDepositsByPeriods(int $tenantId, int $clientId, array $periods): array
{
$result = [];
foreach ($periods as $index => $period) {
$total = Deposit::where('tenant_id', $tenantId)
->where('client_id', $clientId)
->whereBetween('deposit_date', [$period['start'], $period['end']])
->sum('amount');
$result[$index] = (float) $total;
}
return $result;
}
/**
* 기간별 어음 조회
*/
private function getBillsByPeriods(int $tenantId, int $clientId, array $periods): array
{
$result = [];
foreach ($periods as $index => $period) {
$total = Bill::where('tenant_id', $tenantId)
->where('client_id', $clientId)
->where('bill_type', 'received')
->whereBetween('issue_date', [$period['start'], $period['end']])
->sum('amount');
$result[$index] = (float) $total;
}
return $result;
}
/**
* 누적 미수금 계산
* 1월: 이월잔액 + 1월 매출 - 1월 입금 - 1월 어음
* 2월: 1월 미수금 + 2월 매출 - 2월 입금 - 2월 어음
* ...
*/
private function calculateCumulativeReceivables(
float $carryForward,
array $sales,
array $deposits,
array $bills,
int $periodCount
): array {
$result = [];
$cumulative = $carryForward;
for ($i = 0; $i < $periodCount; $i++) {
$monthSales = $sales[$i] ?? 0;
$monthDeposits = $deposits[$i] ?? 0;
$monthBills = $bills[$i] ?? 0;
$cumulative = $cumulative + $monthSales - $monthDeposits - $monthBills;
$result[$i] = $cumulative;
}
return $result;
}
/**
* 기간별 금액을 프론트엔드 형식으로 변환 (매출, 입금, 어음용)
*/
private function formatPeriodAmounts(array $periodData, int $periodCount): array
{
$amounts = [];
$total = 0;
for ($i = 0; $i < $periodCount; $i++) {
$amount = $periodData[$i] ?? 0;
$amounts[] = $amount;
$total += $amount;
}
return [
'values' => $amounts,
'total' => $total,
];
}
/**
* 미수금 금액을 프론트엔드 형식으로 변환 (누적이므로 total = 마지막 값)
*/
private function formatReceivableAmounts(array $receivables): array
{
$values = array_values($receivables);
$total = ! empty($values) ? end($values) : 0;
return [
'values' => $values,
'total' => $total,
];
}
/**
* 연체 상태 일괄 업데이트
*/
public function updateOverdueStatus(array $updates): int
{
$tenantId = $this->tenantId();
$updatedCount = 0;
foreach ($updates as $update) {
$clientId = (int) $update['id'];
$isOverdue = (bool) $update['is_overdue'];
$affected = Client::where('tenant_id', $tenantId)
->where('id', $clientId)
->update(['is_overdue' => $isOverdue]);
$updatedCount += $affected;
}
return $updatedCount;
}
/**
* 거래처 메모 업데이트
*/
public function updateMemo(int $clientId, string $memo): bool
{
$tenantId = $this->tenantId();
$affected = Client::where('tenant_id', $tenantId)
->where('id', $clientId)
->update(['memo' => $memo]);
return $affected > 0;
}
/**
* 거래처 메모 일괄 업데이트
*/
public function updateMemos(array $memos): int
{
$tenantId = $this->tenantId();
$updatedCount = 0;
foreach ($memos as $item) {
$clientId = (int) $item['id'];
$memo = $item['memo'] ?? '';
$affected = Client::where('tenant_id', $tenantId)
->where('id', $clientId)
->update(['memo' => $memo]);
$updatedCount += $affected;
}
return $updatedCount;
}
}