249 lines
9.9 KiB
PHP
249 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]],
|
|
'WIRETRANSFER' => ['wire', ['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];
|
|
}
|
|
}
|