309 lines
10 KiB
PHP
309 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Payments\Danal\Clients;
|
|
|
|
use App\Models\GcPaymentAttempt;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
final class DanalTeleditClient
|
|
{
|
|
/**
|
|
* CI3: payment_danal_cell() -> SClient ITEMSEND2
|
|
* return: redirect info for Start.php
|
|
*/
|
|
public function ready(GcPaymentAttempt $attempt, string $token, array $order): array
|
|
{
|
|
$binPath = (string)config('danal.phone.bin_path', '');
|
|
if ($binPath === '') throw new \RuntimeException('DANAL_PHONE_BIN_PATH missing');
|
|
|
|
$cpid = (string)config('danal.phone.cpid', '');
|
|
$pwd = (string)config('danal.phone.pwd', '');
|
|
if ($cpid === '' || $pwd === '') throw new \RuntimeException('DANAL_PHONE_CPID/PWD missing');
|
|
|
|
// CI3 상수(_PG_ITEMCODE_DANAL) 역할: item_code 필요
|
|
$itemCode = (string)config('danal.phone.item_code', '');
|
|
if ($itemCode === '') throw new \RuntimeException('config(danal.phone.item_code) missing (DANAL_PHONE_ITEMCODE 필요)');
|
|
|
|
$oid = (string)($order['oid'] ?? '');
|
|
$amount = (int)($order['amount'] ?? $order['pay_money'] ?? 0);
|
|
$itemName = (string)($order['itemName'] ?? $order['pin_name'] ?? '상품권');
|
|
$memNo = (string)($order['mem_no'] ?? $attempt->mem_no ?? '');
|
|
|
|
$cellCorp = (string)($order['cell_corp'] ?? '');
|
|
$dstAddr = preg_replace('/\D+/', '', (string)($order['cell_phone'] ?? ''));
|
|
|
|
$carrier = $this->mapCarrier($cellCorp);
|
|
|
|
$trans = [
|
|
'Command' => 'ITEMSEND2',
|
|
'SERVICE' => 'TELEDIT',
|
|
'ItemCount' => '1',
|
|
'OUTPUTOPTION' => 'DEFAULT',
|
|
|
|
'ID' => $cpid,
|
|
'PWD' => $pwd,
|
|
|
|
'SUBCP' => '',
|
|
'USERID' => $memNo,
|
|
'ORDERID' => $oid,
|
|
'IsPreOtbill' => 'N',
|
|
'IsSubscript' => 'N',
|
|
|
|
'ItemInfo' => $this->makeItemInfo($amount, $itemCode, $itemName),
|
|
];
|
|
|
|
$attempt->request_payload = $trans;
|
|
$attempt->save();
|
|
|
|
$res = $this->runClient($binPath, 'SClient', $trans);
|
|
|
|
$attempt->response_payload = $res;
|
|
$attempt->save();
|
|
|
|
if (($res['Result'] ?? '') !== '0') {
|
|
$msg = (string)($res['ErrMsg'] ?? '휴대폰 결제 모듈 실패');
|
|
throw new \RuntimeException("Teledit ITEMSEND2 failed: {$msg}");
|
|
}
|
|
|
|
// CPCGI로 POST 전달되는 bypass
|
|
$byPass = [
|
|
'BgColor' => '00',
|
|
'TargetURL' => route('web.payments.danal.phone.return', [], true),
|
|
'BackURL' => route('web.payments.danal.phone.cancel', [], true),
|
|
'IsUseCI' => 'N',
|
|
'CIURL' => url('/img/main/top_logo.png'),
|
|
'Email' => (string)($order['email'] ?? ''),
|
|
'IsCharSet' => '',
|
|
// 기존 macro->encrypt("DanaLTelediT", "oid/amount/mem_no") 구조 유지 + token 추가
|
|
'Order' => Crypt::encryptString($oid . '/' . $amount . '/' . $memNo . '/' . $token),
|
|
];
|
|
|
|
$isMobile = (bool)($order['is_mobile'] ?? false);
|
|
$actionUrl = $isMobile
|
|
? 'https://ui.teledit.com/Danal/Teledit/Mobile/Start.php'
|
|
: 'https://ui.teledit.com/Danal/Teledit/Web/Start.php';
|
|
|
|
// CI3처럼 Result/ErrMsg 제외하고 Res를 폼에 다 넣음
|
|
$params = [];
|
|
foreach ($res as $k => $v) {
|
|
if ($k === 'Result' || $k === 'ErrMsg') continue;
|
|
$params[$k] = $v;
|
|
}
|
|
foreach ($byPass as $k => $v) $params[$k] = $v;
|
|
|
|
// 추가 Hidden
|
|
$params['CPName'] = (string)($order['cp_name'] ?? '핀포유');
|
|
$params['ItemName'] = $itemName;
|
|
$params['ItemAmt'] = (string)$amount;
|
|
$params['IsPreOtbill'] = (string)$trans['IsPreOtbill'];
|
|
$params['IsSubscript'] = (string)$trans['IsSubscript'];
|
|
$params['IsCarrier'] = $carrier; // SKT/KTF/LGT
|
|
$params['IsDstAddr'] = $dstAddr; // 010xxxxxxxx
|
|
|
|
return [
|
|
'actionUrl' => $actionUrl,
|
|
'params' => $params,
|
|
'acceptCharset' => 'EUC-KR',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* CI3: payment_danal_cell_return()
|
|
* - Order 복호화 -> NCONFIRM -> NBILL
|
|
* - 성공 시 onPaid(oid, tid, amount, payload)
|
|
*/
|
|
public function confirmAndBillOnReturn(array $post, array $order, callable $onPaid): array
|
|
{
|
|
$binPath = (string)config('danal.phone.bin_path', '');
|
|
if ($binPath === '') throw new \RuntimeException('DANAL_PHONE_BIN_PATH missing');
|
|
|
|
$serverInfo = (string)($post['ServerInfo'] ?? '');
|
|
if ($serverInfo === '') return $this->fail('E990', 'ServerInfo 누락');
|
|
|
|
$encOrder = (string)($post['Order'] ?? '');
|
|
if ($encOrder === '') return $this->fail('E991', 'Order 누락');
|
|
|
|
[$oid, $amount, $memNo, $token] = $this->decryptOrder($encOrder);
|
|
|
|
// (보안) 휴대폰번호 변조 체크: order에 cell_phone이 있으면 비교
|
|
$dst = preg_replace('/\D+/', '', (string)($post['IsDstAddr'] ?? ''));
|
|
$expected = preg_replace('/\D+/', '', (string)($order['cell_phone'] ?? ''));
|
|
if ($expected !== '' && $dst !== '' && $dst !== $expected) {
|
|
return $this->fail('E992', '휴대폰번호 변조 의심');
|
|
}
|
|
|
|
// NCONFIRM
|
|
$nConfirm = [
|
|
'Command' => 'NCONFIRM',
|
|
'OUTPUTOPTION' => 'DEFAULT',
|
|
'ServerInfo' => $serverInfo,
|
|
'IFVERSION' => 'V1.1.2',
|
|
'ConfirmOption' => '0',
|
|
];
|
|
$res1 = $this->runClient($binPath, 'SClient', $nConfirm);
|
|
if (($res1['Result'] ?? '') !== '0') {
|
|
return $this->fail((string)($res1['Result'] ?? 'E993'), (string)($res1['ErrMsg'] ?? 'NCONFIRM 실패'));
|
|
}
|
|
|
|
// NBILL
|
|
$nBill = [
|
|
'Command' => 'NBILL',
|
|
'OUTPUTOPTION' => 'DEFAULT',
|
|
'ServerInfo' => $serverInfo,
|
|
'IFVERSION' => 'V1.1.2',
|
|
'BillOption' => '0',
|
|
];
|
|
$res2 = $this->runClient($binPath, 'SClient', $nBill);
|
|
if (($res2['Result'] ?? '') !== '0') {
|
|
return $this->fail((string)($res2['Result'] ?? 'E994'), (string)($res2['ErrMsg'] ?? 'NBILL 실패'));
|
|
}
|
|
|
|
// 성공: CI3처럼 Res(=NCONFIRM 결과)에서 TID/AMOUNT/ORDERID 사용
|
|
$tid = (string)($res1['TID'] ?? '');
|
|
$paidAmount = (int)($res1['AMOUNT'] ?? $amount);
|
|
|
|
$payload = [
|
|
'Res' => $res1,
|
|
'Res2' => $res2,
|
|
'Post' => $post,
|
|
];
|
|
|
|
$onPaid($oid, $tid, $paidAmount, $payload);
|
|
|
|
return [
|
|
'status' => 'success',
|
|
'message' => '결제가 완료되었습니다.',
|
|
'redirectUrl' => url('/mypage/usage'),
|
|
'meta' => [
|
|
'oid' => $oid,
|
|
'tid' => $tid,
|
|
'amount' => $paidAmount,
|
|
'code' => (string)($res2['Result'] ?? '0'),
|
|
'msg' => (string)($res2['ErrMsg'] ?? ''),
|
|
'ret_data' => $payload,
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* CI3: payment_danal_cell_cancel()
|
|
*/
|
|
public function handleCancel(array $post, callable $onCancel): array
|
|
{
|
|
$encOrder = (string)($post['Order'] ?? '');
|
|
if ($encOrder === '') return $this->fail('C990', 'Order 누락');
|
|
|
|
[$oid, $amount, $memNo, $token] = $this->decryptOrder($encOrder);
|
|
|
|
$onCancel($oid, [
|
|
'Post' => $post,
|
|
'oid' => $oid,
|
|
'amount' => $amount,
|
|
'mem_no' => $memNo,
|
|
]);
|
|
|
|
return [
|
|
'status' => 'cancel',
|
|
'message' => '사용자 결제 취소',
|
|
'redirectUrl' => url('/mypage/usage'),
|
|
];
|
|
}
|
|
|
|
public function decryptOrder(string $encOrder): array
|
|
{
|
|
$plain = Crypt::decryptString($encOrder);
|
|
$parts = explode('/', $plain);
|
|
|
|
$oid = (string)($parts[0] ?? '');
|
|
$amount = (int)($parts[1] ?? 0);
|
|
$memNo = (string)($parts[2] ?? '');
|
|
$token = (string)($parts[3] ?? '');
|
|
|
|
if ($oid === '' || $amount <= 0 || $memNo === '' || $token === '') {
|
|
throw new \RuntimeException('Invalid Order payload');
|
|
}
|
|
|
|
return [$oid, $amount, $memNo, $token];
|
|
}
|
|
|
|
private function runClient(string $binPath, string $bin, array $params): array
|
|
{
|
|
$arg = $this->makeParam($params);
|
|
|
|
$proc = new Process([$binPath . '/' . $bin, $arg]);
|
|
$proc->setTimeout((int)config('danal.authtel.timeout', 30));
|
|
$proc->run();
|
|
|
|
$out = $proc->getOutput();
|
|
if ($out === '' && !$proc->isSuccessful()) {
|
|
$err = $proc->getErrorOutput();
|
|
throw new \RuntimeException("Teledit {$bin} failed: {$err}");
|
|
}
|
|
|
|
return $this->parseOutput($out);
|
|
}
|
|
|
|
private function parseOutput(string $out): array
|
|
{
|
|
// CI3 Parsor(): 라인들을 &로 붙이고 key=value 파싱 + urldecode
|
|
$out = str_replace("\r", '', $out);
|
|
$lines = array_filter(array_map('trim', explode("\n", $out)), fn($v) => $v !== '');
|
|
$in = implode('&', $lines);
|
|
|
|
$map = [];
|
|
foreach (explode('&', $in) as $tok) {
|
|
$tok = trim($tok);
|
|
if ($tok === '') continue;
|
|
|
|
$tmp = explode('=', $tok, 2);
|
|
$name = trim($tmp[0] ?? '');
|
|
$value = trim($tmp[1] ?? '');
|
|
|
|
if ($name === '') continue;
|
|
$map[$name] = urldecode($value);
|
|
}
|
|
return $map;
|
|
}
|
|
|
|
private function makeParam(array $arr): string
|
|
{
|
|
// CI3 MakeParam(): key=value;key=value...
|
|
$parts = [];
|
|
foreach ($arr as $k => $v) {
|
|
$parts[] = $k . '=' . $v;
|
|
}
|
|
return implode(';', $parts);
|
|
}
|
|
|
|
private function makeItemInfo(int $amt, string $code, string $name): string
|
|
{
|
|
// CI3 MakeItemInfo(): substr(code,0,1)|amt|1|code|name
|
|
return substr($code, 0, 1) . '|' . $amt . '|1|' . $code . '|' . $name;
|
|
}
|
|
|
|
private function mapCarrier(string $corp): string
|
|
{
|
|
// CI3: 01=SKT, 02=KTF, 03=LGT
|
|
return match ($corp) {
|
|
'01' => 'SKT',
|
|
'02' => 'KTF',
|
|
'03' => 'LGT',
|
|
default => '',
|
|
};
|
|
}
|
|
|
|
private function fail(string $code, string $msg): array
|
|
{
|
|
return [
|
|
'status' => 'fail',
|
|
'message' => $msg ?: '결제에 실패했습니다.',
|
|
'redirectUrl' => url('/mypage/usage'),
|
|
'meta' => [
|
|
'code' => $code,
|
|
'msg' => $msg,
|
|
],
|
|
];
|
|
}
|
|
}
|