orderBy('created_at', 'desc') ->get() ->map(function ($card) { return [ 'id' => $card->id, 'cardName' => $card->card_name, 'cardCompany' => $card->card_company, 'cardNumber' => $card->card_number, 'cardType' => $card->card_type, 'paymentDay' => $card->payment_day, 'creditLimit' => (float) $card->credit_limit, 'currentUsage' => (float) $card->current_usage, 'cardHolderName' => $card->card_holder_name, 'actualUser' => $card->actual_user, 'expiryDate' => $card->expiry_date, 'cvc' => $card->cvc, 'status' => $card->status, 'memo' => $card->memo, ]; }); return response()->json([ 'success' => true, 'data' => $cards, ]); } /** * 카드 등록 */ public function store(Request $request): JsonResponse { $request->validate([ 'cardName' => 'required|string|max:100', 'cardCompany' => 'required|string|max:50', 'cardNumber' => 'required|string|max:30', 'cardType' => 'required|in:credit,debit', 'cardHolderName' => 'required|string|max:100', 'actualUser' => 'required|string|max:100', ]); $tenantId = session('selected_tenant_id', 1); $card = CorporateCard::create([ 'tenant_id' => $tenantId, 'card_name' => $request->input('cardName'), 'card_company' => $request->input('cardCompany'), 'card_number' => $request->input('cardNumber'), 'card_type' => $request->input('cardType'), 'payment_day' => $request->input('paymentDay', 15), 'credit_limit' => $request->input('creditLimit', 0), 'current_usage' => 0, 'card_holder_name' => $request->input('cardHolderName'), 'actual_user' => $request->input('actualUser'), 'expiry_date' => $request->input('expiryDate'), 'cvc' => $request->input('cvc'), 'status' => $request->input('status', 'active'), 'memo' => $request->input('memo'), ]); return response()->json([ 'success' => true, 'message' => '카드가 등록되었습니다.', 'data' => [ 'id' => $card->id, 'cardName' => $card->card_name, 'cardCompany' => $card->card_company, 'cardNumber' => $card->card_number, 'cardType' => $card->card_type, 'paymentDay' => $card->payment_day, 'creditLimit' => (float) $card->credit_limit, 'currentUsage' => (float) $card->current_usage, 'cardHolderName' => $card->card_holder_name, 'actualUser' => $card->actual_user, 'expiryDate' => $card->expiry_date, 'cvc' => $card->cvc, 'status' => $card->status, 'memo' => $card->memo, ], ]); } /** * 카드 수정 */ public function update(Request $request, int $id): JsonResponse { $card = CorporateCard::findOrFail($id); $request->validate([ 'cardName' => 'required|string|max:100', 'cardCompany' => 'required|string|max:50', 'cardNumber' => 'required|string|max:30', 'cardType' => 'required|in:credit,debit', 'cardHolderName' => 'required|string|max:100', 'actualUser' => 'required|string|max:100', ]); $card->update([ 'card_name' => $request->input('cardName'), 'card_company' => $request->input('cardCompany'), 'card_number' => $request->input('cardNumber'), 'card_type' => $request->input('cardType'), 'payment_day' => $request->input('paymentDay', 15), 'credit_limit' => $request->input('creditLimit', 0), 'card_holder_name' => $request->input('cardHolderName'), 'actual_user' => $request->input('actualUser'), 'expiry_date' => $request->input('expiryDate'), 'cvc' => $request->input('cvc'), 'status' => $request->input('status', 'active'), 'memo' => $request->input('memo'), ]); return response()->json([ 'success' => true, 'message' => '카드가 수정되었습니다.', 'data' => [ 'id' => $card->id, 'cardName' => $card->card_name, 'cardCompany' => $card->card_company, 'cardNumber' => $card->card_number, 'cardType' => $card->card_type, 'paymentDay' => $card->payment_day, 'creditLimit' => (float) $card->credit_limit, 'currentUsage' => (float) $card->current_usage, 'cardHolderName' => $card->card_holder_name, 'actualUser' => $card->actual_user, 'expiryDate' => $card->expiry_date, 'cvc' => $card->cvc, 'status' => $card->status, 'memo' => $card->memo, ], ]); } /** * 카드 비활성화 */ public function deactivate(int $id): JsonResponse { $card = CorporateCard::findOrFail($id); $card->update(['status' => 'inactive']); return response()->json([ 'success' => true, 'message' => '카드가 비활성화되었습니다.', ]); } /** * 카드 영구삭제 */ public function destroy(int $id): JsonResponse { $card = CorporateCard::findOrFail($id); $card->forceDelete(); return response()->json([ 'success' => true, 'message' => '카드가 영구 삭제되었습니다.', ]); } /** * 요약 데이터 (결제일, 사용금액, 선불결제) */ public function summary(): JsonResponse { $tenantId = session('selected_tenant_id', 1); $now = Carbon::now(); // 활성 카드 조회 $activeCards = CorporateCard::forTenant($tenantId)->active()->get(); if ($activeCards->isEmpty()) { return response()->json([ 'success' => true, 'data' => [ 'paymentDate' => null, 'paymentDay' => 15, 'originalDate' => null, 'isAdjusted' => false, 'billingPeriod' => null, 'billingUsage' => 0, 'prepaidAmount' => 0, 'prepaidMemo' => '', ], ]); } // 대표 결제일 (첫 번째 활성 신용카드 기준) $creditCard = $activeCards->firstWhere('card_type', 'credit'); $paymentDay = $creditCard ? $creditCard->payment_day : 15; // 휴일 조정 결제일 계산 (현재 월) $originalDate = $this->createPaymentDate($now->year, $now->month, $paymentDay); $adjustedDate = $this->getAdjustedPaymentDate($tenantId, $now->year, $now->month, $paymentDay); // 현재일이 결제일을 지났으면 다음 월로 이동 if ($now->greaterThan($adjustedDate)) { $nextMonth = $now->copy()->addMonth(); $originalDate = $this->createPaymentDate($nextMonth->year, $nextMonth->month, $paymentDay); $adjustedDate = $this->getAdjustedPaymentDate($tenantId, $nextMonth->year, $nextMonth->month, $paymentDay); } $isAdjusted = !$originalDate->isSameDay($adjustedDate); // 청구기간: 결제일 기준 전월 1일 ~ 결제일 $billingStart = $adjustedDate->copy()->subMonth()->startOfMonth(); $billingEnd = $adjustedDate->copy(); // 카드번호 목록 $cardNumbers = $activeCards->pluck('card_number')->toArray(); // 사용금액 계산 (전체 + 카드별) $usageResult = $this->calculateBillingUsage( $tenantId, $billingStart->format('Y-m-d'), $billingEnd->format('Y-m-d'), $cardNumbers ); // 선불결제 금액 (결제일 기준 월) $yearMonth = $adjustedDate->format('Y-m'); $prepayment = CorporateCardPrepayment::getOrCreate($tenantId, $yearMonth); return response()->json([ 'success' => true, 'data' => [ 'paymentDate' => $adjustedDate->format('Y-m-d'), 'paymentDay' => $paymentDay, 'originalDate' => $originalDate->format('Y-m-d'), 'isAdjusted' => $isAdjusted, 'billingPeriod' => [ 'start' => $billingStart->format('Y-m-d'), 'end' => $billingEnd->format('Y-m-d'), ], 'billingUsage' => $usageResult['total'], 'cardUsages' => $usageResult['perCard'], 'prepaidAmount' => (int) $prepayment->amount, 'prepaidMemo' => $prepayment->memo ?? '', 'prepaidItems' => $prepayment->items ?? [], ], ]); } /** * 결제 내역 수정 */ public function updatePrepayment(Request $request): JsonResponse { $request->validate([ 'items' => 'present|array', 'items.*.date' => 'required|date', 'items.*.amount' => 'required|integer|min:0', 'items.*.description' => 'nullable|string|max:200', ]); $tenantId = session('selected_tenant_id', 1); $yearMonth = $this->getBillingYearMonth($tenantId); $items = collect($request->input('items', []))->filter(fn($item) => ($item['amount'] ?? 0) > 0)->values()->toArray(); $amount = collect($items)->sum('amount'); $prepayment = CorporateCardPrepayment::updateOrCreate( ['tenant_id' => $tenantId, 'year_month' => $yearMonth], ['amount' => $amount, 'items' => $items, 'memo' => null] ); return response()->json([ 'success' => true, 'message' => '결제 내역이 저장되었습니다.', 'data' => [ 'amount' => (int) $prepayment->amount, 'items' => $prepayment->items ?? [], ], ]); } /** * 청구 기준 year_month 계산 (summary와 동일한 로직) */ private function getBillingYearMonth(int $tenantId): string { $now = Carbon::now(); $activeCards = CorporateCard::forTenant($tenantId)->active()->get(); $creditCard = $activeCards->firstWhere('card_type', 'credit'); $paymentDay = $creditCard ? $creditCard->payment_day : 15; $adjustedDate = $this->getAdjustedPaymentDate($tenantId, $now->year, $now->month, $paymentDay); if ($now->greaterThan($adjustedDate)) { $nextMonth = $now->copy()->addMonth(); $adjustedDate = $this->getAdjustedPaymentDate($tenantId, $nextMonth->year, $nextMonth->month, $paymentDay); } return $adjustedDate->format('Y-m'); } /** * 결제일 날짜 생성 (월 말일 초과 방지) */ private function createPaymentDate(int $year, int $month, int $day): Carbon { $maxDay = Carbon::create($year, $month)->daysInMonth; return Carbon::create($year, $month, min($day, $maxDay)); } /** * 휴일/주말 조정된 결제일 계산 */ private function getAdjustedPaymentDate(int $tenantId, int $year, int $month, int $paymentDay): Carbon { $date = $this->createPaymentDate($year, $month, $paymentDay); // 해당 기간의 휴일 조회 $holidays = Holiday::forTenant($tenantId) ->where('start_date', '<=', $date->copy()->addDays(10)->format('Y-m-d')) ->where('end_date', '>=', $date->format('Y-m-d')) ->get(); $holidayDates = []; foreach ($holidays as $h) { $current = $h->start_date->copy(); while ($current <= $h->end_date) { $holidayDates[] = $current->format('Y-m-d'); $current->addDay(); } } // 토/일/공휴일이면 다음 영업일로 이동 while ($date->isWeekend() || in_array($date->format('Y-m-d'), $holidayDates)) { $date->addDay(); } return $date; } /** * 청구기간 사용금액 계산 (바로빌 카드거래 합산) */ private function calculateBillingUsage(int $tenantId, string $startDate, string $endDate, array $cardNumbers): array { // 카드번호 정규화 (하이픈 제거) + 원본↔정규화 매핑 $normalizedNums = array_map(fn($num) => str_replace('-', '', $num), $cardNumbers); if (empty($normalizedNums)) { return ['total' => 0, 'perCard' => []]; } // use_date는 YYYYMMDD 형식 $startYmd = str_replace('-', '', $startDate); $endYmd = str_replace('-', '', $endDate); // 숨긴 거래 키 조회 $hiddenKeys = CardTransactionHide::getHiddenKeys($tenantId, $startYmd, $endYmd); // 바로빌 카드거래 조회 $transactions = CardTransaction::where('tenant_id', $tenantId) ->whereIn('card_num', $normalizedNums) ->whereBetween('use_date', [$startYmd, $endYmd]) ->get(); $total = 0; $perCard = []; foreach ($transactions as $tx) { if ($hiddenKeys->contains($tx->unique_key)) { continue; } $amount = 0; if ($tx->approval_type === '1') { $amount = (int) $tx->approval_amount; } elseif ($tx->approval_type === '2') { $amount = -(int) $tx->approval_amount; } $total += $amount; $cardNum = $tx->card_num; $perCard[$cardNum] = ($perCard[$cardNum] ?? 0) + $amount; } return ['total' => $total, 'perCard' => (object) $perCard]; } }