2026-03-03 15:13:16 +09:00

140 lines
4.4 KiB
PHP

<?php
namespace App\Providers\Danal\Gateways;
use App\Models\Payments\GcPinOrder;
use App\Providers\Danal\Clients\DanalCpcgiClient;
use App\Providers\Danal\DanalConfig;
use App\Support\Danal\EucKr;
final class CardGateway
{
public function __construct(
private readonly DanalConfig $cfg,
private readonly DanalCpcgiClient $client,
private readonly EucKr $euc,
) {}
public function auth(GcPinOrder $order, string $attemptToken, string $cardKind, bool $isMobile): array
{
$c = $this->cfg->card($cardKind);
$userAgent = $isMobile ? 'WM' : 'PC';
$returnUrl = route('web.payments.danal.card.return', ['a' => $attemptToken], true);
$cancelUrl = route('web.payments.danal.cancel', ['a' => $attemptToken], true);
// BYPASSVALUE: '&' 금지. 토큰만 넣고 나머지는 서버에서 검증.
$req = [
'SUBCPID' => '',
'AMOUNT' => (string)$order->pay_money,
'CURRENCY' => '410',
'ITEMNAME' => $this->safeItemName($this->orderTitle($order)),
'USERAGENT' => $userAgent,
'ORDERID' => $order->oid,
'OFFERPERIOD' => '',
'USERNAME' => '',
'USERID' => (string)$order->mem_no,
'USEREMAIL' => '',
'CANCELURL' => $cancelUrl,
'RETURNURL' => $returnUrl,
'TXTYPE' => 'AUTH',
'SERVICETYPE' => 'DANALCARD',
'ISNOTI' => 'N',
'BYPASSVALUE' => 'AT=' . $attemptToken,
];
$res = $this->client->call($c['url'], $c['cpid'], $req, $c['key'], $c['iv']);
return [
'req' => $req,
'res' => $res,
'start' => [
'actionUrl' => (string)($res['STARTURL'] ?? ''),
'params' => ['STARTPARAMS' => (string)($res['STARTPARAMS'] ?? '')],
'acceptCharset' => 'EUC-KR',
],
];
}
public function bill(GcPinOrder $order, string $cardKind, string $tid): array
{
$c = $this->cfg->card($cardKind);
$req = [
'TID' => $tid,
'AMOUNT' => (string)$order->pay_money,
'TXTYPE' => 'BILL',
'SERVICETYPE' => 'DANALCARD',
];
$res = $this->client->call($c['url'], $c['cpid'], $req, $c['key'], $c['iv']);
return ['req' => $req, 'res' => $res];
}
public function decryptReturn(string $cardKind, string $returnParams): array
{
$c = $this->cfg->card($cardKind);
return $this->client->decryptReturnParams($returnParams, $c['key'], $c['iv']);
}
private function orderTitle(GcPinOrder $order): string
{
// 아이템 1개면 그 이름, 아니면 "상품권 외 N건"
$items = $order->items()->limit(2)->get();
if ($items->count() === 0) return '상품권';
if ($items->count() === 1) return (string)$items[0]->item_name;
return (string)$items[0]->item_name . ' 외';
}
private function safeItemName(string $s): string
{
// 금칙문자 최소 제거(사고 방지)
$s = str_replace(["&","'","\"","\\","<",">","|","\r","\n","," , "+"], " ", $s);
return trim(preg_replace('/\s+/', ' ', $s)) ?: '상품권';
}
public function cancel(
GcPinOrder $order,
string $cardKind,
string $tid,
?int $amount = null,
string $cancelType = 'C',
string $requester = '',
string $desc = ''
): array {
$c = $this->cfg->card($cardKind);
$amt = $amount ?? (int)$order->pay_money;
$req = [
'TID' => $tid,
'AMOUNT' => (string)$amt,
'CANCELTYPE' => $cancelType, // C:전체, P:부분
'TXTYPE' => 'CANCEL',
'SERVICETYPE' => 'DANALCARD',
];
// optional
$requester = trim($requester);
$desc = $this->safeCancelDesc($desc);
if ($requester !== '') $req['CANCELREQUESTER'] = $requester;
if ($desc !== '') $req['CANCELDESC'] = $desc;
$res = $this->client->call($c['url'], $c['cpid'], $req, $c['key'], $c['iv']);
return ['req' => $req, 'res' => $res];
}
private function safeCancelDesc(string $s): string
{
// 다날 DATA 암복호화 구간에서 문제 일으키는 문자 최소 제거
$s = str_replace(["&","'","\"","\\","<",">","|","\r","\n","," , "+"], " ", $s);
return trim(preg_replace('/\s+/', ' ', $s));
}
}