where('barobill_id', '!=', '') ->get(); $totalCreated = 0; $totalUpdated = 0; $errors = []; foreach ($members as $member) { try { $result = $this->syncTenant($member->tenant_id, $days); $totalCreated += $result['created']; $totalUpdated += $result['updated']; } catch (\Throwable $e) { $errors[] = "tenant:{$member->tenant_id} - {$e->getMessage()}"; Log::error('[CardSync] 테넌트 동기화 실패', [ 'tenantId' => $member->tenant_id, 'error' => $e->getMessage(), ]); } } return [ 'synced' => $members->count(), 'created' => $totalCreated, 'updated' => $totalUpdated, 'errors' => $errors, ]; } /** * 특정 테넌트 카드 거래 동기화 * * @return array{created: int, updated: int} */ public function syncTenant(int $tenantId, int $days = 7): array { $member = BarobillMember::where('tenant_id', $tenantId)->first(); if (! $member || empty($member->barobill_id)) { return ['created' => 0, 'updated' => 0]; } $this->initFromConfig($member); if (! $this->soapClient) { Log::warning('[CardSync] SOAP 클라이언트 초기화 실패', ['tenantId' => $tenantId]); return ['created' => 0, 'updated' => 0]; } // 등록된 카드 목록 조회 $cards = $this->getRegisteredCards($member->barobill_id); if (empty($cards)) { Log::debug('[CardSync] 등록된 카드 없음', ['tenantId' => $tenantId]); return ['created' => 0, 'updated' => 0]; } $endDate = Carbon::now()->format('Ymd'); $startDate = Carbon::now()->subDays($days)->format('Ymd'); $totalCreated = 0; $totalUpdated = 0; foreach ($cards as $cardNum) { $transactions = $this->fetchTransactions($member->barobill_id, $cardNum, $startDate, $endDate); if (! empty($transactions)) { $result = $this->upsertTransactions($tenantId, $transactions); $totalCreated += $result['created']; $totalUpdated += $result['updated']; } } Log::info('[CardSync] 동기화 완료', [ 'tenantId' => $tenantId, 'cards' => count($cards), 'created' => $totalCreated, 'updated' => $totalUpdated, 'period' => "{$startDate}~{$endDate}", ]); return ['created' => $totalCreated, 'updated' => $totalUpdated]; } /** * BarobillConfig + BarobillMember 설정으로 SOAP 초기화 (CARD.asmx) */ private function initFromConfig(BarobillMember $member): void { $memberTestMode = $member->isTestMode(); $targetEnv = $memberTestMode ? 'test' : 'production'; $config = BarobillConfig::where('environment', $targetEnv)->first(); if (! $config) { $config = BarobillConfig::where('is_active', true)->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'; } $this->initSoapClient(); } private function initSoapClient(): void { $this->soapClient = null; 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_BOTH, ]); } catch (\Throwable $e) { Log::error('[CardSync] SOAP 클라이언트 생성 실패: '.$e->getMessage()); } } } /** * 등록된 카드번호 목록 조회 (GetCardEx2) * * @return string[] 카드번호 배열 */ private function getRegisteredCards(string $userId): array { $result = $this->callSoap('GetCardEx2', [ 'ID' => $userId, 'AvailOnly' => 0, ]); if (! $result['success']) { return []; } $data = $result['data']; $cardList = []; // GetCardEx2는 CardEx 배열을 반환 if (isset($data->CardEx)) { $cardList = is_array($data->CardEx) ? $data->CardEx : [$data->CardEx]; } $cards = []; foreach ($cardList as $card) { if (! is_object($card)) { continue; } $cardNum = $card->CardNum ?? ''; if (! empty($cardNum) && ! (is_numeric($cardNum) && $cardNum < 0)) { $cards[] = $cardNum; } } return $cards; } /** * 바로빌에서 카드 사용내역 조회 (GetPeriodCardApprovalLog) * * @return array 파싱된 거래 배열 */ private function fetchTransactions(string $userId, string $cardNum, string $startDate, string $endDate): array { $result = $this->callSoap('GetPeriodCardApprovalLog', [ 'ID' => $userId, 'CardNum' => $cardNum, 'StartDate' => $startDate, 'EndDate' => $endDate, 'CountPerPage' => 10000, 'CurrentPage' => 1, 'OrderDirection' => 2, ]); if (! $result['success']) { return []; } $data = $result['data']; // 에러 코드 체크 if (is_numeric($data) && $data < 0) { return []; } $rawLogs = []; if (isset($data->CardLogList) && isset($data->CardLogList->CardLog)) { $logs = $data->CardLogList->CardLog; $rawLogs = is_array($logs) ? $logs : [$logs]; } // SOAP 응답을 DB 저장용 배열로 변환 $transactions = []; foreach ($rawLogs as $log) { $useDT = $log->UseDT ?? ''; $useDate = strlen($useDT) >= 8 ? substr($useDT, 0, 8) : ''; $useTime = strlen($useDT) >= 14 ? substr($useDT, 8, 6) : ''; $transactions[] = [ 'card_num' => $cardNum, 'card_company' => $log->CardCompany ?? '', 'card_company_name' => $this->getCardBrandName($log->CardCompany ?? ''), 'use_dt' => $useDT, 'use_date' => $useDate, 'use_time' => $useTime, 'approval_num' => $log->ApprovalNum ?? '', 'approval_type' => $log->ApprovalType ?? '1', 'approval_amount' => floatval($log->ApprovalAmount ?? 0), 'tax' => floatval($log->Tax ?? 0), 'service_charge' => floatval($log->ServiceCharge ?? 0), 'payment_plan' => $log->PaymentPlan ?? '', 'currency_code' => $log->CurrencyCode ?? 'KRW', 'merchant_name' => $log->MerchantName ?? '', 'merchant_biz_num' => $log->MerchantBizNum ?? '', 'merchant_addr' => $log->MerchantAddr ?? '', 'merchant_ceo' => $log->MerchantCeo ?? '', 'merchant_biz_type' => $log->MerchantBizType ?? '', 'merchant_tel' => $log->MerchantTel ?? '', 'memo' => $log->Memo ?? '', 'use_key' => $log->UseKey ?? '', ]; } return $transactions; } /** * 거래 데이터 Upsert (사용자 편집 필드 보존) * * @return array{created: int, updated: int} */ private function upsertTransactions(int $tenantId, array $transactions): array { $created = 0; $updated = 0; DB::beginTransaction(); try { foreach ($transactions as $trans) { // 고유 키: tenant_id + card_num + use_dt + approval_num + approval_amount $existing = CardTransaction::where('tenant_id', $tenantId) ->where('card_num', $trans['card_num']) ->where('use_dt', $trans['use_dt']) ->where('approval_num', $trans['approval_num']) ->where('approval_amount', $trans['approval_amount']) ->first(); if ($existing) { // 기존 레코드: 바로빌 원본 필드만 갱신 (사용자 편집 필드 보존) $existing->update([ 'card_company' => $trans['card_company'], 'card_company_name' => $trans['card_company_name'], 'merchant_name' => $trans['merchant_name'], 'merchant_biz_num' => $trans['merchant_biz_num'], 'merchant_addr' => $trans['merchant_addr'], 'merchant_ceo' => $trans['merchant_ceo'], 'merchant_biz_type' => $trans['merchant_biz_type'], 'merchant_tel' => $trans['merchant_tel'], 'memo' => $trans['memo'], 'use_key' => $trans['use_key'], ]); $updated++; } else { // 신규 레코드: 바로빌 데이터로 생성 (사용자 입력 필드는 null) CardTransaction::create(array_merge($trans, [ 'tenant_id' => $tenantId, ])); $created++; } } DB::commit(); } catch (\Throwable $e) { DB::rollBack(); Log::error('[CardSync] Upsert 실패', [ 'tenantId' => $tenantId, 'error' => $e->getMessage(), ]); throw $e; } return ['created' => $created, 'updated' => $updated]; } /** * 카드사 코드 → 카드사명 변환 */ private function getCardBrandName(string $code): string { $brands = [ '01' => 'BC', '02' => 'KB국민', '03' => '하나(외환)', '04' => '삼성', '06' => '신한', '07' => '현대', '08' => '롯데', '11' => 'NH농협', '12' => '수협', '13' => '씨티', '14' => '우리', '15' => '광주', '16' => '전북', '21' => '하나', '22' => '제주', ]; return $brands[$code] ?? $code; } private function callSoap(string $method, array $params = []): array { if (! $this->soapClient) { return ['success' => false, 'error' => 'SOAP 클라이언트 미초기화']; } if (! isset($params['CERTKEY'])) { $params['CERTKEY'] = $this->certKey ?? ''; } if (! isset($params['CorpNum'])) { $params['CorpNum'] = $this->corpNum; } try { $soapStartTime = microtime(true); $result = $this->soapClient->$method($params); $resultProperty = $method.'Result'; $elapsed = round((microtime(true) - $soapStartTime) * 1000); Log::debug("[CardSync] SOAP {$method} 완료 ({$elapsed}ms)"); if (isset($result->$resultProperty)) { $resultData = $result->$resultProperty; if (is_numeric($resultData) && $resultData < 0) { return [ 'success' => false, 'error' => "바로빌 API 오류: {$resultData}", 'data' => $resultData, ]; } return ['success' => true, 'data' => $resultData]; } return ['success' => true, 'data' => $result]; } catch (\Throwable $e) { Log::error('[CardSync] SOAP 오류: '.$e->getMessage()); return ['success' => false, 'error' => $e->getMessage()]; } } }