feat: [tax-invoice] 바로빌 SOAP 연동 및 공급자 설정 API 추가
- BarobillService HTTP→SOAP 전환 (MNG EtaxController 포팅) - TI SOAP 클라이언트, callSoap(), buildTaxInvoiceData MNG 형식 적용 - issueTaxInvoice/cancelTaxInvoice/checkNtsSendStatus SOAP 방식 - 공급자 설정 조회/저장 API (GET/PUT /supplier-settings) - 생성+즉시발행 통합 API (POST /issue-direct) - SaveSupplierSettingsRequest FormRequest 추가
This commit is contained in:
@@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
28
app/Http/Requests/TaxInvoice/SaveSupplierSettingsRequest.php
Normal file
28
app/Http/Requests/TaxInvoice/SaveSupplierSettingsRequest.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\TaxInvoice;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class SaveSupplierSettingsRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'business_number' => ['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'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 통계
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user