Files
sam-api/app/Services/BadDebtService.php
권혁성 09db0da43b feat(API): 부실채권, 재고, 입고 기능 개선
- BadDebt 컨트롤러/서비스 기능 확장
- StockService 재고 조회 로직 개선
- ProcessReceivingRequest 검증 규칙 수정
- Item, Order, CommonCode, Shipment 모델 업데이트
- TodayIssueObserverService 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 21:32:23 +09:00

337 lines
10 KiB
PHP

<?php
namespace App\Services;
use App\Models\BadDebts\BadDebt;
use App\Models\BadDebts\BadDebtDocument;
use App\Models\BadDebts\BadDebtMemo;
use App\Models\Orders\Client;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB;
class BadDebtService extends Service
{
/**
* 악성채권 목록 조회 (거래처 기준)
*
* 거래처별로 is_active=true인 악성채권을 집계하여 조회
*/
public function index(array $params): LengthAwarePaginator
{
$tenantId = $this->tenantId();
$query = Client::query()
->where('tenant_id', $tenantId)
// is_active=true인 악성채권이 있는 거래처만
->whereHas('badDebts', function ($q) {
$q->where('is_active', true);
})
// 활성 악성채권 eager loading (추심중/법적조치 + is_active=true)
->with(['activeBadDebts' => function ($q) {
$q->with('assignedUser:id,name')
->orderBy('created_at', 'desc');
}])
// 집계: 총 미수금액 (is_active=true인 건만)
->withSum(['badDebts as total_debt_amount' => function ($q) {
$q->where('is_active', true);
}], 'debt_amount')
// 집계: 최대 연체일수 (is_active=true인 건만)
->withMax(['badDebts as max_overdue_days' => function ($q) {
$q->where('is_active', true);
}], 'overdue_days')
// 집계: 악성채권 건수 (is_active=true인 건만)
->withCount(['badDebts as active_bad_debt_count' => function ($q) {
$q->where('is_active', true);
}]);
// 거래처 필터
if (! empty($params['client_id'])) {
$query->where('id', $params['client_id']);
}
// 상태 필터 (해당 상태의 악성채권이 있는 거래처만)
if (! empty($params['status'])) {
$query->whereHas('badDebts', function ($q) use ($params) {
$q->where('is_active', true)
->where('status', $params['status']);
});
}
// 검색어 필터
if (! empty($params['search'])) {
$search = $params['search'];
$query->where(function ($q) use ($search) {
$q->where('name', 'like', "%{$search}%")
->orWhere('client_code', 'like', "%{$search}%");
});
}
// 정렬
$sortBy = $params['sort_by'] ?? 'total_debt_amount';
$sortDir = $params['sort_dir'] ?? 'desc';
$query->orderBy($sortBy, $sortDir);
// 페이지네이션
$perPage = $params['per_page'] ?? 20;
return $query->paginate($perPage);
}
/**
* 악성채권 요약 통계 (is_active=true인 건만)
*/
public function summary(array $params = []): array
{
$tenantId = $this->tenantId();
// is_active=true인 악성채권만 통계
$query = BadDebt::query()
->where('tenant_id', $tenantId)
->where('is_active', true);
// 거래처 필터
if (! empty($params['client_id'])) {
$query->where('client_id', $params['client_id']);
}
// 전체 합계
$totalAmount = (clone $query)->sum('debt_amount');
// 상태별 합계
$collectingAmount = (clone $query)->collecting()->sum('debt_amount');
$legalActionAmount = (clone $query)->legalAction()->sum('debt_amount');
$recoveredAmount = (clone $query)->recovered()->sum('debt_amount');
$badDebtAmount = (clone $query)->badDebt()->sum('debt_amount');
// 거래처 수 (is_active=true인 악성채권이 있는)
$clientCount = BadDebt::query()
->where('tenant_id', $tenantId)
->where('is_active', true)
->distinct('client_id')
->count('client_id');
return [
'total_amount' => (float) $totalAmount,
'collecting_amount' => (float) $collectingAmount,
'legal_action_amount' => (float) $legalActionAmount,
'recovered_amount' => (float) $recoveredAmount,
'bad_debt_amount' => (float) $badDebtAmount,
'client_count' => $clientCount,
];
}
/**
* 악성채권 상세 조회
*/
public function show(int $id): BadDebt
{
$tenantId = $this->tenantId();
return BadDebt::query()
->where('tenant_id', $tenantId)
->with([
'client',
'assignedUser:id,name',
'creator:id,name',
'documents.file',
'memos.creator:id,name',
])
->findOrFail($id);
}
/**
* 악성채권 등록
*/
public function store(array $data): BadDebt
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($data, $tenantId, $userId) {
$badDebt = new BadDebt;
$badDebt->tenant_id = $tenantId;
$badDebt->client_id = $data['client_id'];
$badDebt->debt_amount = $data['debt_amount'];
$badDebt->status = $data['status'] ?? BadDebt::STATUS_COLLECTING;
$badDebt->overdue_days = $data['overdue_days'] ?? 0;
$badDebt->assigned_user_id = $data['assigned_user_id'] ?? null;
$badDebt->occurred_at = $data['occurred_at'] ?? null;
$badDebt->closed_at = $data['closed_at'] ?? null;
$badDebt->is_active = $data['is_active'] ?? true;
$badDebt->options = $data['options'] ?? null;
$badDebt->created_by = $userId;
$badDebt->updated_by = $userId;
$badDebt->save();
return $badDebt->load(['client:id,name,client_code', 'assignedUser:id,name']);
});
}
/**
* 악성채권 수정
*/
public function update(int $id, array $data): BadDebt
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $data, $tenantId, $userId) {
$badDebt = BadDebt::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
if (isset($data['client_id'])) {
$badDebt->client_id = $data['client_id'];
}
if (isset($data['debt_amount'])) {
$badDebt->debt_amount = $data['debt_amount'];
}
if (isset($data['status'])) {
$badDebt->status = $data['status'];
}
if (isset($data['overdue_days'])) {
$badDebt->overdue_days = $data['overdue_days'];
}
if (array_key_exists('assigned_user_id', $data)) {
$badDebt->assigned_user_id = $data['assigned_user_id'];
}
if (array_key_exists('occurred_at', $data)) {
$badDebt->occurred_at = $data['occurred_at'];
}
if (array_key_exists('closed_at', $data)) {
$badDebt->closed_at = $data['closed_at'];
}
if (isset($data['is_active'])) {
$badDebt->is_active = $data['is_active'];
}
if (array_key_exists('options', $data)) {
$badDebt->options = $data['options'];
}
$badDebt->updated_by = $userId;
$badDebt->save();
return $badDebt->fresh(['client:id,name,client_code', 'assignedUser:id,name']);
});
}
/**
* 악성채권 삭제
*/
public function destroy(int $id): bool
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
$badDebt = BadDebt::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$badDebt->deleted_by = $userId;
$badDebt->save();
$badDebt->delete();
return true;
});
}
/**
* 설정 토글 (is_active)
*/
public function toggle(int $id): BadDebt
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
return DB::transaction(function () use ($id, $tenantId, $userId) {
$badDebt = BadDebt::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$badDebt->is_active = ! $badDebt->is_active;
$badDebt->updated_by = $userId;
$badDebt->save();
return $badDebt->fresh(['client:id,name,client_code', 'assignedUser:id,name']);
});
}
/**
* 서류 첨부
*/
public function addDocument(int $id, array $data): BadDebtDocument
{
$tenantId = $this->tenantId();
$badDebt = BadDebt::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$document = new BadDebtDocument;
$document->bad_debt_id = $badDebt->id;
$document->document_type = $data['document_type'];
$document->file_id = $data['file_id'];
$document->save();
return $document->load('file');
}
/**
* 서류 삭제
*/
public function removeDocument(int $id, int $documentId): bool
{
$tenantId = $this->tenantId();
$badDebt = BadDebt::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$document = BadDebtDocument::query()
->where('bad_debt_id', $badDebt->id)
->findOrFail($documentId);
return $document->delete();
}
/**
* 메모 추가
*/
public function addMemo(int $id, array $data): BadDebtMemo
{
$tenantId = $this->tenantId();
$userId = $this->apiUserId();
$badDebt = BadDebt::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$memo = new BadDebtMemo;
$memo->bad_debt_id = $badDebt->id;
$memo->content = $data['content'];
$memo->created_by = $userId;
$memo->save();
return $memo->load('creator:id,name');
}
/**
* 메모 삭제
*/
public function removeMemo(int $id, int $memoId): bool
{
$tenantId = $this->tenantId();
$badDebt = BadDebt::query()
->where('tenant_id', $tenantId)
->findOrFail($id);
$memo = BadDebtMemo::query()
->where('bad_debt_id', $badDebt->id)
->findOrFail($memoId);
return $memo->delete();
}
}