- EcardController, HometaxController, EtaxController - WSDL_CACHE_NONE → WSDL_CACHE_BOTH (불필요한 WSDL 재다운로드 방지)
678 lines
25 KiB
PHP
678 lines
25 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Barobill;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Barobill\BarobillConfig;
|
|
use App\Models\Barobill\BarobillMember;
|
|
use App\Models\Tenants\Tenant;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\View\View;
|
|
|
|
/**
|
|
* 바로빌 전자세금계산서 컨트롤러
|
|
*/
|
|
class EtaxController extends Controller
|
|
{
|
|
/**
|
|
* 바로빌 SOAP 설정
|
|
*/
|
|
private ?string $certKey = null;
|
|
|
|
private ?string $corpNum = null;
|
|
|
|
private bool $isTestMode = false;
|
|
|
|
private ?string $soapUrl = null;
|
|
|
|
private ?\SoapClient $soapClient = null;
|
|
|
|
public function __construct()
|
|
{
|
|
// DB에서 활성화된 바로빌 설정 조회
|
|
$activeConfig = BarobillConfig::where('is_active', true)->first();
|
|
|
|
if ($activeConfig) {
|
|
$this->certKey = $activeConfig->cert_key;
|
|
$this->corpNum = $activeConfig->corp_num;
|
|
$this->isTestMode = $activeConfig->environment === 'test';
|
|
$this->soapUrl = $activeConfig->base_url.'/TI.asmx?WSDL';
|
|
} else {
|
|
// 설정이 없으면 기본값 사용
|
|
$this->isTestMode = config('services.barobill.test_mode', true);
|
|
// 테스트 모드에 따라 적절한 CERT_KEY 선택
|
|
$this->certKey = $this->isTestMode
|
|
? config('services.barobill.cert_key_test', '')
|
|
: config('services.barobill.cert_key_prod', '');
|
|
$this->corpNum = config('services.barobill.corp_num', '');
|
|
$this->soapUrl = $this->isTestMode
|
|
? 'https://testws.baroservice.com/TI.asmx?WSDL'
|
|
: 'https://ws.baroservice.com/TI.asmx?WSDL';
|
|
}
|
|
|
|
$this->initSoapClient();
|
|
}
|
|
|
|
/**
|
|
* SOAP 클라이언트 초기화
|
|
*/
|
|
private function initSoapClient(): void
|
|
{
|
|
if (! empty($this->certKey) || $this->isTestMode) {
|
|
try {
|
|
$context = stream_context_create([
|
|
'ssl' => [
|
|
'verify_peer' => false,
|
|
'verify_peer_name' => false,
|
|
'allow_self_signed' => true,
|
|
],
|
|
]);
|
|
|
|
$this->soapClient = new \SoapClient($this->soapUrl, [
|
|
'trace' => true,
|
|
'encoding' => 'UTF-8',
|
|
'exceptions' => true,
|
|
'connection_timeout' => 30,
|
|
'stream_context' => $context,
|
|
'cache_wsdl' => WSDL_CACHE_BOTH,
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('바로빌 SOAP 클라이언트 생성 실패: '.$e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전자세금계산서 메인 페이지
|
|
*/
|
|
public function index(Request $request): View|Response
|
|
{
|
|
if ($request->header('HX-Request')) {
|
|
return response('', 200)->header('HX-Redirect', route('barobill.etax.index'));
|
|
}
|
|
|
|
// 현재 선택된 테넌트 정보
|
|
$tenantId = session('selected_tenant_id');
|
|
$currentTenant = $tenantId ? Tenant::find($tenantId) : null;
|
|
|
|
// 해당 테넌트의 바로빌 회원사 정보 (공급자 정보로 사용)
|
|
$barobillMember = $tenantId
|
|
? BarobillMember::where('tenant_id', $tenantId)->first()
|
|
: null;
|
|
|
|
// 테넌트별 서버 모드 적용 (회원사 설정 우선)
|
|
$isTestMode = $barobillMember ? $barobillMember->isTestMode() : $this->isTestMode;
|
|
|
|
// 서버 모드에 따라 SOAP 설정 재초기화
|
|
if ($barobillMember && $barobillMember->server_mode) {
|
|
$this->applyMemberServerMode($barobillMember);
|
|
}
|
|
|
|
return view('barobill.etax.index', [
|
|
'certKey' => $this->certKey,
|
|
'corpNum' => $this->corpNum,
|
|
'isTestMode' => $isTestMode,
|
|
'hasSoapClient' => $this->soapClient !== null,
|
|
'currentTenant' => $currentTenant,
|
|
'barobillMember' => $barobillMember,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 회원사 서버 모드에 따라 SOAP 설정 적용
|
|
*/
|
|
private function applyMemberServerMode(BarobillMember $member): void
|
|
{
|
|
$memberTestMode = $member->isTestMode();
|
|
$targetEnv = $memberTestMode ? 'test' : 'production';
|
|
|
|
// 해당 환경의 BarobillConfig 조회 (is_active 무관하게 환경에 맞는 설정 사용)
|
|
$config = BarobillConfig::where('environment', $targetEnv)->first();
|
|
|
|
if ($config) {
|
|
$this->isTestMode = $memberTestMode;
|
|
$this->certKey = $config->cert_key;
|
|
$this->corpNum = $config->corp_num;
|
|
$baseUrl = $config->base_url ?: ($memberTestMode
|
|
? 'https://testws.baroservice.com'
|
|
: 'https://ws.baroservice.com');
|
|
$this->soapUrl = $baseUrl.'/TI.asmx?WSDL';
|
|
|
|
// SOAP 클라이언트 재초기화
|
|
$this->initSoapClient();
|
|
|
|
Log::info('[Etax] 서버 모드 적용', [
|
|
'targetEnv' => $targetEnv,
|
|
'certKey' => substr($this->certKey ?? '', 0, 10).'...',
|
|
'corpNum' => $this->corpNum,
|
|
'soapUrl' => $this->soapUrl,
|
|
]);
|
|
} else {
|
|
Log::warning('[Etax] BarobillConfig 없음', ['targetEnv' => $targetEnv]);
|
|
}
|
|
}
|
|
|
|
// 바로빌 파트너사 (본사) 테넌트 ID
|
|
private const HEADQUARTERS_TENANT_ID = 1;
|
|
|
|
/**
|
|
* 세금계산서 목록 조회
|
|
* - 테넌트 1(본사)이면 모든 세금계산서 표시
|
|
* - 다른 테넌트면 해당 테넌트의 세금계산서만 표시
|
|
*/
|
|
public function getInvoices(): JsonResponse
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
$isHeadquarters = $tenantId == self::HEADQUARTERS_TENANT_ID;
|
|
|
|
// 데이터 파일에서 조회 (실제 구현 시 DB 사용)
|
|
$dataFile = storage_path('app/barobill/invoices_data.json');
|
|
$invoices = [];
|
|
|
|
if (file_exists($dataFile)) {
|
|
$data = json_decode(file_get_contents($dataFile), true);
|
|
$allInvoices = $data['invoices'] ?? [];
|
|
|
|
// 본사(테넌트 1)가 아니면 해당 테넌트의 세금계산서만 필터링
|
|
if (! $isHeadquarters && $tenantId) {
|
|
$invoices = array_values(array_filter($allInvoices, function ($invoice) use ($tenantId) {
|
|
return ($invoice['tenant_id'] ?? null) == $tenantId;
|
|
}));
|
|
} else {
|
|
$invoices = $allInvoices;
|
|
}
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'invoices' => $invoices,
|
|
'tenant_id' => $tenantId,
|
|
'is_headquarters' => $isHeadquarters,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 세금계산서 발행
|
|
*/
|
|
public function issue(Request $request): JsonResponse
|
|
{
|
|
// 테넌트별 서버 모드 적용
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
|
if ($barobillMember && $barobillMember->server_mode) {
|
|
$this->applyMemberServerMode($barobillMember);
|
|
}
|
|
|
|
$input = $request->all();
|
|
|
|
$useRealAPI = $this->soapClient !== null && ($this->isTestMode || ! empty($this->certKey));
|
|
|
|
$debugInfo = [
|
|
'hasSoapClient' => $this->soapClient !== null,
|
|
'hasCertKey' => ! empty($this->certKey),
|
|
'hasCorpNum' => ! empty($this->corpNum),
|
|
'isTestMode' => $this->isTestMode,
|
|
'willUseRealAPI' => $useRealAPI,
|
|
];
|
|
|
|
if ($useRealAPI) {
|
|
$apiResult = $this->issueTaxInvoice($input);
|
|
|
|
if ($apiResult['success']) {
|
|
$mgtKey = $input['issueKey'] ?? 'MGT'.date('YmdHis').rand(1000, 9999);
|
|
|
|
$newInvoice = $this->createInvoiceRecord($input, $mgtKey, $apiResult['data'] ?? null);
|
|
$this->saveInvoice($newInvoice);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '세금계산서가 성공적으로 발행되었습니다.',
|
|
'data' => [
|
|
'issueKey' => $newInvoice['issueKey'],
|
|
'mgtKey' => $mgtKey,
|
|
'status' => 'issued',
|
|
],
|
|
'invoice' => $newInvoice,
|
|
'simulation' => false,
|
|
'debug' => $debugInfo,
|
|
]);
|
|
} else {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $apiResult['error'] ?? 'API 호출 실패',
|
|
'error_code' => $apiResult['error_code'] ?? null,
|
|
'debug' => $debugInfo,
|
|
], 400);
|
|
}
|
|
} else {
|
|
// 시뮬레이션 모드
|
|
$issueKey = 'BARO-'.date('Y').'-'.str_pad(rand(1, 9999), 4, '0', STR_PAD_LEFT);
|
|
|
|
$newInvoice = $this->createInvoiceRecord($input, $issueKey, null);
|
|
$this->saveInvoice($newInvoice);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '세금계산서가 성공적으로 발행되었습니다. (시뮬레이션 모드)',
|
|
'data' => [
|
|
'issueKey' => $issueKey,
|
|
'status' => 'issued',
|
|
],
|
|
'invoice' => $newInvoice,
|
|
'simulation' => true,
|
|
'debug' => $debugInfo,
|
|
'warning' => '시뮬레이션 모드입니다. 실제 바로빌 API를 호출하려면 CERTKEY를 설정하세요.',
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 세금계산서 국세청 전송
|
|
*/
|
|
public function sendToNts(Request $request): JsonResponse
|
|
{
|
|
// 테넌트별 서버 모드 적용
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
|
if ($barobillMember && $barobillMember->server_mode) {
|
|
$this->applyMemberServerMode($barobillMember);
|
|
}
|
|
|
|
$invoiceId = $request->input('invoiceId');
|
|
|
|
// 인보이스 조회
|
|
$dataFile = storage_path('app/barobill/invoices_data.json');
|
|
$data = json_decode(file_get_contents($dataFile), true) ?? ['invoices' => []];
|
|
|
|
$invoice = null;
|
|
$invoiceIndex = null;
|
|
foreach ($data['invoices'] as $index => $inv) {
|
|
if ($inv['id'] === $invoiceId) {
|
|
$invoice = $inv;
|
|
$invoiceIndex = $index;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (! $invoice) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '세금계산서를 찾을 수 없습니다.',
|
|
], 404);
|
|
}
|
|
|
|
$useRealAPI = $this->soapClient !== null && ! empty($this->certKey);
|
|
|
|
if ($useRealAPI && ! empty($invoice['mgtKey'])) {
|
|
$result = $this->callBarobillSOAP('SendToNTS', [
|
|
'CorpNum' => $this->corpNum,
|
|
'MgtKey' => $invoice['mgtKey'],
|
|
]);
|
|
|
|
if ($result['success']) {
|
|
$data['invoices'][$invoiceIndex]['status'] = 'sent';
|
|
$data['invoices'][$invoiceIndex]['ntsReceiptNo'] = 'NTS-'.date('YmdHis');
|
|
$data['invoices'][$invoiceIndex]['sentAt'] = date('Y-m-d');
|
|
file_put_contents($dataFile, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '국세청 전송이 완료되었습니다.',
|
|
]);
|
|
} else {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $result['error'] ?? '전송 실패',
|
|
], 400);
|
|
}
|
|
} else {
|
|
// 시뮬레이션
|
|
$data['invoices'][$invoiceIndex]['status'] = 'sent';
|
|
$data['invoices'][$invoiceIndex]['ntsReceiptNo'] = 'NTS-SIM-'.date('YmdHis');
|
|
$data['invoices'][$invoiceIndex]['sentAt'] = date('Y-m-d');
|
|
file_put_contents($dataFile, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '국세청 전송이 완료되었습니다. (시뮬레이션)',
|
|
'simulation' => true,
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 공급자 기초정보 조회
|
|
*/
|
|
public function getSupplier(): JsonResponse
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
if (! $tenantId) {
|
|
return response()->json(['success' => false, 'error' => '테넌트가 선택되지 않았습니다.'], 400);
|
|
}
|
|
|
|
$member = BarobillMember::where('tenant_id', $tenantId)->first();
|
|
if (! $member) {
|
|
return response()->json(['success' => false, 'error' => '바로빌 회원사 정보가 없습니다.'], 404);
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'supplier' => [
|
|
'bizno' => $member->biz_no,
|
|
'name' => $member->corp_name ?? '',
|
|
'ceo' => $member->ceo_name ?? '',
|
|
'addr' => $member->addr ?? '',
|
|
'bizType' => $member->biz_type ?? '',
|
|
'bizClass' => $member->biz_class ?? '',
|
|
'contact' => $member->manager_name ?? '',
|
|
'contactPhone' => $member->manager_hp ?? '',
|
|
'email' => $member->manager_email ?? '',
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 공급자 기초정보 수정
|
|
*/
|
|
public function updateSupplier(Request $request): JsonResponse
|
|
{
|
|
$tenantId = session('selected_tenant_id');
|
|
if (! $tenantId) {
|
|
return response()->json(['success' => false, 'error' => '테넌트가 선택되지 않았습니다.'], 400);
|
|
}
|
|
|
|
$member = BarobillMember::where('tenant_id', $tenantId)->first();
|
|
if (! $member) {
|
|
return response()->json(['success' => false, 'error' => '바로빌 회원사 정보가 없습니다.'], 404);
|
|
}
|
|
|
|
$validated = $request->validate([
|
|
'corp_name' => 'required|string|max:100',
|
|
'ceo_name' => 'required|string|max:50',
|
|
'addr' => 'nullable|string|max:255',
|
|
'biz_type' => 'nullable|string|max:100',
|
|
'biz_class' => 'nullable|string|max:100',
|
|
'manager_name' => 'nullable|string|max:50',
|
|
'manager_email' => 'nullable|email|max:100',
|
|
'manager_hp' => 'nullable|string|max:20',
|
|
]);
|
|
|
|
$member->update($validated);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '공급자 정보가 수정되었습니다.',
|
|
'supplier' => [
|
|
'bizno' => $member->biz_no,
|
|
'name' => $member->corp_name ?? '',
|
|
'ceo' => $member->ceo_name ?? '',
|
|
'addr' => $member->addr ?? '',
|
|
'bizType' => $member->biz_type ?? '',
|
|
'bizClass' => $member->biz_class ?? '',
|
|
'contact' => $member->manager_name ?? '',
|
|
'contactPhone' => $member->manager_hp ?? '',
|
|
'email' => $member->manager_email ?? '',
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 세금계산서 삭제
|
|
*/
|
|
public function delete(Request $request): JsonResponse
|
|
{
|
|
$invoiceId = $request->input('invoiceId');
|
|
|
|
$dataFile = storage_path('app/barobill/invoices_data.json');
|
|
|
|
if (! file_exists($dataFile)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '데이터 파일이 없습니다.',
|
|
], 404);
|
|
}
|
|
|
|
$data = json_decode(file_get_contents($dataFile), true) ?? ['invoices' => []];
|
|
|
|
$originalCount = count($data['invoices']);
|
|
$data['invoices'] = array_values(array_filter($data['invoices'], fn ($inv) => $inv['id'] !== $invoiceId));
|
|
|
|
if (count($data['invoices']) === $originalCount) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '세금계산서를 찾을 수 없습니다.',
|
|
], 404);
|
|
}
|
|
|
|
file_put_contents($dataFile, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '세금계산서가 삭제되었습니다.',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 바로빌 SOAP API 호출
|
|
*/
|
|
private function callBarobillSOAP(string $method, array $params = []): array
|
|
{
|
|
if (! $this->soapClient) {
|
|
return [
|
|
'success' => false,
|
|
'error' => '바로빌 SOAP 클라이언트가 초기화되지 않았습니다.',
|
|
];
|
|
}
|
|
|
|
if (! isset($params['CERTKEY'])) {
|
|
$params['CERTKEY'] = $this->certKey;
|
|
}
|
|
|
|
try {
|
|
$result = $this->soapClient->$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' => $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(),
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 세금계산서 발행 API 호출
|
|
*/
|
|
private function issueTaxInvoice(array $invoiceData): array
|
|
{
|
|
$mgtKey = $invoiceData['issueKey'] ?? 'MGT'.date('YmdHis').rand(1000, 9999);
|
|
|
|
$supplyAmt = 0;
|
|
$vat = 0;
|
|
foreach ($invoiceData['items'] ?? [] as $item) {
|
|
$supplyAmt += floatval($item['supplyAmt'] ?? 0);
|
|
$vat += floatval($item['vat'] ?? 0);
|
|
}
|
|
$total = $supplyAmt + $vat;
|
|
|
|
$taxType = $vat == 0 ? 2 : 1;
|
|
|
|
$taxInvoice = [
|
|
'IssueDirection' => 1, // 1: 정발행
|
|
'TaxInvoiceType' => 1, // 1: 세금계산서
|
|
'ModifyCode' => '', // 수정사유코드 (신규발행시 빈값)
|
|
'TaxType' => $taxType, // 1: 과세, 2: 영세, 3: 면세
|
|
'TaxCalcType' => 1, // 1: 소계합계
|
|
'PurposeType' => 2, // 2: 청구
|
|
'WriteDate' => date('Ymd', strtotime($invoiceData['supplyDate'] ?? date('Y-m-d'))),
|
|
'AmountTotal' => number_format($supplyAmt, 0, '', ''),
|
|
'TaxTotal' => number_format($vat, 0, '', ''),
|
|
'TotalAmount' => number_format($total, 0, '', ''),
|
|
'Cash' => '0',
|
|
'ChkBill' => '0',
|
|
'Note' => '0',
|
|
'Credit' => number_format($total, 0, '', ''),
|
|
'Remark1' => $invoiceData['memo'] ?? '',
|
|
'Remark2' => '',
|
|
'Remark3' => '',
|
|
'Kwon' => '',
|
|
'Ho' => '',
|
|
'SerialNum' => '',
|
|
'InvoicerParty' => [
|
|
'MgtNum' => $mgtKey,
|
|
'CorpNum' => $this->corpNum,
|
|
'TaxRegID' => '', // 종사업장번호
|
|
'CorpName' => $invoiceData['supplierName'] ?? '',
|
|
'CEOName' => $invoiceData['supplierCeo'] ?? '',
|
|
'Addr' => $invoiceData['supplierAddr'] ?? '',
|
|
'BizType' => '', // 업태
|
|
'BizClass' => '', // 종목
|
|
'ContactID' => $invoiceData['supplierContactId'] ?? 'cbx0913', // 바로빌 담당자 ID (필수)
|
|
'ContactName' => $invoiceData['supplierContact'] ?? '',
|
|
'TEL' => $invoiceData['supplierTel'] ?? '',
|
|
'HP' => '',
|
|
'Email' => $invoiceData['supplierEmail'] ?? '',
|
|
],
|
|
'InvoiceeParty' => [
|
|
'MgtNum' => '',
|
|
'CorpNum' => str_replace('-', '', $invoiceData['recipientBizno'] ?? ''),
|
|
'TaxRegID' => '',
|
|
'CorpName' => $invoiceData['recipientName'] ?? '',
|
|
'CEOName' => $invoiceData['recipientCeo'] ?? '',
|
|
'Addr' => $invoiceData['recipientAddr'] ?? '',
|
|
'BizType' => '',
|
|
'BizClass' => '',
|
|
'ContactID' => '',
|
|
'ContactName' => $invoiceData['recipientContact'] ?? '',
|
|
'TEL' => $invoiceData['recipientTel'] ?? '',
|
|
'HP' => '',
|
|
'Email' => $invoiceData['recipientEmail'] ?? '',
|
|
],
|
|
'BrokerParty' => [], // 위수탁 거래시에만 사용
|
|
'TaxInvoiceTradeLineItems' => ['TaxInvoiceTradeLineItem' => []],
|
|
];
|
|
|
|
$year = substr($invoiceData['supplyDate'] ?? date('Y-m-d'), 0, 4);
|
|
|
|
foreach ($invoiceData['items'] ?? [] as $item) {
|
|
$month = str_pad($item['month'] ?? '', 2, '0', STR_PAD_LEFT);
|
|
$day = str_pad($item['day'] ?? '', 2, '0', STR_PAD_LEFT);
|
|
$purchaseExpiry = ($month && $day && $month !== '00' && $day !== '00')
|
|
? $year.$month.$day
|
|
: '';
|
|
|
|
$taxInvoice['TaxInvoiceTradeLineItems']['TaxInvoiceTradeLineItem'][] = [
|
|
'PurchaseExpiry' => $purchaseExpiry, // 거래일자 (YYYYMMDD)
|
|
'Name' => $item['name'] ?? '', // 품명
|
|
'Information' => $item['spec'] ?? '', // 규격
|
|
'ChargeableUnit' => $item['qty'] ?? '1', // 수량
|
|
'UnitPrice' => number_format(floatval($item['unitPrice'] ?? 0), 0, '', ''), // 단가
|
|
'Amount' => number_format(floatval($item['supplyAmt'] ?? 0), 0, '', ''), // 공급가액
|
|
'Tax' => number_format(floatval($item['vat'] ?? 0), 0, '', ''), // 부가세
|
|
'Description' => $item['description'] ?? '', // 비고
|
|
];
|
|
}
|
|
|
|
return $this->callBarobillSOAP('RegistAndIssueTaxInvoice', [
|
|
'CorpNum' => $this->corpNum,
|
|
'Invoice' => $taxInvoice,
|
|
'SendSMS' => false,
|
|
'ForceIssue' => false,
|
|
'MailTitle' => '',
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 인보이스 레코드 생성
|
|
*/
|
|
private function createInvoiceRecord(array $input, string $issueKey, $apiData): array
|
|
{
|
|
return [
|
|
'id' => 'inv_'.time().'_'.rand(1000, 9999),
|
|
'tenant_id' => session('selected_tenant_id'), // 테넌트별 필터링용
|
|
'issueKey' => $issueKey,
|
|
'mgtKey' => $issueKey,
|
|
'supplierBizno' => $input['supplierBizno'] ?? '',
|
|
'supplierName' => $input['supplierName'] ?? '',
|
|
'supplierCeo' => $input['supplierCeo'] ?? '',
|
|
'supplierAddr' => $input['supplierAddr'] ?? '',
|
|
'supplierContact' => $input['supplierContact'] ?? '',
|
|
'supplierEmail' => $input['supplierEmail'] ?? '',
|
|
'recipientBizno' => $input['recipientBizno'] ?? '',
|
|
'recipientName' => $input['recipientName'] ?? '',
|
|
'recipientCeo' => $input['recipientCeo'] ?? '',
|
|
'recipientAddr' => $input['recipientAddr'] ?? '',
|
|
'recipientContact' => $input['recipientContact'] ?? '',
|
|
'recipientEmail' => $input['recipientEmail'] ?? '',
|
|
'supplyDate' => $input['supplyDate'] ?? date('Y-m-d'),
|
|
'items' => $input['items'] ?? [],
|
|
'totalSupplyAmt' => $input['totalSupplyAmt'] ?? 0,
|
|
'totalVat' => $input['totalVat'] ?? 0,
|
|
'total' => $input['total'] ?? 0,
|
|
'status' => 'issued',
|
|
'memo' => $input['memo'] ?? '',
|
|
'createdAt' => date('Y-m-d\TH:i:s'),
|
|
'sentAt' => date('Y-m-d'),
|
|
'barobillInvoiceId' => is_numeric($apiData) ? (string) $apiData : '',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 인보이스 저장
|
|
*/
|
|
private function saveInvoice(array $invoice): bool
|
|
{
|
|
$dataDir = storage_path('app/barobill');
|
|
if (! is_dir($dataDir)) {
|
|
mkdir($dataDir, 0755, true);
|
|
}
|
|
|
|
$dataFile = $dataDir.'/invoices_data.json';
|
|
$existingData = ['invoices' => []];
|
|
|
|
if (file_exists($dataFile)) {
|
|
$content = file_get_contents($dataFile);
|
|
if ($content) {
|
|
$existingData = json_decode($content, true) ?? ['invoices' => []];
|
|
}
|
|
}
|
|
|
|
$existingData['invoices'][] = $invoice;
|
|
|
|
if (count($existingData['invoices']) > 100) {
|
|
$existingData['invoices'] = array_slice($existingData['invoices'], -100);
|
|
}
|
|
|
|
return file_put_contents($dataFile, json_encode($existingData, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) !== false;
|
|
}
|
|
}
|