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:
김보곤
2026-03-17 13:03:24 +09:00
parent ead546e268
commit 269a17b49c
9 changed files with 1727 additions and 5 deletions

View File

@@ -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'));
}

View 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();
}
}

View 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(),
]);
}
}
}
}

View 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;
}
}

View 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;
}
}

View 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' => '씨티카드',
];
}

View 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');
}
}

View File

@@ -20,6 +20,7 @@
use App\Http\Controllers\Api\V1\BarobillCardTransactionController;
use App\Http\Controllers\Api\V1\BarobillController;
use App\Http\Controllers\Api\V1\BarobillSettingController;
use App\Http\Controllers\Api\V1\BarobillSyncController;
use App\Http\Controllers\Api\V1\BillController;
use App\Http\Controllers\Api\V1\CalendarController;
use App\Http\Controllers\Api\V1\CardController;
@@ -286,6 +287,22 @@
Route::get('/account-link-url', [BarobillController::class, 'accountLinkUrl'])->name('v1.barobill.account-link-url');
Route::get('/card-link-url', [BarobillController::class, 'cardLinkUrl'])->name('v1.barobill.card-link-url');
Route::get('/certificate-url', [BarobillController::class, 'certificateUrl'])->name('v1.barobill.certificate-url');
// SOAP 동기화 API
Route::post('/sync/bank', [BarobillSyncController::class, 'syncBank'])->name('v1.barobill.sync.bank');
Route::post('/sync/card', [BarobillSyncController::class, 'syncCard'])->name('v1.barobill.sync.card');
Route::post('/sync/hometax', [BarobillSyncController::class, 'syncHometax'])->name('v1.barobill.sync.hometax');
// SOAP 실시간 조회 API
Route::get('/accounts', [BarobillSyncController::class, 'accounts'])->name('v1.barobill.accounts');
Route::get('/cards', [BarobillSyncController::class, 'cards'])->name('v1.barobill.cards');
Route::get('/certificate', [BarobillSyncController::class, 'certificate'])->name('v1.barobill.certificate');
Route::get('/balance', [BarobillSyncController::class, 'balance'])->name('v1.barobill.balance');
// SOAP 회원관리 API
Route::post('/members', [BarobillSyncController::class, 'registerMember'])->name('v1.barobill.members.register');
Route::put('/members', [BarobillSyncController::class, 'updateMember'])->name('v1.barobill.members.update');
Route::get('/members/status', [BarobillSyncController::class, 'memberStatus'])->name('v1.barobill.members.status');
});
// Barobill Card Transaction API (바로빌 카드 거래 - React 연동)

View File

@@ -191,3 +191,29 @@
->onFailure(function () {
\Illuminate\Support\Facades\Log::error('❌ demo:reset-showcase 스케줄러 실행 실패', ['time' => now()]);
});
// ─── 바로빌 데이터 동기화 ───
// 매일 06:00 — 전일 은행 거래내역 동기화
Schedule::job(new \App\Jobs\Barobill\SyncBarobillDataJob('bank'))
->dailyAt('06:00')
->withoutOverlapping()
->appendOutputTo(storage_path('logs/scheduler.log'))
->onSuccess(function () {
\Illuminate\Support\Facades\Log::info('[Scheduler] 바로빌 은행 동기화 성공', ['time' => now()]);
})
->onFailure(function () {
\Illuminate\Support\Facades\Log::error('[Scheduler] 바로빌 은행 동기화 실패', ['time' => now()]);
});
// 매일 06:30 — 전일 카드 거래내역 동기화
Schedule::job(new \App\Jobs\Barobill\SyncBarobillDataJob('card'))
->dailyAt('06:30')
->withoutOverlapping()
->appendOutputTo(storage_path('logs/scheduler.log'))
->onSuccess(function () {
\Illuminate\Support\Facades\Log::info('[Scheduler] 바로빌 카드 동기화 성공', ['time' => now()]);
})
->onFailure(function () {
\Illuminate\Support\Facades\Log::error('[Scheduler] 바로빌 카드 동기화 실패', ['time' => now()]);
});