input('period'); // Step 1: 기간 → 날짜 범위 변환 [$startDate, $endDate] = $period ? $this->periodToDateRange($period) : [null, null]; $startDateYmd = $startDate ? str_replace('-', '', $startDate) : null; $endDateYmd = $endDate ? str_replace('-', '', $endDate) : null; $taxTypeMap = [ 1 => 'taxable', 2 => 'zero_rated', 3 => 'exempt', ]; // Step 2: 홈택스 매출 조회 $hometaxSalesRecords = collect(); $hometaxPurchaseRecords = collect(); $cardRecords = collect(); if ($startDate && $endDate) { // 홈택스 매출 $hometaxSales = HometaxInvoice::where('tenant_id', $tenantId) ->sales() ->period($startDate, $endDate) ->get(); $hometaxSalesRecords = $hometaxSales->map(function ($inv) use ($period, $taxTypeMap) { return [ 'id' => 'hometax_'.$inv->id, 'period' => $period, 'type' => 'sales', 'taxType' => $taxTypeMap[$inv->tax_type] ?? 'taxable', 'partnerName' => $inv->invoicee_corp_name, 'invoiceNo' => $inv->nts_confirm_num ?? '', 'invoiceDate' => $inv->write_date ? \Carbon\Carbon::parse($inv->write_date)->format('Y-m-d') : null, 'supplyAmount' => (int) $inv->supply_amount, 'vatAmount' => (int) $inv->tax_amount, 'totalAmount' => (int) $inv->total_amount, 'status' => 'filed', 'memo' => null, 'isCardTransaction' => false, 'isHometax' => true, 'source' => 'hometax', ]; }); // Step 3: 홈택스 매입 조회 $hometaxPurchases = HometaxInvoice::where('tenant_id', $tenantId) ->purchase() ->period($startDate, $endDate) ->get(); $hometaxPurchaseRecords = $hometaxPurchases->map(function ($inv) use ($period, $taxTypeMap) { return [ 'id' => 'hometax_'.$inv->id, 'period' => $period, 'type' => 'purchase', 'taxType' => $taxTypeMap[$inv->tax_type] ?? 'taxable', 'partnerName' => $inv->invoicer_corp_name, 'invoiceNo' => $inv->nts_confirm_num ?? '', 'invoiceDate' => $inv->write_date ? \Carbon\Carbon::parse($inv->write_date)->format('Y-m-d') : null, 'supplyAmount' => (int) $inv->supply_amount, 'vatAmount' => (int) $inv->tax_amount, 'totalAmount' => (int) $inv->total_amount, 'status' => 'filed', 'memo' => null, 'isCardTransaction' => false, 'isHometax' => true, 'source' => 'hometax', ]; }); // Step 4: 카드 공제분 조회 if ($startDateYmd && $endDateYmd) { $hiddenKeys = CardTransactionHide::getHiddenKeys($tenantId, $startDateYmd, $endDateYmd); $cardTransactions = BarobillCardTransaction::where('tenant_id', $tenantId) ->whereBetween('use_date', [$startDateYmd, $endDateYmd]) ->orderBy('use_date', 'desc') ->get(); $splitsByKey = CardTransactionSplit::getByDateRange($tenantId, $startDateYmd, $endDateYmd); // 분개가 존재하는 거래의 부분키(금액 제외) 인덱스 생성 // 수동입력 등으로 금액이 달라져도 매칭되도록 함 $splitsByPartialKey = []; foreach ($splitsByKey as $fullKey => $splits) { $parts = explode('|', $fullKey); if (count($parts) >= 3) { $partialKey = $parts[0].'|'.$parts[1].'|'.$parts[2]; $splitsByPartialKey[$partialKey] = $splits; } } foreach ($cardTransactions as $card) { // 숨김 처리된 거래는 skip if ($hiddenKeys->contains($card->unique_key)) { continue; } // 분개 매칭: 정확한 키 → 부분키(금액 제외) 순으로 시도 $splits = $splitsByKey[$card->unique_key] ?? null; if (! $splits) { $cardPartialKey = $card->card_num.'|'.$card->use_dt.'|'.$card->approval_num; $splits = $splitsByPartialKey[$cardPartialKey] ?? null; } if ($splits && count($splits) > 0) { // 분개가 있으면: deductible 분개만 포함 foreach ($splits as $split) { if ($split->deduction_type === 'deductible') { $cardRecords->push([ 'id' => 'card_split_'.$split->id, 'period' => $period, 'type' => 'purchase', 'taxType' => 'taxable', 'partnerName' => $card->merchant_name, 'invoiceNo' => $card->approval_num ?? '', 'invoiceDate' => $card->use_date ? \Carbon\Carbon::createFromFormat('Ymd', $card->use_date)->format('Y-m-d') : null, 'supplyAmount' => (int) $split->split_supply_amount, 'vatAmount' => (int) $split->split_tax, 'totalAmount' => (int) $split->split_amount, 'status' => 'filed', 'memo' => $split->account_name ?? null, 'isCardTransaction' => true, 'isHometax' => false, 'source' => 'card', ]); } } } else { // 분개가 없으면: deduction_type='deductible'인 경우만 포함 if ($card->deduction_type === 'deductible') { $effectiveSupply = $card->modified_supply_amount ?? ($card->approval_amount - $card->tax); $effectiveTax = $card->modified_tax ?? $card->tax; $cardRecords->push([ 'id' => 'card_'.$card->id, 'period' => $period, 'type' => 'purchase', 'taxType' => 'taxable', 'partnerName' => $card->merchant_name, 'invoiceNo' => $card->approval_num ?? '', 'invoiceDate' => $card->use_date ? \Carbon\Carbon::createFromFormat('Ymd', $card->use_date)->format('Y-m-d') : null, 'supplyAmount' => (int) $effectiveSupply, 'vatAmount' => (int) $effectiveTax, 'totalAmount' => (int) ($effectiveSupply + $effectiveTax), 'status' => 'filed', 'memo' => $card->memo, 'isCardTransaction' => true, 'isHometax' => false, 'source' => 'card', ]); } } } } } // Step 5: 수동 입력 (vat_records) - 기존 유지 $manualQuery = VatRecord::forTenant($tenantId); if ($period) { $manualQuery->where('period', $period); } $manualRecords = $manualQuery->orderBy('invoice_date', 'desc') ->get() ->map(function ($record) { return [ 'id' => $record->id, 'period' => $record->period, 'type' => $record->type, 'taxType' => $record->tax_type ?? 'taxable', 'partnerName' => $record->partner_name, 'invoiceNo' => $record->invoice_no, 'invoiceDate' => $record->invoice_date?->format('Y-m-d'), 'supplyAmount' => $record->supply_amount, 'vatAmount' => $record->vat_amount, 'totalAmount' => $record->total_amount, 'status' => $record->status, 'memo' => $record->memo, 'isCardTransaction' => false, 'isHometax' => false, 'source' => 'manual', ]; }); // Step 6: 통합 및 통계 $allRecords = $hometaxSalesRecords ->concat($hometaxPurchaseRecords) ->concat($cardRecords) ->concat($manualRecords) ->values(); // 홈택스 매출 전자세금계산서 (과세 + 영세만, 면세 제외) $hometaxSalesTaxable = $hometaxSalesRecords->whereIn('taxType', ['taxable', 'zero_rated']); $hometaxSalesSupply = $hometaxSalesTaxable->sum('supplyAmount'); $hometaxSalesVat = $hometaxSalesTaxable->sum('vatAmount'); // 홈택스 매입 전자세금계산서 (과세 + 영세만, 면세 제외) $hometaxPurchaseTaxable = $hometaxPurchaseRecords->whereIn('taxType', ['taxable', 'zero_rated']); $hometaxPurchaseSupply = $hometaxPurchaseTaxable->sum('supplyAmount'); $hometaxPurchaseVat = $hometaxPurchaseTaxable->sum('vatAmount'); // 홈택스 면세 계산서 (매입 + 매출 모두) $exemptSalesSupply = $hometaxSalesRecords->where('taxType', 'exempt')->sum('supplyAmount'); $exemptPurchaseSupply = $hometaxPurchaseRecords->where('taxType', 'exempt')->sum('supplyAmount'); // 카드 매입 $cardPurchaseSupply = $cardRecords->sum('supplyAmount'); $cardPurchaseVat = $cardRecords->sum('vatAmount'); // 수동입력 매출 종이세금계산서 (과세+영세) $manualSalesTaxable = $manualRecords->where('type', 'sales')->whereIn('taxType', ['taxable', 'zero_rated']); $manualSalesSupply = $manualSalesTaxable->sum('supplyAmount'); $manualSalesVat = $manualSalesTaxable->sum('vatAmount'); // 수동입력 매입 종이세금계산서 (과세+영세) $manualPurchaseTaxable = $manualRecords->where('type', 'purchase')->whereIn('taxType', ['taxable', 'zero_rated']); $manualPurchaseSupply = $manualPurchaseTaxable->sum('supplyAmount'); $manualPurchaseVat = $manualPurchaseTaxable->sum('vatAmount'); // 수동입력 면세 계산서 $manualExemptSalesSupply = $manualRecords->where('type', 'sales')->where('taxType', 'exempt')->sum('supplyAmount'); $manualExemptPurchaseSupply = $manualRecords->where('type', 'purchase')->where('taxType', 'exempt')->sum('supplyAmount'); // 면세 계산서 합계 (홈택스 + 수동) $exemptSupply = $exemptSalesSupply + $exemptPurchaseSupply + $manualExemptSalesSupply + $manualExemptPurchaseSupply; $stats = [ 'salesSupply' => $hometaxSalesSupply + $manualSalesSupply, 'salesVat' => $hometaxSalesVat + $manualSalesVat, 'purchaseSupply' => $hometaxPurchaseSupply + $cardPurchaseSupply + $manualPurchaseSupply, 'purchaseVat' => $hometaxPurchaseVat + $cardPurchaseVat + $manualPurchaseVat, 'hometaxSalesSupply' => $hometaxSalesSupply, 'hometaxSalesVat' => $hometaxSalesVat, 'manualSalesSupply' => $manualSalesSupply, 'manualSalesVat' => $manualSalesVat, 'hometaxPurchaseSupply' => $hometaxPurchaseSupply, 'hometaxPurchaseVat' => $hometaxPurchaseVat, 'manualPurchaseSupply' => $manualPurchaseSupply, 'manualPurchaseVat' => $manualPurchaseVat, 'exemptSupply' => $exemptSupply, 'cardPurchaseSupply' => $cardPurchaseSupply, 'cardPurchaseVat' => $cardPurchaseVat, 'total' => $allRecords->count(), 'preliminaryVat' => null, ]; // 확정(C) 기간이면 대응하는 예정(P)의 netVat를 계산 if ($period && str_ends_with($period, 'C')) { try { $prelimPeriod = substr($period, 0, -1).'P'; $stats['preliminaryVat'] = $this->calculatePeriodNetVat($tenantId, $prelimPeriod); } catch (\Throwable $e) { \Log::error('예정 세액 계산 실패', [ 'message' => $e->getMessage(), 'file' => $e->getFile().':'.$e->getLine(), 'trace' => array_slice(array_map(fn ($t) => ($t['file'] ?? '').':'.($t['line'] ?? '').' '.($t['function'] ?? ''), $e->getTrace()), 0, 5), 'tenant_id' => $tenantId, 'period' => $prelimPeriod ?? null, ]); $stats['preliminaryVat'] = null; } } // 사용 중인 기간 목록 $periods = VatRecord::forTenant($tenantId) ->select('period') ->distinct() ->orderBy('period', 'desc') ->pluck('period') ->toArray(); return response()->json([ 'success' => true, 'data' => $allRecords, 'stats' => $stats, 'periods' => $periods, ]); } public function store(Request $request): JsonResponse { $request->validate([ 'partnerName' => 'required|string|max:100', 'period' => 'required|string|max:20', 'type' => 'required|in:sales,purchase', 'taxType' => 'nullable|in:taxable,zero_rated,exempt', 'supplyAmount' => 'required|integer|min:0', ]); $tenantId = session('selected_tenant_id', 1); $record = VatRecord::create([ 'tenant_id' => $tenantId, 'period' => $request->input('period'), 'type' => $request->input('type', 'sales'), 'tax_type' => $request->input('taxType', 'taxable'), 'partner_name' => $request->input('partnerName'), 'invoice_no' => $request->input('invoiceNo'), 'invoice_date' => $request->input('invoiceDate'), 'supply_amount' => $request->input('supplyAmount', 0), 'vat_amount' => $request->input('vatAmount', 0), 'total_amount' => $request->input('totalAmount', 0), 'status' => $request->input('status', 'pending'), 'memo' => $request->input('memo'), ]); return response()->json([ 'success' => true, 'message' => '세금계산서가 등록되었습니다.', 'data' => ['id' => $record->id], ]); } public function update(Request $request, int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $record = VatRecord::forTenant($tenantId)->findOrFail($id); $request->validate([ 'partnerName' => 'required|string|max:100', 'period' => 'required|string|max:20', 'type' => 'required|in:sales,purchase', 'taxType' => 'nullable|in:taxable,zero_rated,exempt', 'supplyAmount' => 'required|integer|min:0', ]); $record->update([ 'period' => $request->input('period'), 'type' => $request->input('type'), 'tax_type' => $request->input('taxType', $record->tax_type), 'partner_name' => $request->input('partnerName'), 'invoice_no' => $request->input('invoiceNo'), 'invoice_date' => $request->input('invoiceDate'), 'supply_amount' => $request->input('supplyAmount', 0), 'vat_amount' => $request->input('vatAmount', 0), 'total_amount' => $request->input('totalAmount', 0), 'status' => $request->input('status'), 'memo' => $request->input('memo'), ]); return response()->json([ 'success' => true, 'message' => '세금계산서가 수정되었습니다.', ]); } public function destroy(int $id): JsonResponse { $tenantId = session('selected_tenant_id', 1); $record = VatRecord::forTenant($tenantId)->findOrFail($id); $record->delete(); return response()->json([ 'success' => true, 'message' => '세금계산서가 삭제되었습니다.', ]); } /** * 특정 기간의 순 부가세 계산 (매출세액 - 매입세액) * 양수: 납부세액, 음수: 환급세액 */ private function calculatePeriodNetVat(int $tenantId, string $period): int { [$startDate, $endDate] = $this->periodToDateRange($period); if (! $startDate || ! $endDate) { return 0; } $startDateYmd = str_replace('-', '', $startDate); $endDateYmd = str_replace('-', '', $endDate); $taxTypeMap = [1 => 'taxable', 2 => 'zero_rated', 3 => 'exempt']; // 홈택스 매출세액 (과세+영세) $salesVat = (int) HometaxInvoice::where('tenant_id', $tenantId) ->sales() ->period($startDate, $endDate) ->whereIn('tax_type', [1, 2]) ->sum('tax_amount'); // 홈택스 매입세액 (과세+영세) $purchaseVat = (int) HometaxInvoice::where('tenant_id', $tenantId) ->purchase() ->period($startDate, $endDate) ->whereIn('tax_type', [1, 2]) ->sum('tax_amount'); // 카드 매입세액 (공제분만) $cardVat = 0; $hiddenKeys = CardTransactionHide::getHiddenKeys($tenantId, $startDateYmd, $endDateYmd); $cardTransactions = BarobillCardTransaction::where('tenant_id', $tenantId) ->whereBetween('use_date', [$startDateYmd, $endDateYmd]) ->get(); $splitsByKey = CardTransactionSplit::getByDateRange($tenantId, $startDateYmd, $endDateYmd); $splitsByPartialKey = []; foreach ($splitsByKey as $fullKey => $splits) { $parts = explode('|', $fullKey); if (count($parts) >= 3) { $partialKey = $parts[0].'|'.$parts[1].'|'.$parts[2]; $splitsByPartialKey[$partialKey] = $splits; } } foreach ($cardTransactions as $card) { if ($hiddenKeys->contains($card->unique_key)) { continue; } $splits = $splitsByKey[$card->unique_key] ?? null; if (! $splits) { $cardPartialKey = $card->card_num.'|'.$card->use_dt.'|'.$card->approval_num; $splits = $splitsByPartialKey[$cardPartialKey] ?? null; } if ($splits && count($splits) > 0) { foreach ($splits as $split) { if ($split->deduction_type === 'deductible') { $cardVat += (int) $split->split_tax; } } } else { if ($card->deduction_type === 'deductible') { $cardVat += (int) ($card->modified_tax ?? $card->tax); } } } // 수동입력 세액 (면세 제외한 과세+영세만 합산, index와 동일하게 인메모리 필터링) $manualRecords = VatRecord::forTenant($tenantId) ->where('period', $period) ->get(); $manualSalesVat = (int) $manualRecords ->where('type', 'sales') ->whereIn('tax_type', ['taxable', 'zero_rated', null]) ->sum('vat_amount'); $manualPurchaseVat = (int) $manualRecords ->where('type', 'purchase') ->whereIn('tax_type', ['taxable', 'zero_rated', null]) ->sum('vat_amount'); $totalSalesVat = $salesVat + $manualSalesVat; $totalPurchaseVat = $purchaseVat + $cardVat + $manualPurchaseVat; return $totalSalesVat - $totalPurchaseVat; } /** * 부가세 신고기간을 날짜 범위로 변환 * YYYY-1P: 1기 예정 (0101~0331) * YYYY-1C: 1기 확정 (0401~0630) * YYYY-2P: 2기 예정 (0701~0930) * YYYY-2C: 2기 확정 (1001~1231) */ private function periodToDateRange(string $period): array { $parts = explode('-', $period); if (count($parts) !== 2) { return [null, null]; } $year = $parts[0]; $code = $parts[1]; $ranges = [ '1P' => ["{$year}-01-01", "{$year}-03-31"], '1C' => ["{$year}-04-01", "{$year}-06-30"], '2P' => ["{$year}-07-01", "{$year}-09-30"], '2C' => ["{$year}-10-01", "{$year}-12-31"], ]; return $ranges[$code] ?? [null, null]; } }