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