Files
sam-manage/app/Services/Barobill/BarobillCardSyncService.php
김보곤 ae7dcf2a34 fix: [barobill] 카드 동기화 SOAP 응답 파싱 수정
- GetCardEx2 응답 구조: CardInfo → CardEx로 수정
2026-03-19 19:18:32 +09:00

403 lines
14 KiB
PHP

<?php
namespace App\Services\Barobill;
use App\Models\Barobill\BarobillConfig;
use App\Models\Barobill\BarobillMember;
use App\Models\Barobill\CardTransaction;
use Carbon\Carbon;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
/**
* 바로빌 카드 사용내역 자동 동기화 서비스
*
* 스케줄러에서 주기적으로 호출하여 바로빌 SOAP API의 카드 거래를
* barobill_card_transactions 테이블에 자동 동기화한다.
*
* - 신규 거래: DB에 자동 등록 (계정과목 등 사용자 입력 필드는 비워둠)
* - 기존 거래: 바로빌 원본 필드만 갱신 (사용자 편집 필드 보존)
*/
class BarobillCardSyncService
{
private ?string $certKey = null;
private ?string $corpNum = null;
private bool $isTestMode = false;
private ?string $soapUrl = null;
private ?\SoapClient $soapClient = null;
/**
* 전체 테넌트 카드 거래 동기화
*
* @param int $days 동기화 대상 일수 (기본 7일)
* @return array{synced: int, created: int, updated: int, errors: array}
*/
public function syncAll(int $days = 7): array
{
$members = BarobillMember::whereNotNull('barobill_id')
->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()];
}
}
}