testMode = config('services.barobill.test_mode', true); } // ========================================================================= // 설정 관리 // ========================================================================= /** * 바로빌 설정 조회 */ public function getSetting(): ?BarobillSetting { $tenantId = $this->tenantId(); return BarobillSetting::query() ->where('tenant_id', $tenantId) ->first(); } /** * 바로빌 설정 저장 */ public function saveSetting(array $data): BarobillSetting { $tenantId = $this->tenantId(); $userId = $this->apiUserId(); $setting = BarobillSetting::query() ->where('tenant_id', $tenantId) ->first(); if ($setting) { $setting->fill(array_merge($data, ['updated_by' => $userId])); $setting->save(); } else { $setting = BarobillSetting::create(array_merge($data, [ 'tenant_id' => $tenantId, 'created_by' => $userId, 'updated_by' => $userId, ])); } return $setting->fresh(); } /** * 연동 테스트 */ public function testConnection(): array { $setting = $this->getSetting(); if (! $setting || ! $setting->canConnect()) { throw new BadRequestHttpException(__('error.barobill.setting_not_configured')); } try { // 바로빌 API 토큰 조회로 연동 테스트 $response = $this->callApi('GetAccessToken', [ 'CERTKEY' => $setting->cert_key, 'CorpNum' => $setting->corp_num, 'ID' => $setting->barobill_id, ]); if (! empty($response['AccessToken'])) { // 검증 성공 시 verified_at 업데이트 $setting->verified_at = now(); $setting->save(); return [ 'success' => true, 'message' => __('message.barobill.connection_success'), 'verified_at' => $setting->verified_at->toDateTimeString(), ]; } throw new BadRequestHttpException($response['Message'] ?? __('error.barobill.connection_failed')); } catch (\Exception $e) { Log::error('바로빌 연동 테스트 실패', [ 'tenant_id' => $this->tenantId(), 'error' => $e->getMessage(), ]); throw new BadRequestHttpException(__('error.barobill.connection_failed').': '.$e->getMessage()); } } // ========================================================================= // 세금계산서 발행 // ========================================================================= /** * 세금계산서 발행 */ public function issueTaxInvoice(TaxInvoice $taxInvoice): TaxInvoice { $setting = $this->getSetting(); if (! $setting || ! $setting->canConnect()) { throw new BadRequestHttpException(__('error.barobill.setting_not_configured')); } try { // 바로빌 API 호출을 위한 데이터 구성 $apiData = $this->buildTaxInvoiceData($taxInvoice, $setting); // 세금계산서 발행 API 호출 $response = $this->callApi('RegistAndIssueTaxInvoice', $apiData); if (! empty($response['InvoiceID'])) { // 발행 성공 $taxInvoice->barobill_invoice_id = $response['InvoiceID']; $taxInvoice->nts_confirm_num = $response['NTSConfirmNum'] ?? null; $taxInvoice->status = TaxInvoice::STATUS_ISSUED; $taxInvoice->issued_at = now(); $taxInvoice->error_message = null; $taxInvoice->save(); Log::info('세금계산서 발행 성공', [ 'tenant_id' => $this->tenantId(), 'tax_invoice_id' => $taxInvoice->id, 'barobill_invoice_id' => $response['InvoiceID'], ]); return $taxInvoice->fresh(); } throw new \Exception($response['Message'] ?? '발행 실패'); } catch (\Exception $e) { // 발행 실패 $taxInvoice->status = TaxInvoice::STATUS_FAILED; $taxInvoice->error_message = $e->getMessage(); $taxInvoice->save(); Log::error('세금계산서 발행 실패', [ 'tenant_id' => $this->tenantId(), 'tax_invoice_id' => $taxInvoice->id, 'error' => $e->getMessage(), ]); throw new BadRequestHttpException(__('error.barobill.issue_failed').': '.$e->getMessage()); } } /** * 세금계산서 취소 */ public function cancelTaxInvoice(TaxInvoice $taxInvoice, string $reason): TaxInvoice { $setting = $this->getSetting(); if (! $setting || ! $setting->canConnect()) { throw new BadRequestHttpException(__('error.barobill.setting_not_configured')); } if (! $taxInvoice->canCancel()) { throw new BadRequestHttpException(__('error.barobill.cannot_cancel')); } try { // 세금계산서 취소 API 호출 $response = $this->callApi('CancelTaxInvoice', [ 'CERTKEY' => $setting->cert_key, 'CorpNum' => $setting->corp_num, 'ID' => $setting->barobill_id, 'InvoiceID' => $taxInvoice->barobill_invoice_id, 'Memo' => $reason, ]); if ($response['Result'] === 0 || ! empty($response['Success'])) { $taxInvoice->status = TaxInvoice::STATUS_CANCELLED; $taxInvoice->cancelled_at = now(); $taxInvoice->description = ($taxInvoice->description ? $taxInvoice->description."\n" : '').'취소 사유: '.$reason; $taxInvoice->save(); Log::info('세금계산서 취소 성공', [ 'tenant_id' => $this->tenantId(), 'tax_invoice_id' => $taxInvoice->id, 'reason' => $reason, ]); return $taxInvoice->fresh(); } throw new \Exception($response['Message'] ?? '취소 실패'); } catch (\Exception $e) { Log::error('세금계산서 취소 실패', [ 'tenant_id' => $this->tenantId(), 'tax_invoice_id' => $taxInvoice->id, 'error' => $e->getMessage(), ]); throw new BadRequestHttpException(__('error.barobill.cancel_failed').': '.$e->getMessage()); } } /** * 국세청 전송 상태 조회 */ public function checkNtsSendStatus(TaxInvoice $taxInvoice): TaxInvoice { $setting = $this->getSetting(); if (! $setting || ! $setting->canConnect()) { throw new BadRequestHttpException(__('error.barobill.setting_not_configured')); } if (empty($taxInvoice->barobill_invoice_id)) { throw new BadRequestHttpException(__('error.barobill.not_issued')); } try { $response = $this->callApi('GetTaxInvoiceState', [ 'CERTKEY' => $setting->cert_key, 'CorpNum' => $setting->corp_num, 'ID' => $setting->barobill_id, 'InvoiceID' => $taxInvoice->barobill_invoice_id, ]); if (! empty($response['State'])) { $taxInvoice->nts_send_status = $response['State']; // 국세청 전송 완료 시 상태 업데이트 if ($response['State'] === '전송완료' && ! $taxInvoice->sent_at) { $taxInvoice->status = TaxInvoice::STATUS_SENT; $taxInvoice->sent_at = now(); $taxInvoice->nts_confirm_num = $response['NTSConfirmNum'] ?? $taxInvoice->nts_confirm_num; } $taxInvoice->save(); } return $taxInvoice->fresh(); } catch (\Exception $e) { Log::error('국세청 전송 상태 조회 실패', [ 'tenant_id' => $this->tenantId(), 'tax_invoice_id' => $taxInvoice->id, 'error' => $e->getMessage(), ]); throw new BadRequestHttpException(__('error.barobill.status_check_failed').': '.$e->getMessage()); } } // ========================================================================= // Private 메서드 // ========================================================================= /** * 바로빌 API 호출 */ private function callApi(string $method, array $data): array { $baseUrl = $this->testMode ? self::API_TEST_URL : self::API_BASE_URL; $url = $baseUrl.'/TI/'.$method; $response = Http::timeout(30) ->withHeaders([ 'Content-Type' => 'application/json', ]) ->post($url, $data); if ($response->failed()) { throw new \Exception('API 호출 실패: '.$response->status()); } return $response->json() ?? []; } /** * 세금계산서 발행용 데이터 구성 */ private function buildTaxInvoiceData(TaxInvoice $taxInvoice, BarobillSetting $setting): array { // 품목 데이터 구성 $items = []; foreach ($taxInvoice->items ?? [] as $index => $item) { $items[] = [ 'PurchaseDT' => $taxInvoice->issue_date->format('Ymd'), 'ItemName' => $item['name'] ?? '', 'Spec' => $item['spec'] ?? '', 'Qty' => $item['qty'] ?? 1, 'UnitCost' => $item['unit_price'] ?? 0, 'SupplyCost' => $item['supply_amt'] ?? 0, 'Tax' => $item['tax_amt'] ?? 0, 'Remark' => $item['remark'] ?? '', ]; } // 품목이 없는 경우 기본 품목 추가 if (empty($items)) { $items[] = [ 'PurchaseDT' => $taxInvoice->issue_date->format('Ymd'), 'ItemName' => $taxInvoice->description ?? '품목', 'Spec' => '', 'Qty' => 1, 'UnitCost' => (float) $taxInvoice->supply_amount, 'SupplyCost' => (float) $taxInvoice->supply_amount, 'Tax' => (float) $taxInvoice->tax_amount, 'Remark' => '', ]; } return [ 'CERTKEY' => $setting->cert_key, 'CorpNum' => $setting->corp_num, 'ID' => $setting->barobill_id, 'TaxInvoice' => [ 'InvoiceType' => $this->mapInvoiceType($taxInvoice->invoice_type), 'IssueType' => $this->mapIssueType($taxInvoice->issue_type), 'TaxType' => '과세', 'PurposeType' => '영수', 'WriteDate' => $taxInvoice->issue_date->format('Ymd'), // 공급자 정보 'InvoicerCorpNum' => $taxInvoice->supplier_corp_num, 'InvoicerCorpName' => $taxInvoice->supplier_corp_name, 'InvoicerCEOName' => $taxInvoice->supplier_ceo_name, 'InvoicerAddr' => $taxInvoice->supplier_addr, 'InvoicerBizType' => $taxInvoice->supplier_biz_type, 'InvoicerBizClass' => $taxInvoice->supplier_biz_class, 'InvoicerContactID' => $taxInvoice->supplier_contact_id, // 공급받는자 정보 'InvoiceeCorpNum' => $taxInvoice->buyer_corp_num, 'InvoiceeCorpName' => $taxInvoice->buyer_corp_name, 'InvoiceeCEOName' => $taxInvoice->buyer_ceo_name, 'InvoiceeAddr' => $taxInvoice->buyer_addr, 'InvoiceeBizType' => $taxInvoice->buyer_biz_type, 'InvoiceeBizClass' => $taxInvoice->buyer_biz_class, 'InvoiceeContactID' => $taxInvoice->buyer_contact_id, // 금액 정보 'SupplyCostTotal' => (int) $taxInvoice->supply_amount, 'TaxTotal' => (int) $taxInvoice->tax_amount, 'TotalAmount' => (int) $taxInvoice->total_amount, // 품목 정보 'TaxInvoiceTradeLineItems' => $items, // 비고 'Remark1' => $taxInvoice->description ?? '', ], ]; } /** * 세금계산서 유형 매핑 */ private function mapInvoiceType(string $type): string { return match ($type) { TaxInvoice::TYPE_TAX_INVOICE => '세금계산서', TaxInvoice::TYPE_INVOICE => '계산서', TaxInvoice::TYPE_MODIFIED_TAX_INVOICE => '수정세금계산서', default => '세금계산서', }; } /** * 발행 유형 매핑 */ private function mapIssueType(string $type): string { return match ($type) { TaxInvoice::ISSUE_TYPE_NORMAL => '정발행', TaxInvoice::ISSUE_TYPE_REVERSE => '역발행', TaxInvoice::ISSUE_TYPE_TRUSTEE => '위수탁', default => '정발행', }; } }