Files
sam-api/app/Services/VatService.php
권혁성 f7850e43a7 feat: CEO 대시보드 API 구현 및 DB 컬럼 오류 수정
- StatusBoardService: 현황판 8개 항목 집계 API
- CalendarService: 캘린더 일정 조회 API (작업지시/계약/휴가)
- TodayIssueService: 오늘의 이슈 리스트 API
- VatService: 부가세 신고 현황 API
- EntertainmentService: 접대비 현황 API
- WelfareService: 복리후생 현황 API

버그 수정:
- orders 테이블 status → status_code 컬럼명 수정
- users 테이블 department 관계 → tenantProfile.department로 수정
- Swagger 문서 및 라우트 추가
2026-01-21 10:25:18 +09:00

261 lines
9.2 KiB
PHP

<?php
namespace App\Services;
use App\Models\Tenants\TaxInvoice;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
/**
* 부가세 현황 서비스
*
* CEO 대시보드용 부가세 데이터를 제공합니다.
*/
class VatService extends Service
{
/**
* 부가세 현황 요약 조회
*
* @param string|null $periodType 기간 타입 (quarter|half|year, 기본: quarter)
* @param int|null $year 연도 (기본: 현재 연도)
* @param int|null $period 기간 번호 (quarter: 1-4, half: 1-2, 기본: 현재 기간)
* @return array{cards: array, check_points: array}
*/
public function getSummary(?string $periodType = 'quarter', ?int $year = null, ?int $period = null): array
{
$tenantId = $this->tenantId();
$now = Carbon::now();
// 기본값 설정
$year = $year ?? $now->year;
$periodType = $periodType ?? 'quarter';
$period = $period ?? $this->getCurrentPeriod($periodType, $now);
// 기간 범위 계산
[$startDate, $endDate] = $this->getPeriodDateRange($year, $periodType, $period);
// 발행 완료된 세금계산서만 계산 (status: issued, sent)
$validStatuses = [TaxInvoice::STATUS_ISSUED, TaxInvoice::STATUS_SENT];
// 매출세액 (sales)
$salesTaxAmount = TaxInvoice::where('tenant_id', $tenantId)
->where('direction', TaxInvoice::DIRECTION_SALES)
->whereIn('status', $validStatuses)
->whereBetween('issue_date', [$startDate, $endDate])
->sum('tax_amount');
// 매입세액 (purchases)
$purchasesTaxAmount = TaxInvoice::where('tenant_id', $tenantId)
->where('direction', TaxInvoice::DIRECTION_PURCHASES)
->whereIn('status', $validStatuses)
->whereBetween('issue_date', [$startDate, $endDate])
->sum('tax_amount');
// 예상 납부세액 (매출세액 - 매입세액)
$estimatedPayment = $salesTaxAmount - $purchasesTaxAmount;
// 미발행 세금계산서 건수 (전체 기간, status: draft)
$unissuedCount = TaxInvoice::where('tenant_id', $tenantId)
->where('status', TaxInvoice::STATUS_DRAFT)
->count();
// 카드 데이터 구성
$cards = [
[
'id' => 'vat_sales_tax',
'label' => '매출세액',
'amount' => (int) $salesTaxAmount,
],
[
'id' => 'vat_purchases_tax',
'label' => '매입세액',
'amount' => (int) $purchasesTaxAmount,
],
[
'id' => 'vat_estimated_payment',
'label' => '예상 납부세액',
'amount' => (int) abs($estimatedPayment),
'subLabel' => $estimatedPayment < 0 ? '환급' : null,
],
[
'id' => 'vat_unissued',
'label' => '세금계산서 미발행',
'amount' => $unissuedCount,
'unit' => '건',
],
];
// 체크포인트 생성
$checkPoints = $this->generateCheckPoints(
$year,
$periodType,
$period,
$salesTaxAmount,
$purchasesTaxAmount,
$estimatedPayment,
$tenantId
);
return [
'cards' => $cards,
'check_points' => $checkPoints,
];
}
/**
* 현재 기간 계산
*/
private function getCurrentPeriod(string $periodType, Carbon $date): int
{
return match ($periodType) {
'quarter' => $date->quarter,
'half' => $date->month <= 6 ? 1 : 2,
'year' => 1,
default => $date->quarter,
};
}
/**
* 기간 범위 날짜 계산
*
* @return array{0: string, 1: string} [startDate, endDate]
*/
private function getPeriodDateRange(int $year, string $periodType, int $period): array
{
return match ($periodType) {
'quarter' => [
Carbon::create($year, ($period - 1) * 3 + 1, 1)->format('Y-m-d'),
Carbon::create($year, $period * 3, 1)->endOfMonth()->format('Y-m-d'),
],
'half' => [
Carbon::create($year, ($period - 1) * 6 + 1, 1)->format('Y-m-d'),
Carbon::create($year, $period * 6, 1)->endOfMonth()->format('Y-m-d'),
],
'year' => [
Carbon::create($year, 1, 1)->format('Y-m-d'),
Carbon::create($year, 12, 31)->format('Y-m-d'),
],
default => [
Carbon::create($year, ($period - 1) * 3 + 1, 1)->format('Y-m-d'),
Carbon::create($year, $period * 3, 1)->endOfMonth()->format('Y-m-d'),
],
};
}
/**
* 체크포인트 생성
*/
private function generateCheckPoints(
int $year,
string $periodType,
int $period,
float $salesTaxAmount,
float $purchasesTaxAmount,
float $estimatedPayment,
int $tenantId
): array {
$checkPoints = [];
$periodLabel = $this->getPeriodLabel($year, $periodType, $period);
// 이전 기간 데이터 조회 (전기 대비 비교용)
$previousPeriod = $this->getPreviousPeriod($year, $periodType, $period);
[$prevStartDate, $prevEndDate] = $this->getPeriodDateRange(
$previousPeriod['year'],
$periodType,
$previousPeriod['period']
);
$validStatuses = [TaxInvoice::STATUS_ISSUED, TaxInvoice::STATUS_SENT];
$prevSalesTax = TaxInvoice::where('tenant_id', $tenantId)
->where('direction', TaxInvoice::DIRECTION_SALES)
->whereIn('status', $validStatuses)
->whereBetween('issue_date', [$prevStartDate, $prevEndDate])
->sum('tax_amount');
$prevPurchasesTax = TaxInvoice::where('tenant_id', $tenantId)
->where('direction', TaxInvoice::DIRECTION_PURCHASES)
->whereIn('status', $validStatuses)
->whereBetween('issue_date', [$prevStartDate, $prevEndDate])
->sum('tax_amount');
$prevEstimatedPayment = $prevSalesTax - $prevPurchasesTax;
// 납부/환급 여부에 따른 메시지 생성
if ($estimatedPayment < 0) {
// 환급
$refundAmount = number_format(abs($estimatedPayment));
$message = "{$periodLabel} 기준, 예상 환급세액은 {$refundAmount}원입니다.";
// 원인 분석 추가
if ($purchasesTaxAmount > $salesTaxAmount) {
$message .= ' 매입세액이 매출세액을 초과하여 환급이 예상됩니다.';
}
$checkPoints[] = [
'id' => 'vat_cp_refund',
'type' => 'success',
'message' => $message,
'highlights' => [
['text' => "{$periodLabel} 기준, 예상 환급세액은 {$refundAmount}원입니다.", 'color' => 'blue'],
],
];
} else {
// 납부
$paymentAmount = number_format($estimatedPayment);
$message = "{$periodLabel} 기준, 예상 납부세액은 {$paymentAmount}원입니다.";
// 전기 대비 변동률 계산
if ($prevEstimatedPayment > 0) {
$changeRate = (($estimatedPayment - $prevEstimatedPayment) / $prevEstimatedPayment) * 100;
$changeDirection = $changeRate >= 0 ? '증가' : '감소';
$message .= sprintf(' 전기 대비 %.1f%% %s했습니다.', abs($changeRate), $changeDirection);
}
$checkPoints[] = [
'id' => 'vat_cp_payment',
'type' => 'success',
'message' => $message,
'highlights' => [
['text' => "{$periodLabel} 기준, 예상 납부세액은 {$paymentAmount}원입니다.", 'color' => 'red'],
],
];
}
return $checkPoints;
}
/**
* 기간 라벨 생성
*/
private function getPeriodLabel(int $year, string $periodType, int $period): string
{
return match ($periodType) {
'quarter' => "{$year}{$period}기 예정신고",
'half' => "{$year}" . ($period === 1 ? '상반기' : '하반기') . ' 확정신고',
'year' => "{$year}년 연간",
default => "{$year}{$period}",
};
}
/**
* 이전 기간 계산
*
* @return array{year: int, period: int}
*/
private function getPreviousPeriod(int $year, string $periodType, int $period): array
{
return match ($periodType) {
'quarter' => $period === 1
? ['year' => $year - 1, 'period' => 4]
: ['year' => $year, 'period' => $period - 1],
'half' => $period === 1
? ['year' => $year - 1, 'period' => 2]
: ['year' => $year, 'period' => 1],
'year' => ['year' => $year - 1, 'period' => 1],
default => $period === 1
? ['year' => $year - 1, 'period' => 4]
: ['year' => $year, 'period' => $period - 1],
};
}
}