giftcon_dev/app/Services/Payments/Danal/Clients/DanalTeleditClient.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,
],
];
}
}