Files
sam-api/app/Services/ReceivablesService.php
kent 936c42a3e2 feat: I-5 미수금 현황 API 구현
- ReceivablesController: 미수금 조회 API
- ReceivablesService: 미수금 조회 로직
- Swagger 문서화

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-26 15:47:14 +09:00

302 lines
9.1 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 Illuminate\Support\Collection;
use Illuminate\Support\Facades\DB;
/**
* 채권 현황 서비스
* 거래처별 월별 매출, 입금, 어음, 미수금 현황 조회
*/
class ReceivablesService extends Service
{
/**
* 채권 현황 목록 조회
*/
public function index(array $params): array
{
$tenantId = $this->tenantId();
$year = $params['year'] ?? date('Y');
$search = $params['search'] ?? null;
// 거래처 목록 조회
$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) {
// 월별 데이터 수집
$salesByMonth = $this->getSalesByMonth($tenantId, $client->id, $year);
$depositsByMonth = $this->getDepositsByMonth($tenantId, $client->id, $year);
$billsByMonth = $this->getBillsByMonth($tenantId, $client->id, $year);
// 미수금 계산 (매출 - 입금 - 어음)
$receivablesByMonth = $this->calculateReceivables($salesByMonth, $depositsByMonth, $billsByMonth);
// 카테고리별 데이터 생성
$categories = [
[
'category' => 'sales',
'amounts' => $this->formatMonthlyAmounts($salesByMonth),
],
[
'category' => 'deposit',
'amounts' => $this->formatMonthlyAmounts($depositsByMonth),
],
[
'category' => 'bill',
'amounts' => $this->formatMonthlyAmounts($billsByMonth),
],
[
'category' => 'receivable',
'amounts' => $this->formatMonthlyAmounts($receivablesByMonth),
],
[
'category' => 'memo',
'amounts' => $this->getEmptyMonthlyAmounts(),
],
];
// 미수금이 있는 월 확인 (연체 표시용)
$overdueMonths = [];
foreach ($receivablesByMonth as $month => $amount) {
if ($amount > 0) {
$overdueMonths[] = $month;
}
}
$result[] = [
'id' => (string) $client->id,
'vendor_id' => $client->id,
'vendor_name' => $client->name,
'is_overdue' => count($overdueMonths) > 0,
'overdue_months' => $overdueMonths,
'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 $result;
}
/**
* 요약 통계 조회
*/
public function summary(array $params): array
{
$tenantId = $this->tenantId();
$year = $params['year'] ?? date('Y');
$startDate = "{$year}-01-01";
$endDate = "{$year}-12-31";
// 총 매출
$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 = $totalSales - $totalDeposits - $totalBills;
if ($totalReceivables < 0) {
$totalReceivables = 0;
}
// 거래처 수
$vendorCount = Client::where('tenant_id', $tenantId)
->where('is_active', true)
->whereHas('orders')
->count();
// 연체 거래처 수 (미수금이 있는 거래처)
$overdueVendorCount = DB::table('sales')
->where('tenant_id', $tenantId)
->whereNotNull('client_id')
->whereBetween('sale_date', [$startDate, $endDate])
->select('client_id')
->groupBy('client_id')
->havingRaw('SUM(total_amount) > (
SELECT COALESCE(SUM(d.amount), 0) FROM deposits d
WHERE d.tenant_id = ? AND d.client_id = sales.client_id
AND d.deposit_date BETWEEN ? AND ?
) + (
SELECT COALESCE(SUM(b.amount), 0) FROM bills b
WHERE b.tenant_id = ? AND b.client_id = sales.client_id
AND b.bill_type = "received"
AND b.issue_date BETWEEN ? AND ?
)', [$tenantId, $startDate, $endDate, $tenantId, $startDate, $endDate])
->count();
return [
'total_sales' => (float) $totalSales,
'total_deposits' => (float) $totalDeposits,
'total_bills' => (float) $totalBills,
'total_receivables' => (float) $totalReceivables,
'vendor_count' => $vendorCount,
'overdue_vendor_count' => $overdueVendorCount,
];
}
/**
* 월별 매출 조회
*/
private function getSalesByMonth(int $tenantId, int $clientId, string $year): array
{
$result = [];
$sales = Sale::where('tenant_id', $tenantId)
->where('client_id', $clientId)
->whereYear('sale_date', $year)
->select(
DB::raw('MONTH(sale_date) as month'),
DB::raw('SUM(total_amount) as total')
)
->groupBy(DB::raw('MONTH(sale_date)'))
->get();
foreach ($sales as $sale) {
$result[(int) $sale->month] = (float) $sale->total;
}
return $result;
}
/**
* 월별 입금 조회
*/
private function getDepositsByMonth(int $tenantId, int $clientId, string $year): array
{
$result = [];
$deposits = Deposit::where('tenant_id', $tenantId)
->where('client_id', $clientId)
->whereYear('deposit_date', $year)
->select(
DB::raw('MONTH(deposit_date) as month'),
DB::raw('SUM(amount) as total')
)
->groupBy(DB::raw('MONTH(deposit_date)'))
->get();
foreach ($deposits as $deposit) {
$result[(int) $deposit->month] = (float) $deposit->total;
}
return $result;
}
/**
* 월별 어음 조회
*/
private function getBillsByMonth(int $tenantId, int $clientId, string $year): array
{
$result = [];
$bills = Bill::where('tenant_id', $tenantId)
->where('client_id', $clientId)
->where('bill_type', 'received')
->whereYear('issue_date', $year)
->select(
DB::raw('MONTH(issue_date) as month'),
DB::raw('SUM(amount) as total')
)
->groupBy(DB::raw('MONTH(issue_date)'))
->get();
foreach ($bills as $bill) {
$result[(int) $bill->month] = (float) $bill->total;
}
return $result;
}
/**
* 미수금 계산
*/
private function calculateReceivables(array $sales, array $deposits, array $bills): array
{
$result = [];
for ($month = 1; $month <= 12; $month++) {
$salesAmount = $sales[$month] ?? 0;
$depositAmount = $deposits[$month] ?? 0;
$billAmount = $bills[$month] ?? 0;
$receivable = $salesAmount - $depositAmount - $billAmount;
$result[$month] = max(0, $receivable);
}
return $result;
}
/**
* 월별 금액을 프론트엔드 형식으로 변환
*/
private function formatMonthlyAmounts(array $monthlyData): array
{
$amounts = [];
$total = 0;
for ($month = 1; $month <= 12; $month++) {
$key = "month{$month}";
$amount = $monthlyData[$month] ?? 0;
$amounts[$key] = $amount;
$total += $amount;
}
$amounts['total'] = $total;
return $amounts;
}
/**
* 빈 월별 금액 데이터
*/
private function getEmptyMonthlyAmounts(): array
{
$amounts = [];
for ($month = 1; $month <= 12; $month++) {
$amounts["month{$month}"] = 0;
}
$amounts['total'] = 0;
return $amounts;
}
}