diff --git a/app/Http/Controllers/Api/V1/TaxInvoiceController.php b/app/Http/Controllers/Api/V1/TaxInvoiceController.php index 2fe8dcf..fabda45 100644 --- a/app/Http/Controllers/Api/V1/TaxInvoiceController.php +++ b/app/Http/Controllers/Api/V1/TaxInvoiceController.php @@ -6,6 +6,7 @@ use App\Http\Controllers\Controller; use App\Http\Requests\TaxInvoice\CancelTaxInvoiceRequest; use App\Http\Requests\TaxInvoice\CreateTaxInvoiceRequest; +use App\Http\Requests\TaxInvoice\SaveSupplierSettingsRequest; use App\Http\Requests\TaxInvoice\TaxInvoiceListRequest; use App\Http\Requests\TaxInvoice\TaxInvoiceSummaryRequest; use App\Http\Requests\TaxInvoice\UpdateTaxInvoiceRequest; @@ -148,4 +149,44 @@ public function summary(TaxInvoiceSummaryRequest $request) message: __('message.fetched') ); } + + /** + * 공급자 설정 조회 + */ + public function supplierSettings() + { + $settings = $this->taxInvoiceService->getSupplierSettings(); + + return ApiResponse::handle( + data: $settings, + message: __('message.fetched') + ); + } + + /** + * 공급자 설정 저장 + */ + public function saveSupplierSettings(SaveSupplierSettingsRequest $request) + { + $settings = $this->taxInvoiceService->saveSupplierSettings($request->validated()); + + return ApiResponse::handle( + data: $settings, + message: __('message.updated') + ); + } + + /** + * 세금계산서 생성 + 즉시 발행 + */ + public function storeAndIssue(CreateTaxInvoiceRequest $request) + { + $taxInvoice = $this->taxInvoiceService->createAndIssue($request->validated()); + + return ApiResponse::handle( + data: $taxInvoice, + message: __('message.tax_invoice.issued'), + status: 201 + ); + } } diff --git a/app/Http/Requests/TaxInvoice/SaveSupplierSettingsRequest.php b/app/Http/Requests/TaxInvoice/SaveSupplierSettingsRequest.php new file mode 100644 index 0000000..b1a28bd --- /dev/null +++ b/app/Http/Requests/TaxInvoice/SaveSupplierSettingsRequest.php @@ -0,0 +1,28 @@ + ['required', 'string', 'max:20'], + 'company_name' => ['required', 'string', 'max:100'], + 'representative_name' => ['required', 'string', 'max:50'], + 'address' => ['nullable', 'string', 'max:255'], + 'business_type' => ['nullable', 'string', 'max:100'], + 'business_item' => ['nullable', 'string', 'max:100'], + 'contact_name' => ['nullable', 'string', 'max:50'], + 'contact_phone' => ['nullable', 'string', 'max:20'], + 'contact_email' => ['nullable', 'email', 'max:100'], + ]; + } +} diff --git a/app/Services/BarobillService.php b/app/Services/BarobillService.php index 1d8451f..b9517bc 100644 --- a/app/Services/BarobillService.php +++ b/app/Services/BarobillService.php @@ -4,37 +4,130 @@ use App\Models\Tenants\BarobillSetting; use App\Models\Tenants\TaxInvoice; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; +use SoapClient; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; /** - * 바로빌 API 연동 서비스 + * 바로빌 API 연동 서비스 (SOAP) * * 바로빌 개발자센터: https://dev.barobill.co.kr/ + * 바로빌은 SOAP API만 제공하므로 SoapClient를 사용합니다. */ class BarobillService extends Service { /** - * 바로빌 API 기본 URL + * 바로빌 SOAP 기본 URL */ - private const API_BASE_URL = 'https://ws.barobill.co.kr'; + private const SOAP_BASE_URL = 'https://ws.baroservice.com'; /** - * 바로빌 API 테스트 URL + * 바로빌 SOAP 테스트 URL */ - private const API_TEST_URL = 'https://testws.barobill.co.kr'; + private const SOAP_TEST_URL = 'https://testws.baroservice.com'; /** * 테스트 모드 여부 */ private bool $testMode; + /** + * TI(Tax Invoice) SOAP 클라이언트 + */ + private ?SoapClient $tiSoapClient = null; + public function __construct() { $this->testMode = config('services.barobill.test_mode', true); } + // ========================================================================= + // SOAP 클라이언트 + // ========================================================================= + + /** + * TI SOAP 클라이언트 초기화/반환 + */ + private function getTiSoapClient(): SoapClient + { + if ($this->tiSoapClient === null) { + $baseUrl = $this->testMode ? self::SOAP_TEST_URL : self::SOAP_BASE_URL; + + $context = stream_context_create([ + 'ssl' => [ + 'verify_peer' => ! $this->testMode, + 'verify_peer_name' => ! $this->testMode, + 'allow_self_signed' => $this->testMode, + ], + ]); + + $this->tiSoapClient = new SoapClient($baseUrl.'/TI.asmx?WSDL', [ + 'trace' => true, + 'encoding' => 'UTF-8', + 'exceptions' => true, + 'connection_timeout' => 30, + 'stream_context' => $context, + 'cache_wsdl' => WSDL_CACHE_NONE, + ]); + } + + return $this->tiSoapClient; + } + + /** + * SOAP API 호출 + * + * MNG EtaxController::callBarobillSOAP() 포팅 + * 음수 반환값 = 에러 코드 (바로빌 규격) + */ + private function callSoap(string $method, array $params): array + { + $client = $this->getTiSoapClient(); + + if (! isset($params['CERTKEY'])) { + $setting = $this->getSetting(); + if (! $setting) { + return [ + 'success' => false, + 'error' => '바로빌 설정이 없습니다.', + ]; + } + $params['CERTKEY'] = $setting->cert_key; + } + + try { + $result = $client->$method($params); + $resultProperty = $method.'Result'; + + if (isset($result->$resultProperty)) { + $resultData = $result->$resultProperty; + + // 바로빌 규격: 음수 반환값은 에러 코드 + if (is_numeric($resultData) && $resultData < 0) { + return [ + 'success' => false, + 'error' => '바로빌 API 오류 코드: '.$resultData, + 'error_code' => (int) $resultData, + ]; + } + + return ['success' => true, 'data' => $resultData]; + } + + return ['success' => true, 'data' => $result]; + } catch (\SoapFault $e) { + return [ + 'success' => false, + 'error' => 'SOAP 오류: '.$e->getMessage(), + ]; + } catch (\Throwable $e) { + return [ + 'success' => false, + 'error' => 'API 호출 오류: '.$e->getMessage(), + ]; + } + } + // ========================================================================= // 설정 관리 // ========================================================================= @@ -78,7 +171,7 @@ public function saveSetting(array $data): BarobillSetting } /** - * 연동 테스트 + * 연동 테스트 (SOAP) */ public function testConnection(): array { @@ -89,26 +182,31 @@ public function testConnection(): array } try { - // 바로빌 API 토큰 조회로 연동 테스트 - $response = $this->callApi('GetAccessToken', [ + $response = $this->callSoap('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(); + if ($response['success']) { + $resultData = $response['data']; - return [ - 'success' => true, - 'message' => __('message.barobill.connection_success'), - 'verified_at' => $setting->verified_at->toDateTimeString(), - ]; + // 양수 또는 문자열 토큰 = 성공 + if (is_string($resultData) || (is_numeric($resultData) && $resultData > 0)) { + $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')); + throw new \Exception($response['error'] ?? __('error.barobill.connection_failed')); + } catch (BadRequestHttpException $e) { + throw $e; } catch (\Exception $e) { Log::error('바로빌 연동 테스트 실패', [ 'tenant_id' => $this->tenantId(), @@ -161,52 +259,29 @@ public function checkBusinessNumber(string $businessNumber): array ]; } - // 바로빌 API 조회 시도 + // 바로빌 SOAP API 조회 시도 try { - $response = $this->callApi('CheckCorpNum', [ + $response = $this->callSoap('CheckCorpNum', [ 'CorpNum' => $businessNumber, ]); - // 바로빌 응답 해석 - if (isset($response['CorpState'])) { - $state = $response['CorpState']; - $isValid = in_array($state, ['01', '02']); // 01: 사업중, 02: 휴업 - $statusLabel = match ($state) { - '01' => '사업중', - '02' => '휴업', - '03' => '폐업', - default => '조회 불가', - }; + if ($response['success']) { + $resultData = $response['data']; - return [ - 'valid' => $isValid, - 'status' => $state, - 'status_label' => $statusLabel, - 'corp_name' => $response['CorpName'] ?? null, - 'ceo_name' => $response['CEOName'] ?? null, - 'message' => $isValid - ? __('message.company.business_number_valid') - : __('error.company.business_closed'), - ]; + // 양수 결과 = 유효한 사업자 + if (is_numeric($resultData) && $resultData >= 0) { + return [ + 'valid' => true, + 'status' => 'active', + 'status_label' => '유효함', + 'corp_name' => null, + 'ceo_name' => null, + 'message' => __('message.company.business_number_valid'), + ]; + } } - // 응답 형식이 다른 경우 (결과 코드 방식) - if (isset($response['Result'])) { - $isValid = $response['Result'] >= 0; - - return [ - 'valid' => $isValid, - 'status' => $isValid ? 'active' : 'unknown', - 'status_label' => $isValid ? '유효함' : '조회 불가', - 'corp_name' => $response['CorpName'] ?? null, - 'ceo_name' => $response['CEOName'] ?? null, - 'message' => $isValid - ? __('message.company.business_number_valid') - : ($response['Message'] ?? __('error.company.check_failed')), - ]; - } - - // 기본 응답 (체크섬만 통과한 경우) + // API 실패 시 형식 검증 결과만 반환 return [ 'valid' => true, 'status' => 'format_valid', @@ -266,7 +341,9 @@ private function validateBusinessNumberChecksum(string $businessNumber): bool // ========================================================================= /** - * 세금계산서 발행 + * 세금계산서 발행 (SOAP RegistAndIssueTaxInvoice) + * + * MNG EtaxController::issueTaxInvoice() 포팅 */ public function issueTaxInvoice(TaxInvoice $taxInvoice): TaxInvoice { @@ -277,16 +354,13 @@ public function issueTaxInvoice(TaxInvoice $taxInvoice): TaxInvoice } try { - // 바로빌 API 호출을 위한 데이터 구성 $apiData = $this->buildTaxInvoiceData($taxInvoice, $setting); + $response = $this->callSoap('RegistAndIssueTaxInvoice', $apiData); - // 세금계산서 발행 API 호출 - $response = $this->callApi('RegistAndIssueTaxInvoice', $apiData); - - if (! empty($response['InvoiceID'])) { - // 발행 성공 - $taxInvoice->barobill_invoice_id = $response['InvoiceID']; - $taxInvoice->nts_confirm_num = $response['NTSConfirmNum'] ?? null; + if ($response['success']) { + $resultData = $response['data']; + // 바로빌 규격: 양수 반환값이 Invoice ID + $taxInvoice->barobill_invoice_id = is_numeric($resultData) ? (string) $resultData : null; $taxInvoice->status = TaxInvoice::STATUS_ISSUED; $taxInvoice->issued_at = now(); $taxInvoice->error_message = null; @@ -295,13 +369,15 @@ public function issueTaxInvoice(TaxInvoice $taxInvoice): TaxInvoice Log::info('세금계산서 발행 성공', [ 'tenant_id' => $this->tenantId(), 'tax_invoice_id' => $taxInvoice->id, - 'barobill_invoice_id' => $response['InvoiceID'], + 'barobill_invoice_id' => $taxInvoice->barobill_invoice_id, ]); return $taxInvoice->fresh(); } - throw new \Exception($response['Message'] ?? '발행 실패'); + throw new \Exception($response['error'] ?? '발행 실패'); + } catch (BadRequestHttpException $e) { + throw $e; } catch (\Exception $e) { // 발행 실패 $taxInvoice->status = TaxInvoice::STATUS_FAILED; @@ -319,7 +395,7 @@ public function issueTaxInvoice(TaxInvoice $taxInvoice): TaxInvoice } /** - * 세금계산서 취소 + * 세금계산서 취소 (SOAP CancelTaxInvoice) */ public function cancelTaxInvoice(TaxInvoice $taxInvoice, string $reason): TaxInvoice { @@ -334,16 +410,15 @@ public function cancelTaxInvoice(TaxInvoice $taxInvoice, string $reason): TaxInv } try { - // 세금계산서 취소 API 호출 - $response = $this->callApi('CancelTaxInvoice', [ + $response = $this->callSoap('ProcTaxInvoice', [ 'CERTKEY' => $setting->cert_key, 'CorpNum' => $setting->corp_num, - 'ID' => $setting->barobill_id, - 'InvoiceID' => $taxInvoice->barobill_invoice_id, + 'MgtNum' => $taxInvoice->barobill_invoice_id, + 'ProcType' => 4, // 4: 발행취소 'Memo' => $reason, ]); - if ($response['Result'] === 0 || ! empty($response['Success'])) { + if ($response['success']) { $taxInvoice->status = TaxInvoice::STATUS_CANCELLED; $taxInvoice->cancelled_at = now(); $taxInvoice->description = ($taxInvoice->description ? $taxInvoice->description."\n" : '').'취소 사유: '.$reason; @@ -358,7 +433,9 @@ public function cancelTaxInvoice(TaxInvoice $taxInvoice, string $reason): TaxInv return $taxInvoice->fresh(); } - throw new \Exception($response['Message'] ?? '취소 실패'); + throw new \Exception($response['error'] ?? '취소 실패'); + } catch (BadRequestHttpException $e) { + throw $e; } catch (\Exception $e) { Log::error('세금계산서 취소 실패', [ 'tenant_id' => $this->tenantId(), @@ -371,7 +448,7 @@ public function cancelTaxInvoice(TaxInvoice $taxInvoice, string $reason): TaxInv } /** - * 국세청 전송 상태 조회 + * 국세청 전송 상태 조회 (SOAP GetTaxInvoiceState) */ public function checkNtsSendStatus(TaxInvoice $taxInvoice): TaxInvoice { @@ -386,27 +463,38 @@ public function checkNtsSendStatus(TaxInvoice $taxInvoice): TaxInvoice } try { - $response = $this->callApi('GetTaxInvoiceState', [ + $response = $this->callSoap('GetTaxInvoiceState', [ 'CERTKEY' => $setting->cert_key, 'CorpNum' => $setting->corp_num, - 'ID' => $setting->barobill_id, - 'InvoiceID' => $taxInvoice->barobill_invoice_id, + 'MgtNum' => $taxInvoice->barobill_invoice_id, ]); - if (! empty($response['State'])) { - $taxInvoice->nts_send_status = $response['State']; + if ($response['success'] && $response['data']) { + $stateData = $response['data']; - // 국세청 전송 완료 시 상태 업데이트 - 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; + // SOAP 결과 객체에서 상태 추출 + $state = is_object($stateData) ? ($stateData->NTSSendState ?? null) : null; + + if ($state !== null) { + $taxInvoice->nts_send_status = (string) $state; + + // 국세청 전송 완료 시 상태 업데이트 + if (in_array($state, [3, '3', '전송완료']) && ! $taxInvoice->sent_at) { + $taxInvoice->status = TaxInvoice::STATUS_SENT; + $taxInvoice->sent_at = now(); + + if (is_object($stateData) && ! empty($stateData->NTSConfirmNum)) { + $taxInvoice->nts_confirm_num = $stateData->NTSConfirmNum; + } + } + + $taxInvoice->save(); } - - $taxInvoice->save(); } return $taxInvoice->fresh(); + } catch (BadRequestHttpException $e) { + throw $e; } catch (\Exception $e) { Log::error('국세청 전송 상태 조회 실패', [ 'tenant_id' => $this->tenantId(), @@ -423,126 +511,123 @@ public function checkNtsSendStatus(TaxInvoice $taxInvoice): TaxInvoice // ========================================================================= /** - * 바로빌 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() ?? []; - } - - /** - * 세금계산서 발행용 데이터 구성 + * 세금계산서 발행용 데이터 구성 (MNG SOAP 형식) + * + * MNG EtaxController::issueTaxInvoice() 의 Invoice 구조를 포팅 + * InvoicerParty/InvoiceeParty/TaxInvoiceTradeLineItems 중첩 구조 */ 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'] ?? '', + $supplyAmt = (int) $taxInvoice->supply_amount; + $taxAmt = (int) $taxInvoice->tax_amount; + $total = (int) $taxInvoice->total_amount; + $taxType = $taxAmt == 0 ? 2 : 1; // 1:과세, 2:영세, 3:면세 + + // 관리번호 (유니크) + $mgtNum = 'SAM'.date('YmdHis').$taxInvoice->id; + + // 품목 구성 + $tradeLineItems = []; + foreach ($taxInvoice->items ?? [] as $item) { + $tradeLineItems[] = [ + 'PurchaseExpiry' => $taxInvoice->issue_date->format('Ymd'), + 'Name' => $item['name'] ?? '', + 'Information' => $item['spec'] ?? '', + 'ChargeableUnit' => (string) ($item['qty'] ?? 1), + 'UnitPrice' => (string) ($item['unit_price'] ?? 0), + 'Amount' => (string) ($item['supply_amt'] ?? 0), + 'Tax' => (string) ($item['tax_amt'] ?? 0), + 'Description' => $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' => '', + if (empty($tradeLineItems)) { + $tradeLineItems[] = [ + 'PurchaseExpiry' => $taxInvoice->issue_date->format('Ymd'), + 'Name' => $taxInvoice->description ?? '품목', + 'Information' => '', + 'ChargeableUnit' => '1', + 'UnitPrice' => (string) $supplyAmt, + 'Amount' => (string) $supplyAmt, + 'Tax' => (string) $taxAmt, + 'Description' => '', ]; } 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' => '영수', + 'Invoice' => [ + 'IssueDirection' => 1, // 1: 정발행 + 'TaxInvoiceType' => $this->mapInvoiceTypeToCode($taxInvoice->invoice_type), + 'ModifyCode' => '', + 'TaxType' => $taxType, + 'TaxCalcType' => 1, // 1: 소계합계 + 'PurposeType' => 2, // 2: 청구 '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, - - // 비고 + 'AmountTotal' => (string) $supplyAmt, + 'TaxTotal' => (string) $taxAmt, + 'TotalAmount' => (string) $total, + 'Cash' => '0', + 'ChkBill' => '0', + 'Note' => '0', + 'Credit' => (string) $total, 'Remark1' => $taxInvoice->description ?? '', + 'Remark2' => '', + 'Remark3' => '', + 'InvoicerParty' => [ + 'MgtNum' => $mgtNum, + 'CorpNum' => $taxInvoice->supplier_corp_num, + 'TaxRegID' => '', + 'CorpName' => $taxInvoice->supplier_corp_name, + 'CEOName' => $taxInvoice->supplier_ceo_name ?? '', + 'Addr' => $taxInvoice->supplier_addr ?? '', + 'BizType' => $taxInvoice->supplier_biz_type ?? '', + 'BizClass' => $taxInvoice->supplier_biz_class ?? '', + 'ContactID' => $setting->barobill_id, + 'ContactName' => $setting->contact_name ?? '', + 'TEL' => $setting->contact_tel ?? '', + 'HP' => '', + 'Email' => $setting->contact_id ?? '', + ], + 'InvoiceeParty' => [ + 'MgtNum' => '', + 'CorpNum' => str_replace('-', '', $taxInvoice->buyer_corp_num ?? ''), + 'TaxRegID' => '', + 'CorpName' => $taxInvoice->buyer_corp_name ?? '', + 'CEOName' => $taxInvoice->buyer_ceo_name ?? '', + 'Addr' => $taxInvoice->buyer_addr ?? '', + 'BizType' => $taxInvoice->buyer_biz_type ?? '', + 'BizClass' => $taxInvoice->buyer_biz_class ?? '', + 'ContactID' => '', + 'ContactName' => '', + 'TEL' => '', + 'HP' => '', + 'Email' => $taxInvoice->buyer_contact_id ?? '', + ], + 'BrokerParty' => [], + 'TaxInvoiceTradeLineItems' => [ + 'TaxInvoiceTradeLineItem' => $tradeLineItems, + ], ], + 'SendSMS' => false, + 'ForceIssue' => false, + 'MailTitle' => '', ]; } /** - * 세금계산서 유형 매핑 + * 세금계산서 유형을 바로빌 코드로 매핑 + * + * @return int 1: 세금계산서, 2: 계산서, 4: 수정세금계산서 */ - private function mapInvoiceType(string $type): string + private function mapInvoiceTypeToCode(string $type): int { 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 => '정발행', + TaxInvoice::TYPE_TAX_INVOICE => 1, + TaxInvoice::TYPE_INVOICE => 2, + TaxInvoice::TYPE_MODIFIED_TAX_INVOICE => 4, + default => 1, }; } } diff --git a/app/Services/TaxInvoiceService.php b/app/Services/TaxInvoiceService.php index 19cd6c8..862a46b 100644 --- a/app/Services/TaxInvoiceService.php +++ b/app/Services/TaxInvoiceService.php @@ -271,6 +271,70 @@ public function checkStatus(int $id): TaxInvoice return $this->barobillService->checkNtsSendStatus($taxInvoice); } + // ========================================================================= + // 공급자 설정 + // ========================================================================= + + /** + * 공급자 설정 조회 (BarobillSetting 기반) + */ + public function getSupplierSettings(): array + { + $setting = $this->barobillService->getSetting(); + + if (! $setting) { + return []; + } + + return [ + 'business_number' => $setting->corp_num, + 'company_name' => $setting->corp_name, + 'representative_name' => $setting->ceo_name, + 'address' => $setting->addr, + 'business_type' => $setting->biz_type, + 'business_item' => $setting->biz_class, + 'contact_name' => $setting->contact_name, + 'contact_phone' => $setting->contact_tel, + 'contact_email' => $setting->contact_id, + ]; + } + + /** + * 공급자 설정 저장 + */ + public function saveSupplierSettings(array $data): array + { + $this->barobillService->saveSetting([ + 'corp_num' => $data['business_number'] ?? null, + 'corp_name' => $data['company_name'] ?? null, + 'ceo_name' => $data['representative_name'] ?? null, + 'addr' => $data['address'] ?? null, + 'biz_type' => $data['business_type'] ?? null, + 'biz_class' => $data['business_item'] ?? null, + 'contact_name' => $data['contact_name'] ?? null, + 'contact_tel' => $data['contact_phone'] ?? null, + 'contact_id' => $data['contact_email'] ?? null, + ]); + + return $this->getSupplierSettings(); + } + + // ========================================================================= + // 생성+발행 통합 + // ========================================================================= + + /** + * 세금계산서 생성 후 즉시 발행 + */ + public function createAndIssue(array $data): TaxInvoice + { + return DB::transaction(function () use ($data) { + $taxInvoice = $this->create($data); + + return $this->barobillService->issueTaxInvoice($taxInvoice); + }); + } + // ========================================================================= // 통계 // ========================================================================= diff --git a/routes/api/v1/finance.php b/routes/api/v1/finance.php index 1654646..5668842 100644 --- a/routes/api/v1/finance.php +++ b/routes/api/v1/finance.php @@ -283,13 +283,16 @@ Route::get('', [TaxInvoiceController::class, 'index'])->name('v1.tax-invoices.index'); Route::post('', [TaxInvoiceController::class, 'store'])->name('v1.tax-invoices.store'); Route::get('/summary', [TaxInvoiceController::class, 'summary'])->name('v1.tax-invoices.summary'); + Route::get('/supplier-settings', [TaxInvoiceController::class, 'supplierSettings'])->name('v1.tax-invoices.supplier-settings'); + Route::put('/supplier-settings', [TaxInvoiceController::class, 'saveSupplierSettings'])->name('v1.tax-invoices.save-supplier-settings'); + Route::post('/issue-direct', [TaxInvoiceController::class, 'storeAndIssue'])->name('v1.tax-invoices.issue-direct'); + Route::post('/bulk-issue', [TaxInvoiceController::class, 'bulkIssue'])->name('v1.tax-invoices.bulk-issue'); Route::get('/{id}', [TaxInvoiceController::class, 'show'])->whereNumber('id')->name('v1.tax-invoices.show'); Route::put('/{id}', [TaxInvoiceController::class, 'update'])->whereNumber('id')->name('v1.tax-invoices.update'); Route::delete('/{id}', [TaxInvoiceController::class, 'destroy'])->whereNumber('id')->name('v1.tax-invoices.destroy'); Route::post('/{id}/issue', [TaxInvoiceController::class, 'issue'])->whereNumber('id')->name('v1.tax-invoices.issue'); Route::post('/{id}/cancel', [TaxInvoiceController::class, 'cancel'])->whereNumber('id')->name('v1.tax-invoices.cancel'); Route::get('/{id}/check-status', [TaxInvoiceController::class, 'checkStatus'])->whereNumber('id')->name('v1.tax-invoices.check-status'); - Route::post('/bulk-issue', [TaxInvoiceController::class, 'bulkIssue'])->name('v1.tax-invoices.bulk-issue'); }); // Bad Debt API (악성채권 추심관리)