feat: Phase 3.8 바로빌 세금계산서 연동 API 구현
- 마이그레이션: barobill_settings, tax_invoices 테이블 생성 - 모델: BarobillSetting (인증서 암호화), TaxInvoice (상태/유형 상수) - 서비스: BarobillService (API 연동), TaxInvoiceService (CRUD, 발행/취소) - 컨트롤러: BarobillSettingController, TaxInvoiceController - FormRequest: 6개 요청 검증 클래스 - Swagger: API 문서 완성 (BarobillSettingApi, TaxInvoiceApi)
This commit is contained in:
54
app/Http/Controllers/Api/V1/BarobillSettingController.php
Normal file
54
app/Http/Controllers/Api/V1/BarobillSettingController.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\BarobillSetting\SaveBarobillSettingRequest;
|
||||||
|
use App\Services\BarobillService;
|
||||||
|
|
||||||
|
class BarobillSettingController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private BarobillService $barobillService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바로빌 설정 조회
|
||||||
|
*/
|
||||||
|
public function show()
|
||||||
|
{
|
||||||
|
$setting = $this->barobillService->getSetting();
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: $setting,
|
||||||
|
message: __('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바로빌 설정 저장
|
||||||
|
*/
|
||||||
|
public function save(SaveBarobillSettingRequest $request)
|
||||||
|
{
|
||||||
|
$setting = $this->barobillService->saveSetting($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: $setting,
|
||||||
|
message: __('message.saved')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연동 테스트
|
||||||
|
*/
|
||||||
|
public function testConnection()
|
||||||
|
{
|
||||||
|
$result = $this->barobillService->testConnection();
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: $result,
|
||||||
|
message: __('message.barobill.connection_success')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
137
app/Http/Controllers/Api/V1/TaxInvoiceController.php
Normal file
137
app/Http/Controllers/Api/V1/TaxInvoiceController.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1;
|
||||||
|
|
||||||
|
use App\Helpers\ApiResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\TaxInvoice\CancelTaxInvoiceRequest;
|
||||||
|
use App\Http\Requests\TaxInvoice\CreateTaxInvoiceRequest;
|
||||||
|
use App\Http\Requests\TaxInvoice\TaxInvoiceListRequest;
|
||||||
|
use App\Http\Requests\TaxInvoice\TaxInvoiceSummaryRequest;
|
||||||
|
use App\Http\Requests\TaxInvoice\UpdateTaxInvoiceRequest;
|
||||||
|
use App\Services\TaxInvoiceService;
|
||||||
|
|
||||||
|
class TaxInvoiceController extends Controller
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private TaxInvoiceService $taxInvoiceService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 목록 조회
|
||||||
|
*/
|
||||||
|
public function index(TaxInvoiceListRequest $request)
|
||||||
|
{
|
||||||
|
$taxInvoices = $this->taxInvoiceService->list($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: $taxInvoices,
|
||||||
|
message: __('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 상세 조회
|
||||||
|
*/
|
||||||
|
public function show(int $id)
|
||||||
|
{
|
||||||
|
$taxInvoice = $this->taxInvoiceService->show($id);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: $taxInvoice,
|
||||||
|
message: __('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 생성
|
||||||
|
*/
|
||||||
|
public function store(CreateTaxInvoiceRequest $request)
|
||||||
|
{
|
||||||
|
$taxInvoice = $this->taxInvoiceService->create($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: $taxInvoice,
|
||||||
|
message: __('message.created'),
|
||||||
|
status: 201
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 수정
|
||||||
|
*/
|
||||||
|
public function update(UpdateTaxInvoiceRequest $request, int $id)
|
||||||
|
{
|
||||||
|
$taxInvoice = $this->taxInvoiceService->update($id, $request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: $taxInvoice,
|
||||||
|
message: __('message.updated')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 삭제
|
||||||
|
*/
|
||||||
|
public function destroy(int $id)
|
||||||
|
{
|
||||||
|
$this->taxInvoiceService->delete($id);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: null,
|
||||||
|
message: __('message.deleted')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 발행
|
||||||
|
*/
|
||||||
|
public function issue(int $id)
|
||||||
|
{
|
||||||
|
$taxInvoice = $this->taxInvoiceService->issue($id);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: $taxInvoice,
|
||||||
|
message: __('message.tax_invoice.issued')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 취소
|
||||||
|
*/
|
||||||
|
public function cancel(CancelTaxInvoiceRequest $request, int $id)
|
||||||
|
{
|
||||||
|
$taxInvoice = $this->taxInvoiceService->cancel($id, $request->validated()['reason']);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: $taxInvoice,
|
||||||
|
message: __('message.tax_invoice.cancelled')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 국세청 전송 상태 조회
|
||||||
|
*/
|
||||||
|
public function checkStatus(int $id)
|
||||||
|
{
|
||||||
|
$taxInvoice = $this->taxInvoiceService->checkStatus($id);
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: $taxInvoice,
|
||||||
|
message: __('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 요약 통계
|
||||||
|
*/
|
||||||
|
public function summary(TaxInvoiceSummaryRequest $request)
|
||||||
|
{
|
||||||
|
$summary = $this->taxInvoiceService->summary($request->validated());
|
||||||
|
|
||||||
|
return ApiResponse::handle(
|
||||||
|
data: $summary,
|
||||||
|
message: __('message.fetched')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\BarobillSetting;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class SaveBarobillSettingRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'corp_num' => ['required', 'string', 'size:10'],
|
||||||
|
'cert_key' => ['nullable', 'string', 'max:500'],
|
||||||
|
'barobill_id' => ['nullable', 'string', 'max:100'],
|
||||||
|
'corp_name' => ['required', 'string', 'max:100'],
|
||||||
|
'ceo_name' => ['required', 'string', 'max:50'],
|
||||||
|
'addr' => ['nullable', 'string', 'max:200'],
|
||||||
|
'biz_type' => ['nullable', 'string', 'max:100'],
|
||||||
|
'biz_class' => ['nullable', 'string', 'max:100'],
|
||||||
|
'contact_id' => ['nullable', 'string', 'email', 'max:100'],
|
||||||
|
'contact_name' => ['nullable', 'string', 'max:50'],
|
||||||
|
'contact_tel' => ['nullable', 'string', 'max:20'],
|
||||||
|
'is_active' => ['boolean'],
|
||||||
|
'auto_issue' => ['boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'corp_num' => __('validation.attributes.corp_num'),
|
||||||
|
'cert_key' => __('validation.attributes.cert_key'),
|
||||||
|
'barobill_id' => __('validation.attributes.barobill_id'),
|
||||||
|
'corp_name' => __('validation.attributes.corp_name'),
|
||||||
|
'ceo_name' => __('validation.attributes.ceo_name'),
|
||||||
|
'addr' => __('validation.attributes.addr'),
|
||||||
|
'biz_type' => __('validation.attributes.biz_type'),
|
||||||
|
'biz_class' => __('validation.attributes.biz_class'),
|
||||||
|
'contact_id' => __('validation.attributes.contact_id'),
|
||||||
|
'contact_name' => __('validation.attributes.contact_name'),
|
||||||
|
'contact_tel' => __('validation.attributes.contact_tel'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
27
app/Http/Requests/TaxInvoice/CancelTaxInvoiceRequest.php
Normal file
27
app/Http/Requests/TaxInvoice/CancelTaxInvoiceRequest.php
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\TaxInvoice;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class CancelTaxInvoiceRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'reason' => ['required', 'string', 'max:500'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'reason' => __('validation.attributes.cancel_reason'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
78
app/Http/Requests/TaxInvoice/CreateTaxInvoiceRequest.php
Normal file
78
app/Http/Requests/TaxInvoice/CreateTaxInvoiceRequest.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\TaxInvoice;
|
||||||
|
|
||||||
|
use App\Models\Tenants\TaxInvoice;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class CreateTaxInvoiceRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'invoice_type' => ['required', 'string', Rule::in(TaxInvoice::INVOICE_TYPES)],
|
||||||
|
'issue_type' => ['required', 'string', Rule::in(TaxInvoice::ISSUE_TYPES)],
|
||||||
|
'direction' => ['required', 'string', Rule::in(TaxInvoice::DIRECTIONS)],
|
||||||
|
|
||||||
|
// 공급자 정보
|
||||||
|
'supplier_corp_num' => ['required', 'string', 'max:20'],
|
||||||
|
'supplier_corp_name' => ['required', 'string', 'max:100'],
|
||||||
|
'supplier_ceo_name' => ['nullable', 'string', 'max:50'],
|
||||||
|
'supplier_addr' => ['nullable', 'string', 'max:200'],
|
||||||
|
'supplier_biz_type' => ['nullable', 'string', 'max:100'],
|
||||||
|
'supplier_biz_class' => ['nullable', 'string', 'max:100'],
|
||||||
|
'supplier_contact_id' => ['nullable', 'string', 'email', 'max:100'],
|
||||||
|
|
||||||
|
// 공급받는자 정보
|
||||||
|
'buyer_corp_num' => ['required', 'string', 'max:20'],
|
||||||
|
'buyer_corp_name' => ['required', 'string', 'max:100'],
|
||||||
|
'buyer_ceo_name' => ['nullable', 'string', 'max:50'],
|
||||||
|
'buyer_addr' => ['nullable', 'string', 'max:200'],
|
||||||
|
'buyer_biz_type' => ['nullable', 'string', 'max:100'],
|
||||||
|
'buyer_biz_class' => ['nullable', 'string', 'max:100'],
|
||||||
|
'buyer_contact_id' => ['nullable', 'string', 'email', 'max:100'],
|
||||||
|
|
||||||
|
// 금액 정보
|
||||||
|
'issue_date' => ['required', 'date'],
|
||||||
|
'supply_amount' => ['required', 'numeric', 'min:0'],
|
||||||
|
'tax_amount' => ['required', 'numeric', 'min:0'],
|
||||||
|
|
||||||
|
// 품목 정보
|
||||||
|
'items' => ['nullable', 'array'],
|
||||||
|
'items.*.name' => ['required_with:items', 'string', 'max:100'],
|
||||||
|
'items.*.spec' => ['nullable', 'string', 'max:100'],
|
||||||
|
'items.*.qty' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'items.*.unit_price' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'items.*.supply_amt' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'items.*.tax_amt' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'items.*.remark' => ['nullable', 'string', 'max:200'],
|
||||||
|
|
||||||
|
// 참조 정보
|
||||||
|
'reference_type' => ['nullable', 'string', 'max:50'],
|
||||||
|
'reference_id' => ['nullable', 'integer'],
|
||||||
|
'description' => ['nullable', 'string', 'max:1000'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function attributes(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'invoice_type' => __('validation.attributes.invoice_type'),
|
||||||
|
'issue_type' => __('validation.attributes.issue_type'),
|
||||||
|
'direction' => __('validation.attributes.direction'),
|
||||||
|
'supplier_corp_num' => __('validation.attributes.supplier_corp_num'),
|
||||||
|
'supplier_corp_name' => __('validation.attributes.supplier_corp_name'),
|
||||||
|
'buyer_corp_num' => __('validation.attributes.buyer_corp_num'),
|
||||||
|
'buyer_corp_name' => __('validation.attributes.buyer_corp_name'),
|
||||||
|
'issue_date' => __('validation.attributes.issue_date'),
|
||||||
|
'supply_amount' => __('validation.attributes.supply_amount'),
|
||||||
|
'tax_amount' => __('validation.attributes.tax_amount'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Http/Requests/TaxInvoice/TaxInvoiceListRequest.php
Normal file
31
app/Http/Requests/TaxInvoice/TaxInvoiceListRequest.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\TaxInvoice;
|
||||||
|
|
||||||
|
use App\Models\Tenants\TaxInvoice;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class TaxInvoiceListRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'per_page' => ['nullable', 'integer', 'min:1', 'max:100'],
|
||||||
|
'direction' => ['nullable', 'string', Rule::in(TaxInvoice::DIRECTIONS)],
|
||||||
|
'status' => ['nullable', 'string', Rule::in(TaxInvoice::STATUSES)],
|
||||||
|
'invoice_type' => ['nullable', 'string', Rule::in(TaxInvoice::INVOICE_TYPES)],
|
||||||
|
'issue_type' => ['nullable', 'string', Rule::in(TaxInvoice::ISSUE_TYPES)],
|
||||||
|
'issue_date_from' => ['nullable', 'date'],
|
||||||
|
'issue_date_to' => ['nullable', 'date', 'after_or_equal:issue_date_from'],
|
||||||
|
'corp_num' => ['nullable', 'string', 'max:20'],
|
||||||
|
'corp_name' => ['nullable', 'string', 'max:100'],
|
||||||
|
'nts_confirm_num' => ['nullable', 'string', 'max:24'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
21
app/Http/Requests/TaxInvoice/TaxInvoiceSummaryRequest.php
Normal file
21
app/Http/Requests/TaxInvoice/TaxInvoiceSummaryRequest.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\TaxInvoice;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class TaxInvoiceSummaryRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'issue_date_from' => ['nullable', 'date'],
|
||||||
|
'issue_date_to' => ['nullable', 'date', 'after_or_equal:issue_date_from'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Http/Requests/TaxInvoice/UpdateTaxInvoiceRequest.php
Normal file
62
app/Http/Requests/TaxInvoice/UpdateTaxInvoiceRequest.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\TaxInvoice;
|
||||||
|
|
||||||
|
use App\Models\Tenants\TaxInvoice;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
class UpdateTaxInvoiceRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'invoice_type' => ['sometimes', 'string', Rule::in(TaxInvoice::INVOICE_TYPES)],
|
||||||
|
'issue_type' => ['sometimes', 'string', Rule::in(TaxInvoice::ISSUE_TYPES)],
|
||||||
|
'direction' => ['sometimes', 'string', Rule::in(TaxInvoice::DIRECTIONS)],
|
||||||
|
|
||||||
|
// 공급자 정보
|
||||||
|
'supplier_corp_num' => ['sometimes', 'string', 'max:20'],
|
||||||
|
'supplier_corp_name' => ['sometimes', 'string', 'max:100'],
|
||||||
|
'supplier_ceo_name' => ['nullable', 'string', 'max:50'],
|
||||||
|
'supplier_addr' => ['nullable', 'string', 'max:200'],
|
||||||
|
'supplier_biz_type' => ['nullable', 'string', 'max:100'],
|
||||||
|
'supplier_biz_class' => ['nullable', 'string', 'max:100'],
|
||||||
|
'supplier_contact_id' => ['nullable', 'string', 'email', 'max:100'],
|
||||||
|
|
||||||
|
// 공급받는자 정보
|
||||||
|
'buyer_corp_num' => ['sometimes', 'string', 'max:20'],
|
||||||
|
'buyer_corp_name' => ['sometimes', 'string', 'max:100'],
|
||||||
|
'buyer_ceo_name' => ['nullable', 'string', 'max:50'],
|
||||||
|
'buyer_addr' => ['nullable', 'string', 'max:200'],
|
||||||
|
'buyer_biz_type' => ['nullable', 'string', 'max:100'],
|
||||||
|
'buyer_biz_class' => ['nullable', 'string', 'max:100'],
|
||||||
|
'buyer_contact_id' => ['nullable', 'string', 'email', 'max:100'],
|
||||||
|
|
||||||
|
// 금액 정보
|
||||||
|
'issue_date' => ['sometimes', 'date'],
|
||||||
|
'supply_amount' => ['sometimes', 'numeric', 'min:0'],
|
||||||
|
'tax_amount' => ['sometimes', 'numeric', 'min:0'],
|
||||||
|
|
||||||
|
// 품목 정보
|
||||||
|
'items' => ['nullable', 'array'],
|
||||||
|
'items.*.name' => ['required_with:items', 'string', 'max:100'],
|
||||||
|
'items.*.spec' => ['nullable', 'string', 'max:100'],
|
||||||
|
'items.*.qty' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'items.*.unit_price' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'items.*.supply_amt' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'items.*.tax_amt' => ['nullable', 'numeric', 'min:0'],
|
||||||
|
'items.*.remark' => ['nullable', 'string', 'max:200'],
|
||||||
|
|
||||||
|
// 참조 정보
|
||||||
|
'reference_type' => ['nullable', 'string', 'max:50'],
|
||||||
|
'reference_id' => ['nullable', 'integer'],
|
||||||
|
'description' => ['nullable', 'string', 'max:1000'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
132
app/Models/Tenants/BarobillSetting.php
Normal file
132
app/Models/Tenants/BarobillSetting.php
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Tenants;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Support\Facades\Crypt;
|
||||||
|
|
||||||
|
class BarobillSetting extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id',
|
||||||
|
'corp_num',
|
||||||
|
'cert_key',
|
||||||
|
'barobill_id',
|
||||||
|
'corp_name',
|
||||||
|
'ceo_name',
|
||||||
|
'addr',
|
||||||
|
'biz_type',
|
||||||
|
'biz_class',
|
||||||
|
'contact_id',
|
||||||
|
'contact_name',
|
||||||
|
'contact_tel',
|
||||||
|
'is_active',
|
||||||
|
'auto_issue',
|
||||||
|
'verified_at',
|
||||||
|
'created_by',
|
||||||
|
'updated_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'is_active' => 'boolean',
|
||||||
|
'auto_issue' => 'boolean',
|
||||||
|
'verified_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $hidden = [
|
||||||
|
'cert_key',
|
||||||
|
];
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 암호화 처리 (cert_key)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cert_key 암호화 저장
|
||||||
|
*/
|
||||||
|
public function setCertKeyAttribute(?string $value): void
|
||||||
|
{
|
||||||
|
$this->attributes['cert_key'] = $value ? Crypt::encryptString($value) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cert_key 복호화 조회
|
||||||
|
*/
|
||||||
|
public function getCertKeyAttribute(?string $value): ?string
|
||||||
|
{
|
||||||
|
if (! $value) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return Crypt::decryptString($value);
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 관계 정의
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테넌트 관계
|
||||||
|
*/
|
||||||
|
public function tenant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Tenant::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성자 관계
|
||||||
|
*/
|
||||||
|
public function creator(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\User::class, 'created_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정자 관계
|
||||||
|
*/
|
||||||
|
public function updater(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\User::class, 'updated_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 헬퍼 메서드
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연동 가능 여부
|
||||||
|
*/
|
||||||
|
public function canConnect(): bool
|
||||||
|
{
|
||||||
|
return $this->is_active
|
||||||
|
&& ! empty($this->corp_num)
|
||||||
|
&& ! empty($this->attributes['cert_key'])
|
||||||
|
&& ! empty($this->barobill_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검증 완료 여부
|
||||||
|
*/
|
||||||
|
public function isVerified(): bool
|
||||||
|
{
|
||||||
|
return $this->verified_at !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사업자번호 포맷 (하이픈 포함)
|
||||||
|
*/
|
||||||
|
public function getFormattedCorpNumAttribute(): string
|
||||||
|
{
|
||||||
|
$num = $this->corp_num;
|
||||||
|
if (strlen($num) === 10) {
|
||||||
|
return substr($num, 0, 3).'-'.substr($num, 3, 2).'-'.substr($num, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $num;
|
||||||
|
}
|
||||||
|
}
|
||||||
281
app/Models/Tenants/TaxInvoice.php
Normal file
281
app/Models/Tenants/TaxInvoice.php
Normal file
@@ -0,0 +1,281 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models\Tenants;
|
||||||
|
|
||||||
|
use App\Traits\BelongsToTenant;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||||
|
use Illuminate\Database\Eloquent\SoftDeletes;
|
||||||
|
|
||||||
|
class TaxInvoice extends Model
|
||||||
|
{
|
||||||
|
use BelongsToTenant, SoftDeletes;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 상수 정의
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 유형
|
||||||
|
*/
|
||||||
|
public const TYPE_TAX_INVOICE = 'tax_invoice'; // 세금계산서
|
||||||
|
|
||||||
|
public const TYPE_INVOICE = 'invoice'; // 계산서 (면세)
|
||||||
|
|
||||||
|
public const TYPE_MODIFIED_TAX_INVOICE = 'modified'; // 수정세금계산서
|
||||||
|
|
||||||
|
public const INVOICE_TYPES = [
|
||||||
|
self::TYPE_TAX_INVOICE,
|
||||||
|
self::TYPE_INVOICE,
|
||||||
|
self::TYPE_MODIFIED_TAX_INVOICE,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 유형
|
||||||
|
*/
|
||||||
|
public const ISSUE_TYPE_NORMAL = 'normal'; // 정발행
|
||||||
|
|
||||||
|
public const ISSUE_TYPE_REVERSE = 'reverse'; // 역발행
|
||||||
|
|
||||||
|
public const ISSUE_TYPE_TRUSTEE = 'trustee'; // 위수탁
|
||||||
|
|
||||||
|
public const ISSUE_TYPES = [
|
||||||
|
self::ISSUE_TYPE_NORMAL,
|
||||||
|
self::ISSUE_TYPE_REVERSE,
|
||||||
|
self::ISSUE_TYPE_TRUSTEE,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 방향
|
||||||
|
*/
|
||||||
|
public const DIRECTION_SALES = 'sales'; // 매출
|
||||||
|
|
||||||
|
public const DIRECTION_PURCHASES = 'purchases'; // 매입
|
||||||
|
|
||||||
|
public const DIRECTIONS = [
|
||||||
|
self::DIRECTION_SALES,
|
||||||
|
self::DIRECTION_PURCHASES,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상태
|
||||||
|
*/
|
||||||
|
public const STATUS_DRAFT = 'draft'; // 임시저장
|
||||||
|
|
||||||
|
public const STATUS_ISSUED = 'issued'; // 발행완료
|
||||||
|
|
||||||
|
public const STATUS_SENT = 'sent'; // 국세청 전송완료
|
||||||
|
|
||||||
|
public const STATUS_CANCELLED = 'cancelled'; // 취소
|
||||||
|
|
||||||
|
public const STATUS_FAILED = 'failed'; // 발행실패
|
||||||
|
|
||||||
|
public const STATUSES = [
|
||||||
|
self::STATUS_DRAFT,
|
||||||
|
self::STATUS_ISSUED,
|
||||||
|
self::STATUS_SENT,
|
||||||
|
self::STATUS_CANCELLED,
|
||||||
|
self::STATUS_FAILED,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상태 라벨
|
||||||
|
*/
|
||||||
|
public const STATUS_LABELS = [
|
||||||
|
self::STATUS_DRAFT => '임시저장',
|
||||||
|
self::STATUS_ISSUED => '발행완료',
|
||||||
|
self::STATUS_SENT => '국세청 전송',
|
||||||
|
self::STATUS_CANCELLED => '취소',
|
||||||
|
self::STATUS_FAILED => '발행실패',
|
||||||
|
];
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 모델 설정
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'tenant_id',
|
||||||
|
'nts_confirm_num',
|
||||||
|
'invoice_type',
|
||||||
|
'issue_type',
|
||||||
|
'direction',
|
||||||
|
'supplier_corp_num',
|
||||||
|
'supplier_corp_name',
|
||||||
|
'supplier_ceo_name',
|
||||||
|
'supplier_addr',
|
||||||
|
'supplier_biz_type',
|
||||||
|
'supplier_biz_class',
|
||||||
|
'supplier_contact_id',
|
||||||
|
'buyer_corp_num',
|
||||||
|
'buyer_corp_name',
|
||||||
|
'buyer_ceo_name',
|
||||||
|
'buyer_addr',
|
||||||
|
'buyer_biz_type',
|
||||||
|
'buyer_biz_class',
|
||||||
|
'buyer_contact_id',
|
||||||
|
'issue_date',
|
||||||
|
'supply_amount',
|
||||||
|
'tax_amount',
|
||||||
|
'total_amount',
|
||||||
|
'items',
|
||||||
|
'status',
|
||||||
|
'nts_send_status',
|
||||||
|
'issued_at',
|
||||||
|
'sent_at',
|
||||||
|
'cancelled_at',
|
||||||
|
'barobill_invoice_id',
|
||||||
|
'description',
|
||||||
|
'error_message',
|
||||||
|
'reference_type',
|
||||||
|
'reference_id',
|
||||||
|
'created_by',
|
||||||
|
'updated_by',
|
||||||
|
'deleted_by',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'issue_date' => 'date',
|
||||||
|
'supply_amount' => 'decimal:2',
|
||||||
|
'tax_amount' => 'decimal:2',
|
||||||
|
'total_amount' => 'decimal:2',
|
||||||
|
'items' => 'array',
|
||||||
|
'issued_at' => 'datetime',
|
||||||
|
'sent_at' => 'datetime',
|
||||||
|
'cancelled_at' => 'datetime',
|
||||||
|
];
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 관계 정의
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 참조 관계 (Sale 또는 Purchase)
|
||||||
|
*/
|
||||||
|
public function reference(): MorphTo
|
||||||
|
{
|
||||||
|
return $this->morphTo(__FUNCTION__, 'reference_type', 'reference_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 생성자 관계
|
||||||
|
*/
|
||||||
|
public function creator(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\User::class, 'created_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정자 관계
|
||||||
|
*/
|
||||||
|
public function updater(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(\App\Models\User::class, 'updated_by');
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 접근자 (Accessors)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 상태 라벨
|
||||||
|
*/
|
||||||
|
public function getStatusLabelAttribute(): string
|
||||||
|
{
|
||||||
|
return self::STATUS_LABELS[$this->status] ?? $this->status;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 유형 라벨
|
||||||
|
*/
|
||||||
|
public function getInvoiceTypeLabelAttribute(): string
|
||||||
|
{
|
||||||
|
return match ($this->invoice_type) {
|
||||||
|
self::TYPE_TAX_INVOICE => '세금계산서',
|
||||||
|
self::TYPE_INVOICE => '계산서',
|
||||||
|
self::TYPE_MODIFIED_TAX_INVOICE => '수정세금계산서',
|
||||||
|
default => $this->invoice_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 유형 라벨
|
||||||
|
*/
|
||||||
|
public function getIssueTypeLabelAttribute(): string
|
||||||
|
{
|
||||||
|
return match ($this->issue_type) {
|
||||||
|
self::ISSUE_TYPE_NORMAL => '정발행',
|
||||||
|
self::ISSUE_TYPE_REVERSE => '역발행',
|
||||||
|
self::ISSUE_TYPE_TRUSTEE => '위수탁',
|
||||||
|
default => $this->issue_type,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 방향 라벨
|
||||||
|
*/
|
||||||
|
public function getDirectionLabelAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->direction === self::DIRECTION_SALES ? '매출' : '매입';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공급자 사업자번호 포맷
|
||||||
|
*/
|
||||||
|
public function getFormattedSupplierCorpNumAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->formatCorpNum($this->supplier_corp_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 공급받는자 사업자번호 포맷
|
||||||
|
*/
|
||||||
|
public function getFormattedBuyerCorpNumAttribute(): string
|
||||||
|
{
|
||||||
|
return $this->formatCorpNum($this->buyer_corp_num);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 상태 체크 메서드
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 취소 가능 여부
|
||||||
|
*/
|
||||||
|
public function canCancel(): bool
|
||||||
|
{
|
||||||
|
return in_array($this->status, [self::STATUS_ISSUED, self::STATUS_SENT]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 수정 가능 여부
|
||||||
|
*/
|
||||||
|
public function canEdit(): bool
|
||||||
|
{
|
||||||
|
return $this->status === self::STATUS_DRAFT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 완료 여부
|
||||||
|
*/
|
||||||
|
public function isIssued(): bool
|
||||||
|
{
|
||||||
|
return in_array($this->status, [self::STATUS_ISSUED, self::STATUS_SENT]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 헬퍼 메서드
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 사업자번호 포맷팅
|
||||||
|
*/
|
||||||
|
private function formatCorpNum(?string $num): string
|
||||||
|
{
|
||||||
|
if (! $num || strlen($num) !== 10) {
|
||||||
|
return $num ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($num, 0, 3).'-'.substr($num, 3, 2).'-'.substr($num, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
406
app/Services/BarobillService.php
Normal file
406
app/Services/BarobillService.php
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Tenants\BarobillSetting;
|
||||||
|
use App\Models\Tenants\TaxInvoice;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바로빌 API 연동 서비스
|
||||||
|
*
|
||||||
|
* 바로빌 개발자센터: https://dev.barobill.co.kr/
|
||||||
|
*/
|
||||||
|
class BarobillService extends Service
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* 바로빌 API 기본 URL
|
||||||
|
*/
|
||||||
|
private const API_BASE_URL = 'https://ws.barobill.co.kr';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바로빌 API 테스트 URL
|
||||||
|
*/
|
||||||
|
private const API_TEST_URL = 'https://testws.barobill.co.kr';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 테스트 모드 여부
|
||||||
|
*/
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 연동 테스트
|
||||||
|
*/
|
||||||
|
public function testConnection(): array
|
||||||
|
{
|
||||||
|
$setting = $this->getSetting();
|
||||||
|
|
||||||
|
if (! $setting || ! $setting->canConnect()) {
|
||||||
|
throw new BadRequestHttpException(__('error.barobill.setting_not_configured'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 바로빌 API 토큰 조회로 연동 테스트
|
||||||
|
$response = $this->callApi('GetAccessToken', [
|
||||||
|
'CERTKEY' => $setting->cert_key,
|
||||||
|
'CorpNum' => $setting->corp_num,
|
||||||
|
'ID' => $setting->barobill_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! empty($response['AccessToken'])) {
|
||||||
|
// 검증 성공 시 verified_at 업데이트
|
||||||
|
$setting->verified_at = now();
|
||||||
|
$setting->save();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'message' => __('message.barobill.connection_success'),
|
||||||
|
'verified_at' => $setting->verified_at->toDateTimeString(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestHttpException($response['Message'] ?? __('error.barobill.connection_failed'));
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('바로빌 연동 테스트 실패', [
|
||||||
|
'tenant_id' => $this->tenantId(),
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new BadRequestHttpException(__('error.barobill.connection_failed').': '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 세금계산서 발행
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 발행
|
||||||
|
*/
|
||||||
|
public function issueTaxInvoice(TaxInvoice $taxInvoice): TaxInvoice
|
||||||
|
{
|
||||||
|
$setting = $this->getSetting();
|
||||||
|
|
||||||
|
if (! $setting || ! $setting->canConnect()) {
|
||||||
|
throw new BadRequestHttpException(__('error.barobill.setting_not_configured'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 바로빌 API 호출을 위한 데이터 구성
|
||||||
|
$apiData = $this->buildTaxInvoiceData($taxInvoice, $setting);
|
||||||
|
|
||||||
|
// 세금계산서 발행 API 호출
|
||||||
|
$response = $this->callApi('RegistAndIssueTaxInvoice', $apiData);
|
||||||
|
|
||||||
|
if (! empty($response['InvoiceID'])) {
|
||||||
|
// 발행 성공
|
||||||
|
$taxInvoice->barobill_invoice_id = $response['InvoiceID'];
|
||||||
|
$taxInvoice->nts_confirm_num = $response['NTSConfirmNum'] ?? null;
|
||||||
|
$taxInvoice->status = TaxInvoice::STATUS_ISSUED;
|
||||||
|
$taxInvoice->issued_at = now();
|
||||||
|
$taxInvoice->error_message = null;
|
||||||
|
$taxInvoice->save();
|
||||||
|
|
||||||
|
Log::info('세금계산서 발행 성공', [
|
||||||
|
'tenant_id' => $this->tenantId(),
|
||||||
|
'tax_invoice_id' => $taxInvoice->id,
|
||||||
|
'barobill_invoice_id' => $response['InvoiceID'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $taxInvoice->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception($response['Message'] ?? '발행 실패');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// 발행 실패
|
||||||
|
$taxInvoice->status = TaxInvoice::STATUS_FAILED;
|
||||||
|
$taxInvoice->error_message = $e->getMessage();
|
||||||
|
$taxInvoice->save();
|
||||||
|
|
||||||
|
Log::error('세금계산서 발행 실패', [
|
||||||
|
'tenant_id' => $this->tenantId(),
|
||||||
|
'tax_invoice_id' => $taxInvoice->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new BadRequestHttpException(__('error.barobill.issue_failed').': '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 취소
|
||||||
|
*/
|
||||||
|
public function cancelTaxInvoice(TaxInvoice $taxInvoice, string $reason): TaxInvoice
|
||||||
|
{
|
||||||
|
$setting = $this->getSetting();
|
||||||
|
|
||||||
|
if (! $setting || ! $setting->canConnect()) {
|
||||||
|
throw new BadRequestHttpException(__('error.barobill.setting_not_configured'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $taxInvoice->canCancel()) {
|
||||||
|
throw new BadRequestHttpException(__('error.barobill.cannot_cancel'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 세금계산서 취소 API 호출
|
||||||
|
$response = $this->callApi('CancelTaxInvoice', [
|
||||||
|
'CERTKEY' => $setting->cert_key,
|
||||||
|
'CorpNum' => $setting->corp_num,
|
||||||
|
'ID' => $setting->barobill_id,
|
||||||
|
'InvoiceID' => $taxInvoice->barobill_invoice_id,
|
||||||
|
'Memo' => $reason,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($response['Result'] === 0 || ! empty($response['Success'])) {
|
||||||
|
$taxInvoice->status = TaxInvoice::STATUS_CANCELLED;
|
||||||
|
$taxInvoice->cancelled_at = now();
|
||||||
|
$taxInvoice->description = ($taxInvoice->description ? $taxInvoice->description."\n" : '').'취소 사유: '.$reason;
|
||||||
|
$taxInvoice->save();
|
||||||
|
|
||||||
|
Log::info('세금계산서 취소 성공', [
|
||||||
|
'tenant_id' => $this->tenantId(),
|
||||||
|
'tax_invoice_id' => $taxInvoice->id,
|
||||||
|
'reason' => $reason,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $taxInvoice->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new \Exception($response['Message'] ?? '취소 실패');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('세금계산서 취소 실패', [
|
||||||
|
'tenant_id' => $this->tenantId(),
|
||||||
|
'tax_invoice_id' => $taxInvoice->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new BadRequestHttpException(__('error.barobill.cancel_failed').': '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 국세청 전송 상태 조회
|
||||||
|
*/
|
||||||
|
public function checkNtsSendStatus(TaxInvoice $taxInvoice): TaxInvoice
|
||||||
|
{
|
||||||
|
$setting = $this->getSetting();
|
||||||
|
|
||||||
|
if (! $setting || ! $setting->canConnect()) {
|
||||||
|
throw new BadRequestHttpException(__('error.barobill.setting_not_configured'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($taxInvoice->barobill_invoice_id)) {
|
||||||
|
throw new BadRequestHttpException(__('error.barobill.not_issued'));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $this->callApi('GetTaxInvoiceState', [
|
||||||
|
'CERTKEY' => $setting->cert_key,
|
||||||
|
'CorpNum' => $setting->corp_num,
|
||||||
|
'ID' => $setting->barobill_id,
|
||||||
|
'InvoiceID' => $taxInvoice->barobill_invoice_id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (! empty($response['State'])) {
|
||||||
|
$taxInvoice->nts_send_status = $response['State'];
|
||||||
|
|
||||||
|
// 국세청 전송 완료 시 상태 업데이트
|
||||||
|
if ($response['State'] === '전송완료' && ! $taxInvoice->sent_at) {
|
||||||
|
$taxInvoice->status = TaxInvoice::STATUS_SENT;
|
||||||
|
$taxInvoice->sent_at = now();
|
||||||
|
$taxInvoice->nts_confirm_num = $response['NTSConfirmNum'] ?? $taxInvoice->nts_confirm_num;
|
||||||
|
}
|
||||||
|
|
||||||
|
$taxInvoice->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $taxInvoice->fresh();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
Log::error('국세청 전송 상태 조회 실패', [
|
||||||
|
'tenant_id' => $this->tenantId(),
|
||||||
|
'tax_invoice_id' => $taxInvoice->id,
|
||||||
|
'error' => $e->getMessage(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
throw new BadRequestHttpException(__('error.barobill.status_check_failed').': '.$e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Private 메서드
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 바로빌 API 호출
|
||||||
|
*/
|
||||||
|
private function callApi(string $method, array $data): array
|
||||||
|
{
|
||||||
|
$baseUrl = $this->testMode ? self::API_TEST_URL : self::API_BASE_URL;
|
||||||
|
$url = $baseUrl.'/TI/'.$method;
|
||||||
|
|
||||||
|
$response = Http::timeout(30)
|
||||||
|
->withHeaders([
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
])
|
||||||
|
->post($url, $data);
|
||||||
|
|
||||||
|
if ($response->failed()) {
|
||||||
|
throw new \Exception('API 호출 실패: '.$response->status());
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response->json() ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 발행용 데이터 구성
|
||||||
|
*/
|
||||||
|
private function buildTaxInvoiceData(TaxInvoice $taxInvoice, BarobillSetting $setting): array
|
||||||
|
{
|
||||||
|
// 품목 데이터 구성
|
||||||
|
$items = [];
|
||||||
|
foreach ($taxInvoice->items ?? [] as $index => $item) {
|
||||||
|
$items[] = [
|
||||||
|
'PurchaseDT' => $taxInvoice->issue_date->format('Ymd'),
|
||||||
|
'ItemName' => $item['name'] ?? '',
|
||||||
|
'Spec' => $item['spec'] ?? '',
|
||||||
|
'Qty' => $item['qty'] ?? 1,
|
||||||
|
'UnitCost' => $item['unit_price'] ?? 0,
|
||||||
|
'SupplyCost' => $item['supply_amt'] ?? 0,
|
||||||
|
'Tax' => $item['tax_amt'] ?? 0,
|
||||||
|
'Remark' => $item['remark'] ?? '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 품목이 없는 경우 기본 품목 추가
|
||||||
|
if (empty($items)) {
|
||||||
|
$items[] = [
|
||||||
|
'PurchaseDT' => $taxInvoice->issue_date->format('Ymd'),
|
||||||
|
'ItemName' => $taxInvoice->description ?? '품목',
|
||||||
|
'Spec' => '',
|
||||||
|
'Qty' => 1,
|
||||||
|
'UnitCost' => (float) $taxInvoice->supply_amount,
|
||||||
|
'SupplyCost' => (float) $taxInvoice->supply_amount,
|
||||||
|
'Tax' => (float) $taxInvoice->tax_amount,
|
||||||
|
'Remark' => '',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'CERTKEY' => $setting->cert_key,
|
||||||
|
'CorpNum' => $setting->corp_num,
|
||||||
|
'ID' => $setting->barobill_id,
|
||||||
|
'TaxInvoice' => [
|
||||||
|
'InvoiceType' => $this->mapInvoiceType($taxInvoice->invoice_type),
|
||||||
|
'IssueType' => $this->mapIssueType($taxInvoice->issue_type),
|
||||||
|
'TaxType' => '과세',
|
||||||
|
'PurposeType' => '영수',
|
||||||
|
'WriteDate' => $taxInvoice->issue_date->format('Ymd'),
|
||||||
|
|
||||||
|
// 공급자 정보
|
||||||
|
'InvoicerCorpNum' => $taxInvoice->supplier_corp_num,
|
||||||
|
'InvoicerCorpName' => $taxInvoice->supplier_corp_name,
|
||||||
|
'InvoicerCEOName' => $taxInvoice->supplier_ceo_name,
|
||||||
|
'InvoicerAddr' => $taxInvoice->supplier_addr,
|
||||||
|
'InvoicerBizType' => $taxInvoice->supplier_biz_type,
|
||||||
|
'InvoicerBizClass' => $taxInvoice->supplier_biz_class,
|
||||||
|
'InvoicerContactID' => $taxInvoice->supplier_contact_id,
|
||||||
|
|
||||||
|
// 공급받는자 정보
|
||||||
|
'InvoiceeCorpNum' => $taxInvoice->buyer_corp_num,
|
||||||
|
'InvoiceeCorpName' => $taxInvoice->buyer_corp_name,
|
||||||
|
'InvoiceeCEOName' => $taxInvoice->buyer_ceo_name,
|
||||||
|
'InvoiceeAddr' => $taxInvoice->buyer_addr,
|
||||||
|
'InvoiceeBizType' => $taxInvoice->buyer_biz_type,
|
||||||
|
'InvoiceeBizClass' => $taxInvoice->buyer_biz_class,
|
||||||
|
'InvoiceeContactID' => $taxInvoice->buyer_contact_id,
|
||||||
|
|
||||||
|
// 금액 정보
|
||||||
|
'SupplyCostTotal' => (int) $taxInvoice->supply_amount,
|
||||||
|
'TaxTotal' => (int) $taxInvoice->tax_amount,
|
||||||
|
'TotalAmount' => (int) $taxInvoice->total_amount,
|
||||||
|
|
||||||
|
// 품목 정보
|
||||||
|
'TaxInvoiceTradeLineItems' => $items,
|
||||||
|
|
||||||
|
// 비고
|
||||||
|
'Remark1' => $taxInvoice->description ?? '',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 유형 매핑
|
||||||
|
*/
|
||||||
|
private function mapInvoiceType(string $type): string
|
||||||
|
{
|
||||||
|
return match ($type) {
|
||||||
|
TaxInvoice::TYPE_TAX_INVOICE => '세금계산서',
|
||||||
|
TaxInvoice::TYPE_INVOICE => '계산서',
|
||||||
|
TaxInvoice::TYPE_MODIFIED_TAX_INVOICE => '수정세금계산서',
|
||||||
|
default => '세금계산서',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 발행 유형 매핑
|
||||||
|
*/
|
||||||
|
private function mapIssueType(string $type): string
|
||||||
|
{
|
||||||
|
return match ($type) {
|
||||||
|
TaxInvoice::ISSUE_TYPE_NORMAL => '정발행',
|
||||||
|
TaxInvoice::ISSUE_TYPE_REVERSE => '역발행',
|
||||||
|
TaxInvoice::ISSUE_TYPE_TRUSTEE => '위수탁',
|
||||||
|
default => '정발행',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
297
app/Services/TaxInvoiceService.php
Normal file
297
app/Services/TaxInvoiceService.php
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Tenants\TaxInvoice;
|
||||||
|
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 관리 서비스
|
||||||
|
*/
|
||||||
|
class TaxInvoiceService extends Service
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private BarobillService $barobillService
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 목록 조회
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 목록 조회
|
||||||
|
*/
|
||||||
|
public function list(array $params): LengthAwarePaginator
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$perPage = $params['per_page'] ?? 20;
|
||||||
|
|
||||||
|
$query = TaxInvoice::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->orderBy('issue_date', 'desc')
|
||||||
|
->orderBy('id', 'desc');
|
||||||
|
|
||||||
|
// 방향 (매출/매입)
|
||||||
|
if (! empty($params['direction'])) {
|
||||||
|
$query->where('direction', $params['direction']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 상태
|
||||||
|
if (! empty($params['status'])) {
|
||||||
|
$query->where('status', $params['status']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 세금계산서 유형
|
||||||
|
if (! empty($params['invoice_type'])) {
|
||||||
|
$query->where('invoice_type', $params['invoice_type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 발행 유형
|
||||||
|
if (! empty($params['issue_type'])) {
|
||||||
|
$query->where('issue_type', $params['issue_type']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 기간 검색
|
||||||
|
if (! empty($params['issue_date_from'])) {
|
||||||
|
$query->whereDate('issue_date', '>=', $params['issue_date_from']);
|
||||||
|
}
|
||||||
|
if (! empty($params['issue_date_to'])) {
|
||||||
|
$query->whereDate('issue_date', '<=', $params['issue_date_to']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 거래처 검색 (공급자 또는 공급받는자)
|
||||||
|
if (! empty($params['corp_num'])) {
|
||||||
|
$query->where(function ($q) use ($params) {
|
||||||
|
$q->where('supplier_corp_num', $params['corp_num'])
|
||||||
|
->orWhere('buyer_corp_num', $params['corp_num']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 거래처명 검색
|
||||||
|
if (! empty($params['corp_name'])) {
|
||||||
|
$query->where(function ($q) use ($params) {
|
||||||
|
$q->where('supplier_corp_name', 'like', '%'.$params['corp_name'].'%')
|
||||||
|
->orWhere('buyer_corp_name', 'like', '%'.$params['corp_name'].'%');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 국세청 승인번호 검색
|
||||||
|
if (! empty($params['nts_confirm_num'])) {
|
||||||
|
$query->where('nts_confirm_num', 'like', '%'.$params['nts_confirm_num'].'%');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->paginate($perPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 상세 조회
|
||||||
|
*/
|
||||||
|
public function show(int $id): TaxInvoice
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
return TaxInvoice::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->with(['creator', 'updater'])
|
||||||
|
->findOrFail($id);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 세금계산서 생성/수정
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 생성
|
||||||
|
*/
|
||||||
|
public function create(array $data): TaxInvoice
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
// 합계금액 계산
|
||||||
|
$data['total_amount'] = ($data['supply_amount'] ?? 0) + ($data['tax_amount'] ?? 0);
|
||||||
|
|
||||||
|
$taxInvoice = TaxInvoice::create(array_merge($data, [
|
||||||
|
'tenant_id' => $tenantId,
|
||||||
|
'status' => TaxInvoice::STATUS_DRAFT,
|
||||||
|
'created_by' => $userId,
|
||||||
|
'updated_by' => $userId,
|
||||||
|
]));
|
||||||
|
|
||||||
|
return $taxInvoice->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 수정
|
||||||
|
*/
|
||||||
|
public function update(int $id, array $data): TaxInvoice
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
$taxInvoice = TaxInvoice::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
if (! $taxInvoice->canEdit()) {
|
||||||
|
throw new \Symfony\Component\HttpKernel\Exception\BadRequestHttpException(__('error.tax_invoice.cannot_edit'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 합계금액 계산
|
||||||
|
if (isset($data['supply_amount']) || isset($data['tax_amount'])) {
|
||||||
|
$supplyAmount = $data['supply_amount'] ?? $taxInvoice->supply_amount;
|
||||||
|
$taxAmount = $data['tax_amount'] ?? $taxInvoice->tax_amount;
|
||||||
|
$data['total_amount'] = $supplyAmount + $taxAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
$taxInvoice->fill(array_merge($data, ['updated_by' => $userId]));
|
||||||
|
$taxInvoice->save();
|
||||||
|
|
||||||
|
return $taxInvoice->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 삭제
|
||||||
|
*/
|
||||||
|
public function delete(int $id): bool
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
$userId = $this->apiUserId();
|
||||||
|
|
||||||
|
$taxInvoice = TaxInvoice::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
if (! $taxInvoice->canEdit()) {
|
||||||
|
throw new \Symfony\Component\HttpKernel\Exception\BadRequestHttpException(__('error.tax_invoice.cannot_delete'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$taxInvoice->deleted_by = $userId;
|
||||||
|
$taxInvoice->save();
|
||||||
|
|
||||||
|
return $taxInvoice->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 발행/취소
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 발행
|
||||||
|
*/
|
||||||
|
public function issue(int $id): TaxInvoice
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$taxInvoice = TaxInvoice::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
if (! $taxInvoice->canEdit()) {
|
||||||
|
throw new \Symfony\Component\HttpKernel\Exception\BadRequestHttpException(__('error.tax_invoice.already_issued'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->barobillService->issueTaxInvoice($taxInvoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 취소
|
||||||
|
*/
|
||||||
|
public function cancel(int $id, string $reason): TaxInvoice
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$taxInvoice = TaxInvoice::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
return $this->barobillService->cancelTaxInvoice($taxInvoice, $reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 국세청 전송 상태 조회
|
||||||
|
*/
|
||||||
|
public function checkStatus(int $id): TaxInvoice
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$taxInvoice = TaxInvoice::query()
|
||||||
|
->where('tenant_id', $tenantId)
|
||||||
|
->findOrFail($id);
|
||||||
|
|
||||||
|
return $this->barobillService->checkNtsSendStatus($taxInvoice);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 통계
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 세금계산서 요약 통계
|
||||||
|
*/
|
||||||
|
public function summary(array $params): array
|
||||||
|
{
|
||||||
|
$tenantId = $this->tenantId();
|
||||||
|
|
||||||
|
$query = TaxInvoice::query()
|
||||||
|
->where('tenant_id', $tenantId);
|
||||||
|
|
||||||
|
// 기간 필터
|
||||||
|
if (! empty($params['issue_date_from'])) {
|
||||||
|
$query->whereDate('issue_date', '>=', $params['issue_date_from']);
|
||||||
|
}
|
||||||
|
if (! empty($params['issue_date_to'])) {
|
||||||
|
$query->whereDate('issue_date', '<=', $params['issue_date_to']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 방향별 통계
|
||||||
|
$summary = $query->clone()
|
||||||
|
->select([
|
||||||
|
'direction',
|
||||||
|
DB::raw('COUNT(*) as count'),
|
||||||
|
DB::raw('SUM(supply_amount) as supply_amount'),
|
||||||
|
DB::raw('SUM(tax_amount) as tax_amount'),
|
||||||
|
DB::raw('SUM(total_amount) as total_amount'),
|
||||||
|
])
|
||||||
|
->groupBy('direction')
|
||||||
|
->get()
|
||||||
|
->keyBy('direction')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
// 상태별 통계
|
||||||
|
$byStatus = $query->clone()
|
||||||
|
->select([
|
||||||
|
'status',
|
||||||
|
DB::raw('COUNT(*) as count'),
|
||||||
|
])
|
||||||
|
->groupBy('status')
|
||||||
|
->get()
|
||||||
|
->keyBy('status')
|
||||||
|
->toArray();
|
||||||
|
|
||||||
|
return [
|
||||||
|
'by_direction' => [
|
||||||
|
'sales' => $summary[TaxInvoice::DIRECTION_SALES] ?? [
|
||||||
|
'count' => 0,
|
||||||
|
'supply_amount' => 0,
|
||||||
|
'tax_amount' => 0,
|
||||||
|
'total_amount' => 0,
|
||||||
|
],
|
||||||
|
'purchases' => $summary[TaxInvoice::DIRECTION_PURCHASES] ?? [
|
||||||
|
'count' => 0,
|
||||||
|
'supply_amount' => 0,
|
||||||
|
'tax_amount' => 0,
|
||||||
|
'total_amount' => 0,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'by_status' => [
|
||||||
|
TaxInvoice::STATUS_DRAFT => $byStatus[TaxInvoice::STATUS_DRAFT]['count'] ?? 0,
|
||||||
|
TaxInvoice::STATUS_ISSUED => $byStatus[TaxInvoice::STATUS_ISSUED]['count'] ?? 0,
|
||||||
|
TaxInvoice::STATUS_SENT => $byStatus[TaxInvoice::STATUS_SENT]['count'] ?? 0,
|
||||||
|
TaxInvoice::STATUS_CANCELLED => $byStatus[TaxInvoice::STATUS_CANCELLED]['count'] ?? 0,
|
||||||
|
TaxInvoice::STATUS_FAILED => $byStatus[TaxInvoice::STATUS_FAILED]['count'] ?? 0,
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
165
app/Swagger/v1/BarobillSettingApi.php
Normal file
165
app/Swagger/v1/BarobillSettingApi.php
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Swagger\v1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Tag(name="BarobillSettings", description="바로빌 설정 관리")
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="BarobillSetting",
|
||||||
|
* type="object",
|
||||||
|
* description="바로빌 설정 정보",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="id", type="integer", example=1, description="설정 ID"),
|
||||||
|
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
|
||||||
|
* @OA\Property(property="corp_num", type="string", example="1234567890", description="사업자번호 (10자리)"),
|
||||||
|
* @OA\Property(property="barobill_id", type="string", example="testuser", description="바로빌 아이디"),
|
||||||
|
* @OA\Property(property="corp_name", type="string", example="(주)테스트회사", description="회사명"),
|
||||||
|
* @OA\Property(property="ceo_name", type="string", example="홍길동", description="대표자명"),
|
||||||
|
* @OA\Property(property="addr", type="string", example="서울시 강남구 테헤란로 123", nullable=true, description="주소"),
|
||||||
|
* @OA\Property(property="biz_type", type="string", example="서비스", nullable=true, description="업태"),
|
||||||
|
* @OA\Property(property="biz_class", type="string", example="소프트웨어개발", nullable=true, description="종목"),
|
||||||
|
* @OA\Property(property="contact_id", type="string", example="manager@test.com", nullable=true, description="담당자 이메일"),
|
||||||
|
* @OA\Property(property="contact_name", type="string", example="김담당", nullable=true, description="담당자명"),
|
||||||
|
* @OA\Property(property="contact_tel", type="string", example="02-1234-5678", nullable=true, description="담당자 연락처"),
|
||||||
|
* @OA\Property(property="is_active", type="boolean", example=true, description="활성화 여부"),
|
||||||
|
* @OA\Property(property="auto_issue", type="boolean", example=false, description="자동발행 여부"),
|
||||||
|
* @OA\Property(property="verified_at", type="string", format="date-time", nullable=true, description="검증 완료 일시"),
|
||||||
|
* @OA\Property(property="formatted_corp_num", type="string", example="123-45-67890", description="포맷팅된 사업자번호"),
|
||||||
|
* @OA\Property(property="created_by", type="integer", example=1, nullable=true, description="생성자 ID"),
|
||||||
|
* @OA\Property(property="updated_by", type="integer", example=1, nullable=true, description="수정자 ID"),
|
||||||
|
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||||
|
* @OA\Property(property="updated_at", type="string", format="date-time")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="BarobillSettingSaveRequest",
|
||||||
|
* type="object",
|
||||||
|
* required={"corp_num","cert_key","barobill_id","corp_name","ceo_name"},
|
||||||
|
* description="바로빌 설정 저장 요청",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="corp_num", type="string", example="1234567890", minLength=10, maxLength=10, description="사업자번호 (10자리, 하이픈 제외)"),
|
||||||
|
* @OA\Property(property="cert_key", type="string", example="xxxxxx", description="공인인증서 키 (암호화되어 저장)"),
|
||||||
|
* @OA\Property(property="barobill_id", type="string", example="testuser", maxLength=50, description="바로빌 아이디"),
|
||||||
|
* @OA\Property(property="corp_name", type="string", example="(주)테스트회사", maxLength=200, description="회사명"),
|
||||||
|
* @OA\Property(property="ceo_name", type="string", example="홍길동", maxLength=100, description="대표자명"),
|
||||||
|
* @OA\Property(property="addr", type="string", example="서울시 강남구 테헤란로 123", maxLength=500, nullable=true, description="주소"),
|
||||||
|
* @OA\Property(property="biz_type", type="string", example="서비스", maxLength=100, nullable=true, description="업태"),
|
||||||
|
* @OA\Property(property="biz_class", type="string", example="소프트웨어개발", maxLength=100, nullable=true, description="종목"),
|
||||||
|
* @OA\Property(property="contact_id", type="string", example="manager@test.com", maxLength=100, nullable=true, description="담당자 이메일"),
|
||||||
|
* @OA\Property(property="contact_name", type="string", example="김담당", maxLength=100, nullable=true, description="담당자명"),
|
||||||
|
* @OA\Property(property="contact_tel", type="string", example="02-1234-5678", maxLength=50, nullable=true, description="담당자 연락처"),
|
||||||
|
* @OA\Property(property="is_active", type="boolean", example=true, description="활성화 여부 (기본값: true)"),
|
||||||
|
* @OA\Property(property="auto_issue", type="boolean", example=false, description="자동발행 여부 (기본값: false)")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="BarobillConnectionTestResult",
|
||||||
|
* type="object",
|
||||||
|
* description="바로빌 연동 테스트 결과",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="success", type="boolean", example=true, description="연동 성공 여부"),
|
||||||
|
* @OA\Property(property="remaining_point", type="integer", example=10000, description="잔여 포인트"),
|
||||||
|
* @OA\Property(property="tested_at", type="string", format="date-time", description="테스트 일시")
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class BarobillSettingApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/barobill-settings",
|
||||||
|
* tags={"BarobillSettings"},
|
||||||
|
* summary="바로빌 설정 조회",
|
||||||
|
* description="현재 테넌트의 바로빌 설정을 조회합니다. 설정이 없는 경우 null을 반환합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/BarobillSetting", nullable=true)
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function show() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Put(
|
||||||
|
* path="/api/v1/barobill-settings",
|
||||||
|
* tags={"BarobillSettings"},
|
||||||
|
* summary="바로빌 설정 저장",
|
||||||
|
* description="바로빌 설정을 저장합니다. 기존 설정이 있으면 수정하고, 없으면 새로 생성합니다. cert_key는 암호화되어 저장됩니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/BarobillSettingSaveRequest")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="저장 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/BarobillSetting")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function save() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/v1/barobill-settings/test-connection",
|
||||||
|
* tags={"BarobillSettings"},
|
||||||
|
* summary="바로빌 연동 테스트",
|
||||||
|
* description="저장된 바로빌 설정으로 연동 테스트를 수행합니다. 설정이 없거나 필수 값이 누락되면 에러를 반환합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="연동 테스트 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/BarobillConnectionTestResult")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=400, description="설정 미완료 또는 연동 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function testConnection() {}
|
||||||
|
}
|
||||||
528
app/Swagger/v1/TaxInvoiceApi.php
Normal file
528
app/Swagger/v1/TaxInvoiceApi.php
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Swagger\v1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Tag(name="TaxInvoices", description="세금계산서 관리")
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="TaxInvoice",
|
||||||
|
* type="object",
|
||||||
|
* description="세금계산서 정보",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="id", type="integer", example=1, description="세금계산서 ID"),
|
||||||
|
* @OA\Property(property="tenant_id", type="integer", example=1, description="테넌트 ID"),
|
||||||
|
* @OA\Property(property="nts_confirm_num", type="string", example="20250115-12345678-12345678", nullable=true, description="국세청 승인번호"),
|
||||||
|
* @OA\Property(property="invoice_type", type="string", enum={"tax_invoice","invoice","modified"}, example="tax_invoice", description="세금계산서 유형"),
|
||||||
|
* @OA\Property(property="issue_type", type="string", enum={"normal","reverse","trustee"}, example="normal", description="발행 유형"),
|
||||||
|
* @OA\Property(property="direction", type="string", enum={"sales","purchases"}, example="sales", description="방향 (매출/매입)"),
|
||||||
|
* @OA\Property(property="supplier_corp_num", type="string", example="1234567890", description="공급자 사업자번호"),
|
||||||
|
* @OA\Property(property="supplier_corp_name", type="string", example="(주)공급사", description="공급자 회사명"),
|
||||||
|
* @OA\Property(property="supplier_ceo_name", type="string", example="홍길동", description="공급자 대표자명"),
|
||||||
|
* @OA\Property(property="supplier_addr", type="string", example="서울시 강남구", nullable=true, description="공급자 주소"),
|
||||||
|
* @OA\Property(property="supplier_biz_type", type="string", example="서비스", nullable=true, description="공급자 업태"),
|
||||||
|
* @OA\Property(property="supplier_biz_class", type="string", example="소프트웨어", nullable=true, description="공급자 종목"),
|
||||||
|
* @OA\Property(property="supplier_contact_id", type="string", example="supplier@test.com", nullable=true, description="공급자 담당자 이메일"),
|
||||||
|
* @OA\Property(property="buyer_corp_num", type="string", example="0987654321", description="공급받는자 사업자번호"),
|
||||||
|
* @OA\Property(property="buyer_corp_name", type="string", example="(주)구매사", description="공급받는자 회사명"),
|
||||||
|
* @OA\Property(property="buyer_ceo_name", type="string", example="김대표", description="공급받는자 대표자명"),
|
||||||
|
* @OA\Property(property="buyer_addr", type="string", example="서울시 서초구", nullable=true, description="공급받는자 주소"),
|
||||||
|
* @OA\Property(property="buyer_biz_type", type="string", example="제조", nullable=true, description="공급받는자 업태"),
|
||||||
|
* @OA\Property(property="buyer_biz_class", type="string", example="전자제품", nullable=true, description="공급받는자 종목"),
|
||||||
|
* @OA\Property(property="buyer_contact_id", type="string", example="buyer@test.com", nullable=true, description="공급받는자 담당자 이메일"),
|
||||||
|
* @OA\Property(property="issue_date", type="string", format="date", example="2025-01-15", description="작성일자"),
|
||||||
|
* @OA\Property(property="supply_amount", type="number", format="float", example=1000000, description="공급가액"),
|
||||||
|
* @OA\Property(property="tax_amount", type="number", format="float", example=100000, description="세액"),
|
||||||
|
* @OA\Property(property="total_amount", type="number", format="float", example=1100000, description="합계금액"),
|
||||||
|
* @OA\Property(property="items", type="array", description="품목 목록",
|
||||||
|
*
|
||||||
|
* @OA\Items(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="date", type="string", format="date", example="2025-01-15"),
|
||||||
|
* @OA\Property(property="name", type="string", example="컨설팅 서비스"),
|
||||||
|
* @OA\Property(property="spec", type="string", example="월간"),
|
||||||
|
* @OA\Property(property="quantity", type="number", example=1),
|
||||||
|
* @OA\Property(property="unit_price", type="number", example=1000000),
|
||||||
|
* @OA\Property(property="supply_amount", type="number", example=1000000),
|
||||||
|
* @OA\Property(property="tax_amount", type="number", example=100000),
|
||||||
|
* @OA\Property(property="remark", type="string", example="1월분", nullable=true)
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Property(property="status", type="string", enum={"draft","issued","sent","cancelled","failed"}, example="draft", description="상태"),
|
||||||
|
* @OA\Property(property="status_label", type="string", example="임시저장", description="상태 라벨"),
|
||||||
|
* @OA\Property(property="invoice_type_label", type="string", example="세금계산서", description="세금계산서 유형 라벨"),
|
||||||
|
* @OA\Property(property="issue_type_label", type="string", example="정발행", description="발행 유형 라벨"),
|
||||||
|
* @OA\Property(property="direction_label", type="string", example="매출", description="방향 라벨"),
|
||||||
|
* @OA\Property(property="formatted_supplier_corp_num", type="string", example="123-45-67890", description="포맷팅된 공급자 사업자번호"),
|
||||||
|
* @OA\Property(property="formatted_buyer_corp_num", type="string", example="098-76-54321", description="포맷팅된 공급받는자 사업자번호"),
|
||||||
|
* @OA\Property(property="nts_send_status", type="string", example="success", nullable=true, description="국세청 전송 상태"),
|
||||||
|
* @OA\Property(property="issued_at", type="string", format="date-time", nullable=true, description="발행 일시"),
|
||||||
|
* @OA\Property(property="sent_at", type="string", format="date-time", nullable=true, description="국세청 전송 일시"),
|
||||||
|
* @OA\Property(property="cancelled_at", type="string", format="date-time", nullable=true, description="취소 일시"),
|
||||||
|
* @OA\Property(property="barobill_invoice_id", type="string", nullable=true, description="바로빌 발행 ID"),
|
||||||
|
* @OA\Property(property="description", type="string", nullable=true, description="비고"),
|
||||||
|
* @OA\Property(property="error_message", type="string", nullable=true, description="에러 메시지"),
|
||||||
|
* @OA\Property(property="reference_type", type="string", example="App\\Models\\Tenants\\Sale", nullable=true, description="참조 모델 타입"),
|
||||||
|
* @OA\Property(property="reference_id", type="integer", example=1, nullable=true, description="참조 ID"),
|
||||||
|
* @OA\Property(property="creator", type="object", nullable=true,
|
||||||
|
* @OA\Property(property="id", type="integer", example=1),
|
||||||
|
* @OA\Property(property="name", type="string", example="관리자"),
|
||||||
|
* description="생성자 정보"
|
||||||
|
* ),
|
||||||
|
* @OA\Property(property="created_by", type="integer", example=1, nullable=true, description="생성자 ID"),
|
||||||
|
* @OA\Property(property="updated_by", type="integer", example=1, nullable=true, description="수정자 ID"),
|
||||||
|
* @OA\Property(property="deleted_by", type="integer", example=1, nullable=true, description="삭제자 ID"),
|
||||||
|
* @OA\Property(property="created_at", type="string", format="date-time"),
|
||||||
|
* @OA\Property(property="updated_at", type="string", format="date-time")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="TaxInvoiceCreateRequest",
|
||||||
|
* type="object",
|
||||||
|
* required={"invoice_type","issue_type","direction","supplier_corp_num","supplier_corp_name","supplier_ceo_name","buyer_corp_num","buyer_corp_name","buyer_ceo_name","issue_date","supply_amount","tax_amount"},
|
||||||
|
* description="세금계산서 생성 요청",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="invoice_type", type="string", enum={"tax_invoice","invoice","modified"}, example="tax_invoice", description="세금계산서 유형"),
|
||||||
|
* @OA\Property(property="issue_type", type="string", enum={"normal","reverse","trustee"}, example="normal", description="발행 유형"),
|
||||||
|
* @OA\Property(property="direction", type="string", enum={"sales","purchases"}, example="sales", description="방향 (매출/매입)"),
|
||||||
|
* @OA\Property(property="supplier_corp_num", type="string", example="1234567890", minLength=10, maxLength=10, description="공급자 사업자번호"),
|
||||||
|
* @OA\Property(property="supplier_corp_name", type="string", example="(주)공급사", maxLength=200, description="공급자 회사명"),
|
||||||
|
* @OA\Property(property="supplier_ceo_name", type="string", example="홍길동", maxLength=100, description="공급자 대표자명"),
|
||||||
|
* @OA\Property(property="supplier_addr", type="string", example="서울시 강남구", maxLength=500, nullable=true, description="공급자 주소"),
|
||||||
|
* @OA\Property(property="supplier_biz_type", type="string", example="서비스", maxLength=100, nullable=true, description="공급자 업태"),
|
||||||
|
* @OA\Property(property="supplier_biz_class", type="string", example="소프트웨어", maxLength=100, nullable=true, description="공급자 종목"),
|
||||||
|
* @OA\Property(property="supplier_contact_id", type="string", example="supplier@test.com", maxLength=100, nullable=true, description="공급자 담당자 이메일"),
|
||||||
|
* @OA\Property(property="buyer_corp_num", type="string", example="0987654321", minLength=10, maxLength=10, description="공급받는자 사업자번호"),
|
||||||
|
* @OA\Property(property="buyer_corp_name", type="string", example="(주)구매사", maxLength=200, description="공급받는자 회사명"),
|
||||||
|
* @OA\Property(property="buyer_ceo_name", type="string", example="김대표", maxLength=100, description="공급받는자 대표자명"),
|
||||||
|
* @OA\Property(property="buyer_addr", type="string", example="서울시 서초구", maxLength=500, nullable=true, description="공급받는자 주소"),
|
||||||
|
* @OA\Property(property="buyer_biz_type", type="string", example="제조", maxLength=100, nullable=true, description="공급받는자 업태"),
|
||||||
|
* @OA\Property(property="buyer_biz_class", type="string", example="전자제품", maxLength=100, nullable=true, description="공급받는자 종목"),
|
||||||
|
* @OA\Property(property="buyer_contact_id", type="string", example="buyer@test.com", maxLength=100, nullable=true, description="공급받는자 담당자 이메일"),
|
||||||
|
* @OA\Property(property="issue_date", type="string", format="date", example="2025-01-15", description="작성일자"),
|
||||||
|
* @OA\Property(property="supply_amount", type="number", format="float", example=1000000, minimum=0, description="공급가액"),
|
||||||
|
* @OA\Property(property="tax_amount", type="number", format="float", example=100000, minimum=0, description="세액"),
|
||||||
|
* @OA\Property(property="items", type="array", description="품목 목록 (최대 99개)",
|
||||||
|
*
|
||||||
|
* @OA\Items(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="date", type="string", format="date", example="2025-01-15", description="거래일자"),
|
||||||
|
* @OA\Property(property="name", type="string", example="컨설팅 서비스", maxLength=200, description="품목명"),
|
||||||
|
* @OA\Property(property="spec", type="string", example="월간", maxLength=100, nullable=true, description="규격"),
|
||||||
|
* @OA\Property(property="quantity", type="number", example=1, minimum=0, description="수량"),
|
||||||
|
* @OA\Property(property="unit_price", type="number", example=1000000, minimum=0, description="단가"),
|
||||||
|
* @OA\Property(property="supply_amount", type="number", example=1000000, minimum=0, description="공급가액"),
|
||||||
|
* @OA\Property(property="tax_amount", type="number", example=100000, minimum=0, description="세액"),
|
||||||
|
* @OA\Property(property="remark", type="string", example="1월분", maxLength=200, nullable=true, description="비고")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Property(property="description", type="string", example="월간 컨설팅 비용", maxLength=500, nullable=true, description="비고")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="TaxInvoiceUpdateRequest",
|
||||||
|
* type="object",
|
||||||
|
* description="세금계산서 수정 요청 (임시저장 상태에서만 수정 가능)",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="invoice_type", type="string", enum={"tax_invoice","invoice","modified"}, example="tax_invoice", description="세금계산서 유형"),
|
||||||
|
* @OA\Property(property="issue_type", type="string", enum={"normal","reverse","trustee"}, example="normal", description="발행 유형"),
|
||||||
|
* @OA\Property(property="direction", type="string", enum={"sales","purchases"}, example="sales", description="방향 (매출/매입)"),
|
||||||
|
* @OA\Property(property="supplier_corp_num", type="string", example="1234567890", minLength=10, maxLength=10, description="공급자 사업자번호"),
|
||||||
|
* @OA\Property(property="supplier_corp_name", type="string", example="(주)공급사", maxLength=200, description="공급자 회사명"),
|
||||||
|
* @OA\Property(property="supplier_ceo_name", type="string", example="홍길동", maxLength=100, description="공급자 대표자명"),
|
||||||
|
* @OA\Property(property="supplier_addr", type="string", example="서울시 강남구", maxLength=500, nullable=true, description="공급자 주소"),
|
||||||
|
* @OA\Property(property="supplier_biz_type", type="string", example="서비스", maxLength=100, nullable=true, description="공급자 업태"),
|
||||||
|
* @OA\Property(property="supplier_biz_class", type="string", example="소프트웨어", maxLength=100, nullable=true, description="공급자 종목"),
|
||||||
|
* @OA\Property(property="supplier_contact_id", type="string", example="supplier@test.com", maxLength=100, nullable=true, description="공급자 담당자 이메일"),
|
||||||
|
* @OA\Property(property="buyer_corp_num", type="string", example="0987654321", minLength=10, maxLength=10, description="공급받는자 사업자번호"),
|
||||||
|
* @OA\Property(property="buyer_corp_name", type="string", example="(주)구매사", maxLength=200, description="공급받는자 회사명"),
|
||||||
|
* @OA\Property(property="buyer_ceo_name", type="string", example="김대표", maxLength=100, description="공급받는자 대표자명"),
|
||||||
|
* @OA\Property(property="buyer_addr", type="string", example="서울시 서초구", maxLength=500, nullable=true, description="공급받는자 주소"),
|
||||||
|
* @OA\Property(property="buyer_biz_type", type="string", example="제조", maxLength=100, nullable=true, description="공급받는자 업태"),
|
||||||
|
* @OA\Property(property="buyer_biz_class", type="string", example="전자제품", maxLength=100, nullable=true, description="공급받는자 종목"),
|
||||||
|
* @OA\Property(property="buyer_contact_id", type="string", example="buyer@test.com", maxLength=100, nullable=true, description="공급받는자 담당자 이메일"),
|
||||||
|
* @OA\Property(property="issue_date", type="string", format="date", example="2025-01-15", description="작성일자"),
|
||||||
|
* @OA\Property(property="supply_amount", type="number", format="float", example=1000000, minimum=0, description="공급가액"),
|
||||||
|
* @OA\Property(property="tax_amount", type="number", format="float", example=100000, minimum=0, description="세액"),
|
||||||
|
* @OA\Property(property="items", type="array", description="품목 목록 (최대 99개)",
|
||||||
|
*
|
||||||
|
* @OA\Items(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="date", type="string", format="date", example="2025-01-15", description="거래일자"),
|
||||||
|
* @OA\Property(property="name", type="string", example="컨설팅 서비스", maxLength=200, description="품목명"),
|
||||||
|
* @OA\Property(property="spec", type="string", example="월간", maxLength=100, nullable=true, description="규격"),
|
||||||
|
* @OA\Property(property="quantity", type="number", example=1, minimum=0, description="수량"),
|
||||||
|
* @OA\Property(property="unit_price", type="number", example=1000000, minimum=0, description="단가"),
|
||||||
|
* @OA\Property(property="supply_amount", type="number", example=1000000, minimum=0, description="공급가액"),
|
||||||
|
* @OA\Property(property="tax_amount", type="number", example=100000, minimum=0, description="세액"),
|
||||||
|
* @OA\Property(property="remark", type="string", example="1월분", maxLength=200, nullable=true, description="비고")
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
* @OA\Property(property="description", type="string", example="월간 컨설팅 비용", maxLength=500, nullable=true, description="비고")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="TaxInvoiceCancelRequest",
|
||||||
|
* type="object",
|
||||||
|
* required={"reason"},
|
||||||
|
* description="세금계산서 취소 요청",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="reason", type="string", example="거래 취소로 인한 세금계산서 취소", maxLength=500, description="취소 사유")
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @OA\Schema(
|
||||||
|
* schema="TaxInvoiceSummary",
|
||||||
|
* type="object",
|
||||||
|
* description="세금계산서 요약 통계",
|
||||||
|
*
|
||||||
|
* @OA\Property(property="by_direction", type="object",
|
||||||
|
* @OA\Property(property="sales", type="object",
|
||||||
|
* @OA\Property(property="count", type="integer", example=50, description="매출 건수"),
|
||||||
|
* @OA\Property(property="supply_amount", type="number", format="float", example=50000000, description="공급가액 합계"),
|
||||||
|
* @OA\Property(property="tax_amount", type="number", format="float", example=5000000, description="세액 합계"),
|
||||||
|
* @OA\Property(property="total_amount", type="number", format="float", example=55000000, description="합계금액")
|
||||||
|
* ),
|
||||||
|
* @OA\Property(property="purchases", type="object",
|
||||||
|
* @OA\Property(property="count", type="integer", example=30, description="매입 건수"),
|
||||||
|
* @OA\Property(property="supply_amount", type="number", format="float", example=30000000, description="공급가액 합계"),
|
||||||
|
* @OA\Property(property="tax_amount", type="number", format="float", example=3000000, description="세액 합계"),
|
||||||
|
* @OA\Property(property="total_amount", type="number", format="float", example=33000000, description="합계금액")
|
||||||
|
* ),
|
||||||
|
* description="방향별 통계"
|
||||||
|
* ),
|
||||||
|
* @OA\Property(property="by_status", type="object",
|
||||||
|
* @OA\Property(property="draft", type="integer", example=5, description="임시저장 건수"),
|
||||||
|
* @OA\Property(property="issued", type="integer", example=30, description="발행완료 건수"),
|
||||||
|
* @OA\Property(property="sent", type="integer", example=40, description="국세청 전송 건수"),
|
||||||
|
* @OA\Property(property="cancelled", type="integer", example=3, description="취소 건수"),
|
||||||
|
* @OA\Property(property="failed", type="integer", example=2, description="발행실패 건수"),
|
||||||
|
* description="상태별 건수"
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
class TaxInvoiceApi
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/tax-invoices",
|
||||||
|
* tags={"TaxInvoices"},
|
||||||
|
* summary="세금계산서 목록 조회",
|
||||||
|
* description="세금계산서 목록을 조회합니다. 다양한 필터 조건으로 검색할 수 있습니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="direction", in="query", description="방향 (매출/매입)", @OA\Schema(type="string", enum={"sales","purchases"})),
|
||||||
|
* @OA\Parameter(name="status", in="query", description="상태", @OA\Schema(type="string", enum={"draft","issued","sent","cancelled","failed"})),
|
||||||
|
* @OA\Parameter(name="invoice_type", in="query", description="세금계산서 유형", @OA\Schema(type="string", enum={"tax_invoice","invoice","modified"})),
|
||||||
|
* @OA\Parameter(name="issue_type", in="query", description="발행 유형", @OA\Schema(type="string", enum={"normal","reverse","trustee"})),
|
||||||
|
* @OA\Parameter(name="issue_date_from", in="query", description="작성일 시작", @OA\Schema(type="string", format="date")),
|
||||||
|
* @OA\Parameter(name="issue_date_to", in="query", description="작성일 종료", @OA\Schema(type="string", format="date")),
|
||||||
|
* @OA\Parameter(name="corp_num", in="query", description="거래처 사업자번호 (공급자 또는 공급받는자)", @OA\Schema(type="string")),
|
||||||
|
* @OA\Parameter(name="corp_name", in="query", description="거래처명 검색 (공급자 또는 공급받는자)", @OA\Schema(type="string", maxLength=100)),
|
||||||
|
* @OA\Parameter(name="nts_confirm_num", in="query", description="국세청 승인번호 검색", @OA\Schema(type="string")),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/Page"),
|
||||||
|
* @OA\Parameter(ref="#/components/parameters/Size"),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(
|
||||||
|
* property="data",
|
||||||
|
* type="object",
|
||||||
|
* @OA\Property(property="current_page", type="integer", example=1),
|
||||||
|
* @OA\Property(property="data", type="array", @OA\Items(ref="#/components/schemas/TaxInvoice")),
|
||||||
|
* @OA\Property(property="per_page", type="integer", example=20),
|
||||||
|
* @OA\Property(property="total", type="integer", example=100)
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function index() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/tax-invoices/summary",
|
||||||
|
* tags={"TaxInvoices"},
|
||||||
|
* summary="세금계산서 요약 통계",
|
||||||
|
* description="세금계산서 요약 통계를 조회합니다. 방향별(매출/매입), 상태별 집계 정보를 제공합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="issue_date_from", in="query", description="작성일 시작", @OA\Schema(type="string", format="date")),
|
||||||
|
* @OA\Parameter(name="issue_date_to", in="query", description="작성일 종료", @OA\Schema(type="string", format="date")),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/TaxInvoiceSummary")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function summary() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/v1/tax-invoices",
|
||||||
|
* tags={"TaxInvoices"},
|
||||||
|
* summary="세금계산서 생성",
|
||||||
|
* description="새로운 세금계산서를 생성합니다. 생성 시 임시저장(draft) 상태로 저장됩니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/TaxInvoiceCreateRequest")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=201,
|
||||||
|
* description="생성 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/TaxInvoice")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=400, description="잘못된 요청", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function store() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/tax-invoices/{id}",
|
||||||
|
* tags={"TaxInvoices"},
|
||||||
|
* summary="세금계산서 상세 조회",
|
||||||
|
* description="세금계산서 상세 정보를 조회합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, description="세금계산서 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/TaxInvoice")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=404, description="세금계산서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function show() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Put(
|
||||||
|
* path="/api/v1/tax-invoices/{id}",
|
||||||
|
* tags={"TaxInvoices"},
|
||||||
|
* summary="세금계산서 수정",
|
||||||
|
* description="세금계산서 정보를 수정합니다. 임시저장(draft) 상태에서만 수정 가능합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, description="세금계산서 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/TaxInvoiceUpdateRequest")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="수정 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/TaxInvoice")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=400, description="잘못된 요청 또는 수정 불가 상태", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=404, description="세금계산서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function update() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Delete(
|
||||||
|
* path="/api/v1/tax-invoices/{id}",
|
||||||
|
* tags={"TaxInvoices"},
|
||||||
|
* summary="세금계산서 삭제",
|
||||||
|
* description="세금계산서를 삭제합니다. 임시저장(draft) 상태에서만 삭제 가능합니다. (Soft Delete)",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, description="세금계산서 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="삭제 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/ApiResponse")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=400, description="삭제 불가 상태", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=404, description="세금계산서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function destroy() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/v1/tax-invoices/{id}/issue",
|
||||||
|
* tags={"TaxInvoices"},
|
||||||
|
* summary="세금계산서 발행",
|
||||||
|
* description="세금계산서를 발행합니다. 바로빌 API를 통해 전자세금계산서가 발행됩니다. 임시저장(draft) 상태에서만 발행 가능합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, description="세금계산서 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="발행 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/TaxInvoice")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=400, description="발행 불가 상태 또는 바로빌 설정 미완료", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=404, description="세금계산서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러 또는 바로빌 발행 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function issue() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Post(
|
||||||
|
* path="/api/v1/tax-invoices/{id}/cancel",
|
||||||
|
* tags={"TaxInvoices"},
|
||||||
|
* summary="세금계산서 취소",
|
||||||
|
* description="세금계산서를 취소합니다. 바로빌 API를 통해 전자세금계산서가 취소됩니다. 발행완료(issued) 또는 국세청 전송(sent) 상태에서만 취소 가능합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, description="세금계산서 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\RequestBody(
|
||||||
|
* required=true,
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(ref="#/components/schemas/TaxInvoiceCancelRequest")
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="취소 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/TaxInvoice")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=400, description="취소 불가 상태 또는 바로빌 취소 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=404, description="세금계산서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function cancel() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @OA\Get(
|
||||||
|
* path="/api/v1/tax-invoices/{id}/check-status",
|
||||||
|
* tags={"TaxInvoices"},
|
||||||
|
* summary="국세청 전송 상태 조회",
|
||||||
|
* description="세금계산서의 국세청 전송 상태를 조회합니다. 바로빌 API를 통해 최신 상태를 확인하고 업데이트합니다.",
|
||||||
|
* security={{"ApiKeyAuth":{}},{"BearerAuth":{}}},
|
||||||
|
*
|
||||||
|
* @OA\Parameter(name="id", in="path", required=true, description="세금계산서 ID", @OA\Schema(type="integer")),
|
||||||
|
*
|
||||||
|
* @OA\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="조회 성공",
|
||||||
|
*
|
||||||
|
* @OA\JsonContent(
|
||||||
|
* allOf={
|
||||||
|
*
|
||||||
|
* @OA\Schema(ref="#/components/schemas/ApiResponse"),
|
||||||
|
* @OA\Schema(
|
||||||
|
*
|
||||||
|
* @OA\Property(property="data", ref="#/components/schemas/TaxInvoice")
|
||||||
|
* )
|
||||||
|
* }
|
||||||
|
* )
|
||||||
|
* ),
|
||||||
|
*
|
||||||
|
* @OA\Response(response=401, description="인증 실패", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=404, description="세금계산서 없음", @OA\JsonContent(ref="#/components/schemas/ErrorResponse")),
|
||||||
|
* @OA\Response(response=500, description="서버 에러", @OA\JsonContent(ref="#/components/schemas/ErrorResponse"))
|
||||||
|
* )
|
||||||
|
*/
|
||||||
|
public function checkStatus() {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('barobill_settings', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('tenant_id')->unique()->comment('테넌트 ID');
|
||||||
|
$table->string('corp_num', 20)->comment('사업자번호 (하이픈 제외)');
|
||||||
|
$table->string('cert_key')->nullable()->comment('바로빌 인증키 (암호화)');
|
||||||
|
$table->string('barobill_id', 100)->nullable()->comment('바로빌 아이디');
|
||||||
|
$table->string('corp_name', 100)->comment('상호');
|
||||||
|
$table->string('ceo_name', 50)->comment('대표자명');
|
||||||
|
$table->string('addr', 200)->nullable()->comment('사업장 주소');
|
||||||
|
$table->string('biz_type', 100)->nullable()->comment('업태');
|
||||||
|
$table->string('biz_class', 100)->nullable()->comment('종목');
|
||||||
|
$table->string('contact_id', 100)->nullable()->comment('담당자 이메일');
|
||||||
|
$table->string('contact_name', 50)->nullable()->comment('담당자명');
|
||||||
|
$table->string('contact_tel', 20)->nullable()->comment('담당자 전화번호');
|
||||||
|
$table->boolean('is_active')->default(false)->comment('활성화 여부');
|
||||||
|
$table->boolean('auto_issue')->default(false)->comment('자동 발행 여부');
|
||||||
|
$table->timestamp('verified_at')->nullable()->comment('연동 검증일시');
|
||||||
|
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자');
|
||||||
|
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->index('corp_num', 'idx_corp_num');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('barobill_settings');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('tax_invoices', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('tenant_id')->comment('테넌트 ID');
|
||||||
|
$table->string('nts_confirm_num', 24)->nullable()->comment('국세청 승인번호');
|
||||||
|
$table->string('invoice_type', 20)->comment('세금계산서/계산서/수정세금계산서');
|
||||||
|
$table->string('issue_type', 20)->comment('정발행/역발행/위수탁');
|
||||||
|
$table->string('direction', 10)->comment('매출(sales)/매입(purchases)');
|
||||||
|
|
||||||
|
// 공급자 정보
|
||||||
|
$table->string('supplier_corp_num', 20)->comment('공급자 사업자번호');
|
||||||
|
$table->string('supplier_corp_name', 100)->comment('공급자 상호');
|
||||||
|
$table->string('supplier_ceo_name', 50)->nullable()->comment('공급자 대표자명');
|
||||||
|
$table->string('supplier_addr', 200)->nullable()->comment('공급자 주소');
|
||||||
|
$table->string('supplier_biz_type', 100)->nullable()->comment('공급자 업태');
|
||||||
|
$table->string('supplier_biz_class', 100)->nullable()->comment('공급자 종목');
|
||||||
|
$table->string('supplier_contact_id', 100)->nullable()->comment('공급자 담당자 이메일');
|
||||||
|
|
||||||
|
// 공급받는자 정보
|
||||||
|
$table->string('buyer_corp_num', 20)->comment('공급받는자 사업자번호');
|
||||||
|
$table->string('buyer_corp_name', 100)->comment('공급받는자 상호');
|
||||||
|
$table->string('buyer_ceo_name', 50)->nullable()->comment('공급받는자 대표자명');
|
||||||
|
$table->string('buyer_addr', 200)->nullable()->comment('공급받는자 주소');
|
||||||
|
$table->string('buyer_biz_type', 100)->nullable()->comment('공급받는자 업태');
|
||||||
|
$table->string('buyer_biz_class', 100)->nullable()->comment('공급받는자 종목');
|
||||||
|
$table->string('buyer_contact_id', 100)->nullable()->comment('공급받는자 담당자 이메일');
|
||||||
|
|
||||||
|
// 금액 정보
|
||||||
|
$table->date('issue_date')->comment('발행일자');
|
||||||
|
$table->decimal('supply_amount', 15, 2)->comment('공급가액');
|
||||||
|
$table->decimal('tax_amount', 15, 2)->comment('세액');
|
||||||
|
$table->decimal('total_amount', 15, 2)->comment('합계금액');
|
||||||
|
|
||||||
|
// 품목 정보 (JSON)
|
||||||
|
$table->json('items')->nullable()->comment('품목 목록 [{name, spec, qty, unit_price, supply_amt, tax_amt, remark}]');
|
||||||
|
|
||||||
|
// 상태 정보
|
||||||
|
$table->string('status', 20)->default('draft')->comment('상태: draft/issued/sent/cancelled/failed');
|
||||||
|
$table->string('nts_send_status', 20)->nullable()->comment('국세청 전송상태');
|
||||||
|
$table->timestamp('issued_at')->nullable()->comment('발행일시');
|
||||||
|
$table->timestamp('sent_at')->nullable()->comment('전송일시');
|
||||||
|
$table->timestamp('cancelled_at')->nullable()->comment('취소일시');
|
||||||
|
|
||||||
|
// 연동 정보
|
||||||
|
$table->string('barobill_invoice_id', 50)->nullable()->comment('바로빌 세금계산서 ID');
|
||||||
|
$table->text('description')->nullable()->comment('비고');
|
||||||
|
$table->text('error_message')->nullable()->comment('오류 메시지');
|
||||||
|
|
||||||
|
// 참조 정보
|
||||||
|
$table->string('reference_type', 50)->nullable()->comment('참조 유형 (sale/purchase)');
|
||||||
|
$table->unsignedBigInteger('reference_id')->nullable()->comment('참조 ID');
|
||||||
|
|
||||||
|
$table->unsignedBigInteger('created_by')->nullable()->comment('생성자');
|
||||||
|
$table->unsignedBigInteger('updated_by')->nullable()->comment('수정자');
|
||||||
|
$table->unsignedBigInteger('deleted_by')->nullable()->comment('삭제자');
|
||||||
|
$table->softDeletes();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
// 인덱스
|
||||||
|
$table->index(['tenant_id', 'issue_date'], 'idx_tenant_issue_date');
|
||||||
|
$table->index(['tenant_id', 'direction'], 'idx_tenant_direction');
|
||||||
|
$table->index(['tenant_id', 'status'], 'idx_tenant_status');
|
||||||
|
$table->index('nts_confirm_num', 'idx_nts_confirm_num');
|
||||||
|
$table->index(['reference_type', 'reference_id'], 'idx_reference');
|
||||||
|
$table->index('supplier_corp_num', 'idx_supplier');
|
||||||
|
$table->index('buyer_corp_num', 'idx_buyer');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('tax_invoices');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -65,6 +65,8 @@
|
|||||||
use App\Http\Controllers\Api\V1\RolePermissionController;
|
use App\Http\Controllers\Api\V1\RolePermissionController;
|
||||||
use App\Http\Controllers\Api\V1\SaleController;
|
use App\Http\Controllers\Api\V1\SaleController;
|
||||||
use App\Http\Controllers\Api\V1\SiteController;
|
use App\Http\Controllers\Api\V1\SiteController;
|
||||||
|
use App\Http\Controllers\Api\V1\TaxInvoiceController;
|
||||||
|
use App\Http\Controllers\Api\V1\BarobillSettingController;
|
||||||
// 설계 전용 (디자인 네임스페이스)
|
// 설계 전용 (디자인 네임스페이스)
|
||||||
use App\Http\Controllers\Api\V1\TenantController;
|
use App\Http\Controllers\Api\V1\TenantController;
|
||||||
use App\Http\Controllers\Api\V1\TenantFieldSettingController;
|
use App\Http\Controllers\Api\V1\TenantFieldSettingController;
|
||||||
@@ -427,6 +429,26 @@
|
|||||||
Route::post('/{id}/confirm', [PurchaseController::class, 'confirm'])->whereNumber('id')->name('v1.purchases.confirm');
|
Route::post('/{id}/confirm', [PurchaseController::class, 'confirm'])->whereNumber('id')->name('v1.purchases.confirm');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Barobill Setting API (바로빌 설정)
|
||||||
|
Route::prefix('barobill-settings')->group(function () {
|
||||||
|
Route::get('', [BarobillSettingController::class, 'show'])->name('v1.barobill-settings.show');
|
||||||
|
Route::put('', [BarobillSettingController::class, 'save'])->name('v1.barobill-settings.save');
|
||||||
|
Route::post('/test-connection', [BarobillSettingController::class, 'testConnection'])->name('v1.barobill-settings.test-connection');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tax Invoice API (세금계산서)
|
||||||
|
Route::prefix('tax-invoices')->group(function () {
|
||||||
|
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('/{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');
|
||||||
|
});
|
||||||
|
|
||||||
// Report API (보고서)
|
// Report API (보고서)
|
||||||
Route::prefix('reports')->group(function () {
|
Route::prefix('reports')->group(function () {
|
||||||
Route::get('/daily', [ReportController::class, 'daily'])->name('v1.reports.daily');
|
Route::get('/daily', [ReportController::class, 'daily'])->name('v1.reports.daily');
|
||||||
|
|||||||
Reference in New Issue
Block a user