giftcon_dev/app/Services/Order/OrderCheckoutService.php

248 lines
9.9 KiB
PHP

<?php
namespace App\Services\Order;
use App\Models\Payments\GcPinOrder;
use App\Models\Payments\GcPinOrderItem;
use App\Repositories\Product\ProductRepository;
use App\Repositories\Payments\GcPaymentMethodRepository;
use App\Services\Payments\PaymentService;
use App\Repositories\Member\MemInfoRepository;
use App\Support\LegacyCrypto\CiSeedCrypto;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
final class OrderCheckoutService
{
public function __construct(
private readonly MemInfoRepository $members,
private readonly ProductRepository $products,
private readonly GcPaymentMethodRepository $payMethods,
private readonly PaymentService $payment,
) {}
public function checkoutAndStart(int $memNo, int $skuId, int $qty, int $payId, Request $request): array
{
return DB::transaction(function () use ($memNo, $skuId, $qty, $payId, $request) {
$sku = $this->products->getActiveSkuWithProduct($skuId);
if (!$sku) return $this->fail('SKU_NOT_FOUND', '권종 정보를 찾을 수 없습니다.');
// allowed_payments 체크
$allowed = json_decode($sku->allowed_payments ?? '[]', true);
$allowed = is_array($allowed) ? array_map('intval', $allowed) : [];
if (!in_array((int)$payId, $allowed, true)) {
return $this->fail('PAY_NOT_ALLOWED', '선택한 결제수단은 사용 불가합니다.');
}
// qty 정책
$minQty = (int)($sku->min_buy_qty ?? 1);
$maxQty = (int)($sku->max_buy_qty ?? 0);
$maxAmount = (int)($sku->max_buy_amount ?? 0);
if ($qty < $minQty) $qty = $minQty;
if ($maxQty > 0 && $qty > $maxQty) {
return $this->fail('QTY_MAX', '최대 구매 수량을 초과했습니다.');
}
$unit = (int)($sku->final_price ?? 0);
if ($unit <= 0) return $this->fail('PRICE_INVALID', '판매가 정보가 올바르지 않습니다.');
$subtotal = $unit * $qty;
if ($maxAmount > 0 && $subtotal > $maxAmount) {
return $this->fail('AMOUNT_MAX', '1회 최대 결제 금액을 초과했습니다.');
}
$pm = $this->payMethods->findActiveById($payId);
if (!$pm) return $this->fail('PAY_NOT_FOUND', '결제수단 정보를 찾을 수 없습니다.');
// 수수료(정수연산, 2자리 소수율)
$customerFee = $this->calcFee($subtotal, (string)$pm->customer_fee_rate);
$payMoney = $subtotal + $customerFee;
$pgFee = $this->calcFee($payMoney, (string)$pm->pg_fee_rate);
// 주문 생성
$oid = 'GC' . now()->format('YmdHis') . Str::upper(Str::random(6));
$order = GcPinOrder::create([
'oid' => $oid,
'mem_no' => $memNo,
'order_type' => 'self',
'stat_pay' => 'ready',
'stat_tax' => 'taxfree',
'subtotal_amount' => $subtotal,
'fee_amount' => $customerFee,
'pg_fee_amount' => $pgFee,
'discount_amount' => 0,
'pay_money' => $payMoney,
'provider' => 'danal',
'ordered_at' => now(),
'pay_data' => [
'payment_method' => [
'id' => (int)$pm->id,
'code' => (string)$pm->code,
'name' => (string)$pm->name,
'display_name' => (string)$pm->display_name,
'customer_fee_rate' => (string)$pm->customer_fee_rate,
'pg_fee_rate' => (string)$pm->pg_fee_rate,
],
'calc' => [
'unit_price' => $unit,
'qty' => $qty,
'subtotal' => $subtotal,
'customer_fee' => $customerFee,
'pay_money' => $payMoney,
'pg_fee' => $pgFee,
],
],
]);
GcPinOrderItem::create([
'order_id' => $order->id,
'tbl_pointer' => null,
'pin_seq' => null,
'item_name' => (string)$sku->name,
'item_code' => (string)($sku->sku_code ?? $sku->id),
'qty' => $qty,
'unit_price' => (int)($sku->face_value ?? $unit),
'unit_pay_price' => $unit,
'line_subtotal' => $subtotal,
'line_fee' => $customerFee,
'line_total' => $payMoney,
'meta' => [
'product_id' => (int)$sku->product_id,
'product_name' => (string)$sku->product_name,
'sku_id' => (int)$sku->id,
],
]);
// pay_id -> 결제모듈 옵션 매핑
[$method, $opt] = $this->mapPayMethod((string)$pm->code, $request);
if ($method === 'phone') {
$m = $this->members->findForPhonePay($memNo) ?? $this->members->findByMemNo($memNo);
if (!$m) {
return [
'ok' => false,
'message' => '회원 정보를 찾을 수 없습니다.',
'redirect' => url('/'),
];
}
$phoneEnc = (string)($m->cell_phone ?? ''); // 복호화 가능한 암호문 컬럼
$corpCode = (string)($m->cell_corp ?? ''); // 01~06
if ($phoneEnc === '') {
return [
'ok' => false,
'message' => '휴대폰 번호가 등록되어 있지 않습니다. 휴대폰 번호 등록 후 이용해 주세요.',
'redirect' => url('/mypage/info'),
];
}
$seed = app(CiSeedCrypto::class);
$phonePlain = (string)$seed->decrypt($phoneEnc);
$digits = preg_replace('/\D+/', '', $phonePlain);
if ($digits === '' || strlen($digits) < 10 || strlen($digits) > 11) {
return [
'ok' => false,
'message' => '회원 휴대폰 번호 정보가 올바르지 않습니다. 고객센터에 문의해 주세요.',
'redirect' => url('/'),
];
}
$map = [
'01' => 'SKT', '02' => 'KTF', '03' => 'LGT',
'04' => 'MVNO','05' => 'MVNO','06' => 'MVNO',
];
$corpCode = str_pad(preg_replace('/\D+/', '', $corpCode), 2, '0', STR_PAD_LEFT);
$carrier = $map[$corpCode] ?? '';
if ($carrier === '') {
return [
'ok' => false,
'message' => '회원 통신사 정보가 올바르지 않습니다. 고객센터에 문의해 주세요.',
'redirect' => url('/'),
];
}
$opt['member_phone_digits'] = $digits;
$opt['member_carrier'] = $carrier;
}
// 결제 시작 (PG redirect 정보 생성)
$out = $this->payment->start($order->oid, $memNo, $method, $opt);
if (($out['type'] ?? '') === 'redirect') {
return [
'ok' => true,
'view' => 'autosubmit',
'action' => $out['start']['actionUrl'],
'fields' => $out['start']['params'],
'acceptCharset' => $out['start']['acceptCharset'] ?? 'EUC-KR',
'meta' => $out['meta'] ?? [],
'token' => $out['meta']['token'] ?? '',
'oid' => $out['meta']['oid'] ?? $order->oid,
'method' => $out['meta']['method'] ?? $method,
];
}
// 실패
return [
'ok' => false,
'code' => $out['meta']['code'] ?? 'PG_FAIL',
'message' => $out['message'] ?? '결제 시작 실패',
'redirect' => url("/product/detail/" . (int)$sku->product_id),
];
});
}
private function mapPayMethod(string $code, Request $request): array
{
$ua = strtolower((string)$request->userAgent());
$isMobile = str_contains($ua, 'mobile') || str_contains($ua, 'android') || str_contains($ua, 'iphone');
// 휴대폰 dev/prod 분리: 운영=prod, 그 외=dev 기본
$phoneMode = (string)config('danal.phone.default_mode', 'prod');
// 개발환경에서만 ?phone_mode=dev|prod 로 override 허용(원하면)
if (!app()->environment('production')) {
$q = (string)$request->query('phone_mode', '');
if ($q === 'prod' || $q === 'dev') $phoneMode = $q;
}
return match ($code) {
'MOBILE' => ['phone', ['phone_mode'=>$phoneMode, 'is_mobile'=>$isMobile]],
'CREDIT_CARD' => ['card', ['card_kind'=>'general', 'is_mobile'=>$isMobile]],
'CREDIT_CARD_REFUND' => ['card', ['card_kind'=>'exchange', 'is_mobile'=>$isMobile]],
'VACT' => ['vact', ['is_mobile'=>$isMobile]],
default => ['card', ['card_kind'=>'general', 'is_mobile'=>$isMobile]],
};
}
// rateStr: "5.00" (%), 2자리 소수 -> 정수 계산
private function calcFee(int $amount, string $rateStr): int
{
$rate = (float)$rateStr; // 5.00
$bp = (int) round($rate * 100); // 500 (basis points of percent)
// amount * bp / 10000
return (int) ceil(($amount * $bp) / 10000);
}
private function fail(string $code, string $message): array
{
return ['ok'=>false, 'code'=>$code, 'message'=>$message];
}
}