tenantId(); $startDate = $params['start_date']; $endDate = $params['end_date']; $accountCode = $params['account_code']; $account = DB::table('account_codes') ->where('tenant_id', $tenantId) ->where('code', $accountCode) ->where('is_active', true) ->first(['code', 'name', 'category', 'sub_category']); if (! $account) { return [ 'account' => null, 'period' => compact('startDate', 'endDate'), 'carry_forward' => ['debit' => 0, 'credit' => 0, 'balance' => 0], 'monthly_data' => [], 'grand_total' => ['debit' => 0, 'credit' => 0, 'balance' => 0], ]; } $allLines = DB::table('journal_entry_lines as jel') ->join('journal_entries as je', 'je.id', '=', 'jel.journal_entry_id') ->leftJoin('trading_partners as tp', function ($join) use ($tenantId) { $join->on('tp.id', '=', 'jel.trading_partner_id') ->where('tp.tenant_id', '=', $tenantId); }) ->where('jel.tenant_id', $tenantId) ->where('jel.account_code', $accountCode) ->whereBetween('je.entry_date', [$startDate, $endDate]) ->whereNull('je.deleted_at') ->select([ 'je.entry_date as date', 'jel.description', 'jel.trading_partner_name', 'tp.biz_no', 'jel.debit_amount', 'jel.credit_amount', DB::raw("COALESCE(je.source_type, 'journal') as source_type"), 'jel.journal_entry_id as source_id', 'je.source_key', ]) ->orderBy('je.entry_date') ->get(); // 분리 전표(split) 유효성 필터링 $allLines = $this->filterSplitLines($tenantId, $allLines); // 카드거래 상세 조회 $cardTxMap = $this->fetchCardTransactions($tenantId, $allLines); $carryForward = $this->calculateCarryForward($tenantId, $accountCode, $startDate, $account->category); $isDebitNormal = in_array($account->category, ['asset', 'expense']); $runningBalance = $carryForward['balance']; $monthlyData = []; $grandDebit = 0; $grandCredit = 0; foreach ($allLines as $line) { $month = substr($line->date, 0, 7); if (! isset($monthlyData[$month])) { $monthlyData[$month] = [ 'month' => $month, 'items' => [], 'subtotal' => ['debit' => 0, 'credit' => 0], ]; } $debit = (int) $line->debit_amount; $credit = (int) $line->credit_amount; $runningBalance += $isDebitNormal ? ($debit - $credit) : ($credit - $debit); $cardTx = null; if ($line->source_type === 'ecard_transaction' && $line->source_key) { $cardTx = $cardTxMap[$line->source_key] ?? null; } $monthlyData[$month]['items'][] = [ 'date' => $line->date, 'description' => $line->description, 'trading_partner_name' => $cardTx ? ($cardTx['merchant_name'] ?: $line->trading_partner_name) : $line->trading_partner_name, 'biz_no' => $cardTx ? ($cardTx['merchant_biz_num'] ?: $line->biz_no) : $line->biz_no, 'debit_amount' => $debit, 'credit_amount' => $credit, 'balance' => $runningBalance, 'source_type' => $line->source_type, 'source_id' => (int) $line->source_id, 'card_tx' => $cardTx, ]; $monthlyData[$month]['subtotal']['debit'] += $debit; $monthlyData[$month]['subtotal']['credit'] += $credit; $grandDebit += $debit; $grandCredit += $credit; } $cumulativeDebit = 0; $cumulativeCredit = 0; foreach ($monthlyData as &$md) { $cumulativeDebit += $md['subtotal']['debit']; $cumulativeCredit += $md['subtotal']['credit']; $md['cumulative'] = ['debit' => $cumulativeDebit, 'credit' => $cumulativeCredit]; } unset($md); return [ 'account' => ['code' => $account->code, 'name' => $account->name, 'category' => $account->category], 'period' => ['start_date' => $startDate, 'end_date' => $endDate], 'carry_forward' => $carryForward, 'monthly_data' => array_values($monthlyData), 'grand_total' => ['debit' => $grandDebit, 'credit' => $grandCredit, 'balance' => $runningBalance], ]; } /** * 분리 전표(split) 유효성 필터링 * — 삭제된 split은 제거, split이 존재하는 원본 전표도 제거 */ private function filterSplitLines(int $tenantId, Collection $lines): Collection { $ecardSplitLines = $lines->filter( fn ($l) => $l->source_type === 'ecard_transaction' && $l->source_key && str_contains($l->source_key, '|split:') ); if ($ecardSplitLines->isEmpty()) { return $lines; } $splitBaseKeys = $ecardSplitLines ->map(fn ($l) => explode('|split:', $l->source_key)[0]) ->unique() ->all(); $validSplitIds = DB::table('barobill_card_transaction_splits') ->where('tenant_id', $tenantId) ->whereIn('original_unique_key', $splitBaseKeys) ->pluck('id') ->all(); $validSplitSuffixes = array_map(fn ($id) => '|split:'.$id, $validSplitIds); return $lines->filter(function ($l) use ($splitBaseKeys, $validSplitSuffixes) { if ($l->source_type !== 'ecard_transaction' || ! $l->source_key) { return true; } if (str_contains($l->source_key, '|split:')) { foreach ($validSplitSuffixes as $suffix) { if (str_ends_with($l->source_key, $suffix)) { return true; } } return false; } // 원본(non-split) 전표: 분리 전표가 존재하면 제외 return ! in_array($l->source_key, $splitBaseKeys); })->values(); } /** * 카드거래 상세 일괄 조회 (source_key 기반) */ private function fetchCardTransactions(int $tenantId, Collection $lines): array { $sourceKeys = $lines ->filter(fn ($l) => $l->source_type === 'ecard_transaction' && $l->source_key) ->pluck('source_key') ->unique() ->values() ->all(); if (empty($sourceKeys)) { return []; } // source_key = "card_num|use_dt|approval_num|approval_amount" 또는 "...|split:N" $conditions = []; $keyMap = []; foreach ($sourceKeys as $key) { $baseKey = explode('|split:', $key)[0]; $parts = explode('|', $baseKey); if (count($parts) === 4) { $conditions[$baseKey] = $parts; $keyMap[$key] = $baseKey; } } $conditions = array_values($conditions); if (empty($conditions)) { return []; } $query = DB::table('barobill_card_transactions') ->where('tenant_id', $tenantId); $query->where(function ($q) use ($conditions) { foreach ($conditions as $c) { $q->orWhere(function ($sub) use ($c) { $sub->where('card_num', $c[0]) ->where('use_dt', $c[1]) ->where('approval_num', $c[2]) ->whereRaw('CAST(approval_amount AS SIGNED) = ?', [(int) $c[3]]); }); } }); $txs = $query->get(); $map = []; foreach ($txs as $tx) { $uniqueKey = implode('|', [ $tx->card_num, $tx->use_dt, $tx->approval_num, (int) $tx->approval_amount, ]); $supplyAmount = $tx->modified_supply_amount !== null ? (int) $tx->modified_supply_amount : (int) $tx->approval_amount - (int) $tx->tax; $taxAmount = $tx->modified_tax !== null ? (int) $tx->modified_tax : (int) $tx->tax; $map[$uniqueKey] = [ 'card_num' => $tx->card_num, 'card_company_name' => $tx->card_company_name, 'merchant_name' => $tx->merchant_name, 'merchant_biz_num' => $tx->merchant_biz_num, 'deduction_type' => $tx->deduction_type, 'supply_amount' => $supplyAmount, 'tax_amount' => $taxAmount, 'approval_amount' => (int) $tx->approval_amount, ]; } // 분리 키(uniqueKey|split:N)도 원본 카드 데이터로 매핑 foreach ($keyMap as $sourceKey => $baseKey) { if ($sourceKey !== $baseKey && isset($map[$baseKey])) { $map[$sourceKey] = $map[$baseKey]; } } return $map; } private function calculateCarryForward(int $tenantId, string $accountCode, string $startDate, string $category): array { $sums = DB::table('journal_entry_lines as jel') ->join('journal_entries as je', 'je.id', '=', 'jel.journal_entry_id') ->where('jel.tenant_id', $tenantId) ->where('jel.account_code', $accountCode) ->where('je.entry_date', '<', $startDate) ->whereNull('je.deleted_at') ->selectRaw('COALESCE(SUM(jel.debit_amount), 0) as total_debit, COALESCE(SUM(jel.credit_amount), 0) as total_credit') ->first(); $debit = (int) $sums->total_debit; $credit = (int) $sums->total_credit; $isDebitNormal = in_array($category, ['asset', 'expense']); $balance = $isDebitNormal ? ($debit - $credit) : ($credit - $debit); return compact('debit', 'credit', 'balance'); } }