- BarobillSetting 미설정 시 Tenant 정보를 기본값으로 반환 - corp_num/corp_name이 비어있으면 Fallback 동작 - Tenant 필드 매핑: business_num, company_name, ceo_name, address, phone - Tenant options 매핑: business_type, business_category, tax_invoice_contact, tax_invoice_email
446 lines
14 KiB
PHP
446 lines
14 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Tenants\TaxInvoice;
|
|
use App\Models\Tenants\Tenant;
|
|
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);
|
|
}
|
|
|
|
/**
|
|
* 세금계산서 일괄 발행
|
|
*
|
|
* @param array<int> $ids 발행할 세금계산서 ID 배열
|
|
* @return array{issued: int, failed: int, errors: array<int, string>}
|
|
*/
|
|
public function bulkIssue(array $ids): array
|
|
{
|
|
$tenantId = $this->tenantId();
|
|
$results = [
|
|
'issued' => 0,
|
|
'failed' => 0,
|
|
'errors' => [],
|
|
];
|
|
|
|
$taxInvoices = TaxInvoice::query()
|
|
->where('tenant_id', $tenantId)
|
|
->whereIn('id', $ids)
|
|
->get();
|
|
|
|
foreach ($taxInvoices as $taxInvoice) {
|
|
try {
|
|
if (! $taxInvoice->canEdit()) {
|
|
$results['errors'][$taxInvoice->id] = __('error.tax_invoice.already_issued');
|
|
$results['failed']++;
|
|
|
|
continue;
|
|
}
|
|
|
|
$this->barobillService->issueTaxInvoice($taxInvoice);
|
|
$results['issued']++;
|
|
} catch (\Throwable $e) {
|
|
$results['errors'][$taxInvoice->id] = $e->getMessage();
|
|
$results['failed']++;
|
|
}
|
|
}
|
|
|
|
// 요청된 ID 중 찾지 못한 것들도 실패 처리
|
|
$foundIds = $taxInvoices->pluck('id')->toArray();
|
|
$notFoundIds = array_diff($ids, $foundIds);
|
|
foreach ($notFoundIds as $notFoundId) {
|
|
$results['errors'][$notFoundId] = __('error.tax_invoice.not_found');
|
|
$results['failed']++;
|
|
}
|
|
|
|
return $results;
|
|
}
|
|
|
|
/**
|
|
* 세금계산서 취소
|
|
*/
|
|
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);
|
|
}
|
|
|
|
// =========================================================================
|
|
// 공급자 설정
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 공급자 설정 조회 (BarobillSetting 기반, 미설정 시 Tenant 정보 Fallback)
|
|
*/
|
|
public function getSupplierSettings(): array
|
|
{
|
|
$setting = $this->barobillService->getSetting();
|
|
|
|
if ($setting && $this->hasSupplierData($setting)) {
|
|
return [
|
|
'business_number' => $setting->corp_num,
|
|
'company_name' => $setting->corp_name,
|
|
'representative_name' => $setting->ceo_name,
|
|
'address' => $setting->addr,
|
|
'business_type' => $setting->biz_type,
|
|
'business_item' => $setting->biz_class,
|
|
'contact_name' => $setting->contact_name,
|
|
'contact_phone' => $setting->contact_tel,
|
|
'contact_email' => $setting->contact_id,
|
|
];
|
|
}
|
|
|
|
// BarobillSetting 미설정 시 Tenant 정보를 기본값으로 반환
|
|
return $this->getSupplierSettingsFromTenant();
|
|
}
|
|
|
|
/**
|
|
* BarobillSetting에 공급자 정보가 입력되어 있는지 확인
|
|
*/
|
|
private function hasSupplierData($setting): bool
|
|
{
|
|
return ! empty($setting->corp_num) || ! empty($setting->corp_name);
|
|
}
|
|
|
|
/**
|
|
* Tenant 정보에서 공급자 설정 기본값 조회
|
|
*/
|
|
private function getSupplierSettingsFromTenant(): array
|
|
{
|
|
$tenant = Tenant::find($this->tenantId());
|
|
|
|
if (! $tenant) {
|
|
return [];
|
|
}
|
|
|
|
$options = $tenant->options ?? [];
|
|
|
|
return [
|
|
'business_number' => $tenant->business_num ?? '',
|
|
'company_name' => $tenant->company_name ?? '',
|
|
'representative_name' => $tenant->ceo_name ?? '',
|
|
'address' => $tenant->address ?? '',
|
|
'business_type' => $options['business_type'] ?? '',
|
|
'business_item' => $options['business_category'] ?? '',
|
|
'contact_name' => $options['tax_invoice_contact'] ?? '',
|
|
'contact_phone' => $tenant->phone ?? '',
|
|
'contact_email' => $options['tax_invoice_email'] ?? '',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 공급자 설정 저장
|
|
*/
|
|
public function saveSupplierSettings(array $data): array
|
|
{
|
|
$this->barobillService->saveSetting([
|
|
'corp_num' => $data['business_number'] ?? null,
|
|
'corp_name' => $data['company_name'] ?? null,
|
|
'ceo_name' => $data['representative_name'] ?? null,
|
|
'addr' => $data['address'] ?? null,
|
|
'biz_type' => $data['business_type'] ?? null,
|
|
'biz_class' => $data['business_item'] ?? null,
|
|
'contact_name' => $data['contact_name'] ?? null,
|
|
'contact_tel' => $data['contact_phone'] ?? null,
|
|
'contact_id' => $data['contact_email'] ?? null,
|
|
]);
|
|
|
|
return $this->getSupplierSettings();
|
|
}
|
|
|
|
// =========================================================================
|
|
// 생성+발행 통합
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 세금계산서 생성 후 즉시 발행
|
|
*/
|
|
public function createAndIssue(array $data): TaxInvoice
|
|
{
|
|
return DB::transaction(function () use ($data) {
|
|
$taxInvoice = $this->create($data);
|
|
|
|
return $this->barobillService->issueTaxInvoice($taxInvoice);
|
|
});
|
|
}
|
|
|
|
// =========================================================================
|
|
// 통계
|
|
// =========================================================================
|
|
|
|
/**
|
|
* 세금계산서 요약 통계
|
|
*/
|
|
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,
|
|
],
|
|
];
|
|
}
|
|
}
|