"""전체 충전 흐름 테스트 스크립트 API 서버가 실행 중인 상태에서 실행. 실제 토스 결제 없이 전체 흐름을 시뮬레이션. 사용법: pip3 install httpx python3 test_flow.py """ import asyncio import httpx import json import sys BASE_URL = "http://localhost:8000/api/v1" G = "\033[92m" Y = "\033[93m" C = "\033[96m" R = "\033[91m" E = "\033[0m" def log(step, msg, data=None): print(f"\n{C}{'─'*50}{E}") print(f"{Y}[{step}]{E} {msg}") if data: print(f"{G}{json.dumps(data, indent=2, ensure_ascii=False, default=str)}{E}") def check(resp, step): """응답 확인 - 실패 시 내용 출력 후 중단""" if resp.status_code >= 400: print(f"\n{R}{'━'*50}") print(f" ERROR at [{step}] - HTTP {resp.status_code}") print(f" {resp.json()}") print(f"{'━'*50}{E}\n") sys.exit(1) return resp.json() async def main(): async with httpx.AsyncClient(base_url=BASE_URL, timeout=10.0) as c: # 0. 헬스체크 resp = await c.get("http://localhost:8000/health") log("0. 헬스체크", f"HTTP {resp.status_code}", resp.json()) # 1. 충전기 등록 log("1. 충전기 등록", "CHARGER_001") resp = await c.post("/chargers/", json={ "charge_box_id": "CHARGER_001", "name": "A동 주차장 1번", "location": "수원시 영통구 테스트 아파트 지하1층", "power_kw": 7.0, }) if resp.status_code == 409: log("1. 충전기 등록", "이미 등록됨 - 건너뜀") else: log("1. 충전기 등록", f"결과: {resp.status_code}", resp.json()) # 1-1. 이전 테스트 잔여 세션 정리 log("1-1. 세션 정리", "미완료 세션 취소") resp = await c.post("/sessions/reset/CHARGER_001") log("1-1. 세션 정리", "결과", resp.json()) # 2. 충전기 상태 -> Available log("2. 상태 업데이트", "충전기 Available 설정") resp = await c.post("/ocpp/status", json={ "charge_box_id": "CHARGER_001", "connector_id": 1, "status": "Available", }) log("2. 상태 업데이트", "결과", check(resp, "2")) # 3. 세션 생성 (QR 스캔) log("3. 세션 생성", "QR 스캔 -> 세션 생성") resp = await c.post("/sessions/", json={ "charge_box_id": "CHARGER_001", "connector_id": 1, }) session = check(resp, "3") session_uid = session["session_uid"] id_tag = session["id_tag"] log("3. 세션 생성", f"세션: {session_uid} / tag: {id_tag}", session) # 4. 결제 준비 log("4. 결제 준비", "orderId 발급") resp = await c.post("/payments/prepare", json={ "session_uid": session_uid, "amount": 10000, }) payment = check(resp, "4") log("4. 결제 준비", f"주문: {payment['order_id']}", payment) # 5. 결제 우회 (테스트용) log("5. 결제 우회", "토스 결제 없이 AUTHORIZED 강제 설정") print(f" {Y}(실제로는 토스페이먼츠 결제 UI -> POST /payments/confirm){E}") resp = await c.post(f"/sessions/{session_uid}/force-authorize") log("5. 결제 우회", "결과", check(resp, "5")) # 6. StartTransaction (충전기 -> 서버) log("6. StartTransaction", "충전기가 충전 시작 보고") resp = await c.post("/ocpp/start-transaction", json={ "charge_box_id": "CHARGER_001", "connector_id": 1, "id_tag": id_tag, "meter_start": 100000, "transaction_id": 1001, }) result = check(resp, "6") log("6. StartTransaction", f"결과: {result.get('status')}", result) # 7. MeterValues (충전 중 실시간 전력량) for i, wh in enumerate([105000, 110000, 115000, 120000]): kwh = (wh - 100000) / 1000 log(f"7-{i+1}. MeterValues", f"{wh}Wh (누적 {kwh}kWh 충전)") await c.post("/ocpp/meter-values", json={ "charge_box_id": "CHARGER_001", "connector_id": 1, "transaction_id": 1001, "value": wh, }) # 8. 충전 중 세션 상태 확인 resp = await c.get(f"/sessions/{session_uid}") log("8. 세션 상태", "충전 중", check(resp, "8")) # 9. StopTransaction (충전 종료) log("9. StopTransaction", "충전 종료 - 30kWh 충전 완료") resp = await c.post("/ocpp/stop-transaction", json={ "charge_box_id": "CHARGER_001", "transaction_id": 1001, "meter_stop": 130000, "reason": "Local", }) result = check(resp, "9") log("9. StopTransaction", "정산 결과", result) # 10. 최종 정산 resp = await c.get(f"/sessions/{session_uid}/billing") billing = check(resp, "10") log("10. 최종 정산", "요금 내역", billing) # 11. 대시보드 resp = await c.get("/dashboard/summary") log("11. 대시보드", "전체 요약", check(resp, "11")) # 완료 saved = billing.get("saved_vs_cpo", 0) total = billing.get("total_bill", 0) kwh = billing.get("charged_kwh", 0) print(f"\n{G}{'━'*50}") print(f" 충전 완료: {kwh}kWh") print(f" 요금: {total:,}원 (170원/kWh)") print(f" CPO 대비 절감: {saved:,}원") print(f"{'━'*50}{E}\n") if __name__ == "__main__": asyncio.run(main())