Files
sam-api/app/Services/TaxInvoiceService.php
김보곤 3ae3a1dcda feat: [tax-invoice] 공급자 설정 Tenant Fallback 로직 추가
- 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
2026-02-21 17:19:18 +09:00

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,
],
];
}
}