279 lines
10 KiB
PHP
279 lines
10 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Payments\Danal;
|
|
|
|
use App\Models\GcPaymentAttempt;
|
|
use App\Services\Payments\Danal\Gateways\DanalCardGateway;
|
|
use App\Services\Payments\Danal\Gateways\DanalVactGateway;
|
|
use App\Services\Payments\Danal\Clients\DanalTeleditClient;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
|
|
final class DanalPaymentService
|
|
{
|
|
public function __construct(
|
|
private readonly DanalCardGateway $card,
|
|
private readonly DanalVactGateway $vact,
|
|
private readonly DanalTeleditClient $teledit,
|
|
) {}
|
|
|
|
/**
|
|
* 결제 시작: card/vact/phone 분기
|
|
*/
|
|
public function start(int $memNo, array $data): array
|
|
{
|
|
$oid = (string)$data['oid'];
|
|
$payMethod = (string)$data['pay_method'];
|
|
|
|
// TODO: 여기서 CI3처럼 주문정보를 DB에서 조회해서 변조 방지해야 함
|
|
// - 필요한 값: amount, itemName, stat_pay(w), mid(환금성 판단), mem_no 일치 등
|
|
// - 현재는 최소값만 받되, 반드시 네 주문 테이블에 맞게 연결할 것.
|
|
$order = $this->loadOrderForPaymentOrFail($oid, $memNo);
|
|
|
|
return DB::transaction(function () use ($memNo, $data, $order, $payMethod, $oid) {
|
|
[$attempt, $token] = $this->upsertAttempt($memNo, $order, $data);
|
|
|
|
if ($payMethod === 'card') {
|
|
$kind = $data['card_kind'] ?? 'general'; // general|exchange
|
|
$start = $this->card->auth($attempt, $token, $order, $kind);
|
|
} elseif ($payMethod === 'vact') {
|
|
$kind = $data['vact_kind'] ?? 'a'; // a|v (기본 a)
|
|
$start = $this->vact->auth($attempt, $token, $order, $kind);
|
|
} else { // phone
|
|
$start = $this->teledit->ready($attempt, $token, $order);
|
|
}
|
|
|
|
$attempt->status = 'redirected';
|
|
$attempt->redirected_at = now();
|
|
$attempt->save();
|
|
|
|
return $start;
|
|
});
|
|
}
|
|
|
|
public function handleCardReturn(string $oid, string $token, array $all, array $post): array
|
|
{
|
|
return DB::transaction(function () use ($oid, $token, $post) {
|
|
$attempt = $this->findAttemptOrFail('card', $oid, $token);
|
|
$order = $this->loadOrderForPaymentOrFail($attempt->oid, (int)$attempt->mem_no);
|
|
|
|
$ret = $this->card->billOnReturn($attempt, $token, $order, $post);
|
|
|
|
return $ret;
|
|
});
|
|
}
|
|
|
|
public function handleVactReturn(string $token, array $post): array
|
|
{
|
|
return DB::transaction(function () use ($token, $post) {
|
|
$attempt = $this->findAttemptByTokenOrFail('vact', $token);
|
|
$order = $this->loadOrderForPaymentOrFail($attempt->oid, (int)$attempt->mem_no);
|
|
|
|
return $this->vact->issueVaccountOnReturn($attempt, $token, $order, $post);
|
|
});
|
|
}
|
|
|
|
public function handleVactNoti(array $post): void
|
|
{
|
|
// NOTI는 idempotent하게: 이미 paid면 그냥 OK
|
|
DB::transaction(function () use ($post) {
|
|
$this->vact->handleNoti($post, function (string $oid, string $tid, int $amount, array $payload) {
|
|
// TODO: 여기서 주문 paid 처리 + 장부 처리
|
|
// $this->markOrderPaid($oid, $tid, $amount, $payload);
|
|
|
|
// attempt도 paid로
|
|
$attempt = GcPaymentAttempt::query()
|
|
->where('provider','danal')->where('oid',$oid)->where('pay_method','vact')
|
|
->lockForUpdate()->first();
|
|
|
|
if ($attempt) {
|
|
if ($attempt->status !== 'paid') {
|
|
$attempt->status = 'paid';
|
|
$attempt->pg_tid = $tid ?: $attempt->pg_tid;
|
|
$attempt->amount = $amount ?: $attempt->amount;
|
|
$attempt->noti_payload = $payload;
|
|
$attempt->noti_at = now();
|
|
$attempt->save();
|
|
}
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
public function handlePhoneReturn(array $post): array
|
|
{
|
|
return DB::transaction(function () use ($post) {
|
|
|
|
// Order 복호화로 oid/mem_no/token 확보
|
|
[$oid, $amount, $memNo, $token] = $this->teledit->decryptOrder((string)($post['Order'] ?? ''));
|
|
|
|
$order = $this->loadOrderForPaymentOrFail($oid, (int)$memNo);
|
|
|
|
return $this->teledit->confirmAndBillOnReturn($post, $order, function (string $oid, string $tid, int $amount, array $payload) {
|
|
// TODO: 주문 paid 처리 + 장부 처리
|
|
// $this->markOrderPaid($oid, $tid, $amount, $payload);
|
|
|
|
$attempt = GcPaymentAttempt::query()
|
|
->where('provider','danal')->where('oid',$oid)->where('pay_method','phone')
|
|
->lockForUpdate()->first();
|
|
|
|
if ($attempt) {
|
|
$attempt->status = 'paid';
|
|
$attempt->pg_tid = $tid;
|
|
$attempt->returned_at = now();
|
|
$attempt->return_payload = $payload;
|
|
$attempt->save();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
public function handlePhoneCancel(array $post): array
|
|
{
|
|
return DB::transaction(function () use ($post) {
|
|
|
|
return $this->teledit->handleCancel($post, function (string $oid, array $payload) {
|
|
// TODO: 주문 cancel 처리
|
|
// $this->cancelOrder($oid, 'C999', '사용자 결제 취소', $payload);
|
|
|
|
$attempt = GcPaymentAttempt::query()
|
|
->where('provider','danal')->where('oid',$oid)->where('pay_method','phone')
|
|
->lockForUpdate()->first();
|
|
|
|
if ($attempt) {
|
|
$attempt->status = 'cancelled';
|
|
$attempt->return_payload = $payload;
|
|
$attempt->returned_at = now();
|
|
$attempt->save();
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
|
|
public function handleCancel(string $oid, string $token): array
|
|
{
|
|
return DB::transaction(function () use ($oid, $token) {
|
|
$attempt = $this->findAttemptAnyMethodOrFail($oid, $token);
|
|
|
|
// TODO: 주문 cancel 처리
|
|
// $this->cancelOrder($oid, 'USER_CANCEL', '사용자 결제 취소', []);
|
|
|
|
$attempt->status = 'cancelled';
|
|
$attempt->returned_at = now();
|
|
$attempt->save();
|
|
|
|
return [
|
|
'status' => 'cancel',
|
|
'message' => '구매를 취소했습니다.',
|
|
'redirectUrl' => url('/mypage/usage'),
|
|
];
|
|
});
|
|
}
|
|
|
|
private function upsertAttempt(int $memNo, array $order, array $data): array
|
|
{
|
|
$token = bin2hex(random_bytes(32));
|
|
$tokenHash = hash('sha256', $token);
|
|
|
|
$attempt = GcPaymentAttempt::query()
|
|
->where('provider','danal')
|
|
->where('oid', (string)$order['oid'])
|
|
->where('pay_method', (string)$data['pay_method'])
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if (!$attempt) {
|
|
$attempt = new GcPaymentAttempt();
|
|
$attempt->provider = 'danal';
|
|
$attempt->oid = (string)$order['oid'];
|
|
$attempt->mem_no = $memNo;
|
|
$attempt->pay_method = (string)$data['pay_method'];
|
|
$attempt->amount = (int)$order['amount'];
|
|
$attempt->currency = 'KRW';
|
|
$attempt->ready_at = now();
|
|
}
|
|
|
|
$attempt->status = 'ready';
|
|
$attempt->token_hash = $tokenHash;
|
|
$attempt->card_kind = $data['card_kind'] ?? null;
|
|
$attempt->vact_kind = $data['vact_kind'] ?? null;
|
|
$attempt->user_agent = request()->userAgent();
|
|
$attempt->user_ip = inet_pton(request()->ip() ?: '127.0.0.1');
|
|
$attempt->save();
|
|
|
|
return [$attempt, $token];
|
|
}
|
|
|
|
private function findAttemptOrFail(string $method, string $oid, string $token): GcPaymentAttempt
|
|
{
|
|
$tokenHash = hash('sha256', $token);
|
|
|
|
$attempt = GcPaymentAttempt::query()
|
|
->where('provider','danal')
|
|
->where('oid',$oid)
|
|
->where('pay_method',$method)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if (!$attempt) abort(404);
|
|
if (!hash_equals((string)$attempt->token_hash, $tokenHash)) abort(403);
|
|
|
|
return $attempt;
|
|
}
|
|
|
|
private function findAttemptByTokenOrFail(string $method, string $token): GcPaymentAttempt
|
|
{
|
|
$tokenHash = hash('sha256', $token);
|
|
|
|
$attempt = GcPaymentAttempt::query()
|
|
->where('provider','danal')
|
|
->where('pay_method',$method)
|
|
->where('token_hash',$tokenHash)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if (!$attempt) abort(404);
|
|
|
|
return $attempt;
|
|
}
|
|
|
|
private function findAttemptAnyMethodOrFail(string $oid, string $token): GcPaymentAttempt
|
|
{
|
|
$tokenHash = hash('sha256', $token);
|
|
|
|
$attempt = GcPaymentAttempt::query()
|
|
->where('provider','danal')
|
|
->where('oid',$oid)
|
|
->where('token_hash',$tokenHash)
|
|
->lockForUpdate()
|
|
->first();
|
|
|
|
if (!$attempt) abort(404);
|
|
return $attempt;
|
|
}
|
|
|
|
/**
|
|
* TODO: 네 주문테이블에 맞게 구현해야 하는 핵심 함수
|
|
* - CI3 Product.php payment_danal_ready()에서 하던 검증(oid/mem_no/stat_pay/amount 등)
|
|
*/
|
|
private function loadOrderForPaymentOrFail(string $oid, int $memNo): array
|
|
{
|
|
// ✅ 여기만 너 DB 구조에 맞게 바꾸면 나머지 다날 연동은 그대로 감
|
|
// 예시 형태:
|
|
// $row = DB::table('gc_orders')->where('oid',$oid)->first();
|
|
// if (!$row) abort(404);
|
|
// if ((int)$row->mem_no !== $memNo) abort(403);
|
|
// if ($row->stat_pay !== 'w') abort(409);
|
|
// return ['oid'=>$row->oid,'amount'=>(int)$row->pay_money,'itemName'=>$row->pin_name,'mid'=>$row->mid];
|
|
|
|
return [
|
|
'oid' => $oid,
|
|
'amount' => (int) request('amount', 0), // 임시
|
|
'itemName' => (string) request('itemName', '상품권'),
|
|
'mid' => (string) request('mid', ''),
|
|
];
|
|
}
|
|
}
|