215 lines
7.3 KiB
PHP
215 lines
7.3 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace App\Services;
|
||
|
|
|
||
|
|
use App\Models\Tenants\AccountCode;
|
||
|
|
use App\Models\Tenants\JournalEntry;
|
||
|
|
use App\Models\Tenants\JournalEntryLine;
|
||
|
|
use App\Traits\SyncsExpenseAccounts;
|
||
|
|
use Illuminate\Support\Facades\DB;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 세금계산서/카드거래 등 외부 소스의 분개 통합 관리 서비스
|
||
|
|
*
|
||
|
|
* journal_entries + journal_entry_lines에 저장하고
|
||
|
|
* 복리후생비/접대비는 expense_accounts에 동기화 (CEO 대시보드)
|
||
|
|
*/
|
||
|
|
class JournalSyncService extends Service
|
||
|
|
{
|
||
|
|
use SyncsExpenseAccounts;
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 소스에 대한 분개 저장 (생성 또는 교체)
|
||
|
|
*
|
||
|
|
* @param string $sourceType JournalEntry::SOURCE_TAX_INVOICE 등
|
||
|
|
* @param string $sourceKey 'tax_invoice_123' 등
|
||
|
|
* @param string $entryDate 전표일자 (Y-m-d)
|
||
|
|
* @param string|null $description 적요
|
||
|
|
* @param array $rows 분개 행 [{side, account_code, account_name?, debit_amount, credit_amount, vendor_id?, vendor_name?, memo?}]
|
||
|
|
*/
|
||
|
|
public function saveForSource(
|
||
|
|
string $sourceType,
|
||
|
|
string $sourceKey,
|
||
|
|
string $entryDate,
|
||
|
|
?string $description,
|
||
|
|
array $rows,
|
||
|
|
): JournalEntry {
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
return DB::transaction(function () use ($sourceType, $sourceKey, $entryDate, $description, $rows, $tenantId) {
|
||
|
|
// 기존 전표가 있으면 삭제 후 재생성 (교체 방식)
|
||
|
|
$existing = JournalEntry::query()
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('source_type', $sourceType)
|
||
|
|
->where('source_key', $sourceKey)
|
||
|
|
->first();
|
||
|
|
|
||
|
|
if ($existing) {
|
||
|
|
$this->cleanupExpenseAccounts($tenantId, $existing->id);
|
||
|
|
JournalEntryLine::where('journal_entry_id', $existing->id)->delete();
|
||
|
|
$existing->forceDelete();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 합계 계산
|
||
|
|
$totalDebit = 0;
|
||
|
|
$totalCredit = 0;
|
||
|
|
foreach ($rows as $row) {
|
||
|
|
$totalDebit += (int) ($row['debit_amount'] ?? 0);
|
||
|
|
$totalCredit += (int) ($row['credit_amount'] ?? 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 전표번호 생성
|
||
|
|
$entryNo = $this->generateEntryNo($tenantId, $entryDate);
|
||
|
|
|
||
|
|
// 전표 생성
|
||
|
|
$entry = new JournalEntry;
|
||
|
|
$entry->tenant_id = $tenantId;
|
||
|
|
$entry->entry_no = $entryNo;
|
||
|
|
$entry->entry_date = $entryDate;
|
||
|
|
$entry->entry_type = JournalEntry::TYPE_GENERAL;
|
||
|
|
$entry->description = $description;
|
||
|
|
$entry->total_debit = $totalDebit;
|
||
|
|
$entry->total_credit = $totalCredit;
|
||
|
|
$entry->status = JournalEntry::STATUS_CONFIRMED;
|
||
|
|
$entry->source_type = $sourceType;
|
||
|
|
$entry->source_key = $sourceKey;
|
||
|
|
$entry->save();
|
||
|
|
|
||
|
|
// 분개 행 생성
|
||
|
|
foreach ($rows as $index => $row) {
|
||
|
|
$accountCode = $row['account_code'] ?? '';
|
||
|
|
$accountName = $row['account_name'] ?? $this->resolveAccountName($tenantId, $accountCode);
|
||
|
|
|
||
|
|
$line = new JournalEntryLine;
|
||
|
|
$line->tenant_id = $tenantId;
|
||
|
|
$line->journal_entry_id = $entry->id;
|
||
|
|
$line->line_no = $index + 1;
|
||
|
|
$line->dc_type = $row['side'];
|
||
|
|
$line->account_code = $accountCode;
|
||
|
|
$line->account_name = $accountName;
|
||
|
|
$line->trading_partner_id = ! empty($row['vendor_id']) ? (int) $row['vendor_id'] : null;
|
||
|
|
$line->trading_partner_name = $row['vendor_name'] ?? '';
|
||
|
|
$line->debit_amount = (int) ($row['debit_amount'] ?? 0);
|
||
|
|
$line->credit_amount = (int) ($row['credit_amount'] ?? 0);
|
||
|
|
$line->description = $row['memo'] ?? null;
|
||
|
|
$line->save();
|
||
|
|
}
|
||
|
|
|
||
|
|
// expense_accounts 동기화 (복리후생비/접대비 → CEO 대시보드)
|
||
|
|
$this->syncExpenseAccounts($entry);
|
||
|
|
|
||
|
|
return $entry->load('lines');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 소스에 대한 분개 조회
|
||
|
|
*/
|
||
|
|
public function getForSource(string $sourceType, string $sourceKey): ?array
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
$entry = JournalEntry::query()
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('source_type', $sourceType)
|
||
|
|
->where('source_key', $sourceKey)
|
||
|
|
->whereNull('deleted_at')
|
||
|
|
->with('lines')
|
||
|
|
->first();
|
||
|
|
|
||
|
|
if (! $entry) {
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return [
|
||
|
|
'id' => $entry->id,
|
||
|
|
'entry_no' => $entry->entry_no,
|
||
|
|
'entry_date' => $entry->entry_date->format('Y-m-d'),
|
||
|
|
'description' => $entry->description,
|
||
|
|
'total_debit' => $entry->total_debit,
|
||
|
|
'total_credit' => $entry->total_credit,
|
||
|
|
'rows' => $entry->lines->map(function ($line) {
|
||
|
|
return [
|
||
|
|
'id' => $line->id,
|
||
|
|
'side' => $line->dc_type,
|
||
|
|
'account_code' => $line->account_code,
|
||
|
|
'account_name' => $line->account_name,
|
||
|
|
'vendor_id' => $line->trading_partner_id,
|
||
|
|
'vendor_name' => $line->trading_partner_name ?? '',
|
||
|
|
'debit_amount' => (int) $line->debit_amount,
|
||
|
|
'credit_amount' => (int) $line->credit_amount,
|
||
|
|
'memo' => $line->description ?? '',
|
||
|
|
];
|
||
|
|
})->toArray(),
|
||
|
|
];
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 소스에 대한 분개 삭제
|
||
|
|
*/
|
||
|
|
public function deleteForSource(string $sourceType, string $sourceKey): bool
|
||
|
|
{
|
||
|
|
$tenantId = $this->tenantId();
|
||
|
|
|
||
|
|
return DB::transaction(function () use ($sourceType, $sourceKey, $tenantId) {
|
||
|
|
$entry = JournalEntry::query()
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('source_type', $sourceType)
|
||
|
|
->where('source_key', $sourceKey)
|
||
|
|
->first();
|
||
|
|
|
||
|
|
if (! $entry) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->cleanupExpenseAccounts($tenantId, $entry->id);
|
||
|
|
JournalEntryLine::where('journal_entry_id', $entry->id)->delete();
|
||
|
|
$entry->delete(); // soft delete
|
||
|
|
|
||
|
|
return true;
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 전표번호 생성: JE-YYYYMMDD-NNN
|
||
|
|
*/
|
||
|
|
private function generateEntryNo(int $tenantId, string $date): string
|
||
|
|
{
|
||
|
|
$dateStr = str_replace('-', '', substr($date, 0, 10));
|
||
|
|
$prefix = "JE-{$dateStr}-";
|
||
|
|
|
||
|
|
$lastEntry = DB::table('journal_entries')
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('entry_no', 'like', "{$prefix}%")
|
||
|
|
->lockForUpdate()
|
||
|
|
->orderBy('entry_no', 'desc')
|
||
|
|
->first(['entry_no']);
|
||
|
|
|
||
|
|
if ($lastEntry) {
|
||
|
|
$lastSeq = (int) substr($lastEntry->entry_no, -3);
|
||
|
|
$nextSeq = $lastSeq + 1;
|
||
|
|
} else {
|
||
|
|
$nextSeq = 1;
|
||
|
|
}
|
||
|
|
|
||
|
|
return $prefix . str_pad($nextSeq, 3, '0', STR_PAD_LEFT);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 계정과목 코드 → 이름 조회
|
||
|
|
*/
|
||
|
|
private function resolveAccountName(int $tenantId, string $code): string
|
||
|
|
{
|
||
|
|
if (empty($code)) {
|
||
|
|
return '';
|
||
|
|
}
|
||
|
|
|
||
|
|
$account = AccountCode::query()
|
||
|
|
->where('tenant_id', $tenantId)
|
||
|
|
->where('code', $code)
|
||
|
|
->first(['name']);
|
||
|
|
|
||
|
|
return $account ? $account->name : $code;
|
||
|
|
}
|
||
|
|
}
|