2026-01-21 10:25:18 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use Carbon\Carbon;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 접대비 현황 서비스
|
|
|
|
|
*
|
|
|
|
|
* CEO 대시보드용 접대비 데이터를 제공합니다.
|
|
|
|
|
*/
|
|
|
|
|
class EntertainmentService extends Service
|
|
|
|
|
{
|
|
|
|
|
// 접대비 기본 한도율 (중소기업 기준: 매출의 0.3%)
|
|
|
|
|
private const DEFAULT_LIMIT_RATE = 0.003;
|
|
|
|
|
|
|
|
|
|
// 기업 규모별 기본 한도 (연간)
|
|
|
|
|
private const COMPANY_TYPE_LIMITS = [
|
|
|
|
|
'large' => 36000000, // 대기업: 연 3,600만원
|
|
|
|
|
'medium' => 36000000, // 중견기업: 연 3,600만원
|
|
|
|
|
'small' => 24000000, // 중소기업: 연 2,400만원
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 접대비 현황 요약 조회
|
|
|
|
|
*
|
|
|
|
|
* @param string|null $limitType 기간 타입 (annual|quarterly, 기본: quarterly)
|
|
|
|
|
* @param string|null $companyType 기업 유형 (large|medium|small, 기본: medium)
|
|
|
|
|
* @param int|null $year 연도 (기본: 현재 연도)
|
|
|
|
|
* @param int|null $quarter 분기 (1-4, 기본: 현재 분기)
|
|
|
|
|
* @return array{cards: array, check_points: array}
|
|
|
|
|
*/
|
|
|
|
|
public function getSummary(
|
|
|
|
|
?string $limitType = 'quarterly',
|
|
|
|
|
?string $companyType = 'medium',
|
|
|
|
|
?int $year = null,
|
|
|
|
|
?int $quarter = null
|
|
|
|
|
): array {
|
|
|
|
|
$tenantId = $this->tenantId();
|
|
|
|
|
$now = Carbon::now();
|
|
|
|
|
|
|
|
|
|
// 기본값 설정
|
|
|
|
|
$year = $year ?? $now->year;
|
|
|
|
|
$limitType = $limitType ?? 'quarterly';
|
|
|
|
|
$companyType = $companyType ?? 'medium';
|
|
|
|
|
$quarter = $quarter ?? $now->quarter;
|
|
|
|
|
|
|
|
|
|
// 기간 범위 계산
|
|
|
|
|
if ($limitType === 'annual') {
|
|
|
|
|
$startDate = Carbon::create($year, 1, 1)->format('Y-m-d');
|
|
|
|
|
$endDate = Carbon::create($year, 12, 31)->format('Y-m-d');
|
|
|
|
|
$periodLabel = "{$year}년";
|
|
|
|
|
} else {
|
|
|
|
|
$startDate = Carbon::create($year, ($quarter - 1) * 3 + 1, 1)->format('Y-m-d');
|
|
|
|
|
$endDate = Carbon::create($year, $quarter * 3, 1)->endOfMonth()->format('Y-m-d');
|
|
|
|
|
$periodLabel = "{$quarter}사분기";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 연간 시작일 (매출 계산용)
|
|
|
|
|
$yearStartDate = Carbon::create($year, 1, 1)->format('Y-m-d');
|
|
|
|
|
$yearEndDate = Carbon::create($year, 12, 31)->format('Y-m-d');
|
|
|
|
|
|
|
|
|
|
// 매출액 조회 (연간)
|
|
|
|
|
$annualSales = $this->getAnnualSales($tenantId, $yearStartDate, $yearEndDate);
|
|
|
|
|
|
|
|
|
|
// 접대비 한도 계산
|
|
|
|
|
$annualLimit = $this->calculateLimit($annualSales, $companyType);
|
|
|
|
|
$periodLimit = $limitType === 'annual' ? $annualLimit : ($annualLimit / 4);
|
|
|
|
|
|
|
|
|
|
// 접대비 사용액 조회
|
|
|
|
|
$usedAmount = $this->getUsedAmount($tenantId, $startDate, $endDate);
|
|
|
|
|
|
|
|
|
|
// 잔여 한도
|
|
|
|
|
$remainingLimit = max(0, $periodLimit - $usedAmount);
|
|
|
|
|
|
|
|
|
|
// 카드 데이터 구성
|
|
|
|
|
$cards = [
|
|
|
|
|
[
|
|
|
|
|
'id' => 'et_sales',
|
|
|
|
|
'label' => '매출',
|
|
|
|
|
'amount' => (int) $annualSales,
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'id' => 'et_limit',
|
|
|
|
|
'label' => "{{$periodLabel}} 접대비 총 한도",
|
|
|
|
|
'amount' => (int) $periodLimit,
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'id' => 'et_remaining',
|
|
|
|
|
'label' => "{{$periodLabel}} 접대비 잔여한도",
|
|
|
|
|
'amount' => (int) $remainingLimit,
|
|
|
|
|
],
|
|
|
|
|
[
|
|
|
|
|
'id' => 'et_used',
|
|
|
|
|
'label' => "{{$periodLabel}} 접대비 사용금액",
|
|
|
|
|
'amount' => (int) $usedAmount,
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 체크포인트 생성
|
|
|
|
|
$checkPoints = $this->generateCheckPoints(
|
|
|
|
|
$periodLabel,
|
|
|
|
|
$periodLimit,
|
|
|
|
|
$usedAmount,
|
|
|
|
|
$remainingLimit,
|
|
|
|
|
$tenantId,
|
|
|
|
|
$startDate,
|
|
|
|
|
$endDate
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'cards' => $cards,
|
|
|
|
|
'check_points' => $checkPoints,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 연간 매출액 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getAnnualSales(int $tenantId, string $startDate, string $endDate): float
|
|
|
|
|
{
|
2026-01-21 10:39:54 +09:00
|
|
|
// orders 테이블에서 확정된 수주 합계 조회
|
|
|
|
|
$amount = DB::table('orders')
|
2026-01-21 10:25:18 +09:00
|
|
|
->where('tenant_id', $tenantId)
|
2026-01-21 10:39:54 +09:00
|
|
|
->where('status_code', 'confirmed')
|
|
|
|
|
->whereBetween('received_at', [$startDate, $endDate])
|
2026-01-21 10:25:18 +09:00
|
|
|
->whereNull('deleted_at')
|
2026-01-21 10:39:54 +09:00
|
|
|
->sum('total_amount');
|
|
|
|
|
|
|
|
|
|
return $amount ?: 30530000000; // 임시 기본값 (305억)
|
2026-01-21 10:25:18 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 접대비 한도 계산
|
|
|
|
|
*/
|
|
|
|
|
private function calculateLimit(float $annualSales, string $companyType): float
|
|
|
|
|
{
|
|
|
|
|
// 기본 한도 (기업 규모별)
|
|
|
|
|
$baseLimit = self::COMPANY_TYPE_LIMITS[$companyType] ?? self::COMPANY_TYPE_LIMITS['medium'];
|
|
|
|
|
|
|
|
|
|
// 매출 기반 한도 (0.3%)
|
|
|
|
|
$salesBasedLimit = $annualSales * self::DEFAULT_LIMIT_RATE;
|
|
|
|
|
|
|
|
|
|
// 기본 한도 + 매출 기반 한도 (실제 세법은 더 복잡하지만 간소화)
|
|
|
|
|
return $baseLimit + $salesBasedLimit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 접대비 사용액 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getUsedAmount(int $tenantId, string $startDate, string $endDate): float
|
|
|
|
|
{
|
|
|
|
|
// TODO: 실제 접대비 계정과목에서 조회
|
|
|
|
|
// expense_accounts 또는 card_transactions에서 접대비 항목 합계
|
|
|
|
|
$amount = DB::table('expense_accounts')
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->where('account_type', 'entertainment')
|
|
|
|
|
->whereBetween('expense_date', [$startDate, $endDate])
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
->sum('amount');
|
|
|
|
|
|
|
|
|
|
return $amount ?: 10000000; // 임시 기본값
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 거래처 누락 건수 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getMissingVendorCount(int $tenantId, string $startDate, string $endDate): array
|
|
|
|
|
{
|
|
|
|
|
// TODO: 거래처 정보 누락 건수 조회
|
|
|
|
|
$result = DB::table('expense_accounts')
|
|
|
|
|
->where('tenant_id', $tenantId)
|
|
|
|
|
->where('account_type', 'entertainment')
|
|
|
|
|
->whereBetween('expense_date', [$startDate, $endDate])
|
|
|
|
|
->whereNull('vendor_id')
|
|
|
|
|
->whereNull('deleted_at')
|
|
|
|
|
->selectRaw('COUNT(*) as count, COALESCE(SUM(amount), 0) as total')
|
|
|
|
|
->first();
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'count' => $result->count ?? 0,
|
|
|
|
|
'total' => $result->total ?? 0,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 체크포인트 생성
|
|
|
|
|
*/
|
|
|
|
|
private function generateCheckPoints(
|
|
|
|
|
string $periodLabel,
|
|
|
|
|
float $limit,
|
|
|
|
|
float $used,
|
|
|
|
|
float $remaining,
|
|
|
|
|
int $tenantId,
|
|
|
|
|
string $startDate,
|
|
|
|
|
string $endDate
|
|
|
|
|
): array {
|
|
|
|
|
$checkPoints = [];
|
|
|
|
|
$usageRate = $limit > 0 ? ($used / $limit) * 100 : 0;
|
|
|
|
|
$usedFormatted = number_format($used / 10000);
|
|
|
|
|
$limitFormatted = number_format($limit / 10000);
|
|
|
|
|
$remainingFormatted = number_format($remaining / 10000);
|
|
|
|
|
|
|
|
|
|
// 사용률에 따른 체크포인트
|
|
|
|
|
if ($usageRate <= 75) {
|
|
|
|
|
// 정상 운영
|
2026-01-21 10:43:06 +09:00
|
|
|
$remainingRate = round(100 - $usageRate);
|
2026-01-21 10:25:18 +09:00
|
|
|
$checkPoints[] = [
|
|
|
|
|
'id' => 'et_cp_normal',
|
|
|
|
|
'type' => 'success',
|
2026-01-21 10:43:06 +09:00
|
|
|
'message' => "{$periodLabel} 접대비 사용 {$usedFormatted}만원 / 한도 {$limitFormatted}만원 ({$remainingRate}%). 여유 있게 운영 중입니다.",
|
2026-01-21 10:25:18 +09:00
|
|
|
'highlights' => [
|
|
|
|
|
['text' => "{$usedFormatted}만원", 'color' => 'green'],
|
2026-01-21 10:43:06 +09:00
|
|
|
['text' => "{$limitFormatted}만원 ({$remainingRate}%)", 'color' => 'green'],
|
2026-01-21 10:25:18 +09:00
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
} elseif ($usageRate <= 100) {
|
|
|
|
|
// 주의 (85% 이상)
|
2026-01-21 10:43:06 +09:00
|
|
|
$usageRateRounded = round($usageRate);
|
2026-01-21 10:25:18 +09:00
|
|
|
$checkPoints[] = [
|
|
|
|
|
'id' => 'et_cp_warning',
|
|
|
|
|
'type' => 'warning',
|
2026-01-21 10:43:06 +09:00
|
|
|
'message' => "접대비 한도 {$usageRateRounded}% 도달. 잔여 한도 {$remainingFormatted}만원입니다. 사용 계획을 점검해 주세요.",
|
2026-01-21 10:25:18 +09:00
|
|
|
'highlights' => [
|
|
|
|
|
['text' => "잔여 한도 {$remainingFormatted}만원", 'color' => 'orange'],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
} else {
|
|
|
|
|
// 한도 초과
|
|
|
|
|
$overAmount = $used - $limit;
|
|
|
|
|
$overFormatted = number_format($overAmount / 10000);
|
|
|
|
|
$checkPoints[] = [
|
|
|
|
|
'id' => 'et_cp_over',
|
|
|
|
|
'type' => 'error',
|
|
|
|
|
'message' => "접대비 한도 초과 {$overFormatted}만원 발생. 초과분은 손금불산입되어 법인세 부담이 증가합니다.",
|
|
|
|
|
'highlights' => [
|
|
|
|
|
['text' => "{$overFormatted}만원 발생", 'color' => 'red'],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 거래처 정보 누락 체크
|
|
|
|
|
$missingVendor = $this->getMissingVendorCount($tenantId, $startDate, $endDate);
|
|
|
|
|
if ($missingVendor['count'] > 0) {
|
|
|
|
|
$missingTotal = number_format($missingVendor['total'] / 10000);
|
|
|
|
|
$checkPoints[] = [
|
|
|
|
|
'id' => 'et_cp_missing',
|
|
|
|
|
'type' => 'error',
|
|
|
|
|
'message' => "접대비 사용 중 {$missingVendor['count']}건({$missingTotal}만원)의 거래처 정보가 누락되었습니다. 기록을 보완해 주세요.",
|
|
|
|
|
'highlights' => [
|
|
|
|
|
['text' => "{$missingVendor['count']}건({$missingTotal}만원)", 'color' => 'red'],
|
|
|
|
|
['text' => '거래처 정보가 누락', 'color' => 'red'],
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $checkPoints;
|
|
|
|
|
}
|
2026-01-26 20:29:22 +09:00
|
|
|
}
|