EV 충전 플랫폼 초기 백업
This commit is contained in:
179
app/routers/payments.py
Normal file
179
app/routers/payments.py
Normal file
@@ -0,0 +1,179 @@
|
||||
"""결제 API — 토스페이먼츠 연동
|
||||
|
||||
결제 흐름:
|
||||
1. POST /payments/prepare → orderId 발급 + Redis에 임시 저장
|
||||
2. 프론트에서 토스 결제 UI 호출 → 사용자 카드 입력
|
||||
3. POST /payments/confirm → 백엔드에서 토스 confirm API 호출
|
||||
4. 승인 성공 → 세션 상태 AUTHORIZED → 충전 시작 가능
|
||||
"""
|
||||
|
||||
import uuid
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from app.models import ChargingSession, Payment, SessionStatus, PaymentStatus
|
||||
from app.schemas import PaymentRequest, PaymentConfirm, PaymentOut
|
||||
from app.services.payment import toss_service
|
||||
from app.config import get_settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = get_settings()
|
||||
router = APIRouter(prefix="/payments", tags=["결제"])
|
||||
|
||||
|
||||
@router.post("/prepare")
|
||||
async def prepare_payment(
|
||||
data: PaymentRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""결제 준비 — orderId 발급
|
||||
|
||||
프론트에서 토스 결제 UI를 호출하기 전에 호출.
|
||||
orderId와 클라이언트 키를 반환.
|
||||
"""
|
||||
# 세션 확인
|
||||
result = await db.execute(
|
||||
select(ChargingSession).where(
|
||||
ChargingSession.session_uid == data.session_uid
|
||||
)
|
||||
)
|
||||
session = result.scalar_one_or_none()
|
||||
if not session:
|
||||
raise HTTPException(404, "세션 없음")
|
||||
if session.status != SessionStatus.PENDING:
|
||||
raise HTTPException(400, f"결제 불가 상태: {session.status}")
|
||||
|
||||
# 주문 ID 생성
|
||||
order_id = f"EV-{uuid.uuid4().hex[:16].upper()}"
|
||||
|
||||
# Payment 레코드 생성
|
||||
payment = Payment(
|
||||
session_id=session.id,
|
||||
order_id=order_id,
|
||||
amount=data.amount,
|
||||
status=PaymentStatus.PENDING,
|
||||
)
|
||||
db.add(payment)
|
||||
await db.flush()
|
||||
|
||||
return {
|
||||
"order_id": order_id,
|
||||
"amount": data.amount,
|
||||
"session_uid": data.session_uid,
|
||||
"client_key": settings.TOSS_CLIENT_KEY,
|
||||
"customer_name": "EV 충전",
|
||||
"order_name": f"전기차 충전 ({data.amount:,}원)",
|
||||
}
|
||||
|
||||
|
||||
@router.post("/confirm", response_model=PaymentOut)
|
||||
async def confirm_payment(
|
||||
data: PaymentConfirm,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""결제 최종 승인
|
||||
|
||||
프론트에서 토스 결제 완료 후 paymentKey, orderId, amount 전달.
|
||||
백엔드에서 토스 confirm API 호출 → 실제 청구 확정.
|
||||
성공 시 세션 상태를 AUTHORIZED로 변경.
|
||||
"""
|
||||
# Payment 조회
|
||||
result = await db.execute(
|
||||
select(Payment).where(Payment.order_id == data.order_id)
|
||||
)
|
||||
payment = result.scalar_one_or_none()
|
||||
if not payment:
|
||||
raise HTTPException(404, f"주문 없음: {data.order_id}")
|
||||
|
||||
# 금액 검증
|
||||
if payment.amount != data.amount:
|
||||
raise HTTPException(400, "결제 금액 불일치")
|
||||
|
||||
# 토스페이먼츠 최종 승인
|
||||
toss_result = await toss_service.confirm_payment(
|
||||
payment_key=data.payment_key,
|
||||
order_id=data.order_id,
|
||||
amount=data.amount,
|
||||
)
|
||||
|
||||
if not toss_result["success"]:
|
||||
payment.status = PaymentStatus.FAILED
|
||||
raise HTTPException(400, toss_result.get("message", "결제 승인 실패"))
|
||||
|
||||
# 결제 성공 처리
|
||||
toss_data = toss_result["data"]
|
||||
payment.payment_key = data.payment_key
|
||||
payment.status = PaymentStatus.CONFIRMED
|
||||
payment.method = toss_data.get("method", "")
|
||||
payment.card_company = (
|
||||
toss_data.get("card", {}).get("company", "") if toss_data.get("card") else ""
|
||||
)
|
||||
payment.confirmed_at = datetime.now(timezone.utc)
|
||||
|
||||
# 세션 상태 → AUTHORIZED (충전 시작 가능)
|
||||
session = await db.get(ChargingSession, payment.session_id)
|
||||
session.status = SessionStatus.AUTHORIZED
|
||||
|
||||
logger.info(
|
||||
f"결제 승인 완료: {data.order_id} / {data.amount}원 → "
|
||||
f"세션 {session.session_uid} AUTHORIZED"
|
||||
)
|
||||
|
||||
await db.flush()
|
||||
await db.refresh(payment)
|
||||
return payment
|
||||
|
||||
|
||||
@router.post("/{order_id}/cancel")
|
||||
async def cancel_payment(
|
||||
order_id: str,
|
||||
cancel_reason: str = "사용자 취소",
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""결제 취소"""
|
||||
result = await db.execute(
|
||||
select(Payment).where(Payment.order_id == order_id)
|
||||
)
|
||||
payment = result.scalar_one_or_none()
|
||||
if not payment:
|
||||
raise HTTPException(404, "주문 없음")
|
||||
if not payment.payment_key:
|
||||
raise HTTPException(400, "승인된 결제가 아닙니다")
|
||||
|
||||
cancel_result = await toss_service.cancel_payment(
|
||||
payment_key=payment.payment_key,
|
||||
cancel_reason=cancel_reason,
|
||||
)
|
||||
|
||||
if not cancel_result["success"]:
|
||||
raise HTTPException(400, cancel_result.get("message", "취소 실패"))
|
||||
|
||||
payment.status = PaymentStatus.REFUNDED
|
||||
payment.refunded_at = datetime.now(timezone.utc)
|
||||
|
||||
# 세션 취소
|
||||
session = await db.get(ChargingSession, payment.session_id)
|
||||
session.status = SessionStatus.CANCELLED
|
||||
|
||||
return {"message": "결제 취소 완료", "order_id": order_id}
|
||||
|
||||
|
||||
@router.get("/{order_id}", response_model=PaymentOut)
|
||||
async def get_payment(
|
||||
order_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""결제 상세 조회"""
|
||||
result = await db.execute(
|
||||
select(Payment).where(Payment.order_id == order_id)
|
||||
)
|
||||
payment = result.scalar_one_or_none()
|
||||
if not payment:
|
||||
raise HTTPException(404, "주문 없음")
|
||||
return payment
|
||||
Reference in New Issue
Block a user