feat: [barobill] SOAP 동기화 서비스 신규 구축
- BarobillSoapService: PHP SoapClient 기반 SOAP 래퍼 (회원/계좌/카드/인증서) - BarobillBankSyncService: 은행 거래내역 SOAP 조회 → DB 캐시 동기화 - BarobillCardSyncService: 카드 거래내역 SOAP 조회 → DB 캐시 동기화 - HometaxSyncService: 홈택스 세금계산서 upsert 동기화 - BarobillSyncController: 동기화/회원/인증서/잔액 API 11개 엔드포인트 - SyncBarobillDataJob: 매일 06:00/06:30 자동 동기화 스케줄러 - BarobillController.status() 보강: 실제 계좌/카드 수 표시
This commit is contained in:
@@ -4,13 +4,18 @@
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Barobill\BarobillBankTransaction;
|
||||
use App\Models\Barobill\BarobillCardTransaction;
|
||||
use App\Models\Barobill\BarobillMember;
|
||||
use App\Services\Barobill\BarobillSoapService;
|
||||
use App\Services\BarobillService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BarobillController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private BarobillService $barobillService
|
||||
private BarobillService $barobillService,
|
||||
private BarobillSoapService $soapService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
@@ -19,17 +24,43 @@ public function __construct(
|
||||
public function status()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$tenantId = app('tenant_id');
|
||||
$setting = $this->barobillService->getSetting();
|
||||
$member = BarobillMember::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->first();
|
||||
|
||||
$accountCount = 0;
|
||||
$cardCount = 0;
|
||||
|
||||
if ($member) {
|
||||
$accountCount = BarobillBankTransaction::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->distinct('bank_account_num')
|
||||
->count('bank_account_num');
|
||||
|
||||
$cardCount = BarobillCardTransaction::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->distinct('card_num')
|
||||
->count('card_num');
|
||||
}
|
||||
|
||||
return [
|
||||
'bank_service_count' => 0,
|
||||
'account_link_count' => 0,
|
||||
'member' => $setting ? [
|
||||
'bank_service_count' => $accountCount,
|
||||
'account_link_count' => $accountCount,
|
||||
'card_count' => $cardCount,
|
||||
'member' => $member ? [
|
||||
'barobill_id' => $member->barobill_id,
|
||||
'biz_no' => $member->formatted_biz_no,
|
||||
'corp_name' => $member->corp_name,
|
||||
'status' => $member->status,
|
||||
'server_mode' => $member->server_mode ?? 'test',
|
||||
] : ($setting ? [
|
||||
'barobill_id' => $setting->barobill_id,
|
||||
'biz_no' => $setting->corp_num,
|
||||
'status' => $setting->isVerified() ? 'active' : 'inactive',
|
||||
'server_mode' => $this->barobillService->isTestMode() ? 'test' : 'production',
|
||||
] : null,
|
||||
] : null),
|
||||
];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
306
app/Http/Controllers/Api/V1/BarobillSyncController.php
Normal file
306
app/Http/Controllers/Api/V1/BarobillSyncController.php
Normal file
@@ -0,0 +1,306 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1;
|
||||
|
||||
use App\Helpers\ApiResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\Barobill\BarobillMember;
|
||||
use App\Services\Barobill\BarobillBankSyncService;
|
||||
use App\Services\Barobill\BarobillCardSyncService;
|
||||
use App\Services\Barobill\BarobillSoapService;
|
||||
use App\Services\Barobill\HometaxSyncService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class BarobillSyncController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private BarobillSoapService $soapService,
|
||||
private BarobillBankSyncService $bankSyncService,
|
||||
private BarobillCardSyncService $cardSyncService,
|
||||
private HometaxSyncService $hometaxSyncService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 수동 은행 동기화
|
||||
*/
|
||||
public function syncBank(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'start_date' => 'nullable|date_format:Ymd',
|
||||
'end_date' => 'nullable|date_format:Ymd',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($data) {
|
||||
$tenantId = app('tenant_id');
|
||||
$startDate = $data['start_date'] ?? Carbon::now()->subMonth()->format('Ymd');
|
||||
$endDate = $data['end_date'] ?? Carbon::now()->format('Ymd');
|
||||
|
||||
return $this->bankSyncService->syncIfNeeded($tenantId, $startDate, $endDate);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 카드 동기화
|
||||
*/
|
||||
public function syncCard(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'start_date' => 'nullable|date_format:Ymd',
|
||||
'end_date' => 'nullable|date_format:Ymd',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($data) {
|
||||
$tenantId = app('tenant_id');
|
||||
$startDate = $data['start_date'] ?? Carbon::now()->subMonth()->format('Ymd');
|
||||
$endDate = $data['end_date'] ?? Carbon::now()->format('Ymd');
|
||||
|
||||
return $this->cardSyncService->syncCardTransactions($tenantId, $startDate, $endDate);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 수동 홈택스 동기화
|
||||
*/
|
||||
public function syncHometax(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'invoices' => 'required|array',
|
||||
'invoices.*.ntsConfirmNum' => 'required|string',
|
||||
'invoice_type' => 'required|in:sales,purchase',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($data) {
|
||||
$tenantId = app('tenant_id');
|
||||
|
||||
return $this->hometaxSyncService->syncInvoices(
|
||||
$data['invoices'],
|
||||
$tenantId,
|
||||
$data['invoice_type']
|
||||
);
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 바로빌 등록계좌 목록 (SOAP 실시간)
|
||||
*/
|
||||
public function accounts()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$member = $this->getMember();
|
||||
if (! $member) {
|
||||
return ['accounts' => [], 'message' => '바로빌 회원 정보 없음'];
|
||||
}
|
||||
|
||||
$this->soapService->initForMember($member);
|
||||
|
||||
return [
|
||||
'accounts' => $this->bankSyncService->getRegisteredAccounts($member),
|
||||
];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 바로빌 등록카드 목록 (SOAP 실시간)
|
||||
*/
|
||||
public function cards()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$member = $this->getMember();
|
||||
if (! $member) {
|
||||
return ['cards' => [], 'message' => '바로빌 회원 정보 없음'];
|
||||
}
|
||||
|
||||
$this->soapService->initForMember($member);
|
||||
|
||||
return [
|
||||
'cards' => $this->cardSyncService->getRegisteredCards($member),
|
||||
];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증서 상태 조회 (만료일, 유효성)
|
||||
*/
|
||||
public function certificate()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$member = $this->getMember();
|
||||
if (! $member) {
|
||||
return ['certificate' => null, 'message' => '바로빌 회원 정보 없음'];
|
||||
}
|
||||
|
||||
$this->soapService->initForMember($member);
|
||||
$corpNum = $member->biz_no;
|
||||
|
||||
$valid = $this->soapService->checkCertificateValid($corpNum);
|
||||
$expireDate = $this->soapService->getCertificateExpireDate($corpNum);
|
||||
$registDate = $this->soapService->getCertificateRegistDate($corpNum);
|
||||
|
||||
return [
|
||||
'certificate' => [
|
||||
'is_valid' => $valid['success'] && ($valid['data'] ?? 0) >= 0,
|
||||
'expire_date' => $expireDate['success'] ? ($expireDate['data'] ?? null) : null,
|
||||
'regist_date' => $registDate['success'] ? ($registDate['data'] ?? null) : null,
|
||||
],
|
||||
];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 충전잔액 조회
|
||||
*/
|
||||
public function balance()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$member = $this->getMember();
|
||||
if (! $member) {
|
||||
return ['balance' => null, 'message' => '바로빌 회원 정보 없음'];
|
||||
}
|
||||
|
||||
$this->soapService->initForMember($member);
|
||||
$result = $this->soapService->getBalanceCostAmount($member->biz_no);
|
||||
|
||||
return [
|
||||
'balance' => $result['success'] ? ($result['data'] ?? 0) : null,
|
||||
'success' => $result['success'],
|
||||
'error' => $result['error'] ?? null,
|
||||
];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 바로빌 회원 등록 (SOAP RegistCorp)
|
||||
*/
|
||||
public function registerMember(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'biz_no' => 'required|string|size:10',
|
||||
'corp_name' => 'required|string',
|
||||
'ceo_name' => 'required|string',
|
||||
'biz_type' => 'nullable|string',
|
||||
'biz_class' => 'nullable|string',
|
||||
'addr' => 'nullable|string',
|
||||
'barobill_id' => 'required|string',
|
||||
'barobill_pwd' => 'required|string',
|
||||
'manager_name' => 'nullable|string',
|
||||
'manager_hp' => 'nullable|string',
|
||||
'manager_email' => 'nullable|email',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($data) {
|
||||
$tenantId = app('tenant_id');
|
||||
$this->soapService->initForMember(
|
||||
BarobillMember::withoutGlobalScopes()->where('tenant_id', $tenantId)->first()
|
||||
?? new BarobillMember(['server_mode' => 'test'])
|
||||
);
|
||||
|
||||
$result = $this->soapService->registCorp($data);
|
||||
|
||||
if ($result['success']) {
|
||||
BarobillMember::withoutGlobalScopes()->updateOrCreate(
|
||||
['tenant_id' => $tenantId],
|
||||
[
|
||||
'biz_no' => $data['biz_no'],
|
||||
'corp_name' => $data['corp_name'],
|
||||
'ceo_name' => $data['ceo_name'],
|
||||
'biz_type' => $data['biz_type'] ?? null,
|
||||
'biz_class' => $data['biz_class'] ?? null,
|
||||
'addr' => $data['addr'] ?? null,
|
||||
'barobill_id' => $data['barobill_id'],
|
||||
'barobill_pwd' => $data['barobill_pwd'],
|
||||
'manager_name' => $data['manager_name'] ?? null,
|
||||
'manager_hp' => $data['manager_hp'] ?? null,
|
||||
'manager_email' => $data['manager_email'] ?? null,
|
||||
'status' => 'active',
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}, __('message.created'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 바로빌 회원 수정 (SOAP UpdateCorpInfo)
|
||||
*/
|
||||
public function updateMember(Request $request)
|
||||
{
|
||||
$data = $request->validate([
|
||||
'corp_name' => 'required|string',
|
||||
'ceo_name' => 'required|string',
|
||||
'biz_type' => 'nullable|string',
|
||||
'biz_class' => 'nullable|string',
|
||||
'addr' => 'nullable|string',
|
||||
'manager_name' => 'nullable|string',
|
||||
'manager_hp' => 'nullable|string',
|
||||
'manager_email' => 'nullable|email',
|
||||
]);
|
||||
|
||||
return ApiResponse::handle(function () use ($data) {
|
||||
$member = $this->getMember();
|
||||
if (! $member) {
|
||||
return ['error' => 'NO_MEMBER', 'code' => 404, 'message' => '바로빌 회원 정보 없음'];
|
||||
}
|
||||
|
||||
$this->soapService->initForMember($member);
|
||||
|
||||
$data['biz_no'] = $member->biz_no;
|
||||
$result = $this->soapService->updateCorpInfo($data);
|
||||
|
||||
if ($result['success']) {
|
||||
$member->update([
|
||||
'corp_name' => $data['corp_name'],
|
||||
'ceo_name' => $data['ceo_name'],
|
||||
'biz_type' => $data['biz_type'] ?? $member->biz_type,
|
||||
'biz_class' => $data['biz_class'] ?? $member->biz_class,
|
||||
'addr' => $data['addr'] ?? $member->addr,
|
||||
'manager_name' => $data['manager_name'] ?? $member->manager_name,
|
||||
'manager_hp' => $data['manager_hp'] ?? $member->manager_hp,
|
||||
'manager_email' => $data['manager_email'] ?? $member->manager_email,
|
||||
]);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}, __('message.updated'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 바로빌 회원 상태 (SOAP GetCorpState)
|
||||
*/
|
||||
public function memberStatus()
|
||||
{
|
||||
return ApiResponse::handle(function () {
|
||||
$member = $this->getMember();
|
||||
if (! $member) {
|
||||
return ['status' => null, 'message' => '바로빌 회원 정보 없음'];
|
||||
}
|
||||
|
||||
$this->soapService->initForMember($member);
|
||||
$result = $this->soapService->getCorpState($member->biz_no);
|
||||
|
||||
return [
|
||||
'member' => [
|
||||
'biz_no' => $member->formatted_biz_no,
|
||||
'corp_name' => $member->corp_name,
|
||||
'status' => $member->status,
|
||||
'server_mode' => $member->server_mode,
|
||||
],
|
||||
'barobill_state' => $result['success'] ? $result['data'] : null,
|
||||
'error' => $result['error'] ?? null,
|
||||
];
|
||||
}, __('message.fetched'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 현재 테넌트의 바로빌 회원 조회
|
||||
*/
|
||||
private function getMember(): ?BarobillMember
|
||||
{
|
||||
$tenantId = app('tenant_id');
|
||||
|
||||
return BarobillMember::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->first();
|
||||
}
|
||||
}
|
||||
85
app/Jobs/Barobill/SyncBarobillDataJob.php
Normal file
85
app/Jobs/Barobill/SyncBarobillDataJob.php
Normal file
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs\Barobill;
|
||||
|
||||
use App\Models\Barobill\BarobillMember;
|
||||
use App\Services\Barobill\BarobillBankSyncService;
|
||||
use App\Services\Barobill\BarobillCardSyncService;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 바로빌 데이터 자동 동기화 Job
|
||||
*
|
||||
* 스케줄러에서 매일 실행하여 활성 회원의 은행/카드 거래내역을 동기화한다.
|
||||
*/
|
||||
class SyncBarobillDataJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public int $tries = 3;
|
||||
|
||||
public int $timeout = 300;
|
||||
|
||||
public function __construct(
|
||||
private string $syncType = 'all',
|
||||
) {}
|
||||
|
||||
public function handle(
|
||||
BarobillBankSyncService $bankSyncService,
|
||||
BarobillCardSyncService $cardSyncService,
|
||||
): void {
|
||||
$members = BarobillMember::withoutGlobalScopes()
|
||||
->where('status', 'active')
|
||||
->where('server_mode', 'production')
|
||||
->get();
|
||||
|
||||
if ($members->isEmpty()) {
|
||||
Log::info('[SyncBarobill] 활성 회원 없음, 스킵');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$yesterday = Carbon::yesterday()->format('Ymd');
|
||||
$today = Carbon::today()->format('Ymd');
|
||||
|
||||
foreach ($members as $member) {
|
||||
try {
|
||||
if (in_array($this->syncType, ['all', 'bank'])) {
|
||||
$result = $bankSyncService->syncIfNeeded(
|
||||
$member->tenant_id,
|
||||
$yesterday,
|
||||
$today
|
||||
);
|
||||
Log::info('[SyncBarobill] 은행 동기화 완료', [
|
||||
'tenant_id' => $member->tenant_id,
|
||||
'result' => $result,
|
||||
]);
|
||||
}
|
||||
|
||||
if (in_array($this->syncType, ['all', 'card'])) {
|
||||
$result = $cardSyncService->syncCardTransactions(
|
||||
$member->tenant_id,
|
||||
$yesterday,
|
||||
$today
|
||||
);
|
||||
Log::info('[SyncBarobill] 카드 동기화 완료', [
|
||||
'tenant_id' => $member->tenant_id,
|
||||
'result' => $result,
|
||||
]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::error('[SyncBarobill] 동기화 실패', [
|
||||
'tenant_id' => $member->tenant_id,
|
||||
'sync_type' => $this->syncType,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
293
app/Services/Barobill/BarobillBankSyncService.php
Normal file
293
app/Services/Barobill/BarobillBankSyncService.php
Normal file
@@ -0,0 +1,293 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Barobill;
|
||||
|
||||
use App\Models\Barobill\BarobillBankSyncStatus;
|
||||
use App\Models\Barobill\BarobillMember;
|
||||
use App\Services\Service;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 바로빌 은행 거래내역 동기화 서비스 (API 독립 구현)
|
||||
*
|
||||
* MNG의 BarobillBankSyncService 패턴을 참고하여 독립 작성.
|
||||
* SOAP API를 호출하여 은행 거래내역을 DB에 캐시/동기화한다.
|
||||
*/
|
||||
class BarobillBankSyncService extends Service
|
||||
{
|
||||
public function __construct(
|
||||
protected BarobillSoapService $soapService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 지정 기간의 거래내역이 최신인지 확인하고, 필요 시 바로빌 API에서 동기화
|
||||
*/
|
||||
public function syncIfNeeded(int $tenantId, string $startDateYmd, string $endDateYmd): array
|
||||
{
|
||||
$member = BarobillMember::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->first();
|
||||
|
||||
if (! $member || empty($member->barobill_id)) {
|
||||
return ['success' => false, 'error' => '바로빌 회원 정보 없음'];
|
||||
}
|
||||
|
||||
$this->soapService->initForMember($member);
|
||||
|
||||
$accounts = $this->getRegisteredAccounts($member);
|
||||
if (empty($accounts)) {
|
||||
return ['success' => true, 'message' => '등록된 계좌 없음', 'synced' => 0];
|
||||
}
|
||||
|
||||
$currentYearMonth = Carbon::now()->format('Ym');
|
||||
$chunks = $this->splitDateRangeMonthly($startDateYmd, $endDateYmd);
|
||||
$totalSynced = 0;
|
||||
|
||||
foreach ($accounts as $acc) {
|
||||
$accNum = $acc['bankAccountNum'];
|
||||
|
||||
foreach ($chunks as $chunk) {
|
||||
$yearMonth = substr($chunk['start'], 0, 6);
|
||||
$isCurrentMonth = ($yearMonth === $currentYearMonth);
|
||||
|
||||
$syncStatus = BarobillBankSyncStatus::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('bank_account_num', $accNum)
|
||||
->where('synced_year_month', $yearMonth)
|
||||
->first();
|
||||
|
||||
if ($syncStatus) {
|
||||
if (! $isCurrentMonth) {
|
||||
continue;
|
||||
}
|
||||
if ($syncStatus->synced_at && $syncStatus->synced_at->diffInMinutes(now()) < 10) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
$count = $this->fetchAndCache(
|
||||
$tenantId,
|
||||
$member->barobill_id,
|
||||
$accNum,
|
||||
$acc['bankName'],
|
||||
$acc['bankCode'],
|
||||
$chunk['start'],
|
||||
$chunk['end'],
|
||||
$yearMonth
|
||||
);
|
||||
$totalSynced += $count;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'synced' => $totalSynced,
|
||||
'accounts' => count($accounts),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 바로빌 등록 계좌 목록 조회 (SOAP)
|
||||
*/
|
||||
public function getRegisteredAccounts(BarobillMember $member): array
|
||||
{
|
||||
$result = $this->soapService->getBankAccounts($member->biz_no, false);
|
||||
if (! $result['success']) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = $result['data'];
|
||||
$accountList = [];
|
||||
if (isset($data->BankAccount)) {
|
||||
$accountList = is_array($data->BankAccount) ? $data->BankAccount : [$data->BankAccount];
|
||||
} elseif (isset($data->BankAccountEx)) {
|
||||
$accountList = is_array($data->BankAccountEx) ? $data->BankAccountEx : [$data->BankAccountEx];
|
||||
}
|
||||
|
||||
$accounts = [];
|
||||
foreach ($accountList as $acc) {
|
||||
if (! is_object($acc)) {
|
||||
continue;
|
||||
}
|
||||
$bankAccountNum = $acc->BankAccountNum ?? '';
|
||||
if (empty($bankAccountNum) || (is_numeric($bankAccountNum) && $bankAccountNum < 0)) {
|
||||
continue;
|
||||
}
|
||||
$accounts[] = [
|
||||
'bankAccountNum' => $bankAccountNum,
|
||||
'bankCode' => $acc->BankCode ?? '',
|
||||
'bankName' => $acc->BankName ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return $accounts;
|
||||
}
|
||||
|
||||
/**
|
||||
* 바로빌 API에서 거래내역을 가져와 DB에 캐시
|
||||
*/
|
||||
protected function fetchAndCache(
|
||||
int $tenantId,
|
||||
string $userId,
|
||||
string $accNum,
|
||||
string $bankName,
|
||||
string $bankCode,
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
string $yearMonth
|
||||
): int {
|
||||
$result = $this->soapService->call('bankaccount', 'GetPeriodBankAccountTransLog', [
|
||||
'ID' => $userId,
|
||||
'BankAccountNum' => $accNum,
|
||||
'StartDate' => $startDate,
|
||||
'EndDate' => $endDate,
|
||||
'TransDirection' => 1,
|
||||
'CountPerPage' => 1000,
|
||||
'CurrentPage' => 1,
|
||||
'OrderDirection' => 2,
|
||||
]);
|
||||
|
||||
if (! $result['success']) {
|
||||
$errorCode = $result['error_code'] ?? 0;
|
||||
if (in_array($errorCode, [-25005, -25001])) {
|
||||
$this->updateSyncStatus($tenantId, $accNum, $yearMonth);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$chunkData = $result['data'];
|
||||
|
||||
if (is_numeric($chunkData) && $chunkData < 0) {
|
||||
if (in_array((int) $chunkData, [-25005, -25001])) {
|
||||
$this->updateSyncStatus($tenantId, $accNum, $yearMonth);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
$rawLogs = [];
|
||||
if (isset($chunkData->BankAccountLogList) && isset($chunkData->BankAccountLogList->BankAccountTransLog)) {
|
||||
$logs = $chunkData->BankAccountLogList->BankAccountTransLog;
|
||||
$rawLogs = is_array($logs) ? $logs : [$logs];
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
if (! empty($rawLogs)) {
|
||||
$count = $this->cacheTransactions($tenantId, $accNum, $bankName, $bankCode, $rawLogs);
|
||||
Log::debug("[BankSync] 캐시 저장 ({$startDate}~{$endDate}): {$count}건");
|
||||
}
|
||||
|
||||
$this->updateSyncStatus($tenantId, $accNum, $yearMonth);
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답을 DB에 배치 저장
|
||||
*/
|
||||
protected function cacheTransactions(int $tenantId, string $accNum, string $bankName, string $bankCode, array $rawLogs): int
|
||||
{
|
||||
$rows = [];
|
||||
$now = now();
|
||||
|
||||
foreach ($rawLogs as $log) {
|
||||
$transDT = $log->TransDT ?? '';
|
||||
$transDate = strlen($transDT) >= 8 ? substr($transDT, 0, 8) : '';
|
||||
$transTime = strlen($transDT) >= 14 ? substr($transDT, 8, 6) : '';
|
||||
$deposit = floatval($log->Deposit ?? 0);
|
||||
$withdraw = floatval($log->Withdraw ?? 0);
|
||||
$balance = floatval($log->Balance ?? 0);
|
||||
$summary = $log->TransRemark1 ?? $log->Summary ?? '';
|
||||
$remark2 = $log->TransRemark2 ?? '';
|
||||
|
||||
$cleanSummary = $this->cleanSummary($summary, $remark2);
|
||||
|
||||
$rows[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'bank_account_num' => $log->BankAccountNum ?? $accNum,
|
||||
'bank_code' => $log->BankCode ?? $bankCode,
|
||||
'bank_name' => $log->BankName ?? $bankName,
|
||||
'trans_date' => $transDate,
|
||||
'trans_time' => $transTime,
|
||||
'trans_dt' => $transDT,
|
||||
'deposit' => $deposit,
|
||||
'withdraw' => $withdraw,
|
||||
'balance' => $balance,
|
||||
'summary' => $cleanSummary,
|
||||
'cast' => $remark2,
|
||||
'memo' => $log->Memo ?? '',
|
||||
'trans_office' => $log->TransOffice ?? '',
|
||||
'is_manual' => false,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
$inserted = 0;
|
||||
foreach (array_chunk($rows, 100) as $batch) {
|
||||
$inserted += DB::table('barobill_bank_transactions')->insertOrIgnore($batch);
|
||||
}
|
||||
|
||||
return $inserted;
|
||||
}
|
||||
|
||||
protected function updateSyncStatus(int $tenantId, string $accNum, string $yearMonth): void
|
||||
{
|
||||
BarobillBankSyncStatus::withoutGlobalScopes()->updateOrCreate(
|
||||
[
|
||||
'tenant_id' => $tenantId,
|
||||
'bank_account_num' => $accNum,
|
||||
'synced_year_month' => $yearMonth,
|
||||
],
|
||||
['synced_at' => now()]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 기간을 월별 청크로 분할
|
||||
*/
|
||||
protected function splitDateRangeMonthly(string $startDate, string $endDate): array
|
||||
{
|
||||
$start = Carbon::createFromFormat('Ymd', $startDate)->startOfDay();
|
||||
$end = Carbon::createFromFormat('Ymd', $endDate)->endOfDay();
|
||||
|
||||
$chunks = [];
|
||||
$cursor = $start->copy();
|
||||
|
||||
while ($cursor->lte($end)) {
|
||||
$chunkStart = $cursor->copy();
|
||||
$chunkEnd = $cursor->copy()->endOfMonth()->startOfDay();
|
||||
|
||||
if ($chunkEnd->gt($end)) {
|
||||
$chunkEnd = $end->copy()->startOfDay();
|
||||
}
|
||||
|
||||
$chunks[] = [
|
||||
'start' => $chunkStart->format('Ymd'),
|
||||
'end' => $chunkEnd->format('Ymd'),
|
||||
];
|
||||
|
||||
$cursor = $chunkStart->copy()->addMonth()->startOfMonth();
|
||||
}
|
||||
|
||||
return $chunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* 요약 정리 (중복 정보 제거)
|
||||
*/
|
||||
protected function cleanSummary(string $summary, string $remark): string
|
||||
{
|
||||
$summary = trim($summary);
|
||||
$remark = trim($remark);
|
||||
|
||||
if (! empty($remark) && str_contains($summary, $remark)) {
|
||||
$summary = trim(str_replace($remark, '', $summary));
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
186
app/Services/Barobill/BarobillCardSyncService.php
Normal file
186
app/Services/Barobill/BarobillCardSyncService.php
Normal file
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Barobill;
|
||||
|
||||
use App\Models\Barobill\BarobillMember;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 바로빌 카드 거래내역 동기화 서비스 (API 독립 구현)
|
||||
*
|
||||
* MNG의 EcardController 카드 조회 패턴을 참고하여 독립 작성.
|
||||
* SOAP API를 호출하여 카드 거래내역을 DB에 캐시/동기화한다.
|
||||
*/
|
||||
class BarobillCardSyncService extends Service
|
||||
{
|
||||
public function __construct(
|
||||
protected BarobillSoapService $soapService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 카드 거래내역 동기화
|
||||
*/
|
||||
public function syncCardTransactions(int $tenantId, string $startDateYmd, string $endDateYmd): array
|
||||
{
|
||||
$member = BarobillMember::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->first();
|
||||
|
||||
if (! $member || empty($member->barobill_id)) {
|
||||
return ['success' => false, 'error' => '바로빌 회원 정보 없음'];
|
||||
}
|
||||
|
||||
$this->soapService->initForMember($member);
|
||||
|
||||
$cards = $this->getRegisteredCards($member);
|
||||
if (empty($cards)) {
|
||||
return ['success' => true, 'message' => '등록된 카드 없음', 'synced' => 0];
|
||||
}
|
||||
|
||||
$totalSynced = 0;
|
||||
|
||||
foreach ($cards as $card) {
|
||||
$cardNum = $card['cardNum'];
|
||||
|
||||
$result = $this->soapService->getPeriodCardLog(
|
||||
$member->biz_no,
|
||||
$member->barobill_id,
|
||||
$cardNum,
|
||||
$startDateYmd,
|
||||
$endDateYmd
|
||||
);
|
||||
|
||||
if (! $result['success']) {
|
||||
Log::warning("[CardSync] 카드 조회 실패: {$cardNum}", [
|
||||
'error' => $result['error'] ?? '',
|
||||
]);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$data = $result['data'];
|
||||
|
||||
if (is_numeric($data) && $data < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rawLogs = [];
|
||||
if (isset($data->CardLogList) && isset($data->CardLogList->CardLog)) {
|
||||
$logs = $data->CardLogList->CardLog;
|
||||
$rawLogs = is_array($logs) ? $logs : [$logs];
|
||||
}
|
||||
|
||||
if (! empty($rawLogs)) {
|
||||
$count = $this->cacheTransactions(
|
||||
$tenantId,
|
||||
$cardNum,
|
||||
$card['cardCompany'],
|
||||
$card['cardCompanyName'],
|
||||
$rawLogs
|
||||
);
|
||||
$totalSynced += $count;
|
||||
Log::debug("[CardSync] 카드 {$cardNum}: {$count}건 저장");
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'synced' => $totalSynced,
|
||||
'cards' => count($cards),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 바로빌 등록 카드 목록 조회 (SOAP)
|
||||
*/
|
||||
public function getRegisteredCards(BarobillMember $member): array
|
||||
{
|
||||
$result = $this->soapService->getCards($member->biz_no, false);
|
||||
if (! $result['success']) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$data = $result['data'];
|
||||
$cardList = [];
|
||||
if (isset($data->CardEx2)) {
|
||||
$cardList = is_array($data->CardEx2) ? $data->CardEx2 : [$data->CardEx2];
|
||||
} elseif (isset($data->Card)) {
|
||||
$cardList = is_array($data->Card) ? $data->Card : [$data->Card];
|
||||
}
|
||||
|
||||
$cards = [];
|
||||
foreach ($cardList as $card) {
|
||||
if (! is_object($card)) {
|
||||
continue;
|
||||
}
|
||||
$cardNum = $card->CardNum ?? '';
|
||||
if (empty($cardNum) || (is_numeric($cardNum) && $cardNum < 0)) {
|
||||
continue;
|
||||
}
|
||||
$cards[] = [
|
||||
'cardNum' => $cardNum,
|
||||
'cardCompany' => $card->CardCompany ?? '',
|
||||
'cardCompanyName' => BarobillSoapService::$cardCompanyCodes[$card->CardCompany ?? ''] ?? '',
|
||||
'alias' => $card->Alias ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
return $cards;
|
||||
}
|
||||
|
||||
/**
|
||||
* API 응답을 DB에 배치 저장
|
||||
*/
|
||||
protected function cacheTransactions(
|
||||
int $tenantId,
|
||||
string $cardNum,
|
||||
string $cardCompany,
|
||||
string $cardCompanyName,
|
||||
array $rawLogs
|
||||
): int {
|
||||
$rows = [];
|
||||
$now = now();
|
||||
|
||||
foreach ($rawLogs as $log) {
|
||||
$useDT = $log->UseDT ?? '';
|
||||
$useDate = strlen($useDT) >= 8 ? substr($useDT, 0, 8) : '';
|
||||
$useTime = strlen($useDT) >= 14 ? substr($useDT, 8, 6) : '';
|
||||
|
||||
$rows[] = [
|
||||
'tenant_id' => $tenantId,
|
||||
'card_num' => $log->CardNum ?? $cardNum,
|
||||
'card_company' => $log->CardCompany ?? $cardCompany,
|
||||
'card_company_name' => $cardCompanyName,
|
||||
'use_dt' => $useDT,
|
||||
'use_date' => $useDate,
|
||||
'use_time' => $useTime,
|
||||
'approval_num' => $log->ApprovalNum ?? '',
|
||||
'approval_type' => $log->ApprovalType ?? '',
|
||||
'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 ?? '',
|
||||
'merchant_name' => $log->MerchantName ?? '',
|
||||
'merchant_biz_num' => $log->MerchantBizNum ?? '',
|
||||
'merchant_addr' => $log->MerchantAddr ?? '',
|
||||
'merchant_ceo' => '',
|
||||
'merchant_biz_type' => '',
|
||||
'merchant_tel' => '',
|
||||
'use_key' => $log->UseKey ?? '',
|
||||
'is_manual' => false,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
];
|
||||
}
|
||||
|
||||
$inserted = 0;
|
||||
foreach (array_chunk($rows, 100) as $batch) {
|
||||
$inserted += DB::table('barobill_card_transactions')->insertOrIgnore($batch);
|
||||
}
|
||||
|
||||
return $inserted;
|
||||
}
|
||||
}
|
||||
662
app/Services/Barobill/BarobillSoapService.php
Normal file
662
app/Services/Barobill/BarobillSoapService.php
Normal file
@@ -0,0 +1,662 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Barobill;
|
||||
|
||||
use App\Models\Barobill\BarobillConfig;
|
||||
use App\Models\Barobill\BarobillMember;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use SoapClient;
|
||||
use SoapFault;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 바로빌 SOAP API 서비스 (API 프로젝트 독립 구현)
|
||||
*
|
||||
* MNG의 BarobillService 패턴을 참고하여 API 프로젝트용으로 독립 작성.
|
||||
* 서비스 고객(tenant_id=2~N)이 사용하는 SOAP 호출을 담당한다.
|
||||
*
|
||||
* @see https://dev.barobill.co.kr/
|
||||
* @see https://ws.baroservice.com/CORPSTATE.asmx (회원관리)
|
||||
* @see https://ws.baroservice.com/BANKACCOUNT.asmx (계좌조회)
|
||||
* @see https://ws.baroservice.com/CARD.asmx (카드조회)
|
||||
* @see https://ws.baroservice.com/TI.asmx (세금계산서)
|
||||
*/
|
||||
class BarobillSoapService extends Service
|
||||
{
|
||||
protected ?SoapClient $corpStateClient = null;
|
||||
|
||||
protected ?SoapClient $bankAccountClient = null;
|
||||
|
||||
protected ?SoapClient $cardClient = null;
|
||||
|
||||
protected ?SoapClient $tiClient = null;
|
||||
|
||||
protected string $certKey = '';
|
||||
|
||||
protected string $corpNum = '';
|
||||
|
||||
protected bool $isTestMode = true;
|
||||
|
||||
protected array $soapUrls = [];
|
||||
|
||||
/**
|
||||
* 에러 코드 매핑
|
||||
*/
|
||||
protected array $errorMessages = [
|
||||
-11101 => '사업자번호가 설정되지 않았거나 유효하지 않습니다.',
|
||||
-11102 => 'CERTKEY가 유효하지 않습니다.',
|
||||
-11103 => '인증서가 만료되었거나 유효하지 않습니다.',
|
||||
-11104 => '해당 사업자가 등록되어 있지 않습니다.',
|
||||
-11105 => '이미 등록된 사업자입니다.',
|
||||
-11106 => '아이디가 이미 존재합니다.',
|
||||
-11201 => '필수 파라미터가 누락되었습니다.',
|
||||
-25001 => '조회 결과가 없습니다.',
|
||||
-25005 => '조회 기간에 데이터가 없습니다.',
|
||||
-26001 => '공동인증서가 등록되어 있지 않습니다.',
|
||||
-32000 => '알 수 없는 오류가 발생했습니다.',
|
||||
-32001 => '사업자번호가 유효하지 않습니다.',
|
||||
-32002 => '아이디가 유효하지 않습니다.',
|
||||
-32003 => '비밀번호가 유효하지 않습니다.',
|
||||
-32004 => '상호명이 유효하지 않습니다.',
|
||||
-32005 => '대표자명이 유효하지 않습니다.',
|
||||
-32006 => '이메일 형식이 유효하지 않습니다.',
|
||||
-32010 => '이미 등록된 사업자번호입니다.',
|
||||
-32011 => '이미 등록된 아이디입니다.',
|
||||
-32012 => '이미 등록된 아이디입니다. 다른 아이디를 사용해주세요.',
|
||||
-32013 => '비밀번호 형식이 유효하지 않습니다. (영문/숫자/특수문자 조합 8자리 이상)',
|
||||
-32014 => '연락처 형식이 유효하지 않습니다.',
|
||||
-32020 => '파트너 사업자번호가 유효하지 않습니다.',
|
||||
-32021 => '파트너 인증키(CERTKEY)가 유효하지 않습니다.',
|
||||
-99999 => '서버 내부 오류가 발생했습니다.',
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->isTestMode = (bool) config('services.barobill.test_mode', true);
|
||||
$this->initializeConfig();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 설정/초기화
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 서버 모드 전환 (회원사별 설정 적용)
|
||||
*/
|
||||
public function switchServerMode(bool $isTestMode): self
|
||||
{
|
||||
if ($this->isTestMode !== $isTestMode) {
|
||||
$this->isTestMode = $isTestMode;
|
||||
$this->resetClients();
|
||||
$this->initializeConfig();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 서버 모드 문자열로 전환
|
||||
*/
|
||||
public function setServerMode(string $mode): self
|
||||
{
|
||||
return $this->switchServerMode($mode === 'test');
|
||||
}
|
||||
|
||||
public function getServerMode(): string
|
||||
{
|
||||
return $this->isTestMode ? 'test' : 'production';
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 초기화 (서버 모드에 따른 설정 로드)
|
||||
*/
|
||||
protected function initializeConfig(): void
|
||||
{
|
||||
$dbConfig = $this->loadConfigFromDatabase();
|
||||
|
||||
if ($dbConfig) {
|
||||
$this->certKey = $dbConfig->cert_key;
|
||||
$this->corpNum = $dbConfig->corp_num ?? '';
|
||||
$this->soapUrls = $this->buildSoapUrls($dbConfig->base_url);
|
||||
} else {
|
||||
$this->certKey = $this->isTestMode
|
||||
? config('services.barobill.cert_key_test', '')
|
||||
: config('services.barobill.cert_key_prod', '');
|
||||
$this->corpNum = config('services.barobill.corp_num', '');
|
||||
$baseUrl = $this->isTestMode
|
||||
? 'https://testws.baroservice.com'
|
||||
: 'https://ws.baroservice.com';
|
||||
$this->soapUrls = $this->buildSoapUrls($baseUrl);
|
||||
}
|
||||
}
|
||||
|
||||
protected function loadConfigFromDatabase(): ?BarobillConfig
|
||||
{
|
||||
try {
|
||||
return BarobillConfig::getActive($this->isTestMode);
|
||||
} catch (\Exception $e) {
|
||||
Log::warning('[BarobillSoap] DB 설정 로드 실패', ['error' => $e->getMessage()]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function buildSoapUrls(string $baseUrl): array
|
||||
{
|
||||
$baseUrl = rtrim($baseUrl, '/');
|
||||
|
||||
return [
|
||||
'corpstate' => $baseUrl.'/CORPSTATE.asmx?WSDL',
|
||||
'ti' => $baseUrl.'/TI.asmx?WSDL',
|
||||
'bankaccount' => $baseUrl.'/BANKACCOUNT.asmx?WSDL',
|
||||
'card' => $baseUrl.'/CARD.asmx?WSDL',
|
||||
];
|
||||
}
|
||||
|
||||
protected function resetClients(): void
|
||||
{
|
||||
$this->corpStateClient = null;
|
||||
$this->bankAccountClient = null;
|
||||
$this->cardClient = null;
|
||||
$this->tiClient = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 설정 상태 확인
|
||||
*/
|
||||
public function checkConfiguration(): array
|
||||
{
|
||||
$dbConfig = $this->loadConfigFromDatabase();
|
||||
|
||||
return [
|
||||
'cert_key_set' => ! empty($this->certKey),
|
||||
'corp_num_set' => ! empty($this->corpNum),
|
||||
'test_mode' => $this->isTestMode,
|
||||
'mode_label' => $this->isTestMode ? '테스트' : '운영',
|
||||
'soap_url' => $this->soapUrls['corpstate'] ?? '',
|
||||
'config_source' => $dbConfig ? 'database' : 'env',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원사 설정 기반 초기화 (동기화 서비스에서 사용)
|
||||
*/
|
||||
public function initForMember(BarobillMember $member): self
|
||||
{
|
||||
$this->switchServerMode($member->isTestMode());
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// SOAP 클라이언트
|
||||
// =========================================================================
|
||||
|
||||
protected function createSoapClient(string $wsdlUrl): ?SoapClient
|
||||
{
|
||||
try {
|
||||
$context = stream_context_create([
|
||||
'ssl' => [
|
||||
'verify_peer' => false,
|
||||
'verify_peer_name' => false,
|
||||
'allow_self_signed' => true,
|
||||
],
|
||||
]);
|
||||
|
||||
return new SoapClient($wsdlUrl, [
|
||||
'trace' => true,
|
||||
'encoding' => 'UTF-8',
|
||||
'exceptions' => true,
|
||||
'connection_timeout' => 30,
|
||||
'stream_context' => $context,
|
||||
'cache_wsdl' => WSDL_CACHE_BOTH,
|
||||
]);
|
||||
} catch (Throwable $e) {
|
||||
Log::error('[BarobillSoap] 클라이언트 생성 실패', [
|
||||
'url' => $wsdlUrl,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
protected function getCorpStateClient(): ?SoapClient
|
||||
{
|
||||
return $this->corpStateClient ??= $this->createSoapClient($this->soapUrls['corpstate']);
|
||||
}
|
||||
|
||||
protected function getBankAccountClient(): ?SoapClient
|
||||
{
|
||||
return $this->bankAccountClient ??= $this->createSoapClient($this->soapUrls['bankaccount']);
|
||||
}
|
||||
|
||||
protected function getCardClient(): ?SoapClient
|
||||
{
|
||||
return $this->cardClient ??= $this->createSoapClient($this->soapUrls['card']);
|
||||
}
|
||||
|
||||
protected function getTiClient(): ?SoapClient
|
||||
{
|
||||
return $this->tiClient ??= $this->createSoapClient($this->soapUrls['ti']);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// SOAP 호출 (핵심)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* SOAP 메서드 호출
|
||||
*/
|
||||
public function call(string $service, string $method, array $params = []): array
|
||||
{
|
||||
$client = match ($service) {
|
||||
'corpstate' => $this->getCorpStateClient(),
|
||||
'ti' => $this->getTiClient(),
|
||||
'bankaccount' => $this->getBankAccountClient(),
|
||||
'card' => $this->getCardClient(),
|
||||
default => null,
|
||||
};
|
||||
|
||||
if (! $client) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => "SOAP 클라이언트 초기화 실패 ({$service})",
|
||||
'error_code' => -1,
|
||||
];
|
||||
}
|
||||
|
||||
if (empty($this->certKey)) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'CERTKEY가 설정되지 않았습니다.',
|
||||
'error_code' => -2,
|
||||
];
|
||||
}
|
||||
|
||||
if (! isset($params['CERTKEY'])) {
|
||||
$params['CERTKEY'] = $this->certKey;
|
||||
}
|
||||
|
||||
try {
|
||||
$startTime = microtime(true);
|
||||
$result = $client->$method($params);
|
||||
$resultProperty = $method.'Result';
|
||||
$elapsed = round((microtime(true) - $startTime) * 1000);
|
||||
|
||||
Log::debug("[BarobillSoap] {$method} 완료 ({$elapsed}ms)");
|
||||
|
||||
if (isset($result->$resultProperty)) {
|
||||
$resultData = $result->$resultProperty;
|
||||
|
||||
if (is_numeric($resultData) && $resultData < 0) {
|
||||
$errorMessage = $this->errorMessages[(int) $resultData]
|
||||
?? "바로빌 API 오류 코드: {$resultData}";
|
||||
|
||||
Log::error('[BarobillSoap] API 오류', [
|
||||
'method' => $method,
|
||||
'error_code' => $resultData,
|
||||
'error_message' => $errorMessage,
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $errorMessage,
|
||||
'error_code' => (int) $resultData,
|
||||
'data' => $resultData,
|
||||
];
|
||||
}
|
||||
|
||||
return ['success' => true, 'data' => $resultData];
|
||||
}
|
||||
|
||||
return ['success' => true, 'data' => $result];
|
||||
|
||||
} catch (SoapFault $e) {
|
||||
Log::error('[BarobillSoap] SOAP 오류', [
|
||||
'method' => $method,
|
||||
'fault' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'SOAP 오류: '.$e->getMessage(),
|
||||
'error_code' => $e->getCode(),
|
||||
];
|
||||
} catch (Throwable $e) {
|
||||
Log::error('[BarobillSoap] 호출 오류', [
|
||||
'method' => $method,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => 'API 호출 오류: '.$e->getMessage(),
|
||||
'error_code' => -999,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 회원관리 (CORPSTATE)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 회원사 등록
|
||||
*/
|
||||
public function registCorp(array $data): array
|
||||
{
|
||||
$params = [
|
||||
'CorpNum' => $this->formatBizNo($data['biz_no']),
|
||||
'CorpName' => $data['corp_name'],
|
||||
'CEOName' => $data['ceo_name'],
|
||||
'BizType' => $data['biz_type'] ?? '',
|
||||
'BizClass' => $data['biz_class'] ?? '',
|
||||
'PostNum' => $data['post_num'] ?? '',
|
||||
'Addr1' => $data['addr'] ?? '',
|
||||
'Addr2' => '',
|
||||
'MemberName' => $data['manager_name'] ?? '',
|
||||
'JuminNum' => '',
|
||||
'ID' => $data['barobill_id'],
|
||||
'PWD' => $data['barobill_pwd'],
|
||||
'Grade' => '2',
|
||||
'TEL' => $data['tel'] ?? '',
|
||||
'HP' => $data['manager_hp'] ?? '',
|
||||
'Email' => $data['manager_email'] ?? '',
|
||||
];
|
||||
|
||||
return $this->call('corpstate', 'RegistCorp', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원사 정보 수정
|
||||
*/
|
||||
public function updateCorpInfo(array $data): array
|
||||
{
|
||||
$params = [
|
||||
'CorpNum' => $this->formatBizNo($data['biz_no']),
|
||||
'CorpName' => $data['corp_name'],
|
||||
'CEOName' => $data['ceo_name'],
|
||||
'BizType' => $data['biz_type'] ?? '',
|
||||
'BizClass' => $data['biz_class'] ?? '',
|
||||
'PostNum' => $data['post_num'] ?? '',
|
||||
'Addr1' => $data['addr'] ?? '',
|
||||
'Addr2' => '',
|
||||
'MemberName' => $data['manager_name'] ?? '',
|
||||
'TEL' => $data['tel'] ?? '',
|
||||
'HP' => $data['manager_hp'] ?? '',
|
||||
'Email' => $data['manager_email'] ?? '',
|
||||
];
|
||||
|
||||
return $this->call('corpstate', 'UpdateCorpInfo', $params);
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원사 상태 조회
|
||||
*/
|
||||
public function getCorpState(string $corpNum): array
|
||||
{
|
||||
return $this->call('corpstate', 'GetCorpState', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 계좌조회 (BANKACCOUNT)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 등록계좌 목록 조회
|
||||
*/
|
||||
public function getBankAccounts(string $corpNum, bool $availOnly = true): array
|
||||
{
|
||||
return $this->call('bankaccount', 'GetBankAccountEx', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'AvailOnly' => $availOnly ? 1 : 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 계좌 기간별 입출금내역 조회
|
||||
*/
|
||||
public function getPeriodBankAccountTransLog(
|
||||
string $corpNum,
|
||||
string $id,
|
||||
string $bankAccountNum,
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
int $countPerPage = 1000,
|
||||
int $currentPage = 1
|
||||
): array {
|
||||
return $this->call('bankaccount', 'GetPeriodBankAccountTransLog', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $id,
|
||||
'BankAccountNum' => $bankAccountNum,
|
||||
'StartDate' => $startDate,
|
||||
'EndDate' => $endDate,
|
||||
'TransDirection' => 1,
|
||||
'CountPerPage' => $countPerPage,
|
||||
'CurrentPage' => $currentPage,
|
||||
'OrderDirection' => 2,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 계좌조회 신청 URL 조회
|
||||
*/
|
||||
public function getBankAccountScrapRequestUrl(string $corpNum, string $userId, string $userPwd): array
|
||||
{
|
||||
return $this->call('bankaccount', 'GetBankAccountScrapRequestURL', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $userId,
|
||||
'PWD' => $userPwd,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증서 등록 URL 조회
|
||||
*/
|
||||
public function getCertificateRegistUrl(string $corpNum, string $userId, string $userPwd): array
|
||||
{
|
||||
return $this->call('bankaccount', 'GetCertificateRegistURL', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $userId,
|
||||
'PWD' => $userPwd,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증서 유효성 확인
|
||||
*/
|
||||
public function checkCertificateValid(string $corpNum): array
|
||||
{
|
||||
return $this->call('bankaccount', 'CheckCERTIsValid', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증서 만료일 조회
|
||||
*/
|
||||
public function getCertificateExpireDate(string $corpNum): array
|
||||
{
|
||||
return $this->call('bankaccount', 'GetCertificateExpireDate', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증서 등록일 조회
|
||||
*/
|
||||
public function getCertificateRegistDate(string $corpNum): array
|
||||
{
|
||||
return $this->call('bankaccount', 'GetCertificateRegistDate', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 충전잔액 조회
|
||||
*/
|
||||
public function getBalanceCostAmount(string $corpNum): array
|
||||
{
|
||||
return $this->call('bankaccount', 'GetBalanceCostAmountEx', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 현금충전 URL 조회
|
||||
*/
|
||||
public function getCashChargeUrl(string $corpNum, string $userId, string $userPwd): array
|
||||
{
|
||||
return $this->call('bankaccount', 'GetCashChargeURL', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $userId,
|
||||
'PWD' => $userPwd,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 카드조회 (CARD)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 등록카드 목록 조회
|
||||
*/
|
||||
public function getCards(string $corpNum, bool $availOnly = true): array
|
||||
{
|
||||
return $this->call('card', 'GetCardEx2', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'AvailOnly' => $availOnly ? 1 : 0,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 등록
|
||||
*/
|
||||
public function registCard(string $corpNum, array $cardData): array
|
||||
{
|
||||
return $this->call('card', 'RegistCardEx', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'CardCompany' => $cardData['card_company'] ?? '',
|
||||
'CardType' => $cardData['card_type'] ?? '1',
|
||||
'CardNum' => preg_replace('/[^0-9]/', '', $cardData['card_num'] ?? ''),
|
||||
'WebId' => $cardData['web_id'] ?? '',
|
||||
'WebPwd' => $cardData['web_pwd'] ?? '',
|
||||
'Alias' => $cardData['alias'] ?? '',
|
||||
'Usage' => $cardData['usage'] ?? '',
|
||||
'CollectCycle' => $cardData['collect_cycle'] ?? '1',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 기간별 사용내역 조회
|
||||
*/
|
||||
public function getPeriodCardLog(
|
||||
string $corpNum,
|
||||
string $id,
|
||||
string $cardNum,
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
int $countPerPage = 1000,
|
||||
int $currentPage = 1
|
||||
): array {
|
||||
return $this->call('card', 'GetPeriodCardLog', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $id,
|
||||
'CardNum' => preg_replace('/[^0-9]/', '', $cardNum),
|
||||
'StartDate' => $startDate,
|
||||
'EndDate' => $endDate,
|
||||
'CountPerPage' => $countPerPage,
|
||||
'CurrentPage' => $currentPage,
|
||||
'OrderDirection' => 'D',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 등록 신청 URL 조회
|
||||
*/
|
||||
public function getCardScrapRequestUrl(string $corpNum, string $userId, string $userPwd): array
|
||||
{
|
||||
return $this->call('card', 'GetCardScrapRequestURL', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $userId,
|
||||
'PWD' => $userPwd,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 카드 관리 URL 조회
|
||||
*/
|
||||
public function getCardManagementUrl(string $corpNum, string $userId, string $userPwd): array
|
||||
{
|
||||
return $this->call('card', 'GetCardManagementURL', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $userId,
|
||||
'PWD' => $userPwd,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 홈택스 (TI)
|
||||
// =========================================================================
|
||||
|
||||
/**
|
||||
* 홈택스 매입/매출 URL 조회
|
||||
*/
|
||||
public function getHomeTaxUrl(string $corpNum, string $userId, string $userPwd): array
|
||||
{
|
||||
return $this->call('ti', 'GetHomeTaxURL', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $userId,
|
||||
'PWD' => $userPwd,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 세금계산서 발행 URL 조회
|
||||
*/
|
||||
public function getTaxInvoiceIssueUrl(string $corpNum, string $userId, string $userPwd): array
|
||||
{
|
||||
return $this->call('ti', 'GetTaxInvoiceIssueURL', [
|
||||
'CorpNum' => $this->formatBizNo($corpNum),
|
||||
'ID' => $userId,
|
||||
'PWD' => $userPwd,
|
||||
]);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// 유틸리티
|
||||
// =========================================================================
|
||||
|
||||
protected function formatBizNo(string $bizNo): string
|
||||
{
|
||||
return preg_replace('/[^0-9]/', '', $bizNo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 은행 코드 목록
|
||||
*/
|
||||
public static array $bankCodes = [
|
||||
'02' => '산업은행', '03' => '기업은행', '04' => '국민은행',
|
||||
'07' => '수협은행', '11' => 'NH농협은행', '20' => '우리은행',
|
||||
'23' => '제일은행(SC)', '26' => '신한은행', '27' => '한국씨티은행',
|
||||
'31' => '대구은행', '32' => '부산은행', '34' => '광주은행',
|
||||
'35' => '제주은행', '37' => '전북은행', '39' => '경남은행',
|
||||
'45' => '새마을금고연합', '48' => '신협', '71' => '우체국',
|
||||
'81' => 'KEB하나은행', '88' => '신한은행(통합)',
|
||||
'89' => '케이뱅크', '90' => '카카오뱅크', '92' => '토스뱅크',
|
||||
];
|
||||
|
||||
/**
|
||||
* 카드사 코드 목록
|
||||
*/
|
||||
public static array $cardCompanyCodes = [
|
||||
'01' => '비씨카드', '02' => '국민카드', '04' => '삼성카드',
|
||||
'05' => '현대카드', '06' => '롯데카드', '07' => '신한카드',
|
||||
'08' => '하나카드', '09' => '우리카드', '10' => 'NH농협카드',
|
||||
'11' => '씨티카드',
|
||||
];
|
||||
}
|
||||
116
app/Services/Barobill/HometaxSyncService.php
Normal file
116
app/Services/Barobill/HometaxSyncService.php
Normal file
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Barobill;
|
||||
|
||||
use App\Models\Barobill\HometaxInvoice;
|
||||
use App\Services\Service;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* 홈택스 세금계산서 동기화 서비스 (API 독립 구현)
|
||||
*
|
||||
* MNG의 HometaxSyncService 패턴을 참고하여 독립 작성.
|
||||
* 바로빌 API 응답 데이터를 로컬 DB에 upsert한다.
|
||||
*/
|
||||
class HometaxSyncService extends Service
|
||||
{
|
||||
/**
|
||||
* API 응답 데이터를 로컬 DB에 동기화
|
||||
*
|
||||
* @param array $invoices API에서 받은 세금계산서 목록
|
||||
* @param int $tenantId 테넌트 ID
|
||||
* @param string $invoiceType 'sales' 또는 'purchase'
|
||||
* @return array 동기화 결과
|
||||
*/
|
||||
public function syncInvoices(array $invoices, int $tenantId, string $invoiceType): array
|
||||
{
|
||||
$result = [
|
||||
'inserted' => 0,
|
||||
'updated' => 0,
|
||||
'failed' => 0,
|
||||
'total' => count($invoices),
|
||||
];
|
||||
|
||||
if (empty($invoices)) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
DB::beginTransaction();
|
||||
|
||||
try {
|
||||
foreach ($invoices as $apiData) {
|
||||
if (empty($apiData['ntsConfirmNum'])) {
|
||||
$result['failed']++;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$modelData = HometaxInvoice::fromApiData($apiData, $tenantId, $invoiceType);
|
||||
|
||||
$existing = HometaxInvoice::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('nts_confirm_num', $modelData['nts_confirm_num'])
|
||||
->where('invoice_type', $invoiceType)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$existing->update([
|
||||
'write_date' => $modelData['write_date'],
|
||||
'issue_date' => $modelData['issue_date'],
|
||||
'invoicer_corp_num' => $modelData['invoicer_corp_num'],
|
||||
'invoicer_corp_name' => $modelData['invoicer_corp_name'],
|
||||
'invoicer_ceo_name' => $modelData['invoicer_ceo_name'],
|
||||
'invoicee_corp_num' => $modelData['invoicee_corp_num'],
|
||||
'invoicee_corp_name' => $modelData['invoicee_corp_name'],
|
||||
'invoicee_ceo_name' => $modelData['invoicee_ceo_name'],
|
||||
'supply_amount' => $modelData['supply_amount'],
|
||||
'tax_amount' => $modelData['tax_amount'],
|
||||
'total_amount' => $modelData['total_amount'],
|
||||
'tax_type' => $modelData['tax_type'],
|
||||
'purpose_type' => $modelData['purpose_type'],
|
||||
'item_name' => $modelData['item_name'],
|
||||
'remark' => $modelData['remark'],
|
||||
'synced_at' => now(),
|
||||
]);
|
||||
$result['updated']++;
|
||||
} else {
|
||||
HometaxInvoice::create($modelData);
|
||||
$result['inserted']++;
|
||||
}
|
||||
}
|
||||
|
||||
DB::commit();
|
||||
|
||||
Log::info('[HometaxSync] 동기화 완료', [
|
||||
'tenant_id' => $tenantId,
|
||||
'invoice_type' => $invoiceType,
|
||||
'result' => $result,
|
||||
]);
|
||||
} catch (\Throwable $e) {
|
||||
DB::rollBack();
|
||||
Log::error('[HometaxSync] 동기화 실패', [
|
||||
'tenant_id' => $tenantId,
|
||||
'invoice_type' => $invoiceType,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
throw $e;
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 마지막 동기화 시간 조회
|
||||
*/
|
||||
public function getLastSyncTime(int $tenantId, string $invoiceType): ?string
|
||||
{
|
||||
$lastSync = HometaxInvoice::withoutGlobalScopes()
|
||||
->where('tenant_id', $tenantId)
|
||||
->where('invoice_type', $invoiceType)
|
||||
->orderByDesc('synced_at')
|
||||
->value('synced_at');
|
||||
|
||||
return $lastSync?->format('Y-m-d H:i:s');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user