2025-12-18 15:31:59 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Services;
|
|
|
|
|
|
|
|
|
|
use App\Models\Tenants\BarobillSetting;
|
|
|
|
|
use App\Models\Tenants\TaxInvoice;
|
2026-02-23 10:17:37 +09:00
|
|
|
use Illuminate\Support\Facades\Http;
|
2025-12-18 15:31:59 +09:00
|
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-23 10:17:37 +09:00
|
|
|
* 바로빌 API 연동 서비스
|
2025-12-18 15:31:59 +09:00
|
|
|
*
|
|
|
|
|
* 바로빌 개발자센터: https://dev.barobill.co.kr/
|
|
|
|
|
*/
|
|
|
|
|
class BarobillService extends Service
|
|
|
|
|
{
|
|
|
|
|
/**
|
2026-02-23 10:17:37 +09:00
|
|
|
* 바로빌 API 기본 URL
|
2025-12-18 15:31:59 +09:00
|
|
|
*/
|
2026-02-23 10:17:37 +09:00
|
|
|
private const API_BASE_URL = 'https://ws.barobill.co.kr';
|
2025-12-18 15:31:59 +09:00
|
|
|
|
|
|
|
|
/**
|
2026-02-23 10:17:37 +09:00
|
|
|
* 바로빌 API 테스트 URL
|
2025-12-18 15:31:59 +09:00
|
|
|
*/
|
2026-02-23 10:17:37 +09:00
|
|
|
private const API_TEST_URL = 'https://testws.barobill.co.kr';
|
2025-12-18 15:31:59 +09:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 테스트 모드 여부
|
|
|
|
|
*/
|
|
|
|
|
private bool $testMode;
|
|
|
|
|
|
|
|
|
|
public function __construct()
|
|
|
|
|
{
|
|
|
|
|
$this->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();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-23 10:17:37 +09:00
|
|
|
* 연동 테스트
|
2025-12-18 15:31:59 +09:00
|
|
|
*/
|
|
|
|
|
public function testConnection(): array
|
|
|
|
|
{
|
|
|
|
|
$setting = $this->getSetting();
|
|
|
|
|
|
|
|
|
|
if (! $setting || ! $setting->canConnect()) {
|
|
|
|
|
throw new BadRequestHttpException(__('error.barobill.setting_not_configured'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-23 10:17:37 +09:00
|
|
|
// 바로빌 API 토큰 조회로 연동 테스트
|
|
|
|
|
$response = $this->callApi('GetAccessToken', [
|
2025-12-18 15:31:59 +09:00
|
|
|
'CERTKEY' => $setting->cert_key,
|
|
|
|
|
'CorpNum' => $setting->corp_num,
|
|
|
|
|
'ID' => $setting->barobill_id,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
if (! empty($response['AccessToken'])) {
|
|
|
|
|
// 검증 성공 시 verified_at 업데이트
|
|
|
|
|
$setting->verified_at = now();
|
|
|
|
|
$setting->save();
|
2025-12-18 15:31:59 +09:00
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
return [
|
|
|
|
|
'success' => true,
|
|
|
|
|
'message' => __('message.barobill.connection_success'),
|
|
|
|
|
'verified_at' => $setting->verified_at->toDateTimeString(),
|
|
|
|
|
];
|
2025-12-18 15:31:59 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
throw new BadRequestHttpException($response['Message'] ?? __('error.barobill.connection_failed'));
|
2025-12-18 15:31:59 +09:00
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
Log::error('바로빌 연동 테스트 실패', [
|
|
|
|
|
'tenant_id' => $this->tenantId(),
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
throw new BadRequestHttpException(__('error.barobill.connection_failed').': '.$e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-22 15:30:38 +09:00
|
|
|
// =========================================================================
|
|
|
|
|
// 사업자등록번호 검증
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사업자등록번호 유효성 검사 (휴폐업 조회)
|
|
|
|
|
*
|
|
|
|
|
* 바로빌 API를 통해 사업자등록번호의 유효성을 검증합니다.
|
|
|
|
|
* 바로빌 설정이 없는 경우 기본 형식 검증만 수행합니다.
|
|
|
|
|
*
|
|
|
|
|
* @param string $businessNumber 사업자등록번호 (10자리, 하이픈 제거)
|
|
|
|
|
* @return array{valid: bool, status: string, status_label: string, corp_name: ?string, ceo_name: ?string, message: string}
|
|
|
|
|
*/
|
|
|
|
|
public function checkBusinessNumber(string $businessNumber): array
|
|
|
|
|
{
|
|
|
|
|
// 하이픈 제거 및 숫자만 추출
|
|
|
|
|
$businessNumber = preg_replace('/[^0-9]/', '', $businessNumber);
|
|
|
|
|
|
|
|
|
|
// 기본 형식 검증 (10자리)
|
|
|
|
|
if (strlen($businessNumber) !== 10) {
|
|
|
|
|
return [
|
|
|
|
|
'valid' => false,
|
|
|
|
|
'status' => 'invalid_format',
|
|
|
|
|
'status_label' => '형식 오류',
|
|
|
|
|
'corp_name' => null,
|
|
|
|
|
'ceo_name' => null,
|
|
|
|
|
'message' => __('error.company.invalid_business_number_format'),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 체크섬 검증 (사업자등록번호 자체 유효성)
|
|
|
|
|
if (! $this->validateBusinessNumberChecksum($businessNumber)) {
|
|
|
|
|
return [
|
|
|
|
|
'valid' => false,
|
|
|
|
|
'status' => 'invalid_checksum',
|
|
|
|
|
'status_label' => '유효하지 않음',
|
|
|
|
|
'corp_name' => null,
|
|
|
|
|
'ceo_name' => null,
|
|
|
|
|
'message' => __('error.company.invalid_business_number'),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
// 바로빌 API 조회 시도
|
2025-12-22 15:30:38 +09:00
|
|
|
try {
|
2026-02-23 10:17:37 +09:00
|
|
|
$response = $this->callApi('CheckCorpNum', [
|
2025-12-22 15:30:38 +09:00
|
|
|
'CorpNum' => $businessNumber,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
// 바로빌 응답 해석
|
|
|
|
|
if (isset($response['CorpState'])) {
|
|
|
|
|
$state = $response['CorpState'];
|
|
|
|
|
$isValid = in_array($state, ['01', '02']); // 01: 사업중, 02: 휴업
|
|
|
|
|
$statusLabel = match ($state) {
|
|
|
|
|
'01' => '사업중',
|
|
|
|
|
'02' => '휴업',
|
|
|
|
|
'03' => '폐업',
|
|
|
|
|
default => '조회 불가',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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 (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')),
|
|
|
|
|
];
|
2025-12-22 15:30:38 +09:00
|
|
|
}
|
|
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
// 기본 응답 (체크섬만 통과한 경우)
|
2025-12-22 15:30:38 +09:00
|
|
|
return [
|
|
|
|
|
'valid' => true,
|
|
|
|
|
'status' => 'format_valid',
|
|
|
|
|
'status_label' => '형식 유효',
|
|
|
|
|
'corp_name' => null,
|
|
|
|
|
'ceo_name' => null,
|
|
|
|
|
'message' => __('message.company.business_number_format_valid'),
|
|
|
|
|
];
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
// API 호출 실패 시 형식 검증 결과만 반환
|
|
|
|
|
Log::warning('바로빌 사업자번호 조회 실패', [
|
|
|
|
|
'business_number' => $businessNumber,
|
|
|
|
|
'error' => $e->getMessage(),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
'valid' => true,
|
|
|
|
|
'status' => 'format_valid',
|
|
|
|
|
'status_label' => '형식 유효 (API 조회 불가)',
|
|
|
|
|
'corp_name' => null,
|
|
|
|
|
'ceo_name' => null,
|
|
|
|
|
'message' => __('message.company.business_number_format_valid'),
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 사업자등록번호 체크섬 검증
|
|
|
|
|
*
|
|
|
|
|
* @param string $businessNumber 10자리 사업자등록번호
|
|
|
|
|
*/
|
|
|
|
|
private function validateBusinessNumberChecksum(string $businessNumber): bool
|
|
|
|
|
{
|
|
|
|
|
if (strlen($businessNumber) !== 10) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$digits = str_split($businessNumber);
|
|
|
|
|
$multipliers = [1, 3, 7, 1, 3, 7, 1, 3, 5];
|
|
|
|
|
$sum = 0;
|
|
|
|
|
|
|
|
|
|
for ($i = 0; $i < 9; $i++) {
|
|
|
|
|
$sum += intval($digits[$i]) * $multipliers[$i];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 8번째 자리 (인덱스 8)에 대한 추가 처리
|
|
|
|
|
$sum += intval(floor(intval($digits[8]) * 5 / 10));
|
|
|
|
|
|
|
|
|
|
$remainder = $sum % 10;
|
|
|
|
|
$checkDigit = (10 - $remainder) % 10;
|
|
|
|
|
|
|
|
|
|
return $checkDigit === intval($digits[9]);
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-18 15:31:59 +09:00
|
|
|
// =========================================================================
|
|
|
|
|
// 세금계산서 발행
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-23 10:17:37 +09:00
|
|
|
* 세금계산서 발행
|
2025-12-18 15:31:59 +09:00
|
|
|
*/
|
|
|
|
|
public function issueTaxInvoice(TaxInvoice $taxInvoice): TaxInvoice
|
|
|
|
|
{
|
|
|
|
|
$setting = $this->getSetting();
|
|
|
|
|
|
|
|
|
|
if (! $setting || ! $setting->canConnect()) {
|
|
|
|
|
throw new BadRequestHttpException(__('error.barobill.setting_not_configured'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
2026-02-23 10:17:37 +09:00
|
|
|
// 바로빌 API 호출을 위한 데이터 구성
|
2025-12-18 15:31:59 +09:00
|
|
|
$apiData = $this->buildTaxInvoiceData($taxInvoice, $setting);
|
|
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
// 세금계산서 발행 API 호출
|
|
|
|
|
$response = $this->callApi('RegistAndIssueTaxInvoice', $apiData);
|
|
|
|
|
|
|
|
|
|
if (! empty($response['InvoiceID'])) {
|
|
|
|
|
// 발행 성공
|
|
|
|
|
$taxInvoice->barobill_invoice_id = $response['InvoiceID'];
|
|
|
|
|
$taxInvoice->nts_confirm_num = $response['NTSConfirmNum'] ?? null;
|
2025-12-18 15:31:59 +09:00
|
|
|
$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,
|
2026-02-23 10:17:37 +09:00
|
|
|
'barobill_invoice_id' => $response['InvoiceID'],
|
2025-12-18 15:31:59 +09:00
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
return $taxInvoice->fresh();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
throw new \Exception($response['Message'] ?? '발행 실패');
|
2025-12-18 15:31:59 +09:00
|
|
|
} 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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-23 10:17:37 +09:00
|
|
|
* 세금계산서 취소
|
2025-12-18 15:31:59 +09:00
|
|
|
*/
|
|
|
|
|
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 {
|
2026-02-23 10:17:37 +09:00
|
|
|
// 세금계산서 취소 API 호출
|
|
|
|
|
$response = $this->callApi('CancelTaxInvoice', [
|
2025-12-18 15:31:59 +09:00
|
|
|
'CERTKEY' => $setting->cert_key,
|
|
|
|
|
'CorpNum' => $setting->corp_num,
|
2026-02-23 10:17:37 +09:00
|
|
|
'ID' => $setting->barobill_id,
|
|
|
|
|
'InvoiceID' => $taxInvoice->barobill_invoice_id,
|
2025-12-18 15:31:59 +09:00
|
|
|
'Memo' => $reason,
|
|
|
|
|
]);
|
|
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
if ($response['Result'] === 0 || ! empty($response['Success'])) {
|
2025-12-18 15:31:59 +09:00
|
|
|
$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();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
throw new \Exception($response['Message'] ?? '취소 실패');
|
2025-12-18 15:31:59 +09:00
|
|
|
} 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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-23 10:17:37 +09:00
|
|
|
* 국세청 전송 상태 조회
|
2025-12-18 15:31:59 +09:00
|
|
|
*/
|
|
|
|
|
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 {
|
2026-02-23 10:17:37 +09:00
|
|
|
$response = $this->callApi('GetTaxInvoiceState', [
|
2025-12-18 15:31:59 +09:00
|
|
|
'CERTKEY' => $setting->cert_key,
|
|
|
|
|
'CorpNum' => $setting->corp_num,
|
2026-02-23 10:17:37 +09:00
|
|
|
'ID' => $setting->barobill_id,
|
|
|
|
|
'InvoiceID' => $taxInvoice->barobill_invoice_id,
|
2025-12-18 15:31:59 +09:00
|
|
|
]);
|
|
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
if (! empty($response['State'])) {
|
|
|
|
|
$taxInvoice->nts_send_status = $response['State'];
|
2025-12-18 15:31:59 +09:00
|
|
|
|
2026-02-23 10:17:37 +09:00
|
|
|
// 국세청 전송 완료 시 상태 업데이트
|
|
|
|
|
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;
|
2026-02-21 08:31:34 +09:00
|
|
|
}
|
2026-02-23 10:17:37 +09:00
|
|
|
|
|
|
|
|
$taxInvoice->save();
|
2025-12-18 15:31:59 +09:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 메서드
|
|
|
|
|
// =========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-23 10:17:37 +09:00
|
|
|
* 바로빌 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() ?? [];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 세금계산서 발행용 데이터 구성
|
2025-12-18 15:31:59 +09:00
|
|
|
*/
|
|
|
|
|
private function buildTaxInvoiceData(TaxInvoice $taxInvoice, BarobillSetting $setting): array
|
|
|
|
|
{
|
2026-02-23 10:17:37 +09:00
|
|
|
// 품목 데이터 구성
|
|
|
|
|
$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'] ?? '',
|
2025-12-18 15:31:59 +09:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 품목이 없는 경우 기본 품목 추가
|
2026-02-23 10:17:37 +09:00
|
|
|
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' => '',
|
2025-12-18 15:31:59 +09:00
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [
|
2026-02-23 10:17:37 +09:00
|
|
|
'CERTKEY' => $setting->cert_key,
|
2025-12-18 15:31:59 +09:00
|
|
|
'CorpNum' => $setting->corp_num,
|
2026-02-23 10:17:37 +09:00
|
|
|
'ID' => $setting->barobill_id,
|
|
|
|
|
'TaxInvoice' => [
|
|
|
|
|
'InvoiceType' => $this->mapInvoiceType($taxInvoice->invoice_type),
|
|
|
|
|
'IssueType' => $this->mapIssueType($taxInvoice->issue_type),
|
|
|
|
|
'TaxType' => '과세',
|
|
|
|
|
'PurposeType' => '영수',
|
2025-12-18 15:31:59 +09:00
|
|
|
'WriteDate' => $taxInvoice->issue_date->format('Ymd'),
|
2026-02-23 10:17:37 +09:00
|
|
|
|
|
|
|
|
// 공급자 정보
|
|
|
|
|
'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,
|
|
|
|
|
|
|
|
|
|
// 비고
|
2025-12-18 15:31:59 +09:00
|
|
|
'Remark1' => $taxInvoice->description ?? '',
|
|
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-02-23 10:17:37 +09:00
|
|
|
* 세금계산서 유형 매핑
|
|
|
|
|
*/
|
|
|
|
|
private function mapInvoiceType(string $type): string
|
|
|
|
|
{
|
|
|
|
|
return match ($type) {
|
|
|
|
|
TaxInvoice::TYPE_TAX_INVOICE => '세금계산서',
|
|
|
|
|
TaxInvoice::TYPE_INVOICE => '계산서',
|
|
|
|
|
TaxInvoice::TYPE_MODIFIED_TAX_INVOICE => '수정세금계산서',
|
|
|
|
|
default => '세금계산서',
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 발행 유형 매핑
|
2025-12-18 15:31:59 +09:00
|
|
|
*/
|
2026-02-23 10:17:37 +09:00
|
|
|
private function mapIssueType(string $type): string
|
2025-12-18 15:31:59 +09:00
|
|
|
{
|
|
|
|
|
return match ($type) {
|
2026-02-23 10:17:37 +09:00
|
|
|
TaxInvoice::ISSUE_TYPE_NORMAL => '정발행',
|
|
|
|
|
TaxInvoice::ISSUE_TYPE_REVERSE => '역발행',
|
|
|
|
|
TaxInvoice::ISSUE_TYPE_TRUSTEE => '위수탁',
|
|
|
|
|
default => '정발행',
|
2025-12-18 15:31:59 +09:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|