- BadDebt 컨트롤러/서비스 기능 확장 - StockService 재고 조회 로직 개선 - ProcessReceivingRequest 검증 규칙 수정 - Item, Order, CommonCode, Shipment 모델 업데이트 - TodayIssueObserverService 개선 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
337 lines
10 KiB
PHP
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();
|
|
}
|
|
}
|