From dced7b7fd320fac989f24a908e977d56983f473e Mon Sep 17 00:00:00 2001 From: kent Date: Fri, 26 Dec 2025 15:46:31 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20I-2=20=EA=B1=B0=EB=9E=98=EC=B2=98=20?= =?UTF-8?q?=EC=9B=90=EC=9E=A5=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - VendorLedgerController: 거래처별 원장 조회 API - VendorLedgerService: 원장 조회 비즈니스 로직 - Swagger 문서화 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Api/V1/VendorLedgerController.php | 65 ++++ app/Services/VendorLedgerService.php | 306 ++++++++++++++++++ app/Swagger/v1/VendorLedgerApi.php | 291 +++++++++++++++++ 3 files changed, 662 insertions(+) create mode 100644 app/Http/Controllers/Api/V1/VendorLedgerController.php create mode 100644 app/Services/VendorLedgerService.php create mode 100644 app/Swagger/v1/VendorLedgerApi.php diff --git a/app/Http/Controllers/Api/V1/VendorLedgerController.php b/app/Http/Controllers/Api/V1/VendorLedgerController.php new file mode 100644 index 0000000..f5c144b --- /dev/null +++ b/app/Http/Controllers/Api/V1/VendorLedgerController.php @@ -0,0 +1,65 @@ +only([ + 'search', + 'start_date', + 'end_date', + 'sort_by', + 'sort_dir', + 'per_page', + 'page', + ]); + + $ledger = $this->service->index($params); + + return ApiResponse::success($ledger, __('message.fetched')); + } + + /** + * 거래처원장 요약 통계 + */ + public function summary(Request $request) + { + $params = $request->only([ + 'start_date', + 'end_date', + ]); + + $summary = $this->service->summary($params); + + return ApiResponse::success($summary, __('message.fetched')); + } + + /** + * 거래처원장 상세 (거래처별 거래 내역) + */ + public function show(int $clientId, Request $request) + { + $params = $request->only([ + 'start_date', + 'end_date', + ]); + + $detail = $this->service->show($clientId, $params); + + return ApiResponse::success($detail, __('message.fetched')); + } +} diff --git a/app/Services/VendorLedgerService.php b/app/Services/VendorLedgerService.php new file mode 100644 index 0000000..850d730 --- /dev/null +++ b/app/Services/VendorLedgerService.php @@ -0,0 +1,306 @@ +tenantId(); + $startDate = $params['start_date'] ?? null; + $endDate = $params['end_date'] ?? null; + $search = $params['search'] ?? null; + $perPage = $params['per_page'] ?? 20; + + // 거래처 목록 조회 + $query = Client::query() + ->where('tenant_id', $tenantId) + ->where('is_active', true); + + // 거래처명 검색 + if (! empty($search)) { + $query->where('name', 'like', "%{$search}%"); + } + + // 거래처 ID 목록 + $clientsQuery = clone $query; + $clients = $clientsQuery->select('id', 'name', 'sales_payment_day')->get(); + + // 매출 집계 서브쿼리 + $salesSubquery = Sale::query() + ->select('client_id', DB::raw('SUM(total_amount) as total_sales')) + ->where('tenant_id', $tenantId) + ->when($startDate, fn ($q) => $q->where('sale_date', '>=', $startDate)) + ->when($endDate, fn ($q) => $q->where('sale_date', '<=', $endDate)) + ->groupBy('client_id'); + + // 수금 집계 서브쿼리 + $depositsSubquery = Deposit::query() + ->select('client_id', DB::raw('SUM(amount) as total_collection')) + ->where('tenant_id', $tenantId) + ->when($startDate, fn ($q) => $q->where('deposit_date', '>=', $startDate)) + ->when($endDate, fn ($q) => $q->where('deposit_date', '<=', $endDate)) + ->groupBy('client_id'); + + // 이월잔액 서브쿼리 (기간 시작 전 매출 - 수금) + $carryoverSalesSubquery = null; + $carryoverDepositsSubquery = null; + + if ($startDate) { + $carryoverSalesSubquery = Sale::query() + ->select('client_id', DB::raw('COALESCE(SUM(total_amount), 0) as carryover_sales')) + ->where('tenant_id', $tenantId) + ->where('sale_date', '<', $startDate) + ->groupBy('client_id'); + + $carryoverDepositsSubquery = Deposit::query() + ->select('client_id', DB::raw('COALESCE(SUM(amount), 0) as carryover_deposits')) + ->where('tenant_id', $tenantId) + ->where('deposit_date', '<', $startDate) + ->groupBy('client_id'); + } + + // 메인 쿼리 + $mainQuery = Client::query() + ->select([ + 'clients.id', + 'clients.name', + 'clients.sales_payment_day', + DB::raw('COALESCE(sales_agg.total_sales, 0) as period_sales'), + DB::raw('COALESCE(deposits_agg.total_collection, 0) as period_collection'), + ]) + ->where('clients.tenant_id', $tenantId) + ->where('clients.is_active', true) + ->leftJoinSub($salesSubquery, 'sales_agg', 'clients.id', '=', 'sales_agg.client_id') + ->leftJoinSub($depositsSubquery, 'deposits_agg', 'clients.id', '=', 'deposits_agg.client_id'); + + if ($carryoverSalesSubquery && $carryoverDepositsSubquery) { + $mainQuery->addSelect([ + DB::raw('COALESCE(carryover_sales_agg.carryover_sales, 0) as carryover_sales'), + DB::raw('COALESCE(carryover_deposits_agg.carryover_deposits, 0) as carryover_deposits'), + ]) + ->leftJoinSub($carryoverSalesSubquery, 'carryover_sales_agg', 'clients.id', '=', 'carryover_sales_agg.client_id') + ->leftJoinSub($carryoverDepositsSubquery, 'carryover_deposits_agg', 'clients.id', '=', 'carryover_deposits_agg.client_id'); + } else { + $mainQuery->addSelect([ + DB::raw('0 as carryover_sales'), + DB::raw('0 as carryover_deposits'), + ]); + } + + // 검색 + if (! empty($search)) { + $mainQuery->where('clients.name', 'like', "%{$search}%"); + } + + // 정렬 + $sortBy = $params['sort_by'] ?? 'name'; + $sortDir = $params['sort_dir'] ?? 'asc'; + $mainQuery->orderBy("clients.{$sortBy}", $sortDir); + + return $mainQuery->paginate($perPage); + } + + /** + * 거래처원장 요약 통계 + */ + public function summary(array $params): array + { + $tenantId = $this->tenantId(); + $startDate = $params['start_date'] ?? null; + $endDate = $params['end_date'] ?? null; + + // 기간 내 매출 합계 + $totalSales = Sale::query() + ->where('tenant_id', $tenantId) + ->when($startDate, fn ($q) => $q->where('sale_date', '>=', $startDate)) + ->when($endDate, fn ($q) => $q->where('sale_date', '<=', $endDate)) + ->sum('total_amount'); + + // 기간 내 수금 합계 + $totalCollection = Deposit::query() + ->where('tenant_id', $tenantId) + ->when($startDate, fn ($q) => $q->where('deposit_date', '>=', $startDate)) + ->when($endDate, fn ($q) => $q->where('deposit_date', '<=', $endDate)) + ->sum('amount'); + + // 이월잔액 (기간 시작 전 매출 - 수금) + $carryoverBalance = 0; + if ($startDate) { + $carryoverSales = Sale::query() + ->where('tenant_id', $tenantId) + ->where('sale_date', '<', $startDate) + ->sum('total_amount'); + + $carryoverDeposits = Deposit::query() + ->where('tenant_id', $tenantId) + ->where('deposit_date', '<', $startDate) + ->sum('amount'); + + $carryoverBalance = (float) $carryoverSales - (float) $carryoverDeposits; + } + + // 현재 잔액 = 이월잔액 + 매출 - 수금 + $balance = $carryoverBalance + (float) $totalSales - (float) $totalCollection; + + return [ + 'carryover_balance' => $carryoverBalance, + 'total_sales' => (float) $totalSales, + 'total_collection' => (float) $totalCollection, + 'balance' => $balance, + ]; + } + + /** + * 거래처원장 상세 (거래처별 거래 내역) + */ + public function show(int $clientId, array $params): array + { + $tenantId = $this->tenantId(); + $startDate = $params['start_date'] ?? null; + $endDate = $params['end_date'] ?? null; + + // 거래처 정보 + $client = Client::query() + ->where('tenant_id', $tenantId) + ->findOrFail($clientId); + + // 이월잔액 계산 + $carryoverBalance = 0; + if ($startDate) { + $carryoverSales = Sale::query() + ->where('tenant_id', $tenantId) + ->where('client_id', $clientId) + ->where('sale_date', '<', $startDate) + ->sum('total_amount'); + + $carryoverDeposits = Deposit::query() + ->where('tenant_id', $tenantId) + ->where('client_id', $clientId) + ->where('deposit_date', '<', $startDate) + ->sum('amount'); + + $carryoverBalance = (float) $carryoverSales - (float) $carryoverDeposits; + } + + // 거래 내역 조회 (매출 + 수금) + $transactions = collect(); + + // 매출 내역 + $sales = Sale::query() + ->where('tenant_id', $tenantId) + ->where('client_id', $clientId) + ->when($startDate, fn ($q) => $q->where('sale_date', '>=', $startDate)) + ->when($endDate, fn ($q) => $q->where('sale_date', '<=', $endDate)) + ->orderBy('sale_date') + ->get() + ->map(fn ($sale) => [ + 'id' => $sale->id, + 'date' => $sale->sale_date->format('Y-m-d'), + 'type' => 'sales', + 'description' => $sale->description ?? '매출', + 'sales_amount' => (float) $sale->total_amount, + 'collection_amount' => 0, + 'reference_id' => $sale->id, + 'reference_type' => 'sale', + 'has_action' => false, + 'is_highlighted' => ! $sale->tax_invoice_issued, + ]); + + // 수금 내역 + $deposits = Deposit::query() + ->where('tenant_id', $tenantId) + ->where('client_id', $clientId) + ->when($startDate, fn ($q) => $q->where('deposit_date', '>=', $startDate)) + ->when($endDate, fn ($q) => $q->where('deposit_date', '<=', $endDate)) + ->orderBy('deposit_date') + ->get() + ->map(fn ($deposit) => [ + 'id' => $deposit->id, + 'date' => $deposit->deposit_date->format('Y-m-d'), + 'type' => 'collection', + 'description' => $deposit->description ?? '입금', + 'sales_amount' => 0, + 'collection_amount' => (float) $deposit->amount, + 'reference_id' => $deposit->id, + 'reference_type' => 'deposit', + 'has_action' => false, + 'is_highlighted' => false, + 'is_parenthesis' => $deposit->payment_method === 'check', + ]); + + // 어음 내역 + $bills = Bill::query() + ->where('tenant_id', $tenantId) + ->where('client_id', $clientId) + ->when($startDate, fn ($q) => $q->where('issue_date', '>=', $startDate)) + ->when($endDate, fn ($q) => $q->where('issue_date', '<=', $endDate)) + ->orderBy('issue_date') + ->get() + ->map(fn ($bill) => [ + 'id' => $bill->id, + 'date' => $bill->issue_date, + 'type' => 'note', + 'description' => "수취 어음 (만기 {$bill->maturity_date})", + 'sales_amount' => 0, + 'collection_amount' => (float) $bill->amount, + 'reference_id' => $bill->id, + 'reference_type' => 'bill', + 'has_action' => true, + 'is_highlighted' => false, + 'is_parenthesis' => true, + 'note_info' => $bill->maturity_date, + ]); + + // 전체 거래 내역 합치기 및 정렬 + $allTransactions = $sales->merge($deposits)->merge($bills) + ->sortBy('date') + ->values(); + + // 잔액 계산 + $runningBalance = $carryoverBalance; + $transactions = $allTransactions->map(function ($item) use (&$runningBalance) { + $runningBalance = $runningBalance + $item['sales_amount'] - $item['collection_amount']; + $item['balance'] = $runningBalance; + + return $item; + }); + + // 합계 계산 + $totalSales = $transactions->sum('sales_amount'); + $totalCollection = $transactions->sum('collection_amount'); + + return [ + 'client' => [ + 'id' => $client->id, + 'name' => $client->name, + 'business_number' => $client->business_no, + 'representative_name' => $client->contact_person, + 'phone' => $client->phone, + 'mobile' => $client->mobile, + 'fax' => $client->fax, + 'email' => $client->email, + 'address' => $client->address, + ], + 'period' => [ + 'start_date' => $startDate, + 'end_date' => $endDate, + ], + 'carryover_balance' => $carryoverBalance, + 'total_sales' => $totalSales, + 'total_collection' => $totalCollection, + 'balance' => $runningBalance, + 'transactions' => $transactions->toArray(), + ]; + } +} diff --git a/app/Swagger/v1/VendorLedgerApi.php b/app/Swagger/v1/VendorLedgerApi.php new file mode 100644 index 0000000..f3877d7 --- /dev/null +++ b/app/Swagger/v1/VendorLedgerApi.php @@ -0,0 +1,291 @@ +