278 lines
9.8 KiB
PHP
278 lines
9.8 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Payments\Danal\Gateways;
|
|
|
|
use App\Models\GcPaymentAttempt;
|
|
use App\Support\DanalAes256CbcHex;
|
|
|
|
final class DanalVactGateway
|
|
{
|
|
public function __construct(
|
|
private readonly DanalAes256CbcHex $aes,
|
|
) {}
|
|
|
|
/**
|
|
* CI3: payment_danal_vact() -> AUTH
|
|
*/
|
|
public function auth(GcPaymentAttempt $attempt, string $token, array $order, string $vactKind = 'a'): array
|
|
{
|
|
// 현재 config는 vact만 있으니 a(무통장) 기준 구현
|
|
// v(인증계좌/WIRETRANSFER)는 config 확장 시 추가
|
|
if ($vactKind !== 'a') {
|
|
throw new \RuntimeException("Vact kind not supported yet: {$vactKind}");
|
|
}
|
|
|
|
$cfg = config('danal.vact');
|
|
if (!$cfg || empty($cfg['cpid']) || empty($cfg['key']) || empty($cfg['iv']) || empty($cfg['url'])) {
|
|
throw new \RuntimeException("Danal vact config missing");
|
|
}
|
|
|
|
$oid = (string)($order['oid'] ?? '');
|
|
$amount = (int)($order['amount'] ?? $order['pay_money'] ?? 0);
|
|
$itemName = (string)($order['itemName'] ?? $order['pin_name'] ?? '상품권');
|
|
|
|
$isMobile = (bool)($order['is_mobile'] ?? false);
|
|
$userAgent = $isMobile ? 'MW' : 'PC';
|
|
|
|
$holder = (string)($cfg['holder'] ?? '핀포유');
|
|
|
|
$returnUrl = route('web.payments.danal.vact.return', ['t' => $token], true);
|
|
$cancelUrl = route('web.payments.danal.cancel', ['o' => $oid, 't' => $token], true);
|
|
$notiUrl = route('web.payments.danal.vact.noti', [], true);
|
|
|
|
// BYPASSVALUE는 ; 구분 (token 포함)
|
|
$memNo = (string)($order['mem_no'] ?? $attempt->mem_no ?? '');
|
|
$bypass = "PAYMETHOD=VACT;MEMNO={$memNo};AMOUNT={$amount};TOKEN={$token};";
|
|
|
|
$req = [
|
|
// CI3처럼 CPID를 내부 DATA에도 넣는다
|
|
'CPID' => (string)$cfg['cpid'],
|
|
'SUBCPID' => '',
|
|
'ACCOUNTHOLDER' => $this->toEucKr($holder),
|
|
'EXPIREDATE' => now()->addHours(23)->format('Ymd'),
|
|
|
|
'ORDERID' => $oid,
|
|
'ITEMNAME' => $this->toEucKr($itemName),
|
|
'AMOUNT' => (string)$amount,
|
|
'ISCASHRECEIPTUI' => 'N',
|
|
|
|
'USERNAME' => $this->toEucKr((string)($order['user_name'] ?? '')),
|
|
'USERID' => (string)($order['user_id'] ?? $order['mem_no'] ?? ''),
|
|
'USEREMAIL' => (string)($order['user_email'] ?? ''),
|
|
'USERPHONE' => '',
|
|
'USERAGENT' => $userAgent,
|
|
|
|
'TXTYPE' => 'AUTH',
|
|
'SERVICETYPE' => 'DANALVACCOUNT',
|
|
|
|
'RETURNURL' => $returnUrl,
|
|
'NOTIURL' => $notiUrl,
|
|
'CANCELURL' => $cancelUrl,
|
|
'BYPASSVALUE' => $bypass,
|
|
];
|
|
|
|
$attempt->vact_kind = 'a';
|
|
$attempt->request_payload = $req;
|
|
$attempt->save();
|
|
|
|
$res = $this->postCpcgi(
|
|
(string)$cfg['url'],
|
|
(string)$cfg['cpid'],
|
|
$req,
|
|
(string)$cfg['key'],
|
|
(string)$cfg['iv']
|
|
);
|
|
|
|
$attempt->response_payload = $res;
|
|
$attempt->save();
|
|
|
|
if (($res['RETURNCODE'] ?? '') !== '0000') {
|
|
$msg = (string)($res['RETURNMSG'] ?? '모듈 호출 실패');
|
|
throw new \RuntimeException("Danal vact AUTH failed: {$res['RETURNCODE']} {$msg}");
|
|
}
|
|
|
|
return [
|
|
'actionUrl' => (string)$res['STARTURL'],
|
|
'params' => [
|
|
'STARTPARAMS' => (string)$res['STARTPARAMS'],
|
|
],
|
|
'acceptCharset' => 'EUC-KR',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* CI3: payment_danal_vact_return() -> RETURN decrypt -> ISSUEVACCOUNT
|
|
*/
|
|
public function issueVaccountOnReturn(GcPaymentAttempt $attempt, string $token, array $order, array $post): array
|
|
{
|
|
$cfg = config('danal.vact');
|
|
if (!$cfg || empty($cfg['cpid']) || empty($cfg['key']) || empty($cfg['iv']) || empty($cfg['url'])) {
|
|
throw new \RuntimeException("Danal vact config missing");
|
|
}
|
|
|
|
$returnParams = (string)($post['RETURNPARAMS'] ?? '');
|
|
if ($returnParams === '') {
|
|
return $this->fail('C990', 'RETURNPARAMS 누락');
|
|
}
|
|
|
|
$retStr = $this->aes->decrypt($returnParams, (string)$cfg['key'], (string)$cfg['iv']);
|
|
$retMap = $this->aes->parseQuery($retStr);
|
|
|
|
$oid = (string)($retMap['ORDERID'] ?? '');
|
|
if ($oid === '' || $oid !== (string)$attempt->oid) {
|
|
return $this->fail('C991', '주문번호 불일치');
|
|
}
|
|
|
|
$retCode = (string)($retMap['RETURNCODE'] ?? '');
|
|
$retMsg = (string)($retMap['RETURNMSG'] ?? '');
|
|
|
|
if ($retCode !== '0000') {
|
|
return $this->fail($retCode ?: 'C992', $retMsg ?: '가상계좌 인증 실패');
|
|
}
|
|
|
|
$amount = (int)($order['amount'] ?? $order['pay_money'] ?? $attempt->amount ?? 0);
|
|
|
|
$issueReq = [
|
|
'CPID' => (string)$cfg['cpid'],
|
|
'TID' => (string)($retMap['TID'] ?? ''),
|
|
'AMOUNT' => (string)$amount,
|
|
'TXTYPE' => 'ISSUEVACCOUNT',
|
|
'SERVICETYPE' => 'DANALVACCOUNT',
|
|
];
|
|
|
|
$res = $this->postCpcgi(
|
|
(string)$cfg['url'],
|
|
(string)$cfg['cpid'],
|
|
$issueReq,
|
|
(string)$cfg['key'],
|
|
(string)$cfg['iv']
|
|
);
|
|
|
|
$res = $this->eucKrArrayToUtf8($res);
|
|
|
|
if (($res['RETURNCODE'] ?? '') === '0000') {
|
|
$attempt->status = 'issued';
|
|
$attempt->pg_tid = (string)($res['TID'] ?? $retMap['TID'] ?? '');
|
|
$attempt->returned_at = now();
|
|
$attempt->return_payload = ['ret' => $retMap, 'issue' => $res];
|
|
$attempt->save();
|
|
|
|
return [
|
|
'status' => 'success',
|
|
'message' => '가상계좌가 발급되었습니다. 마이페이지에서 확인해주세요.',
|
|
'redirectUrl' => url('/mypage/usage'),
|
|
'meta' => [
|
|
'oid' => $oid,
|
|
'tid' => (string)($res['TID'] ?? ''),
|
|
'amount' => $amount,
|
|
'code' => (string)($res['RETURNCODE'] ?? ''),
|
|
'msg' => (string)($res['RETURNMSG'] ?? ''),
|
|
'ret_data' => $res,
|
|
],
|
|
];
|
|
}
|
|
|
|
return $this->fail((string)($res['RETURNCODE'] ?? 'C993'), (string)($res['RETURNMSG'] ?? '가상계좌 발급 실패'));
|
|
}
|
|
|
|
/**
|
|
* NOTI 수신 처리 (서버->서버)
|
|
* - post에 DATA가 오면 복호화해서 맵으로 변환
|
|
* - 성공이면 onPaid 콜백 실행
|
|
*/
|
|
public function handleNoti(array $post, callable $onPaid): void
|
|
{
|
|
$cfg = config('danal.vact');
|
|
if (!$cfg || empty($cfg['key']) || empty($cfg['iv'])) {
|
|
throw new \RuntimeException("Danal vact config missing");
|
|
}
|
|
|
|
$map = $post;
|
|
|
|
// NOTI가 CPID/DATA 형태로 오면 DATA 복호화
|
|
if (isset($post['DATA']) && is_string($post['DATA']) && $post['DATA'] !== '') {
|
|
$dec = $this->aes->decrypt((string)$post['DATA'], (string)$cfg['key'], (string)$cfg['iv']);
|
|
$map = $this->aes->parseQuery($dec);
|
|
}
|
|
|
|
$oid = (string)($map['ORDERID'] ?? '');
|
|
$tid = (string)($map['TID'] ?? '');
|
|
$amount = (int)($map['AMOUNT'] ?? 0);
|
|
|
|
// 성공 판단은 최소한으로: RETURNCODE=0000 + 필수키 존재
|
|
$code = (string)($map['RETURNCODE'] ?? '');
|
|
if ($oid === '' || $tid === '' || $amount <= 0) return;
|
|
if ($code !== '' && $code !== '0000') return;
|
|
|
|
$onPaid($oid, $tid, $amount, $map);
|
|
}
|
|
|
|
private function postCpcgi(string $url, string $outerCpid, array $reqData, string $hexKey, string $hexIv): array
|
|
{
|
|
$plain = $this->aes->buildQuery($reqData);
|
|
$enc = $this->aes->encrypt($plain, $hexKey, $hexIv);
|
|
$payload = 'CPID=' . $outerCpid . '&DATA=' . urlencode($enc);
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_POST, 1);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
|
|
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (int)config('danal.authtel.connect_timeout', 5));
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, (int)config('danal.authtel.timeout', 30));
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-type:application/x-www-form-urlencoded; charset=euc-kr"]);
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
|
|
|
|
$resStr = curl_exec($ch);
|
|
|
|
if (($errno = curl_errno($ch)) !== 0) {
|
|
$err = curl_error($ch);
|
|
curl_close($ch);
|
|
return [
|
|
'RETURNCODE' => 'E_NETWORK',
|
|
'RETURNMSG' => "NETWORK({$errno}:{$err})",
|
|
];
|
|
}
|
|
|
|
curl_close($ch);
|
|
|
|
$resMap = $this->aes->parseQuery((string)$resStr);
|
|
if (isset($resMap['DATA'])) {
|
|
$dec = $this->aes->decrypt((string)$resMap['DATA'], $hexKey, $hexIv);
|
|
$resMap = $this->aes->parseQuery($dec);
|
|
}
|
|
|
|
return $resMap;
|
|
}
|
|
|
|
private function toEucKr(string $s): string
|
|
{
|
|
if ($s === '') return '';
|
|
$out = @iconv('UTF-8', 'EUC-KR//IGNORE', $s);
|
|
return $out === false ? $s : $out;
|
|
}
|
|
|
|
private function eucKrArrayToUtf8(array $arr): array
|
|
{
|
|
foreach ($arr as $k => $v) {
|
|
if (!is_string($v) || $v === '') continue;
|
|
$u = @iconv('EUC-KR', 'UTF-8//IGNORE', $v);
|
|
if ($u !== false) $arr[$k] = $u;
|
|
}
|
|
return $arr;
|
|
}
|
|
|
|
private function fail(string $code, string $msg): array
|
|
{
|
|
return [
|
|
'status' => 'fail',
|
|
'message' => $msg ?: '결제에 실패했습니다.',
|
|
'redirectUrl' => url('/mypage/usage'),
|
|
'meta' => [
|
|
'code' => $code,
|
|
'msg' => $msg,
|
|
],
|
|
];
|
|
}
|
|
}
|