403 lines
14 KiB
PHP
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()];
|
|
}
|
|
}
|
|
}
|