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}기", }; } /** * 부가세 상세 조회 (모달용) * * @param string|null $periodType 기간 타입 (quarter|half|year) * @param int|null $year 연도 * @param int|null $period 기간 번호 * @return array */ public function getDetail(?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); $periodLabel = $this->getPeriodLabel($year, $periodType, $period); $validStatuses = [TaxInvoice::STATUS_ISSUED, TaxInvoice::STATUS_SENT]; // 매출 공급가액 + 세액 $salesData = TaxInvoice::where('tenant_id', $tenantId) ->where('direction', TaxInvoice::DIRECTION_SALES) ->whereIn('status', $validStatuses) ->whereBetween('issue_date', [$startDate, $endDate]) ->selectRaw('COALESCE(SUM(supply_amount), 0) as supply_amount, COALESCE(SUM(tax_amount), 0) as tax_amount') ->first(); // 매입 공급가액 + 세액 $purchasesData = TaxInvoice::where('tenant_id', $tenantId) ->where('direction', TaxInvoice::DIRECTION_PURCHASES) ->whereIn('status', $validStatuses) ->whereBetween('issue_date', [$startDate, $endDate]) ->selectRaw('COALESCE(SUM(supply_amount), 0) as supply_amount, COALESCE(SUM(tax_amount), 0) as tax_amount') ->first(); $salesSupplyAmount = (int) ($salesData->supply_amount ?? 0); $salesTaxAmount = (int) ($salesData->tax_amount ?? 0); $purchasesSupplyAmount = (int) ($purchasesData->supply_amount ?? 0); $purchasesTaxAmount = (int) ($purchasesData->tax_amount ?? 0); $estimatedPayment = $salesTaxAmount - $purchasesTaxAmount; // 신고기간 옵션 생성 $periodOptions = $this->generatePeriodOptions($year, $periodType, $period); // 부가세 요약 테이블 (direction + invoice_type 별 GROUP BY) $referenceTable = TaxInvoice::where('tenant_id', $tenantId) ->whereIn('status', $validStatuses) ->whereBetween('issue_date', [$startDate, $endDate]) ->selectRaw(" direction, invoice_type, COALESCE(SUM(supply_amount), 0) as supply_amount, COALESCE(SUM(tax_amount), 0) as tax_amount ") ->groupBy('direction', 'invoice_type') ->get() ->map(fn ($row) => [ 'direction' => $row->direction, 'direction_label' => $row->direction === TaxInvoice::DIRECTION_SALES ? '매출' : '매입', 'invoice_type' => $row->invoice_type, 'invoice_type_label' => match ($row->invoice_type) { TaxInvoice::TYPE_TAX_INVOICE => '전자세금계산서', TaxInvoice::TYPE_INVOICE => '계산서', TaxInvoice::TYPE_MODIFIED_TAX_INVOICE => '수정세금계산서', default => $row->invoice_type, }, 'supply_amount' => (int) $row->supply_amount, 'tax_amount' => (int) $row->tax_amount, ]) ->toArray(); // 미발행/미수취 세금계산서 목록 (status=draft) $unissuedInvoices = TaxInvoice::where('tenant_id', $tenantId) ->where('status', TaxInvoice::STATUS_DRAFT) ->orderBy('issue_date', 'desc') ->limit(100) ->get() ->map(fn ($invoice) => [ 'id' => $invoice->id, 'direction' => $invoice->direction, 'direction_label' => $invoice->direction === TaxInvoice::DIRECTION_SALES ? '매출' : '매입', 'issue_date' => $invoice->issue_date, 'vendor_name' => $invoice->direction === TaxInvoice::DIRECTION_SALES ? ($invoice->buyer_corp_name ?? '-') : ($invoice->supplier_corp_name ?? '-'), 'tax_amount' => (int) $invoice->tax_amount, 'status' => $invoice->direction === TaxInvoice::DIRECTION_SALES ? '미발행' : '미수취', ]) ->toArray(); return [ 'period_label' => $periodLabel, 'period_options' => $periodOptions, 'summary' => [ 'sales_supply_amount' => $salesSupplyAmount, 'sales_tax_amount' => $salesTaxAmount, 'purchases_supply_amount' => $purchasesSupplyAmount, 'purchases_tax_amount' => $purchasesTaxAmount, 'estimated_payment' => (int) abs($estimatedPayment), 'is_refund' => $estimatedPayment < 0, ], 'reference_table' => $referenceTable, 'unissued_invoices' => $unissuedInvoices, ]; } /** * 신고기간 드롭다운 옵션 생성 * 현재 기간 포함 최근 8개 기간 */ private function generatePeriodOptions(int $currentYear, string $periodType, int $currentPeriod): array { $options = []; $year = $currentYear; $period = $currentPeriod; for ($i = 0; $i < 8; $i++) { $label = $this->getPeriodLabel($year, $periodType, $period); $value = "{$year}-{$periodType}-{$period}"; $options[] = ['value' => $value, 'label' => $label]; // 이전 기간으로 이동 $prev = $this->getPreviousPeriod($year, $periodType, $period); $year = $prev['year']; $period = $prev['period']; } return $options; } /** * 이전 기간 계산 * * @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], }; } }