244 lines
8.4 KiB
PHP

<?php
namespace App\Services\Payments\Danal\Gateways;
use App\Models\GcPaymentAttempt;
use App\Support\DanalAes256CbcHex;
final class DanalCardGateway
{
public function __construct(
private readonly DanalAes256CbcHex $aes,
) {}
/**
* CI3: payment_danal_card() -> AUTH
* return: redirect form info
*/
public function auth(GcPaymentAttempt $attempt, string $token, array $order, string $cardKind = 'general'): array
{
$cfg = config("danal.card.$cardKind");
if (!$cfg || empty($cfg['cpid']) || empty($cfg['key']) || empty($cfg['iv']) || empty($cfg['url'])) {
throw new \RuntimeException("Danal card config missing: $cardKind");
}
$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 ? 'WM' : 'PC';
// Return/Cancel: token을 query로 붙여서 세션 의존 제거
$returnUrl = route('web.payments.danal.card.return', ['o' => $oid, 't' => $token], true);
$cancelUrl = route('web.payments.danal.cancel', ['o' => $oid, 't' => $token], true);
// BYPASSVALUE는 '&' 금지(다날 주의) → ; 구분
$bypass = "TOKEN={$token};OID={$oid};AMOUNT={$amount};";
$req = [
'SUBCPID' => '',
'AMOUNT' => (string)$amount,
'CURRENCY' => '410',
'ITEMNAME' => $this->toEucKr($itemName),
'USERAGENT' => $userAgent,
'ORDERID' => $oid,
'OFFERPERIOD' => '',
'USERNAME' => '',
'USERID' => (string)($order['mem_no'] ?? ''),
'USEREMAIL' => '',
'CANCELURL' => $cancelUrl,
'RETURNURL' => $returnUrl,
'TXTYPE' => 'AUTH',
'SERVICETYPE' => 'DANALCARD',
'ISNOTI' => 'N',
'BYPASSVALUE' => $bypass,
];
$attempt->card_kind = $cardKind;
$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 card AUTH failed: {$res['RETURNCODE']} {$msg}");
}
return [
'actionUrl' => (string)$res['STARTURL'],
'params' => [
'STARTPARAMS' => (string)$res['STARTPARAMS'],
],
'acceptCharset' => 'EUC-KR',
];
}
/**
* CI3: payment_danal_card_return() -> RETURN decrypt -> BILL
* return: result view payload
*/
public function billOnReturn(GcPaymentAttempt $attempt, string $token, array $order, array $post): array
{
$cardKind = $attempt->card_kind ?: 'general';
$cfg = config("danal.card.$cardKind");
if (!$cfg || empty($cfg['cpid']) || empty($cfg['key']) || empty($cfg['iv']) || empty($cfg['url'])) {
throw new \RuntimeException("Danal card config missing: $cardKind");
}
$oid = (string)($order['oid'] ?? $attempt->oid);
$amount = (int)($order['amount'] ?? $order['pay_money'] ?? $attempt->amount ?? 0);
$returnParams = (string)($post['RETURNPARAMS'] ?? '');
if ($returnParams === '') {
return $this->fail('C990', 'RETURNPARAMS 누락');
}
// RETURNPARAMS 복호화
$retStr = $this->aes->decrypt($returnParams, (string)$cfg['key'], (string)$cfg['iv']);
$retMap = $this->aes->parseQuery($retStr);
// 주문번호 일치 검증
if (($retMap['ORDERID'] ?? '') !== $oid) {
return $this->fail('C991', '주문번호 불일치');
}
// 인증 결과 확인
$retCode = (string)($retMap['RETURNCODE'] ?? '');
$retMsg = (string)($retMap['RETURNMSG'] ?? '');
if ($retCode !== '0000') {
return $this->fail($retCode ?: 'C992', $retMsg ?: '카드 인증 실패');
}
// BILL 요청
$billReq = [
'TID' => (string)($retMap['TID'] ?? ''),
'AMOUNT' => (string)$amount,
'TXTYPE' => 'BILL',
'SERVICETYPE' => 'DANALCARD',
];
$res = $this->postCpcgi(
(string)$cfg['url'],
(string)$cfg['cpid'],
$billReq,
(string)$cfg['key'],
(string)$cfg['iv']
);
$res = $this->eucKrArrayToUtf8($res);
if (($res['RETURNCODE'] ?? '') === '0000') {
$attempt->status = 'paid';
$attempt->pg_tid = (string)($res['TID'] ?? '');
$attempt->return_code = (string)($res['RETURNCODE'] ?? '');
$attempt->return_msg = (string)($res['RETURNMSG'] ?? '');
$attempt->returned_at = now();
$attempt->return_payload = ['ret' => $retMap, 'bill' => $res];
$attempt->save();
return [
'status' => 'success',
'message' => '결제가 완료되었습니다.',
'redirectUrl' => url('/mypage/usage'),
// 필요하면 아래를 서비스에서 order_complete에 사용
'meta' => [
'oid' => $oid,
'tid' => (string)($res['TID'] ?? ''),
'amount' => (int)($res['AMOUNT'] ?? $amount),
'code' => (string)($res['RETURNCODE'] ?? ''),
'msg' => (string)($res['RETURNMSG'] ?? ''),
'ret_data' => $res,
],
];
}
$msg = (string)($res['RETURNMSG'] ?? '카드 결제 실패');
return $this->fail((string)($res['RETURNCODE'] ?? 'C993'), $msg);
}
private function postCpcgi(string $url, string $outerCpid, array $reqData, string $hexKey, string $hexIv): array
{
// CI3 CallCredit/CallVAccount 규칙 그대로:
// data2str -> AES -> base64 -> urlencode -> CPID=...&DATA=...
$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,
],
];
}
}