Files
sam-api/app/Services/ClientService.php
hskwon 8686b199ee feat: 더미 데이터 시더 추가 및 회계 관련 마이그레이션
- DummyDataSeeder 및 개별 시더 추가 (Client, BadDebt, Deposit 등)
- payments.paid_at nullable 마이그레이션
- subscriptions 취소 컬럼 추가
- clients 테이블 bad_debt 컬럼 제거
- PlanController, ClientService 수정
- 불필요한 claudedocs, flow-test 파일 정리
2025-12-24 08:54:52 +09:00

221 lines
7.1 KiB
PHP

<?php
namespace App\Services;
use App\Models\BadDebts\BadDebt;
use App\Models\Orders\Client;
use App\Models\Tenants\Deposit;
use App\Models\Tenants\Sale;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class ClientService extends Service
{
/** 목록(검색/페이징) */
public function index(array $params)
{
$tenantId = $this->tenantId();
$page = (int) ($params['page'] ?? 1);
$size = (int) ($params['size'] ?? 20);
$q = trim((string) ($params['q'] ?? ''));
$onlyActive = $params['only_active'] ?? null;
$query = Client::query()->where('tenant_id', $tenantId);
if ($q !== '') {
$query->where(function ($qq) use ($q) {
$qq->where('name', 'like', "%{$q}%")
->orWhere('client_code', 'like', "%{$q}%")
->orWhere('contact_person', 'like', "%{$q}%");
});
}
if ($onlyActive !== null) {
$query->where('is_active', (bool) $onlyActive);
}
$query->orderBy('client_code')->orderBy('id');
$paginator = $query->paginate($size, ['*'], 'page', $page);
// 미수금 계산: 매출 합계 - 입금 합계
$clientIds = $paginator->pluck('id')->toArray();
if (! empty($clientIds)) {
// 거래처별 매출 합계
$salesByClient = Sale::where('tenant_id', $tenantId)
->whereIn('client_id', $clientIds)
->whereNull('deleted_at')
->groupBy('client_id')
->select('client_id', DB::raw('SUM(total_amount) as total_sales'))
->pluck('total_sales', 'client_id');
// 거래처별 입금 합계
$depositsByClient = Deposit::where('tenant_id', $tenantId)
->whereIn('client_id', $clientIds)
->whereNull('deleted_at')
->groupBy('client_id')
->select('client_id', DB::raw('SUM(amount) as total_deposits'))
->pluck('total_deposits', 'client_id');
// 거래처별 활성 악성채권 합계 (추심중, 법적조치)
$badDebtsByClient = BadDebt::where('tenant_id', $tenantId)
->whereIn('client_id', $clientIds)
->whereIn('status', [BadDebt::STATUS_COLLECTING, BadDebt::STATUS_LEGAL_ACTION])
->where('is_active', true)
->whereNull('deleted_at')
->groupBy('client_id')
->select('client_id', DB::raw('SUM(debt_amount) as total_bad_debt'))
->pluck('total_bad_debt', 'client_id');
// 각 거래처에 미수금/악성채권 정보 추가
$paginator->getCollection()->transform(function ($client) use ($salesByClient, $depositsByClient, $badDebtsByClient) {
$totalSales = $salesByClient[$client->id] ?? 0;
$totalDeposits = $depositsByClient[$client->id] ?? 0;
$client->outstanding_amount = max(0, $totalSales - $totalDeposits);
$client->bad_debt_total = $badDebtsByClient[$client->id] ?? 0;
$client->has_bad_debt = ($badDebtsByClient[$client->id] ?? 0) > 0;
return $client;
});
}
return $paginator;
}
/** 단건 */
public function show(int $id)
{
$tenantId = $this->tenantId();
$client = Client::where('tenant_id', $tenantId)->find($id);
if (! $client) {
throw new NotFoundHttpException(__('error.not_found'));
}
// 미수금 계산
$totalSales = Sale::where('tenant_id', $tenantId)
->where('client_id', $id)
->whereNull('deleted_at')
->sum('total_amount');
$totalDeposits = Deposit::where('tenant_id', $tenantId)
->where('client_id', $id)
->whereNull('deleted_at')
->sum('amount');
$client->outstanding_amount = max(0, $totalSales - $totalDeposits);
// 악성채권 정보
$badDebtTotal = BadDebt::where('tenant_id', $tenantId)
->where('client_id', $id)
->whereIn('status', [BadDebt::STATUS_COLLECTING, BadDebt::STATUS_LEGAL_ACTION])
->where('is_active', true)
->whereNull('deleted_at')
->sum('debt_amount');
$client->bad_debt_total = $badDebtTotal;
$client->has_bad_debt = $badDebtTotal > 0;
return $client;
}
/** 생성 */
public function store(array $data)
{
$tenantId = $this->tenantId();
// client_code 자동 생성 (프론트에서 보내도 무시)
$data['client_code'] = $this->generateClientCode($tenantId);
$data['tenant_id'] = $tenantId;
$data['is_active'] = $data['is_active'] ?? true;
return Client::create($data);
}
/**
* 유니크한 client_code 자동 생성
* 형식: 8자리 영숫자 (예: A3B7X9K2)
*/
private function generateClientCode(int $tenantId): string
{
$maxAttempts = 10;
$attempt = 0;
do {
// 8자리 영숫자 코드 생성 (대문자 + 숫자)
$code = strtoupper(substr(bin2hex(random_bytes(4)), 0, 8));
// 중복 검사
$exists = Client::withoutGlobalScopes()
->where('tenant_id', $tenantId)
->where('client_code', $code)
->exists();
$attempt++;
} while ($exists && $attempt < $maxAttempts);
if ($exists) {
// 극히 드문 경우: timestamp 추가하여 유니크성 보장
$code = strtoupper(substr(bin2hex(random_bytes(2)), 0, 4).dechex(time() % 0xFFFF));
}
return $code;
}
/** 수정 */
public function update(int $id, array $data)
{
$tenantId = $this->tenantId();
$client = Client::where('tenant_id', $tenantId)->find($id);
if (! $client) {
throw new NotFoundHttpException(__('error.not_found'));
}
// client_code 변경 불가 (프론트에서 보내도 무시)
unset($data['client_code']);
$client->update($data);
return $client->refresh();
}
/** 삭제 */
public function destroy(int $id)
{
$tenantId = $this->tenantId();
$client = Client::where('tenant_id', $tenantId)->find($id);
if (! $client) {
throw new NotFoundHttpException(__('error.not_found'));
}
// 주문 존재 검사
if ($client->orders()->exists()) {
throw new BadRequestHttpException(__('error.has_orders'));
}
$client->delete();
return 'success';
}
/** 활성/비활성 토글 */
public function toggle(int $id)
{
$tenantId = $this->tenantId();
$client = Client::where('tenant_id', $tenantId)->find($id);
if (! $client) {
throw new NotFoundHttpException(__('error.not_found'));
}
$client->is_active = ! $client->is_active;
$client->save();
return $client->refresh();
}
}