EV 충전 플랫폼 초기 백업

This commit is contained in:
root
2026-04-18 05:59:31 +09:00
commit 4558ac10c0
40 changed files with 6246 additions and 0 deletions

179
app/routers/payments.py Normal file
View 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