262 lines
9.2 KiB
PHP
262 lines
9.2 KiB
PHP
<?php
|
|
|
|
namespace App\Providers\Danal\Gateways;
|
|
|
|
use App\Models\Payments\GcPinOrder;
|
|
use App\Providers\Danal\DanalConfig;
|
|
use Illuminate\Support\Facades\Crypt;
|
|
use Symfony\Component\Process\Process;
|
|
|
|
final class PhoneGateway
|
|
{
|
|
public function __construct(
|
|
private readonly DanalConfig $cfg,
|
|
) {}
|
|
|
|
public function ready(GcPinOrder $order, string $attemptToken, string $mode, bool $isMobile, array $extra = []): array
|
|
{
|
|
$c = $this->cfg->phone($mode);
|
|
|
|
$binPath = rtrim($c['bin_path'], '/');
|
|
$itemCode = (string)$c['item_code'];
|
|
|
|
// ITEMSEND2
|
|
$trans = [
|
|
'Command' => 'ITEMSEND2',
|
|
'SERVICE' => 'TELEDIT',
|
|
'ItemCount' => '1',
|
|
'OUTPUTOPTION' => 'DEFAULT',
|
|
|
|
'ID' => $c['cpid'],
|
|
'PWD' => $c['pwd'],
|
|
|
|
'SUBCP' => '',
|
|
'USERID' => (string)$order->mem_no,
|
|
'ORDERID' => (string)$order->oid,
|
|
'IsPreOtbill' => 'N',
|
|
'IsSubscript' => 'N',
|
|
'ItemInfo' => $this->makeItemInfo((int)$order->pay_money, $itemCode, $this->orderTitle($order)),
|
|
];
|
|
|
|
$res = $this->runSClient($binPath, $trans);
|
|
|
|
if (($res['Result'] ?? '') !== '0') {
|
|
$msg = (string)($res['ErrMsg'] ?? '휴대폰 결제 모듈 실패');
|
|
return [
|
|
'req' => $trans,
|
|
'res' => $res,
|
|
'start' => null,
|
|
'error' => ['code' => (string)($res['Result'] ?? 'E_TELEDIT'), 'msg' => $msg],
|
|
];
|
|
}
|
|
|
|
// RETURN 시 검증용(oid/amount/mem_no/token/mode)
|
|
$orderPayload = Crypt::encryptString($order->oid . '/' . $order->pay_money . '/' . $order->mem_no . '/' . $attemptToken . '/' . $mode);
|
|
|
|
$byPass = [
|
|
'BgColor' => '00',
|
|
'TargetURL' => route('web.payments.danal.phone.return', [], true),
|
|
'BackURL' => route('web.payments.danal.phone.cancel', [], true),
|
|
'IsUseCI' => 'N',
|
|
'CIURL' => $extra['ci_url'] ?? url('/img/main/top_logo.png'),
|
|
'Email' => $extra['email'] ?? '',
|
|
'IsCharSet' => '',
|
|
'Order' => $orderPayload,
|
|
];
|
|
|
|
$actionUrl = $isMobile ? $c['start_url_mobile'] : $c['start_url_web'];
|
|
|
|
$params = [];
|
|
foreach ($res as $k => $v) {
|
|
if ($k === 'Result' || $k === 'ErrMsg') continue;
|
|
$params[$k] = $v;
|
|
}
|
|
foreach ($byPass as $k => $v) $params[$k] = $v;
|
|
|
|
// 폼 히든 추가(필요시)
|
|
$params['CPName'] = $extra['cp_name'] ?? '핀포유';
|
|
$params['ItemName'] = $this->orderTitle($order);
|
|
$params['ItemAmt'] = (string)$order->pay_money;
|
|
$params['IsCarrier'] = (string)$extra['member_cell_corp'];
|
|
$params['IsDstAddr'] = (string)$extra['member_phone_enc'];
|
|
|
|
logger()->info('danal_phone_mode_config', [
|
|
'params' => $extra,
|
|
'default_mode' => config('danal.phone.default_mode'),
|
|
]);
|
|
|
|
return [
|
|
'req' => $trans,
|
|
'res' => $res,
|
|
'start' => [
|
|
'actionUrl' => $actionUrl,
|
|
'params' => $params,
|
|
'acceptCharset' => 'EUC-KR',
|
|
],
|
|
];
|
|
}
|
|
|
|
public function confirmAndBill(array $post, callable $onPaid): array
|
|
{
|
|
$serverInfo = (string)($post['ServerInfo'] ?? '');
|
|
$encOrder = (string)($post['Order'] ?? '');
|
|
|
|
if ($serverInfo === '' || $encOrder === '') {
|
|
return ['ok' => false, 'code' => 'E_PHONE_PARAM', 'msg' => 'ServerInfo/Order 누락', 'payload' => ['post'=>$post]];
|
|
}
|
|
|
|
[$oid, $amount, $memNo, $token, $mode] = $this->decryptOrder($encOrder);
|
|
$c = $this->cfg->phone($mode);
|
|
$binPath = rtrim($c['bin_path'], '/');
|
|
|
|
// NCONFIRM
|
|
$nConfirm = [
|
|
'Command' => 'NCONFIRM',
|
|
'OUTPUTOPTION' => 'DEFAULT',
|
|
'ServerInfo' => $serverInfo,
|
|
'IFVERSION' => 'V1.1.2',
|
|
'ConfirmOption' => '0',
|
|
];
|
|
$res1 = $this->runSClient($binPath, $nConfirm);
|
|
if (($res1['Result'] ?? '') !== '0') {
|
|
return ['ok' => false, 'code' => (string)($res1['Result'] ?? 'E_NCONFIRM'), 'msg' => (string)($res1['ErrMsg'] ?? 'NCONFIRM 실패'), 'payload'=>['post'=>$post,'res1'=>$res1]];
|
|
}
|
|
|
|
// NBILL
|
|
$nBill = [
|
|
'Command' => 'NBILL',
|
|
'OUTPUTOPTION' => 'DEFAULT',
|
|
'ServerInfo' => $serverInfo,
|
|
'IFVERSION' => 'V1.1.2',
|
|
'BillOption' => '0',
|
|
];
|
|
$res2 = $this->runSClient($binPath, $nBill);
|
|
if (($res2['Result'] ?? '') !== '0') {
|
|
return ['ok' => false, 'code' => (string)($res2['Result'] ?? 'E_NBILL'), 'msg' => (string)($res2['ErrMsg'] ?? 'NBILL 실패'), 'payload'=>['post'=>$post,'res1'=>$res1,'res2'=>$res2]];
|
|
}
|
|
|
|
$tid = (string)($res1['TID'] ?? '');
|
|
$paidAmount = (int)($res1['AMOUNT'] ?? $amount);
|
|
|
|
$payload = ['post'=>$post,'nconfirm'=>$res1,'nbill'=>$res2,'meta'=>['oid'=>$oid,'mem_no'=>$memNo,'token'=>$token,'mode'=>$mode]];
|
|
|
|
$onPaid($oid, $token, $tid, $paidAmount, $payload);
|
|
|
|
return ['ok'=>true, 'oid'=>$oid, 'token'=>$token, 'tid'=>$tid, 'amount'=>$paidAmount, 'payload'=>$payload];
|
|
}
|
|
|
|
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] ?? '');
|
|
$mode = (string)($parts[4] ?? 'prod');
|
|
|
|
if ($oid === '' || $amount <= 0 || $memNo === '' || $token === '') {
|
|
throw new \RuntimeException('Invalid phone Order payload');
|
|
}
|
|
if ($mode !== 'prod' && $mode !== 'dev') $mode = 'prod';
|
|
|
|
return [$oid, $amount, $memNo, $token, $mode];
|
|
}
|
|
|
|
public function cancel(array $post, callable $onCancel): array
|
|
{
|
|
$encOrder = (string)($post['Order'] ?? '');
|
|
if ($encOrder === '') return ['ok'=>false, 'code'=>'E_CANCEL', 'msg'=>'Order 누락', 'payload'=>['post'=>$post]];
|
|
|
|
[$oid, $amount, $memNo, $token, $mode] = $this->decryptOrder($encOrder);
|
|
$payload = ['post'=>$post,'meta'=>compact('oid','amount','memNo','token','mode')];
|
|
|
|
$onCancel($oid, $token, $payload);
|
|
|
|
return ['ok'=>true, 'oid'=>$oid, 'token'=>$token, 'payload'=>$payload];
|
|
}
|
|
|
|
private function runSClient(string $binPath, array $params): array
|
|
{
|
|
$arg = $this->makeParam($params);
|
|
$proc = new Process([$binPath . '/SClient', $arg]);
|
|
$proc->setTimeout((int)config('danal.http.timeout', 30));
|
|
$proc->run();
|
|
|
|
$out = $proc->getOutput();
|
|
if ($out === '' && !$proc->isSuccessful()) {
|
|
$err = $proc->getErrorOutput();
|
|
throw new \RuntimeException("Teledit SClient failed: {$err}");
|
|
}
|
|
|
|
return $this->parseOutput($out);
|
|
}
|
|
|
|
private function parseOutput(string $out): array
|
|
{
|
|
$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] ?? '');
|
|
if ($name === '') continue;
|
|
|
|
$value = trim($tmp[1] ?? '');
|
|
$value = urldecode($value);
|
|
|
|
// 이미 UTF-8이면 그대로 두고, 아니면 EUC-KR -> UTF-8 변환
|
|
if (!function_exists('mb_check_encoding') || !mb_check_encoding($value, 'UTF-8')) {
|
|
$value = @iconv('EUC-KR', 'UTF-8//IGNORE', $value) ?: $value;
|
|
// 마지막 안전망(깨진 UTF-8 제거)
|
|
if (function_exists('mb_check_encoding') && !mb_check_encoding($value, 'UTF-8')) {
|
|
$value = @iconv('UTF-8', 'UTF-8//IGNORE', $value) ?: '';
|
|
}
|
|
}
|
|
|
|
$map[$name] = $value;
|
|
}
|
|
|
|
return $map;
|
|
}
|
|
|
|
|
|
private function makeParam(array $arr): string
|
|
{
|
|
$parts = [];
|
|
foreach ($arr as $k => $v) {
|
|
$sv = (string)$v;
|
|
// SClient 전송용만 EUC-KR
|
|
$sv = @iconv('UTF-8', 'EUC-KR//IGNORE', $sv) ?: $sv;
|
|
$parts[] = $k . '=' . $sv;
|
|
}
|
|
return implode(';', $parts);
|
|
}
|
|
private function makeItemInfo(int $amt, string $code, string $name): string
|
|
{
|
|
$name = $this->safeItemName($name);
|
|
return substr($code, 0, 1) . '|' . $amt . '|1|' . $code . '|' . $name;
|
|
}
|
|
|
|
private function orderTitle(GcPinOrder $order): string
|
|
{
|
|
$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); // teledit 금칙 최소
|
|
$s = str_replace(["&","\"","\\","<",">","," , "+"], " ", $s);
|
|
return trim(preg_replace('/\s+/', ' ', $s)) ?: '상품권';
|
|
}
|
|
}
|