- /finance/card-transactions에서 EcardController 직접 호출 (리디렉션 제거) - /barobill/ecard index는 재무관리로 리디렉션 - 데이터 API 라우트(cards, transactions 등)는 barobill 하위 유지 - 바로빌 메뉴에서 카드 사용내역 제거 (재무관리에 이미 존재) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1873 lines
74 KiB
PHP
1873 lines
74 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Barobill;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Barobill\AccountCode;
|
|
use App\Models\Barobill\BarobillConfig;
|
|
use App\Models\Barobill\BarobillMember;
|
|
use App\Models\Barobill\CardTransaction;
|
|
use App\Models\Barobill\CardTransactionAmountLog;
|
|
use App\Models\Barobill\CardTransactionHide;
|
|
use App\Models\Barobill\CardTransactionSplit;
|
|
use App\Models\Tenants\Tenant;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\Response;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Illuminate\View\View;
|
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
|
|
|
/**
|
|
* 바로빌 카드 사용내역 조회 컨트롤러
|
|
*/
|
|
class EcardController extends Controller
|
|
{
|
|
/**
|
|
* 바로빌 SOAP 설정
|
|
*/
|
|
private ?string $certKey = null;
|
|
private ?string $corpNum = null;
|
|
private bool $isTestMode = false;
|
|
private ?string $soapUrl = null;
|
|
private ?\SoapClient $soapClient = null;
|
|
|
|
// 바로빌 파트너사 (본사) 테넌트 ID
|
|
private const HEADQUARTERS_TENANT_ID = 1;
|
|
|
|
public function __construct()
|
|
{
|
|
// DB에서 활성화된 바로빌 설정 조회
|
|
$activeConfig = BarobillConfig::where('is_active', true)->first();
|
|
|
|
if ($activeConfig) {
|
|
$this->certKey = $activeConfig->cert_key;
|
|
$this->corpNum = $activeConfig->corp_num;
|
|
$this->isTestMode = $activeConfig->environment === 'test';
|
|
// 카드 조회는 CARD.asmx 사용
|
|
$baseUrl = $this->isTestMode
|
|
? 'https://testws.baroservice.com'
|
|
: 'https://ws.baroservice.com';
|
|
$this->soapUrl = $baseUrl . '/CARD.asmx?WSDL';
|
|
} else {
|
|
$this->isTestMode = config('services.barobill.test_mode', true);
|
|
$this->certKey = $this->isTestMode
|
|
? config('services.barobill.cert_key_test', '')
|
|
: config('services.barobill.cert_key_prod', '');
|
|
$this->corpNum = config('services.barobill.corp_num', '');
|
|
$this->soapUrl = $this->isTestMode
|
|
? 'https://testws.baroservice.com/CARD.asmx?WSDL'
|
|
: 'https://ws.baroservice.com/CARD.asmx?WSDL';
|
|
}
|
|
|
|
$this->initSoapClient();
|
|
}
|
|
|
|
/**
|
|
* SOAP 클라이언트 초기화
|
|
*/
|
|
private function initSoapClient(): void
|
|
{
|
|
if (!empty($this->certKey) || $this->isTestMode) {
|
|
try {
|
|
$context = stream_context_create([
|
|
'ssl' => [
|
|
'verify_peer' => false,
|
|
'verify_peer_name' => false,
|
|
'allow_self_signed' => true
|
|
]
|
|
]);
|
|
|
|
$this->soapClient = new \SoapClient($this->soapUrl, [
|
|
'trace' => true,
|
|
'encoding' => 'UTF-8',
|
|
'exceptions' => true,
|
|
'connection_timeout' => 30,
|
|
'stream_context' => $context,
|
|
'cache_wsdl' => WSDL_CACHE_NONE
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('바로빌 카드 SOAP 클라이언트 생성 실패: ' . $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 카드 사용내역 메인 페이지
|
|
*/
|
|
public function index(Request $request): View|Response
|
|
{
|
|
if ($request->header('HX-Request')) {
|
|
return response('', 200)->header('HX-Redirect', route('finance.card-transactions'));
|
|
}
|
|
|
|
// 현재 선택된 테넌트 정보
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$currentTenant = Tenant::find($tenantId);
|
|
|
|
// 해당 테넌트의 바로빌 회원사 정보
|
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
|
|
|
// 테넌트별 서버 모드 적용 (회원사 설정 우선)
|
|
$isTestMode = $barobillMember ? $barobillMember->isTestMode() : $this->isTestMode;
|
|
|
|
// 서버 모드에 따라 SOAP 설정 재초기화
|
|
if ($barobillMember && $barobillMember->server_mode) {
|
|
$this->applyMemberServerMode($barobillMember);
|
|
}
|
|
|
|
return view('barobill.ecard.index', [
|
|
'certKey' => $this->certKey,
|
|
'corpNum' => $this->corpNum,
|
|
'isTestMode' => $isTestMode,
|
|
'hasSoapClient' => $this->soapClient !== null,
|
|
'currentTenant' => $currentTenant,
|
|
'barobillMember' => $barobillMember,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 회원사 서버 모드에 따라 SOAP 설정 적용
|
|
*/
|
|
private function applyMemberServerMode(BarobillMember $member): void
|
|
{
|
|
$memberTestMode = $member->isTestMode();
|
|
$targetEnv = $memberTestMode ? 'test' : 'production';
|
|
|
|
// 해당 환경의 BarobillConfig 조회 (is_active 무관하게 환경에 맞는 설정 사용)
|
|
$config = BarobillConfig::where('environment', $targetEnv)->first();
|
|
|
|
if ($config) {
|
|
$this->isTestMode = $memberTestMode;
|
|
$this->certKey = $config->cert_key;
|
|
$this->corpNum = $config->corp_num;
|
|
$baseUrl = $config->base_url ?: ($memberTestMode
|
|
? 'https://testws.baroservice.com'
|
|
: 'https://ws.baroservice.com');
|
|
$this->soapUrl = $baseUrl . '/CARD.asmx?WSDL';
|
|
|
|
// SOAP 클라이언트 재초기화
|
|
$this->initSoapClient();
|
|
|
|
Log::info('[Ecard] 서버 모드 적용', [
|
|
'targetEnv' => $targetEnv,
|
|
'certKey' => substr($this->certKey ?? '', 0, 10) . '...',
|
|
'corpNum' => $this->corpNum,
|
|
'soapUrl' => $this->soapUrl,
|
|
]);
|
|
} else {
|
|
Log::warning('[Ecard] BarobillConfig 없음', ['targetEnv' => $targetEnv]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 등록된 카드 목록 조회 (GetCardEx2)
|
|
* 레퍼런스: https://dev.barobill.co.kr/docs/references/카드조회-API#GetCardEx2
|
|
*/
|
|
public function cards(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
// 테넌트별 서버 모드 적용
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
|
if ($barobillMember && $barobillMember->server_mode) {
|
|
$this->applyMemberServerMode($barobillMember);
|
|
}
|
|
|
|
$availOnly = $request->input('availOnly', 0);
|
|
|
|
$result = $this->callSoap('GetCardEx2', [
|
|
'AvailOnly' => (int)$availOnly
|
|
]);
|
|
|
|
if (!$result['success']) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $result['error'],
|
|
'error_code' => $result['error_code'] ?? null
|
|
]);
|
|
}
|
|
|
|
$cards = [];
|
|
$data = $result['data'];
|
|
|
|
// GetCardEx2는 CardEx 배열을 반환
|
|
$cardList = [];
|
|
if (isset($data->CardEx)) {
|
|
$cardList = is_array($data->CardEx) ? $data->CardEx : [$data->CardEx];
|
|
}
|
|
|
|
foreach ($cardList as $card) {
|
|
if (!is_object($card)) continue;
|
|
|
|
$cardNum = $card->CardNum ?? '';
|
|
// 에러 체크: CardNum이 음수면 에러 코드
|
|
if (empty($cardNum) || (is_numeric($cardNum) && $cardNum < 0)) {
|
|
continue;
|
|
}
|
|
|
|
$cardCompanyCode = $card->CardCompanyCode ?? $card->CardCompany ?? '';
|
|
$cardCompanyName = !empty($card->CardCompanyName)
|
|
? $card->CardCompanyName
|
|
: $this->getCardCompanyName($cardCompanyCode);
|
|
|
|
$cards[] = [
|
|
'cardNum' => $cardNum,
|
|
'cardCompany' => $cardCompanyCode,
|
|
'cardBrand' => $cardCompanyName,
|
|
'alias' => $card->Alias ?? '',
|
|
'cardType' => $card->CardType ?? '',
|
|
'cardTypeName' => ($card->CardType ?? '') === '2' ? '법인카드' : '개인카드',
|
|
'status' => $card->Status ?? '',
|
|
'statusName' => $this->getCardStatusName($card->Status ?? ''),
|
|
'webId' => $card->WebId ?? '',
|
|
];
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'cards' => $cards,
|
|
'count' => count($cards)
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('카드 목록 조회 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '서버 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 카드 상태 코드 -> 이름 변환
|
|
*/
|
|
private function getCardStatusName(string $status): string
|
|
{
|
|
$statuses = [
|
|
'0' => '대기중',
|
|
'1' => '정상',
|
|
'2' => '해지',
|
|
'3' => '수집오류',
|
|
'4' => '일시중지'
|
|
];
|
|
return $statuses[$status] ?? $status;
|
|
}
|
|
|
|
/**
|
|
* 카드 사용내역 조회 (GetPeriodCardApprovalLog)
|
|
*/
|
|
public function transactions(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$startDate = $request->input('startDate', date('Ymd'));
|
|
$endDate = $request->input('endDate', date('Ymd'));
|
|
$cardNum = str_replace('-', '', $request->input('cardNum', ''));
|
|
$page = (int)$request->input('page', 1);
|
|
$limit = (int)$request->input('limit', 50);
|
|
|
|
// 현재 테넌트의 바로빌 회원 정보 조회
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$barobillMember = BarobillMember::where('tenant_id', $tenantId)->first();
|
|
$userId = $barobillMember?->barobill_id ?? '';
|
|
|
|
// 테넌트별 서버 모드 적용
|
|
if ($barobillMember && $barobillMember->server_mode) {
|
|
$this->applyMemberServerMode($barobillMember);
|
|
}
|
|
|
|
// 디버그 로그
|
|
Log::info('[ECard] 조회 요청', [
|
|
'tenantId' => $tenantId,
|
|
'userId' => $userId,
|
|
'startDate' => $startDate,
|
|
'endDate' => $endDate,
|
|
'cardNum' => $cardNum,
|
|
'page' => $page,
|
|
'limit' => $limit,
|
|
]);
|
|
|
|
// DB에서 저장된 계정과목 데이터 조회
|
|
$savedData = CardTransaction::getByDateRange($tenantId, $startDate, $endDate, $cardNum ?: null);
|
|
|
|
// DB에서 수동 입력 건 조회
|
|
$manualTransactions = CardTransaction::where('tenant_id', $tenantId)
|
|
->where('is_manual', true)
|
|
->whereBetween('use_date', [$startDate, $endDate])
|
|
->when($cardNum, fn($q) => $q->where('card_num', $cardNum))
|
|
->orderBy('use_date', 'desc')
|
|
->orderBy('use_time', 'desc')
|
|
->get();
|
|
|
|
// 전체 카드 조회: 빈 값이면 모든 카드의 사용 내역 조회
|
|
if (empty($cardNum)) {
|
|
return $this->getAllCardsTransactions($userId, $startDate, $endDate, $page, $limit, $savedData, $manualTransactions);
|
|
}
|
|
|
|
// 단일 카드 조회 (전체 데이터를 가져와서 통계 계산 후 로컬 페이지네이션)
|
|
$params = [
|
|
'ID' => $userId,
|
|
'CardNum' => $cardNum,
|
|
'StartDate' => $startDate,
|
|
'EndDate' => $endDate,
|
|
'CountPerPage' => 10000,
|
|
'CurrentPage' => 1,
|
|
'OrderDirection' => 2 // 2:내림차순
|
|
];
|
|
|
|
Log::info('[ECard] GetPeriodCardApprovalLog 호출', $params);
|
|
|
|
$result = $this->callSoap('GetPeriodCardApprovalLog', $params);
|
|
|
|
Log::info('[ECard] GetPeriodCardApprovalLog 응답', [
|
|
'success' => $result['success'],
|
|
'error' => $result['error'] ?? null,
|
|
'error_code' => $result['error_code'] ?? null,
|
|
'data_keys' => $result['success'] && isset($result['data']) ? (is_object($result['data']) ? get_object_vars($result['data']) : 'not_object') : null,
|
|
]);
|
|
|
|
if (!$result['success']) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $result['error'],
|
|
'error_code' => $result['error_code'] ?? null
|
|
]);
|
|
}
|
|
|
|
$resultData = $result['data'];
|
|
|
|
// 에러 코드 체크
|
|
$errorCode = $this->checkErrorCode($resultData);
|
|
Log::info('[ECard] 에러 코드 체크', ['errorCode' => $errorCode]);
|
|
|
|
if ($errorCode && !in_array($errorCode, [-24005, -24001])) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $this->getErrorMessage($errorCode),
|
|
'error_code' => $errorCode
|
|
]);
|
|
}
|
|
|
|
// 데이터가 없는 경우
|
|
if ($errorCode && in_array($errorCode, [-24005, -24001])) {
|
|
Log::info('[ECard] 데이터 없음 (에러코드로 판단)');
|
|
// API 데이터 없어도 수동 건은 표시
|
|
$manualLogs = $this->convertManualToLogs($manualTransactions);
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'logs' => $manualLogs['logs'],
|
|
'summary' => $manualLogs['summary'],
|
|
'pagination' => ['currentPage' => 1, 'maxPageNum' => 1]
|
|
]
|
|
]);
|
|
}
|
|
|
|
// 데이터 파싱 (저장된 계정과목 병합)
|
|
$logs = $this->parseTransactionLogs($resultData, $savedData);
|
|
|
|
// 숨김 키 필터링
|
|
$hiddenKeys = CardTransactionHide::getHiddenKeys($tenantId, $startDate, $endDate);
|
|
if ($hiddenKeys->isNotEmpty()) {
|
|
$hiddenSet = $hiddenKeys->flip();
|
|
$logs = $this->filterHiddenLogs($logs, $hiddenSet);
|
|
}
|
|
|
|
// 수동 입력 건 병합
|
|
$manualLogs = $this->convertManualToLogs($manualTransactions);
|
|
$allLogs = array_merge($logs['logs'], $manualLogs['logs']);
|
|
|
|
// 날짜/시간 기준 정렬 (최신순)
|
|
usort($allLogs, fn($a, $b) => strcmp($b['useDt'] ?? '', $a['useDt'] ?? ''));
|
|
|
|
// 전체 데이터에서 통계 계산 (공제/불공제 포함)
|
|
$mergedSummary = $this->mergeSummaries($logs['summary'], $manualLogs['summary']);
|
|
|
|
// 로컬 페이지네이션
|
|
$totalCount = count($allLogs);
|
|
$maxPageNum = (int)ceil($totalCount / $limit);
|
|
$startIndex = ($page - 1) * $limit;
|
|
$paginatedLogs = array_slice($allLogs, $startIndex, $limit);
|
|
|
|
Log::info('[ECard] 파싱 결과', [
|
|
'total_count' => $totalCount,
|
|
'page_count' => count($paginatedLogs),
|
|
'manual_count' => count($manualLogs['logs']),
|
|
'summary' => $mergedSummary,
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'logs' => $paginatedLogs,
|
|
'pagination' => [
|
|
'currentPage' => $page,
|
|
'countPerPage' => $limit,
|
|
'maxPageNum' => $maxPageNum,
|
|
'maxIndex' => $totalCount
|
|
],
|
|
'summary' => $mergedSummary
|
|
]
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('카드 사용내역 조회 오류: ' . $e->getMessage(), [
|
|
'trace' => $e->getTraceAsString()
|
|
]);
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '서버 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 전체 카드의 사용 내역 조회
|
|
*/
|
|
private function getAllCardsTransactions(string $userId, string $startDate, string $endDate, int $page, int $limit, $savedData = null, $manualTransactions = null): JsonResponse
|
|
{
|
|
Log::info('[ECard] 전체 카드 조회 시작', [
|
|
'userId' => $userId,
|
|
'startDate' => $startDate,
|
|
'endDate' => $endDate,
|
|
]);
|
|
|
|
// 먼저 카드 목록 조회 (GetCardEx2 사용)
|
|
$cardResult = $this->callSoap('GetCardEx2', ['AvailOnly' => 0]);
|
|
|
|
Log::info('[ECard] GetCardEx2 응답', [
|
|
'success' => $cardResult['success'],
|
|
'error' => $cardResult['error'] ?? null,
|
|
]);
|
|
|
|
if (!$cardResult['success']) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => $cardResult['error']
|
|
]);
|
|
}
|
|
|
|
$cardList = [];
|
|
$data = $cardResult['data'];
|
|
if (isset($data->CardEx)) {
|
|
$cardList = is_array($data->CardEx) ? $data->CardEx : [$data->CardEx];
|
|
}
|
|
|
|
Log::info('[ECard] 카드 목록', [
|
|
'count' => count($cardList),
|
|
'cards' => array_map(fn($c) => [
|
|
'CardNum' => $c->CardNum ?? 'N/A',
|
|
'Alias' => $c->Alias ?? 'N/A',
|
|
'Status' => $c->Status ?? 'N/A',
|
|
], $cardList),
|
|
]);
|
|
|
|
// 숨김 키 조회
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$hiddenKeys = CardTransactionHide::getHiddenKeys($tenantId, $startDate, $endDate);
|
|
$hiddenSet = $hiddenKeys->isNotEmpty() ? $hiddenKeys->flip() : collect();
|
|
|
|
$allLogs = [];
|
|
$totalAmount = 0;
|
|
$approvalCount = 0;
|
|
$cancelCount = 0;
|
|
$totalTax = 0;
|
|
|
|
foreach ($cardList as $card) {
|
|
if (!is_object($card)) continue;
|
|
|
|
$cardNum = $card->CardNum ?? '';
|
|
if (empty($cardNum) || (is_numeric($cardNum) && $cardNum < 0)) continue;
|
|
|
|
$params = [
|
|
'ID' => $userId,
|
|
'CardNum' => $cardNum,
|
|
'StartDate' => $startDate,
|
|
'EndDate' => $endDate,
|
|
'CountPerPage' => 1000,
|
|
'CurrentPage' => 1,
|
|
'OrderDirection' => 2
|
|
];
|
|
|
|
Log::info('[ECard] 카드별 사용내역 조회', ['cardNum' => $cardNum, 'params' => $params]);
|
|
|
|
$cardResult = $this->callSoap('GetPeriodCardApprovalLog', $params);
|
|
|
|
Log::info('[ECard] 카드별 응답', [
|
|
'cardNum' => $cardNum,
|
|
'success' => $cardResult['success'],
|
|
'error' => $cardResult['error'] ?? null,
|
|
'data_type' => isset($cardResult['data']) ? gettype($cardResult['data']) : 'null',
|
|
'data_props' => isset($cardResult['data']) && is_object($cardResult['data'])
|
|
? array_keys(get_object_vars($cardResult['data']))
|
|
: null,
|
|
]);
|
|
|
|
if ($cardResult['success']) {
|
|
$cardData = $cardResult['data'];
|
|
$errorCode = $this->checkErrorCode($cardData);
|
|
|
|
Log::info('[ECard] 카드별 에러코드', [
|
|
'cardNum' => $cardNum,
|
|
'errorCode' => $errorCode,
|
|
'CurrentPage' => $cardData->CurrentPage ?? 'N/A',
|
|
'MaxIndex' => $cardData->MaxIndex ?? 'N/A',
|
|
'hasCardLogList' => isset($cardData->CardLogList),
|
|
'hasCardApprovalLog' => isset($cardData->CardLogList) && isset($cardData->CardLogList->CardApprovalLog),
|
|
]);
|
|
|
|
if (!$errorCode || in_array($errorCode, [-24005, -24001])) {
|
|
$parsed = $this->parseTransactionLogs($cardData, $savedData);
|
|
|
|
// 숨김 키 필터링
|
|
if ($hiddenSet->isNotEmpty()) {
|
|
$parsed = $this->filterHiddenLogs($parsed, $hiddenSet);
|
|
}
|
|
|
|
Log::info('[ECard] 카드별 파싱 결과', [
|
|
'cardNum' => $cardNum,
|
|
'logs_count' => count($parsed['logs']),
|
|
]);
|
|
foreach ($parsed['logs'] as $log) {
|
|
$log['cardBrand'] = $this->getCardCompanyName($card->CardCompany ?? '');
|
|
$allLogs[] = $log;
|
|
}
|
|
$totalAmount += $parsed['summary']['totalAmount'];
|
|
$approvalCount += $parsed['summary']['approvalCount'];
|
|
$cancelCount += $parsed['summary']['cancelCount'];
|
|
$totalTax += $parsed['summary']['totalTax'] ?? 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
// 수동 입력 건 병합
|
|
if ($manualTransactions && $manualTransactions->isNotEmpty()) {
|
|
$manualLogs = $this->convertManualToLogs($manualTransactions);
|
|
foreach ($manualLogs['logs'] as $mLog) {
|
|
$allLogs[] = $mLog;
|
|
}
|
|
$totalAmount += $manualLogs['summary']['totalAmount'];
|
|
$approvalCount += $manualLogs['summary']['approvalCount'];
|
|
$cancelCount += $manualLogs['summary']['cancelCount'];
|
|
}
|
|
|
|
// 날짜/시간 기준 정렬 (최신순)
|
|
usort($allLogs, function ($a, $b) {
|
|
return strcmp($b['useDt'] ?? '', $a['useDt'] ?? '');
|
|
});
|
|
|
|
// 전체 데이터에서 공제/불공제 통계 계산 (불공제 부가세는 합계 제외)
|
|
$deductibleAmount = 0;
|
|
$deductibleCount = 0;
|
|
$nonDeductibleAmount = 0;
|
|
$nonDeductibleCount = 0;
|
|
$totalTax = 0;
|
|
|
|
foreach ($allLogs as $log) {
|
|
$type = $log['deductionType'] ?? 'non_deductible';
|
|
$amount = abs($log['approvalAmount'] ?? 0);
|
|
if ($type === 'deductible') {
|
|
$deductibleAmount += $amount;
|
|
$deductibleCount++;
|
|
$totalTax += abs($log['effectiveTax'] ?? $log['tax'] ?? 0);
|
|
} else {
|
|
$nonDeductibleAmount += $amount;
|
|
$nonDeductibleCount++;
|
|
}
|
|
}
|
|
|
|
// 페이지네이션
|
|
$totalCount = count($allLogs);
|
|
$maxPageNum = (int)ceil($totalCount / $limit);
|
|
$startIndex = ($page - 1) * $limit;
|
|
$paginatedLogs = array_slice($allLogs, $startIndex, $limit);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => [
|
|
'logs' => $paginatedLogs,
|
|
'pagination' => [
|
|
'currentPage' => $page,
|
|
'countPerPage' => $limit,
|
|
'maxPageNum' => $maxPageNum,
|
|
'maxIndex' => $totalCount
|
|
],
|
|
'summary' => [
|
|
'totalAmount' => $totalAmount,
|
|
'count' => $totalCount,
|
|
'approvalCount' => $approvalCount,
|
|
'cancelCount' => $cancelCount,
|
|
'totalTax' => $totalTax,
|
|
'deductibleAmount' => $deductibleAmount,
|
|
'deductibleCount' => $deductibleCount,
|
|
'nonDeductibleAmount' => $nonDeductibleAmount,
|
|
'nonDeductibleCount' => $nonDeductibleCount,
|
|
]
|
|
]
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 거래 내역 파싱 (저장된 계정과목 병합)
|
|
*/
|
|
private function parseTransactionLogs($resultData, $savedData = null): array
|
|
{
|
|
$logs = [];
|
|
$totalAmount = 0;
|
|
$approvalCount = 0;
|
|
$cancelCount = 0;
|
|
$totalTax = 0;
|
|
$deductibleAmount = 0;
|
|
$deductibleCount = 0;
|
|
$nonDeductibleAmount = 0;
|
|
$nonDeductibleCount = 0;
|
|
|
|
$rawLogs = [];
|
|
if (isset($resultData->CardLogList) && isset($resultData->CardLogList->CardApprovalLog)) {
|
|
$rawLogs = is_array($resultData->CardLogList->CardApprovalLog)
|
|
? $resultData->CardLogList->CardApprovalLog
|
|
: [$resultData->CardLogList->CardApprovalLog];
|
|
}
|
|
|
|
foreach ($rawLogs as $log) {
|
|
$amount = floatval($log->ApprovalAmount ?? 0);
|
|
$rawApprovalType = $log->ApprovalType ?? null;
|
|
$approvalType = (string)($rawApprovalType ?? '1');
|
|
|
|
// ApprovalType: 1=승인 or "승인", 2=취소 or "취소"
|
|
// API에서 한글 텍스트로 반환될 수 있으므로 취소가 아닌 경우 모두 승인으로 처리
|
|
$isApproval = !in_array($approvalType, ['2', '취소'], true);
|
|
|
|
// 디버깅: ApprovalType 값 확인 (첫 번째 로그만)
|
|
if (count($logs) === 0) {
|
|
Log::info('[ECard] 첫 번째 로그 전체 데이터', [
|
|
'log_object' => json_encode($log),
|
|
'ApprovalType_raw' => $rawApprovalType,
|
|
'ApprovalType_type' => gettype($rawApprovalType),
|
|
'ApprovalType_casted' => $approvalType,
|
|
'isApproval' => $isApproval,
|
|
'amount' => $amount,
|
|
]);
|
|
}
|
|
|
|
if ($isApproval) {
|
|
$totalAmount += $amount;
|
|
$approvalCount++;
|
|
} else {
|
|
$cancelCount++;
|
|
}
|
|
|
|
// 사용일시 파싱
|
|
$useDT = $log->UseDT ?? '';
|
|
$useDate = '';
|
|
$useTime = '';
|
|
$dateTime = '';
|
|
|
|
if (!empty($useDT) && strlen($useDT) >= 8) {
|
|
$useDate = substr($useDT, 0, 8);
|
|
if (strlen($useDT) >= 14) {
|
|
$useTime = substr($useDT, 8, 6);
|
|
$dateTime = substr($useDT, 0, 4) . '-' . substr($useDT, 4, 2) . '-' . substr($useDT, 6, 2) . ' ' .
|
|
substr($useDT, 8, 2) . ':' . substr($useDT, 10, 2) . ':' . substr($useDT, 12, 2);
|
|
} else {
|
|
$dateTime = substr($useDT, 0, 4) . '-' . substr($useDT, 4, 2) . '-' . substr($useDT, 6, 2);
|
|
}
|
|
}
|
|
|
|
$cardNum = $log->CardNum ?? '';
|
|
$approvalNum = $log->ApprovalNum ?? '';
|
|
|
|
// 고유 키 생성하여 저장된 데이터와 매칭
|
|
$uniqueKey = implode('|', [$cardNum, $useDT, $approvalNum, (int) $amount]);
|
|
$savedItem = $savedData?->get($uniqueKey);
|
|
|
|
$logItem = [
|
|
'cardNum' => $cardNum,
|
|
'cardNumMasked' => $this->maskCardNumber($cardNum),
|
|
'cardCompany' => $log->CardCompany ?? '',
|
|
'cardBrand' => $this->getCardCompanyName($log->CardCompany ?? ''),
|
|
'useDt' => $useDT,
|
|
'useDate' => $useDate,
|
|
'useTime' => $useTime,
|
|
'useDateTime' => $dateTime,
|
|
'approvalNum' => $approvalNum,
|
|
'approvalType' => $isApproval ? '1' : '2',
|
|
'approvalTypeName' => $isApproval ? '승인' : '취소',
|
|
'approvalAmount' => $amount,
|
|
'approvalAmountFormatted' => number_format($amount),
|
|
'tax' => floatval($log->Tax ?? 0),
|
|
'taxFormatted' => number_format(floatval($log->Tax ?? 0)),
|
|
'serviceCharge' => floatval($log->ServiceCharge ?? 0),
|
|
'paymentPlan' => $log->PaymentPlan ?? '0',
|
|
'paymentPlanName' => $this->getPaymentPlanName($log->PaymentPlan ?? '0'),
|
|
'currencyCode' => $log->CurrencyCode ?? 'KRW',
|
|
'merchantName' => $log->UseStoreName ?? '',
|
|
'merchantBizNum' => $log->UseStoreCorpNum ?? '',
|
|
'merchantAddr' => $log->UseStoreAddr ?? '',
|
|
'merchantCeo' => $log->UseStoreCeo ?? '',
|
|
'merchantBizType' => $log->UseStoreBizType ?? '',
|
|
'merchantTel' => $log->UseStoreTel ?? '',
|
|
'memo' => $log->Memo ?? '',
|
|
'useKey' => $log->UseKey ?? '',
|
|
// 저장된 계정과목 정보 병합
|
|
'accountCode' => $savedItem?->account_code ?? '',
|
|
'accountName' => $savedItem?->account_name ?? '',
|
|
// 수정 가능한 필드들 (저장된 값 또는 기본값)
|
|
'deductionType' => $savedItem?->deduction_type ?? ($log->UseStoreCorpNum ? 'deductible' : 'non_deductible'),
|
|
'evidenceName' => $savedItem?->evidence_name ?? ($log->UseStoreName ?? ''),
|
|
'description' => $savedItem?->description ?? ($log->UseStoreBizType ?? $log->Memo ?? ''),
|
|
'isSaved' => $savedItem !== null,
|
|
// 금액 수정 관련 필드
|
|
'modifiedSupplyAmount' => $savedItem?->modified_supply_amount !== null ? (float) $savedItem->modified_supply_amount : null,
|
|
'modifiedTax' => $savedItem?->modified_tax !== null ? (float) $savedItem->modified_tax : null,
|
|
'effectiveSupplyAmount' => $savedItem?->modified_supply_amount !== null
|
|
? (float) $savedItem->modified_supply_amount
|
|
: ($amount - floatval($log->Tax ?? 0)),
|
|
'effectiveTax' => $savedItem?->modified_tax !== null
|
|
? (float) $savedItem->modified_tax
|
|
: floatval($log->Tax ?? 0),
|
|
'isAmountModified' => $savedItem?->modified_supply_amount !== null || $savedItem?->modified_tax !== null,
|
|
'isManual' => (bool) ($savedItem?->is_manual ?? false),
|
|
'dbId' => $savedItem?->id ?? null,
|
|
];
|
|
|
|
// 공제/불공제 통계 집계
|
|
$deductionType = $logItem['deductionType'];
|
|
$absAmount = abs($amount);
|
|
if ($deductionType === 'deductible') {
|
|
$deductibleAmount += $absAmount;
|
|
$deductibleCount++;
|
|
$totalTax += abs($logItem['effectiveTax']);
|
|
} else {
|
|
$nonDeductibleAmount += $absAmount;
|
|
$nonDeductibleCount++;
|
|
}
|
|
|
|
$logs[] = $logItem;
|
|
}
|
|
|
|
return [
|
|
'logs' => $logs,
|
|
'summary' => [
|
|
'totalAmount' => $totalAmount,
|
|
'count' => count($logs),
|
|
'approvalCount' => $approvalCount,
|
|
'cancelCount' => $cancelCount,
|
|
'totalTax' => $totalTax,
|
|
'deductibleAmount' => $deductibleAmount,
|
|
'deductibleCount' => $deductibleCount,
|
|
'nonDeductibleAmount' => $nonDeductibleAmount,
|
|
'nonDeductibleCount' => $nonDeductibleCount,
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 에러 코드 체크
|
|
*/
|
|
private function checkErrorCode($data): ?int
|
|
{
|
|
if (isset($data->CurrentPage) && is_numeric($data->CurrentPage) && $data->CurrentPage < 0) {
|
|
return (int)$data->CurrentPage;
|
|
}
|
|
if (isset($data->CardNum) && is_numeric($data->CardNum) && $data->CardNum < 0) {
|
|
return (int)$data->CardNum;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* 에러 메시지 반환
|
|
*/
|
|
private function getErrorMessage(int $errorCode): string
|
|
{
|
|
$messages = [
|
|
-10002 => '인증 실패 (-10002). CERTKEY가 올바르지 않거나 만료되었습니다.',
|
|
-24005 => '사용자 정보 불일치 (-24005). 사업자번호를 확인해주세요.',
|
|
-24001 => '등록된 카드가 없습니다 (-24001).',
|
|
-24005 => '조회된 데이터가 없습니다 (-24005).',
|
|
-25006 => '카드번호가 잘못되었습니다 (-25006).',
|
|
-25007 => '조회 기간이 잘못되었습니다 (-25007).',
|
|
];
|
|
return $messages[$errorCode] ?? '바로빌 API 오류: ' . $errorCode;
|
|
}
|
|
|
|
/**
|
|
* 카드번호 마스킹
|
|
*/
|
|
private function maskCardNumber(string $cardNum): string
|
|
{
|
|
if (strlen($cardNum) < 8) return $cardNum;
|
|
return substr($cardNum, 0, 4) . '-****-****-' . substr($cardNum, -4);
|
|
}
|
|
|
|
/**
|
|
* 카드사 코드 -> 카드사명 변환
|
|
*/
|
|
private function getCardCompanyName(string $code): string
|
|
{
|
|
$companies = [
|
|
'01' => '비씨',
|
|
'02' => 'KB국민',
|
|
'03' => '하나(외환)',
|
|
'04' => '삼성',
|
|
'06' => '신한',
|
|
'07' => '현대',
|
|
'08' => '롯데',
|
|
'11' => 'NH농협',
|
|
'12' => '수협',
|
|
'13' => '씨티',
|
|
'14' => '우리',
|
|
'15' => '광주',
|
|
'16' => '전북',
|
|
'21' => '하나',
|
|
'22' => '제주',
|
|
'23' => 'SC제일',
|
|
'25' => 'KDB산업',
|
|
'26' => 'IBK기업',
|
|
'27' => '새마을금고',
|
|
'28' => '신협',
|
|
'29' => '저축은행',
|
|
'30' => '우체국',
|
|
'31' => '카카오뱅크',
|
|
'32' => 'K뱅크',
|
|
'33' => '토스뱅크'
|
|
];
|
|
return $companies[$code] ?? $code;
|
|
}
|
|
|
|
/**
|
|
* 할부 이름 반환
|
|
*/
|
|
private function getPaymentPlanName(string $plan): string
|
|
{
|
|
if (empty($plan) || $plan === '0' || $plan === '00') {
|
|
return '일시불';
|
|
}
|
|
return $plan . '개월';
|
|
}
|
|
|
|
/**
|
|
* 계정과목 목록 조회
|
|
*/
|
|
public function accountCodes(): JsonResponse
|
|
{
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$codes = AccountCode::getActiveByTenant($tenantId);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $codes->map(fn($c) => [
|
|
'id' => $c->id,
|
|
'code' => $c->code,
|
|
'name' => $c->name,
|
|
'category' => $c->category,
|
|
])
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* 카드 사용내역 저장 (계정과목 포함)
|
|
*/
|
|
public function save(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$transactions = $request->input('transactions', []);
|
|
|
|
if (empty($transactions)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '저장할 데이터가 없습니다.'
|
|
]);
|
|
}
|
|
|
|
$saved = 0;
|
|
$updated = 0;
|
|
|
|
DB::beginTransaction();
|
|
|
|
foreach ($transactions as $trans) {
|
|
$data = [
|
|
'tenant_id' => $tenantId,
|
|
'card_num' => $trans['cardNum'] ?? '',
|
|
'card_company' => $trans['cardCompany'] ?? '',
|
|
'card_company_name' => $trans['cardBrand'] ?? '',
|
|
'use_dt' => $trans['useDt'] ?? '',
|
|
'use_date' => $trans['useDate'] ?? '',
|
|
'use_time' => $trans['useTime'] ?? '',
|
|
'approval_num' => $trans['approvalNum'] ?? '',
|
|
'approval_type' => $trans['approvalType'] ?? '',
|
|
'approval_amount' => floatval($trans['approvalAmount'] ?? 0),
|
|
'tax' => floatval($trans['tax'] ?? 0),
|
|
'service_charge' => floatval($trans['serviceCharge'] ?? 0),
|
|
'payment_plan' => $trans['paymentPlan'] ?? '',
|
|
'currency_code' => $trans['currencyCode'] ?? 'KRW',
|
|
'merchant_name' => $trans['merchantName'] ?? '',
|
|
'merchant_biz_num' => $trans['merchantBizNum'] ?? '',
|
|
'merchant_addr' => $trans['merchantAddr'] ?? '',
|
|
'merchant_ceo' => $trans['merchantCeo'] ?? '',
|
|
'merchant_biz_type' => $trans['merchantBizType'] ?? '',
|
|
'merchant_tel' => $trans['merchantTel'] ?? '',
|
|
'memo' => $trans['memo'] ?? '',
|
|
'use_key' => $trans['useKey'] ?? '',
|
|
'account_code' => $trans['accountCode'] ?? null,
|
|
'account_name' => $trans['accountName'] ?? null,
|
|
'deduction_type' => $trans['deductionType'] ?? null,
|
|
'evidence_name' => $trans['evidenceName'] ?? null,
|
|
'description' => $trans['description'] ?? null,
|
|
];
|
|
|
|
// Upsert: 있으면 업데이트, 없으면 생성
|
|
$existing = CardTransaction::where('tenant_id', $tenantId)
|
|
->where('card_num', $data['card_num'])
|
|
->where('use_dt', $data['use_dt'])
|
|
->where('approval_num', $data['approval_num'])
|
|
->where('approval_amount', $data['approval_amount'])
|
|
->first();
|
|
|
|
// 수정 금액 처리
|
|
$modifiedSupplyAmount = isset($trans['modifiedSupplyAmount']) && $trans['modifiedSupplyAmount'] !== null
|
|
? floatval($trans['modifiedSupplyAmount']) : null;
|
|
$modifiedTax = isset($trans['modifiedTax']) && $trans['modifiedTax'] !== null
|
|
? floatval($trans['modifiedTax']) : null;
|
|
|
|
$data['modified_supply_amount'] = $modifiedSupplyAmount;
|
|
$data['modified_tax'] = $modifiedTax;
|
|
|
|
if ($existing) {
|
|
// 금액 변경 감지 및 이력 기록
|
|
$oldSupply = $existing->modified_supply_amount !== null
|
|
? (float) $existing->modified_supply_amount
|
|
: ((float) $existing->approval_amount - (float) $existing->tax);
|
|
$oldTax = $existing->modified_tax !== null
|
|
? (float) $existing->modified_tax
|
|
: (float) $existing->tax;
|
|
|
|
$newSupply = $modifiedSupplyAmount !== null
|
|
? $modifiedSupplyAmount
|
|
: ((float) $existing->approval_amount - (float) $existing->tax);
|
|
$newTax = $modifiedTax !== null
|
|
? $modifiedTax
|
|
: (float) $existing->tax;
|
|
|
|
$amountChanged = abs($oldSupply - $newSupply) > 0.01 || abs($oldTax - $newTax) > 0.01;
|
|
|
|
if ($amountChanged) {
|
|
$uniqueKey = implode('|', [
|
|
$existing->card_num,
|
|
$existing->use_dt,
|
|
$existing->approval_num,
|
|
(int) $existing->approval_amount,
|
|
]);
|
|
|
|
CardTransactionAmountLog::create([
|
|
'card_transaction_id' => $existing->id,
|
|
'original_unique_key' => $uniqueKey,
|
|
'before_supply_amount' => $oldSupply,
|
|
'before_tax' => $oldTax,
|
|
'after_supply_amount' => $newSupply,
|
|
'after_tax' => $newTax,
|
|
'modified_by' => auth()->id(),
|
|
'modified_by_name' => auth()->user()?->name ?? '',
|
|
'ip_address' => $request->ip(),
|
|
]);
|
|
}
|
|
|
|
// 계정과목 및 수정 가능한 필드들 업데이트
|
|
$existing->update([
|
|
'account_code' => $data['account_code'],
|
|
'account_name' => $data['account_name'],
|
|
'deduction_type' => $data['deduction_type'],
|
|
'evidence_name' => $data['evidence_name'],
|
|
'description' => $data['description'],
|
|
'modified_supply_amount' => $data['modified_supply_amount'],
|
|
'modified_tax' => $data['modified_tax'],
|
|
]);
|
|
$updated++;
|
|
} else {
|
|
CardTransaction::create($data);
|
|
$saved++;
|
|
}
|
|
}
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => "저장 완료: 신규 {$saved}건, 수정 {$updated}건",
|
|
'saved' => $saved,
|
|
'updated' => $updated
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
DB::rollBack();
|
|
Log::error('카드 사용내역 저장 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '저장 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 엑셀 다운로드
|
|
*/
|
|
public function exportExcel(Request $request): StreamedResponse|JsonResponse
|
|
{
|
|
try {
|
|
$startDate = $request->input('startDate', date('Ymd'));
|
|
$endDate = $request->input('endDate', date('Ymd'));
|
|
$logs = $request->input('logs', []);
|
|
$splitsData = $request->input('splits', []);
|
|
|
|
// 데이터가 없으면 안내
|
|
if (empty($logs)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '내보낼 데이터가 없습니다.'
|
|
]);
|
|
}
|
|
|
|
$filename = "카드사용내역_{$startDate}_{$endDate}.csv";
|
|
|
|
return response()->streamDownload(function () use ($logs, $splitsData) {
|
|
$handle = fopen('php://output', 'w');
|
|
|
|
// UTF-8 BOM for Excel
|
|
fprintf($handle, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
|
|
|
// 헤더
|
|
fputcsv($handle, [
|
|
'구분',
|
|
'사용일시',
|
|
'카드번호',
|
|
'카드사',
|
|
'공제여부',
|
|
'증빙/판매자상호',
|
|
'내역',
|
|
'승인금액',
|
|
'부가세',
|
|
'승인번호',
|
|
'계정과목코드',
|
|
'계정과목명',
|
|
'메모'
|
|
]);
|
|
|
|
// 데이터
|
|
foreach ($logs as $log) {
|
|
$dateTime = $log['useDateTime'] ?? '';
|
|
$cardNum = $log['cardNum'] ?? '';
|
|
$cardBrand = $log['cardBrand'] ?? '';
|
|
$approvalNum = $log['approvalNum'] ?? '';
|
|
$approvalAmount = $log['approvalAmount'] ?? 0;
|
|
$tax = $log['tax'] ?? 0;
|
|
|
|
// 고유키로 분개 데이터 확인
|
|
$uniqueKey = $log['uniqueKey'] ?? implode('|', [
|
|
$cardNum,
|
|
$log['useDt'] ?? '',
|
|
$approvalNum,
|
|
(int) $approvalAmount,
|
|
]);
|
|
|
|
$splits = $splitsData[$uniqueKey] ?? [];
|
|
$hasSplits = count($splits) > 0;
|
|
|
|
// 공제여부
|
|
$deductionType = $log['deductionType'] ?? ($log['merchantBizNum'] ? 'deductible' : 'non_deductible');
|
|
$deductionText = ($deductionType === 'non_deductible') ? '불공' : '공제';
|
|
|
|
// 증빙/판매자상호, 내역
|
|
$evidenceName = $log['evidenceName'] ?? $log['merchantName'] ?? '';
|
|
$description = $log['description'] ?? $log['merchantBizType'] ?? '';
|
|
|
|
if ($hasSplits) {
|
|
// 분개가 있는 경우: 원본 행 (합계 표시)
|
|
fputcsv($handle, [
|
|
'원본',
|
|
$dateTime,
|
|
$cardNum,
|
|
$cardBrand,
|
|
'-', // 분개된 경우 공제여부는 각 분개에서 표시
|
|
$evidenceName,
|
|
$description,
|
|
number_format($approvalAmount),
|
|
number_format($tax),
|
|
$approvalNum,
|
|
'-', // 분개된 경우 계정과목은 각 분개에서 표시
|
|
'분개됨 (' . count($splits) . '건)',
|
|
''
|
|
]);
|
|
|
|
// 각 분개 행 출력
|
|
foreach ($splits as $index => $split) {
|
|
$splitDeductionType = $split['deduction_type'] ?? $split['deductionType'] ?? 'deductible';
|
|
$splitDeductionText = ($splitDeductionType === 'non_deductible') ? '불공' : '공제';
|
|
$splitSupplyAmount = $split['split_supply_amount'] ?? $split['supplyAmount'] ?? null;
|
|
$splitTax = $split['split_tax'] ?? $split['tax'] ?? null;
|
|
$splitAmount = ($splitSupplyAmount !== null && $splitTax !== null)
|
|
? floatval($splitSupplyAmount) + floatval($splitTax)
|
|
: ($split['split_amount'] ?? $split['amount'] ?? 0);
|
|
$splitEvidenceName = $split['evidence_name'] ?? $split['evidenceName'] ?? '';
|
|
$splitDescription = $split['description'] ?? '';
|
|
$splitAccountCode = $split['account_code'] ?? $split['accountCode'] ?? '';
|
|
$splitAccountName = $split['account_name'] ?? $split['accountName'] ?? '';
|
|
$splitMemo = $split['memo'] ?? '';
|
|
|
|
fputcsv($handle, [
|
|
'└ 분개 #' . ($index + 1),
|
|
'', // 사용일시 (원본과 동일하므로 생략)
|
|
'', // 카드번호
|
|
'', // 카드사
|
|
$splitDeductionText,
|
|
$splitEvidenceName,
|
|
$splitDescription,
|
|
number_format($splitAmount),
|
|
$splitTax !== null ? number_format(floatval($splitTax)) : '',
|
|
'', // 승인번호
|
|
$splitAccountCode,
|
|
$splitAccountName,
|
|
$splitMemo
|
|
]);
|
|
}
|
|
} else {
|
|
// 분개가 없는 경우: 일반 행
|
|
fputcsv($handle, [
|
|
'일반',
|
|
$dateTime,
|
|
$cardNum,
|
|
$cardBrand,
|
|
$deductionText,
|
|
$evidenceName,
|
|
$description,
|
|
number_format($approvalAmount),
|
|
number_format($tax),
|
|
$approvalNum,
|
|
$log['accountCode'] ?? '',
|
|
$log['accountName'] ?? '',
|
|
''
|
|
]);
|
|
}
|
|
}
|
|
|
|
fclose($handle);
|
|
}, $filename, [
|
|
'Content-Type' => 'text/csv; charset=utf-8',
|
|
'Content-Disposition' => 'attachment; filename="' . $filename . '"',
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('엑셀 다운로드 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '다운로드 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 분개 내역 조회
|
|
*/
|
|
public function splits(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$startDate = $request->input('startDate', date('Ymd'));
|
|
$endDate = $request->input('endDate', date('Ymd'));
|
|
|
|
$splits = CardTransactionSplit::getByDateRange($tenantId, $startDate, $endDate);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $splits
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('분개 내역 조회 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '조회 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 분개 저장
|
|
*/
|
|
public function saveSplits(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$uniqueKey = $request->input('uniqueKey');
|
|
$originalData = $request->input('originalData', []);
|
|
$splits = $request->input('splits', []);
|
|
|
|
if (empty($uniqueKey)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '고유키가 없습니다.'
|
|
]);
|
|
}
|
|
|
|
// 분개 금액 합계 검증 (공급가액 + 부가세 합계)
|
|
$originalAmount = floatval($originalData['originalAmount'] ?? 0);
|
|
$splitTotal = array_sum(array_map(function ($s) {
|
|
if (isset($s['supplyAmount']) && isset($s['tax'])) {
|
|
return floatval($s['supplyAmount']) + floatval($s['tax']);
|
|
}
|
|
return floatval($s['amount'] ?? 0);
|
|
}, $splits));
|
|
|
|
if (abs($originalAmount - $splitTotal) > 0.01) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => "분개 금액 합계({$splitTotal})가 원본 금액({$originalAmount})과 일치하지 않습니다."
|
|
]);
|
|
}
|
|
|
|
DB::beginTransaction();
|
|
|
|
CardTransactionSplit::saveSplits($tenantId, $uniqueKey, $originalData, $splits);
|
|
|
|
DB::commit();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '분개가 저장되었습니다.',
|
|
'splitCount' => count($splits)
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
DB::rollBack();
|
|
Log::error('분개 저장 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '저장 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 분개 삭제 (원본으로 복원)
|
|
*/
|
|
public function deleteSplits(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$uniqueKey = $request->input('uniqueKey');
|
|
|
|
if (empty($uniqueKey)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '고유키가 없습니다.'
|
|
]);
|
|
}
|
|
|
|
$deleted = CardTransactionSplit::deleteSplits($tenantId, $uniqueKey);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '분개가 삭제되었습니다.',
|
|
'deleted' => $deleted
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('분개 삭제 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '삭제 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 수동 거래 등록
|
|
*/
|
|
public function storeManual(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
|
|
$validated = $request->validate([
|
|
'card_num' => 'required|string',
|
|
'card_company' => 'required|string',
|
|
'use_date' => 'required|date_format:Ymd',
|
|
'use_time' => 'nullable|string|max:6',
|
|
'approval_num' => 'nullable|string',
|
|
'approval_type' => 'required|in:1,2',
|
|
'approval_amount' => 'required|numeric',
|
|
'tax' => 'required|numeric',
|
|
'merchant_name' => 'nullable|string',
|
|
'merchant_biz_num' => 'nullable|string',
|
|
'deduction_type' => 'required|in:deductible,non_deductible',
|
|
'account_code' => 'nullable|string',
|
|
'account_name' => 'nullable|string',
|
|
'evidence_name' => 'nullable|string',
|
|
'description' => 'nullable|string',
|
|
'memo' => 'nullable|string',
|
|
]);
|
|
|
|
$useTime = $validated['use_time'] ?? '000000';
|
|
$useDt = $validated['use_date'] . $useTime;
|
|
|
|
$transaction = CardTransaction::create([
|
|
'tenant_id' => $tenantId,
|
|
'card_num' => $validated['card_num'],
|
|
'card_company' => $validated['card_company'],
|
|
'card_company_name' => $this->getCardCompanyName($validated['card_company']),
|
|
'use_dt' => $useDt,
|
|
'use_date' => $validated['use_date'],
|
|
'use_time' => $useTime,
|
|
'approval_num' => $validated['approval_num'] ?? '',
|
|
'approval_type' => $validated['approval_type'],
|
|
'approval_amount' => $validated['approval_amount'],
|
|
'tax' => $validated['tax'],
|
|
'service_charge' => 0,
|
|
'payment_plan' => '0',
|
|
'currency_code' => 'KRW',
|
|
'merchant_name' => $validated['merchant_name'] ?? '',
|
|
'merchant_biz_num' => $validated['merchant_biz_num'] ?? '',
|
|
'deduction_type' => $validated['deduction_type'],
|
|
'account_code' => $validated['account_code'] ?? null,
|
|
'account_name' => $validated['account_name'] ?? null,
|
|
'evidence_name' => $validated['evidence_name'] ?? '',
|
|
'description' => $validated['description'] ?? '',
|
|
'memo' => $validated['memo'] ?? '',
|
|
'use_key' => '',
|
|
'is_manual' => true,
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '수동 거래가 등록되었습니다.',
|
|
'data' => ['id' => $transaction->id]
|
|
]);
|
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '입력 데이터가 올바르지 않습니다.',
|
|
'errors' => $e->errors()
|
|
], 422);
|
|
} catch (\Throwable $e) {
|
|
Log::error('수동 거래 등록 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '등록 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 수동 거래 수정
|
|
*/
|
|
public function updateManual(Request $request, int $id): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
|
|
$transaction = CardTransaction::where('id', $id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('is_manual', true)
|
|
->firstOrFail();
|
|
|
|
$validated = $request->validate([
|
|
'card_num' => 'required|string',
|
|
'card_company' => 'required|string',
|
|
'use_date' => 'required|date_format:Ymd',
|
|
'use_time' => 'nullable|string|max:6',
|
|
'approval_num' => 'nullable|string',
|
|
'approval_type' => 'required|in:1,2',
|
|
'approval_amount' => 'required|numeric',
|
|
'tax' => 'required|numeric',
|
|
'merchant_name' => 'nullable|string',
|
|
'merchant_biz_num' => 'nullable|string',
|
|
'deduction_type' => 'required|in:deductible,non_deductible',
|
|
'account_code' => 'nullable|string',
|
|
'account_name' => 'nullable|string',
|
|
'evidence_name' => 'nullable|string',
|
|
'description' => 'nullable|string',
|
|
'memo' => 'nullable|string',
|
|
]);
|
|
|
|
$useTime = $validated['use_time'] ?? '000000';
|
|
$useDt = $validated['use_date'] . $useTime;
|
|
|
|
$transaction->update([
|
|
'card_num' => $validated['card_num'],
|
|
'card_company' => $validated['card_company'],
|
|
'card_company_name' => $this->getCardCompanyName($validated['card_company']),
|
|
'use_dt' => $useDt,
|
|
'use_date' => $validated['use_date'],
|
|
'use_time' => $useTime,
|
|
'approval_num' => $validated['approval_num'] ?? '',
|
|
'approval_type' => $validated['approval_type'],
|
|
'approval_amount' => $validated['approval_amount'],
|
|
'tax' => $validated['tax'],
|
|
'merchant_name' => $validated['merchant_name'] ?? '',
|
|
'merchant_biz_num' => $validated['merchant_biz_num'] ?? '',
|
|
'deduction_type' => $validated['deduction_type'],
|
|
'account_code' => $validated['account_code'] ?? null,
|
|
'account_name' => $validated['account_name'] ?? null,
|
|
'evidence_name' => $validated['evidence_name'] ?? '',
|
|
'description' => $validated['description'] ?? '',
|
|
'memo' => $validated['memo'] ?? '',
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '수동 거래가 수정되었습니다.',
|
|
]);
|
|
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '해당 거래를 찾을 수 없거나 수동 입력 건이 아닙니다.'
|
|
], 404);
|
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '입력 데이터가 올바르지 않습니다.',
|
|
'errors' => $e->errors()
|
|
], 422);
|
|
} catch (\Throwable $e) {
|
|
Log::error('수동 거래 수정 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '수정 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 수동 거래 삭제
|
|
*/
|
|
public function destroyManual(int $id): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
|
|
$transaction = CardTransaction::where('id', $id)
|
|
->where('tenant_id', $tenantId)
|
|
->where('is_manual', true)
|
|
->firstOrFail();
|
|
|
|
$transaction->delete();
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '수동 거래가 삭제되었습니다.',
|
|
]);
|
|
} catch (\Illuminate\Database\Eloquent\ModelNotFoundException $e) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '해당 거래를 찾을 수 없거나 수동 입력 건이 아닙니다.'
|
|
], 404);
|
|
} catch (\Throwable $e) {
|
|
Log::error('수동 거래 삭제 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '삭제 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 수동 입력 건을 로그 배열로 변환
|
|
*/
|
|
private function convertManualToLogs($manualTransactions): array
|
|
{
|
|
$logs = [];
|
|
$totalAmount = 0;
|
|
$approvalCount = 0;
|
|
$cancelCount = 0;
|
|
$totalTax = 0;
|
|
$deductibleAmount = 0;
|
|
$deductibleCount = 0;
|
|
$nonDeductibleAmount = 0;
|
|
$nonDeductibleCount = 0;
|
|
|
|
if (!$manualTransactions || $manualTransactions->isEmpty()) {
|
|
return [
|
|
'logs' => [],
|
|
'summary' => [
|
|
'totalAmount' => 0, 'count' => 0, 'approvalCount' => 0, 'cancelCount' => 0,
|
|
'totalTax' => 0, 'deductibleAmount' => 0, 'deductibleCount' => 0,
|
|
'nonDeductibleAmount' => 0, 'nonDeductibleCount' => 0,
|
|
]
|
|
];
|
|
}
|
|
|
|
foreach ($manualTransactions as $t) {
|
|
// 수동입력: approval_amount = 공급가액, tax = 부가세
|
|
$supplyAmount = (float) $t->approval_amount;
|
|
$tax = (float) $t->tax;
|
|
$totalApproval = $supplyAmount + $tax; // 합계금액 = 공급가액 + 부가세
|
|
$isApproval = $t->approval_type !== '2';
|
|
$useDt = $t->use_dt;
|
|
|
|
if ($isApproval) {
|
|
$totalAmount += $totalApproval;
|
|
$approvalCount++;
|
|
} else {
|
|
$cancelCount++;
|
|
}
|
|
|
|
$dateTime = '';
|
|
if (!empty($useDt) && strlen($useDt) >= 8) {
|
|
$dateTime = substr($useDt, 0, 4) . '-' . substr($useDt, 4, 2) . '-' . substr($useDt, 6, 2);
|
|
if (strlen($useDt) >= 14) {
|
|
$dateTime .= ' ' . substr($useDt, 8, 2) . ':' . substr($useDt, 10, 2) . ':' . substr($useDt, 12, 2);
|
|
}
|
|
}
|
|
|
|
$deductionType = $t->deduction_type ?? 'non_deductible';
|
|
$absAmount = abs($totalApproval);
|
|
if ($deductionType === 'deductible') {
|
|
$deductibleAmount += $absAmount;
|
|
$deductibleCount++;
|
|
$totalTax += abs($tax);
|
|
} else {
|
|
$nonDeductibleAmount += $absAmount;
|
|
$nonDeductibleCount++;
|
|
}
|
|
|
|
$logs[] = [
|
|
'cardNum' => $t->card_num,
|
|
'cardNumMasked' => $this->maskCardNumber($t->card_num),
|
|
'cardCompany' => $t->card_company,
|
|
'cardBrand' => $t->card_company_name ?: $this->getCardCompanyName($t->card_company),
|
|
'useDt' => $useDt,
|
|
'useDate' => $t->use_date,
|
|
'useTime' => $t->use_time,
|
|
'useDateTime' => $dateTime,
|
|
'approvalNum' => $t->approval_num ?? '',
|
|
'approvalType' => $isApproval ? '1' : '2',
|
|
'approvalTypeName' => $isApproval ? '승인' : '취소',
|
|
'approvalAmount' => $totalApproval,
|
|
'approvalAmountFormatted' => number_format($totalApproval),
|
|
'tax' => $tax,
|
|
'taxFormatted' => number_format($tax),
|
|
'serviceCharge' => 0,
|
|
'paymentPlan' => '0',
|
|
'paymentPlanName' => '일시불',
|
|
'currencyCode' => 'KRW',
|
|
'merchantName' => $t->merchant_name ?? '',
|
|
'merchantBizNum' => $t->merchant_biz_num ?? '',
|
|
'merchantAddr' => $t->merchant_addr ?? '',
|
|
'merchantCeo' => $t->merchant_ceo ?? '',
|
|
'merchantBizType' => $t->merchant_biz_type ?? '',
|
|
'merchantTel' => $t->merchant_tel ?? '',
|
|
'memo' => $t->memo ?? '',
|
|
'useKey' => $t->use_key ?? '',
|
|
'accountCode' => $t->account_code ?? '',
|
|
'accountName' => $t->account_name ?? '',
|
|
'deductionType' => $deductionType,
|
|
'evidenceName' => $t->evidence_name ?? '',
|
|
'description' => $t->description ?? '',
|
|
'isSaved' => true,
|
|
'modifiedSupplyAmount' => null,
|
|
'modifiedTax' => null,
|
|
'effectiveSupplyAmount' => $supplyAmount,
|
|
'effectiveTax' => $tax,
|
|
'isAmountModified' => false,
|
|
'isManual' => true,
|
|
'dbId' => $t->id,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'logs' => $logs,
|
|
'summary' => [
|
|
'totalAmount' => $totalAmount,
|
|
'count' => count($logs),
|
|
'approvalCount' => $approvalCount,
|
|
'cancelCount' => $cancelCount,
|
|
'totalTax' => $totalTax,
|
|
'deductibleAmount' => $deductibleAmount,
|
|
'deductibleCount' => $deductibleCount,
|
|
'nonDeductibleAmount' => $nonDeductibleAmount,
|
|
'nonDeductibleCount' => $nonDeductibleCount,
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 두 summary 배열 합산
|
|
*/
|
|
private function mergeSummaries(array $a, array $b): array
|
|
{
|
|
return [
|
|
'totalAmount' => ($a['totalAmount'] ?? 0) + ($b['totalAmount'] ?? 0),
|
|
'count' => ($a['count'] ?? 0) + ($b['count'] ?? 0),
|
|
'approvalCount' => ($a['approvalCount'] ?? 0) + ($b['approvalCount'] ?? 0),
|
|
'cancelCount' => ($a['cancelCount'] ?? 0) + ($b['cancelCount'] ?? 0),
|
|
'totalTax' => ($a['totalTax'] ?? 0) + ($b['totalTax'] ?? 0),
|
|
'deductibleAmount' => ($a['deductibleAmount'] ?? 0) + ($b['deductibleAmount'] ?? 0),
|
|
'deductibleCount' => ($a['deductibleCount'] ?? 0) + ($b['deductibleCount'] ?? 0),
|
|
'nonDeductibleAmount' => ($a['nonDeductibleAmount'] ?? 0) + ($b['nonDeductibleAmount'] ?? 0),
|
|
'nonDeductibleCount' => ($a['nonDeductibleCount'] ?? 0) + ($b['nonDeductibleCount'] ?? 0),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 숨김 키로 로그 필터링
|
|
*/
|
|
private function filterHiddenLogs(array $parsed, $hiddenSet): array
|
|
{
|
|
$filteredLogs = [];
|
|
$totalAmount = 0;
|
|
$approvalCount = 0;
|
|
$cancelCount = 0;
|
|
$totalTax = 0;
|
|
$deductibleAmount = 0;
|
|
$deductibleCount = 0;
|
|
$nonDeductibleAmount = 0;
|
|
$nonDeductibleCount = 0;
|
|
|
|
foreach ($parsed['logs'] as $log) {
|
|
$uniqueKey = implode('|', [
|
|
$log['cardNum'],
|
|
$log['useDt'],
|
|
$log['approvalNum'],
|
|
(int) $log['approvalAmount'],
|
|
]);
|
|
|
|
if ($hiddenSet->has($uniqueKey)) {
|
|
continue;
|
|
}
|
|
|
|
$filteredLogs[] = $log;
|
|
|
|
$amount = floatval($log['approvalAmount']);
|
|
$isApproval = $log['approvalType'] !== '2';
|
|
if ($isApproval) {
|
|
$totalAmount += $amount;
|
|
$approvalCount++;
|
|
} else {
|
|
$cancelCount++;
|
|
}
|
|
|
|
$deductionType = $log['deductionType'] ?? 'non_deductible';
|
|
$absAmount = abs($amount);
|
|
if ($deductionType === 'deductible') {
|
|
$deductibleAmount += $absAmount;
|
|
$deductibleCount++;
|
|
$totalTax += abs($log['effectiveTax'] ?? $log['tax'] ?? 0);
|
|
} else {
|
|
$nonDeductibleAmount += $absAmount;
|
|
$nonDeductibleCount++;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'logs' => $filteredLogs,
|
|
'summary' => [
|
|
'totalAmount' => $totalAmount,
|
|
'count' => count($filteredLogs),
|
|
'approvalCount' => $approvalCount,
|
|
'cancelCount' => $cancelCount,
|
|
'totalTax' => $totalTax,
|
|
'deductibleAmount' => $deductibleAmount,
|
|
'deductibleCount' => $deductibleCount,
|
|
'nonDeductibleAmount' => $nonDeductibleAmount,
|
|
'nonDeductibleCount' => $nonDeductibleCount,
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* 거래 숨김 처리
|
|
*/
|
|
public function hideTransaction(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$uniqueKey = $request->input('uniqueKey');
|
|
$originalData = $request->input('originalData', []);
|
|
|
|
if (empty($uniqueKey)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '고유키가 없습니다.'
|
|
]);
|
|
}
|
|
|
|
// 이미 숨김 처리된 건인지 확인
|
|
$exists = CardTransactionHide::where('tenant_id', $tenantId)
|
|
->where('original_unique_key', $uniqueKey)
|
|
->exists();
|
|
|
|
if ($exists) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '이미 숨김 처리된 거래입니다.'
|
|
]);
|
|
}
|
|
|
|
CardTransactionHide::hideTransaction($tenantId, $uniqueKey, $originalData, auth()->id());
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '거래가 숨김 처리되었습니다.'
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('거래 숨김 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '숨김 처리 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 거래 복원 (숨김 해제)
|
|
*/
|
|
public function restoreTransaction(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$uniqueKey = $request->input('uniqueKey');
|
|
|
|
if (empty($uniqueKey)) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '고유키가 없습니다.'
|
|
]);
|
|
}
|
|
|
|
$deleted = CardTransactionHide::restoreTransaction($tenantId, $uniqueKey);
|
|
|
|
if ($deleted === 0) {
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '숨김 데이터를 찾을 수 없습니다.'
|
|
]);
|
|
}
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'message' => '거래가 복원되었습니다.'
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('거래 복원 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '복원 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 숨김 처리된 거래 목록 조회
|
|
*/
|
|
public function hiddenTransactions(Request $request): JsonResponse
|
|
{
|
|
try {
|
|
$tenantId = session('selected_tenant_id', self::HEADQUARTERS_TENANT_ID);
|
|
$startDate = $request->input('startDate', date('Ymd'));
|
|
$endDate = $request->input('endDate', date('Ymd'));
|
|
|
|
$hidden = CardTransactionHide::where('tenant_id', $tenantId)
|
|
->where('use_date', '>=', $startDate)
|
|
->where('use_date', '<=', $endDate)
|
|
->orderBy('use_date', 'desc')
|
|
->get()
|
|
->map(fn($h) => [
|
|
'id' => $h->id,
|
|
'uniqueKey' => $h->original_unique_key,
|
|
'cardNum' => $h->card_num,
|
|
'useDate' => $h->use_date,
|
|
'approvalNum' => $h->approval_num,
|
|
'originalAmount' => (float) $h->original_amount,
|
|
'originalAmountFormatted' => number_format((float) $h->original_amount),
|
|
'merchantName' => $h->merchant_name,
|
|
'hiddenAt' => $h->created_at?->format('Y-m-d H:i'),
|
|
]);
|
|
|
|
return response()->json([
|
|
'success' => true,
|
|
'data' => $hidden,
|
|
'count' => $hidden->count()
|
|
]);
|
|
} catch (\Throwable $e) {
|
|
Log::error('숨김 목록 조회 오류: ' . $e->getMessage());
|
|
return response()->json([
|
|
'success' => false,
|
|
'error' => '조회 오류: ' . $e->getMessage()
|
|
]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* SOAP 호출
|
|
*/
|
|
private function callSoap(string $method, array $params = []): array
|
|
{
|
|
if (!$this->soapClient) {
|
|
return [
|
|
'success' => false,
|
|
'error' => '바로빌 SOAP 클라이언트가 초기화되지 않았습니다.'
|
|
];
|
|
}
|
|
|
|
if (empty($this->certKey) && !$this->isTestMode) {
|
|
return [
|
|
'success' => false,
|
|
'error' => 'CERTKEY가 설정되지 않았습니다.'
|
|
];
|
|
}
|
|
|
|
if (empty($this->corpNum)) {
|
|
return [
|
|
'success' => false,
|
|
'error' => '사업자번호가 설정되지 않았습니다.'
|
|
];
|
|
}
|
|
|
|
// CERTKEY와 CorpNum 자동 추가
|
|
if (!isset($params['CERTKEY'])) {
|
|
$params['CERTKEY'] = $this->certKey ?? '';
|
|
}
|
|
if (!isset($params['CorpNum'])) {
|
|
$params['CorpNum'] = $this->corpNum;
|
|
}
|
|
|
|
try {
|
|
Log::info("바로빌 카드 API 호출 - Method: {$method}, CorpNum: {$this->corpNum}");
|
|
|
|
$result = $this->soapClient->$method($params);
|
|
$resultProperty = $method . 'Result';
|
|
|
|
if (isset($result->$resultProperty)) {
|
|
$resultData = $result->$resultProperty;
|
|
|
|
// 에러 코드 체크
|
|
if (is_numeric($resultData) && $resultData < 0) {
|
|
return [
|
|
'success' => false,
|
|
'error' => $this->getErrorMessage((int)$resultData),
|
|
'error_code' => (int)$resultData
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => $resultData
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'data' => $result
|
|
];
|
|
} catch (\SoapFault $e) {
|
|
Log::error('바로빌 SOAP 오류: ' . $e->getMessage());
|
|
return [
|
|
'success' => false,
|
|
'error' => 'SOAP 오류: ' . $e->getMessage(),
|
|
'error_code' => $e->getCode()
|
|
];
|
|
} catch (\Throwable $e) {
|
|
Log::error('바로빌 API 호출 오류: ' . $e->getMessage());
|
|
return [
|
|
'success' => false,
|
|
'error' => 'API 호출 오류: ' . $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
}
|