From 75e2ff98cd630d9394761f664943b2dd3870e372 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=EC=9D=B4=EC=98=81=EC=9D=80=28Tony=29?=
<52942409+Monsteel@users.noreply.github.com>
Date: Tue, 26 Sep 2023 18:47:03 +0900
Subject: [PATCH] =?UTF-8?q?=EA=B2=B0=EC=A0=9C=EA=B8=B0=EB=8A=A5=20?=
=?UTF-8?q?=EC=B6=94=EA=B0=80=20(#249)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
SRT/constants.py | 1 +
SRT/srt.py | 84 +++++++
docs/advanced.md | 27 +++
.../pay_with_card_fail_bad_request.json | 22 ++
.../pay_with_card_fail_cant_installment.json | 22 ++
.../pay_with_card_fail_card_password.json | 22 ++
.../pay_with_card_fail_expired_card.json | 22 ++
...ay_with_card_fail_invalid_auth_number.json | 22 ++
...ay_with_card_fail_invalid_card_number.json | 22 ++
...ith_card_fail_invalid_expiration_date.json | 22 ++
...ay_with_card_fail_invalid_reservation.json | 22 ++
.../pay_with_card_fail_over_limit.json | 22 ++
.../pay_with_card_fail_suspension_card.json | 22 ++
.../mock_responses/pay_with_card_success.json | 192 +++++++++++++++
tests/test_srt.py | 46 ++++
tests/test_srt_mock.py | 222 ++++++++++++++++++
16 files changed, 792 insertions(+)
create mode 100644 tests/mock_responses/pay_with_card_fail_bad_request.json
create mode 100644 tests/mock_responses/pay_with_card_fail_cant_installment.json
create mode 100644 tests/mock_responses/pay_with_card_fail_card_password.json
create mode 100644 tests/mock_responses/pay_with_card_fail_expired_card.json
create mode 100644 tests/mock_responses/pay_with_card_fail_invalid_auth_number.json
create mode 100644 tests/mock_responses/pay_with_card_fail_invalid_card_number.json
create mode 100644 tests/mock_responses/pay_with_card_fail_invalid_expiration_date.json
create mode 100644 tests/mock_responses/pay_with_card_fail_invalid_reservation.json
create mode 100644 tests/mock_responses/pay_with_card_fail_over_limit.json
create mode 100644 tests/mock_responses/pay_with_card_fail_suspension_card.json
create mode 100644 tests/mock_responses/pay_with_card_success.json
diff --git a/SRT/constants.py b/SRT/constants.py
index b164891..f537edc 100644
--- a/SRT/constants.py
+++ b/SRT/constants.py
@@ -61,4 +61,5 @@
"ticket_info": f"{SRT_MOBILE}/ard/selectListArd02017_n.do?",
"cancel": f"{SRT_MOBILE}/ard/selectListArd02045_n.do",
"standby_option": f"{SRT_MOBILE}/ata/selectListAta01135_n.do",
+ "payment": f"{SRT_MOBILE}/ata/selectListAta09036_n.do",
}
diff --git a/SRT/srt.py b/SRT/srt.py
index d47a415..28cbb03 100644
--- a/SRT/srt.py
+++ b/SRT/srt.py
@@ -1,3 +1,4 @@
+import json
import re
from datetime import datetime, timedelta
@@ -126,6 +127,7 @@ def login(self, srt_id: str | None = None, srt_pw: str | None = None):
raise SRTLoginError(r.json()["MSG"])
self.is_login = True
+ self.membership_number = json.loads(r.text).get("userMap").get("MB_CRD_NO")
return True
@@ -144,6 +146,8 @@ def logout(self) -> bool:
raise SRTResponseError(r.text)
self.is_login = False
+ self.membership_number = None
+
return True
def search_train(
@@ -571,3 +575,83 @@ def cancel(self, reservation: SRTReservation | int) -> bool:
self._log(parser.message())
return True
+
+ def pay_with_card(
+ self,
+ reservation: SRTReservation,
+ number: str,
+ password: str,
+ card_validation_number: str,
+ card_expire_date: str,
+ card_installment: int = 0,
+ card_type: str = "J",
+ ) -> bool:
+ """결제합니다.
+
+ >>> reservation = srt.reserve(train)
+ >>> srt.pay_with_card(reservation, "1234567890123456", "12", "981204", "2309", 0, "J")
+
+ Args:
+ reservation (:class:`SRTReservation`): 예약 내역
+ number (str): 결제신용카드번호 (하이픈(-) 제외)
+ password (str): 카드비밀번호 앞 2자리
+ card_validation_number (str): 생년월일 (card_type이 J인 경우) || 사업자번호 (card_type이 S인 경우)
+ card_expire_date (str): 카드유효기간(YYMM)
+ card_installment (int): 할부선택 (0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 24)
+ card_type (str): 카드타입 (J : 개인, S : 법인)
+
+ Returns:
+ bool: 결제 성공 여부
+ """
+ if not self.is_login:
+ raise SRTNotLoggedInError()
+
+ url = constants.API_ENDPOINTS["payment"]
+
+ data = {
+ "stlDmnDt": datetime.now().strftime("%Y%m%d"), # 날짜 (yyyyMMdd)
+ "mbCrdNo": self.membership_number, # 회원번호
+ "stlMnsSqno1": "1", # 결제수단 일련번호1 (1고정값인듯)
+ "ststlGridcnt": "1", # 결제수단건수 (1고정값인듯)
+ "totNewStlAmt": reservation.total_cost, # 총 신규 결제금액
+ "athnDvCd1": card_type, # 카드타입 (J : 개인, S : 법인)
+ "vanPwd1": password, # 카드비밀번호 앞 2자리
+ "crdVlidTrm1": card_expire_date, # 카드유효기간(YYMM)
+ "stlMnsCd1": "02", # 결제수단코드1: (02:신용카드, 11:전자지갑, 12:포인트)
+ "rsvChgTno": "0", # 예약변경번호 (0 고정값인듯)
+ "chgMcs": "0", # 변경마이크로초 (0고정값인듯)
+ "ismtMnthNum1": card_installment, # 할부선택 (0, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 24)
+ "ctlDvCd": "3102", # 조정구분코드(3102 고정값인듯)
+ "cgPsId": "korail", # korail 고정
+ "pnrNo": reservation.reservation_number, # 예약번호
+ "totPrnb": reservation.seat_count, # 승차인원
+ "mnsStlAmt1": reservation.total_cost, # 결제금액1
+ "crdInpWayCd1": "@", # 카드입력방식코드 (@: 신용카드/ok포인트, "": 전자지갑)
+ "athnVal1": card_validation_number, # 생년월일/사업자번호
+ "stlCrCrdNo1": number, # 결제신용카드번호1
+ "jrnyCnt": "1", # 여정수(1 고정)
+ "strJobId": "3102", # 업무구분코드(3102 고정값인듯)
+ "inrecmnsGridcnt": "1", # 1 고정값인듯
+ "dptTm": reservation.dep_time, # 출발시간
+ "arvTm": reservation.arr_time, # 도착시간
+ "dptStnConsOrdr2": "000000", # 출발역구성순서2 (000000 고정)
+ "arvStnConsOrdr2": "000000", # 도착역구성순서2 (000000 고정)
+ "trnGpCd": "300", # 열차그룹코드(300 고정)
+ "pageNo": "-", # 페이지번호(- 고정)
+ "rowCnt": "-", # 한페이지당건수(- 고정)
+ "pageUrl": "", # 페이지URL (빈값 고정)
+ }
+
+ r = self._session.post(url=url, data=data)
+
+ parser = json.loads(r.text)
+
+ if (
+ parser.get("outDataSets").get("dsOutput0")[0].get("strResult")
+ == RESULT_FAIL
+ ):
+ raise SRTResponseError(
+ parser.get("outDataSets").get("dsOutput0")[0].get("msgTxt")
+ )
+
+ return True
diff --git a/docs/advanced.md b/docs/advanced.md
index a0aa5a2..1a94db2 100644
--- a/docs/advanced.md
+++ b/docs/advanced.md
@@ -15,6 +15,7 @@ SRT 로그인, 승차표 찾기 설명은 생략합니다.
Highly inspired by [@dotaitch](https://github.com/dotaitch).
예시) 어른 2명, 어린이 1명 예약
+
```python
>>> from SRT.passenger import Adult, Child
>>> srt.reserve(trains[0], passengers=[Adult(), Adult(), Child()])
@@ -29,6 +30,7 @@ Highly inspired by [@dotaitch](https://github.com/dotaitch).
## 일반실 / 특실 좌석 옵션 선택하기
예시) 일반실 우선 예약
+
```python
>>> from SRT import SeatType
>>> srt.reserve(self, trains[0], special_seat=SeatType.GENERAL_FIRST)
@@ -38,3 +40,28 @@ Highly inspired by [@dotaitch](https://github.com/dotaitch).
- SeatType.GENERAL_ONLY : 일반실만
- SeatType.SPECIAL_FIRST : 특실 우선
- SeatType.SPECIAL_ONLY : 특실만
+
+## 결제하기
+
+**⚠️ 주의: 이 API는 실제로 결제를 수행합니다**
+해당 결제 API는 비공식적인 API를 사용하기 때문에 언제든지 제대로 동작하지 않을 수 있습니다.
+이에 따른 문제 발생에 대한 책임은 API 사용자 본인에게 있음을 알립니다.
+
+---
+
+```python
+>>> reservation = srt.reserve(trains[0])
+>>> srt.pay_with_card(reservation, "1234567890123456", "12", "981204", "2309", 0, "J")
+```
+
+### 각 파라미터 기입요령
+
+| 순서 | 변수명 | 설명 | 예시 | 기본 값 |
+| ---- | ---------------------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------- | ------- |
+| 1 | reservation | **결제대상 예약내역**
_\* `SRTReservation` 타입을 준수_ | - | - |
+| 2 | number | **결제 카드번호**
_\* 하이픈(-) 제외_ | `"1234000056780000"` | - |
+| 3 | password | **카드비밀번호 앞 2자리** | `"12"` | - |
+| 4 | card_validation_number | **개인(`J`)인 경우: 생년월일
법인(`S`)인 경우: 사업자번호** | `"981204"` | - |
+| 5 | card_expire_date | **카드유효기간**
_\* YYMM(연도+월) 형식의 만료일을 입력
\*카드 표현방식 MMYY(월+연도)형식과 착오에 주의_ | `"2309"` | - |
+| 6 | card_installment | **할부선택**
_\* 할부 개월 수 입력, 0의 경우 일시불.
\* 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 24 개월 선택 가능_ | `0` | `0` |
+| 7 | card_type | **카드 유형**
_\* J : 개인, S : 법인_ | `"J"` | `"J"` |
diff --git a/tests/mock_responses/pay_with_card_fail_bad_request.json b/tests/mock_responses/pay_with_card_fail_bad_request.json
new file mode 100644
index 0000000..02666d8
--- /dev/null
+++ b/tests/mock_responses/pay_with_card_fail_bad_request.json
@@ -0,0 +1,22 @@
+{
+ "ERROR_MSG": "",
+ "ERROR_CODE": "0",
+ "outDataSets": {
+ "dsOutput0": [
+ {
+ "msgCd": "WZZ000001",
+ "strResult": "FAIL",
+ "msgTxt": "% 입력이 잘못되었습니다."
+ }
+ ]
+ },
+ "dsResultMap": {
+ "dsOutput0": [
+ {
+ "msgCd": "WZZ000001",
+ "strResult": "FAIL",
+ "msgTxt": "% 입력이 잘못되었습니다."
+ }
+ ]
+ }
+}
diff --git a/tests/mock_responses/pay_with_card_fail_cant_installment.json b/tests/mock_responses/pay_with_card_fail_cant_installment.json
new file mode 100644
index 0000000..8b12824
--- /dev/null
+++ b/tests/mock_responses/pay_with_card_fail_cant_installment.json
@@ -0,0 +1,22 @@
+{
+ "ERROR_MSG": "",
+ "ERROR_CODE": "0",
+ "outDataSets": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV07574",
+ "strResult": "FAIL",
+ "msgTxt": "할부불가카드"
+ }
+ ]
+ },
+ "dsResultMap": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV07574",
+ "strResult": "FAIL",
+ "msgTxt": "할부불가카드"
+ }
+ ]
+ }
+}
diff --git a/tests/mock_responses/pay_with_card_fail_card_password.json b/tests/mock_responses/pay_with_card_fail_card_password.json
new file mode 100644
index 0000000..9c3030a
--- /dev/null
+++ b/tests/mock_responses/pay_with_card_fail_card_password.json
@@ -0,0 +1,22 @@
+{
+ "ERROR_MSG": "",
+ "ERROR_CODE": "0",
+ "outDataSets": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV06003",
+ "strResult": "FAIL",
+ "msgTxt": "비밀번호오류
사용하신 각 신용카드사의 고객센터로 문의 바랍니다."
+ }
+ ]
+ },
+ "dsResultMap": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV06003",
+ "strResult": "FAIL",
+ "msgTxt": "비밀번호오류
사용하신 각 신용카드사의 고객센터로 문의 바랍니다."
+ }
+ ]
+ }
+}
diff --git a/tests/mock_responses/pay_with_card_fail_expired_card.json b/tests/mock_responses/pay_with_card_fail_expired_card.json
new file mode 100644
index 0000000..19afc2a
--- /dev/null
+++ b/tests/mock_responses/pay_with_card_fail_expired_card.json
@@ -0,0 +1,22 @@
+{
+ "ERROR_MSG": "",
+ "ERROR_CODE": "0",
+ "outDataSets": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV08314",
+ "strResult": "FAIL",
+ "msgTxt": "유효기간경과카드"
+ }
+ ]
+ },
+ "dsResultMap": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV08314",
+ "strResult": "FAIL",
+ "msgTxt": "유효기간경과카드"
+ }
+ ]
+ }
+}
diff --git a/tests/mock_responses/pay_with_card_fail_invalid_auth_number.json b/tests/mock_responses/pay_with_card_fail_invalid_auth_number.json
new file mode 100644
index 0000000..c6fb03d
--- /dev/null
+++ b/tests/mock_responses/pay_with_card_fail_invalid_auth_number.json
@@ -0,0 +1,22 @@
+{
+ "ERROR_MSG": "",
+ "ERROR_CODE": "0",
+ "outDataSets": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV06006",
+ "strResult": "FAIL",
+ "msgTxt": "주민번호 또는 사업자번호 오류입니다."
+ }
+ ]
+ },
+ "dsResultMap": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV06006",
+ "strResult": "FAIL",
+ "msgTxt": "주민번호 또는 사업자번호 오류입니다."
+ }
+ ]
+ }
+}
diff --git a/tests/mock_responses/pay_with_card_fail_invalid_card_number.json b/tests/mock_responses/pay_with_card_fail_invalid_card_number.json
new file mode 100644
index 0000000..f0c65ea
--- /dev/null
+++ b/tests/mock_responses/pay_with_card_fail_invalid_card_number.json
@@ -0,0 +1,22 @@
+{
+ "ERROR_MSG": "",
+ "ERROR_CODE": "0",
+ "outDataSets": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV08037",
+ "strResult": "FAIL",
+ "msgTxt": "카드번호오류"
+ }
+ ]
+ },
+ "dsResultMap": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV08037",
+ "strResult": "FAIL",
+ "msgTxt": "카드번호오류"
+ }
+ ]
+ }
+}
diff --git a/tests/mock_responses/pay_with_card_fail_invalid_expiration_date.json b/tests/mock_responses/pay_with_card_fail_invalid_expiration_date.json
new file mode 100644
index 0000000..dd81dad
--- /dev/null
+++ b/tests/mock_responses/pay_with_card_fail_invalid_expiration_date.json
@@ -0,0 +1,22 @@
+{
+ "ERROR_MSG": "",
+ "ERROR_CODE": "0",
+ "outDataSets": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV08315",
+ "strResult": "FAIL",
+ "msgTxt": "유효기간을 잘못입력하셨습니다."
+ }
+ ]
+ },
+ "dsResultMap": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV08315",
+ "strResult": "FAIL",
+ "msgTxt": "유효기간을 잘못입력하셨습니다."
+ }
+ ]
+ }
+}
diff --git a/tests/mock_responses/pay_with_card_fail_invalid_reservation.json b/tests/mock_responses/pay_with_card_fail_invalid_reservation.json
new file mode 100644
index 0000000..2363b73
--- /dev/null
+++ b/tests/mock_responses/pay_with_card_fail_invalid_reservation.json
@@ -0,0 +1,22 @@
+{
+ "ERROR_MSG": "",
+ "ERROR_CODE": "0",
+ "outDataSets": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRT200178",
+ "strResult": "FAIL",
+ "msgTxt": "취소된 여정이므로 발매할 수 없습니다.
비회원은 다시 예약하셔야합니다."
+ }
+ ]
+ },
+ "dsResultMap": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRT200178",
+ "strResult": "FAIL",
+ "msgTxt": "취소된 여정이므로 발매할 수 없습니다.
비회원은 다시 예약하셔야합니다."
+ }
+ ]
+ }
+}
diff --git a/tests/mock_responses/pay_with_card_fail_over_limit.json b/tests/mock_responses/pay_with_card_fail_over_limit.json
new file mode 100644
index 0000000..32f932e
--- /dev/null
+++ b/tests/mock_responses/pay_with_card_fail_over_limit.json
@@ -0,0 +1,22 @@
+{
+ "ERROR_MSG": "",
+ "ERROR_CODE": "0",
+ "outDataSets": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV08326",
+ "strResult": "FAIL",
+ "msgTxt": "사용한도초과"
+ }
+ ]
+ },
+ "dsResultMap": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV08326",
+ "strResult": "FAIL",
+ "msgTxt": "사용한도초과"
+ }
+ ]
+ }
+}
diff --git a/tests/mock_responses/pay_with_card_fail_suspension_card.json b/tests/mock_responses/pay_with_card_fail_suspension_card.json
new file mode 100644
index 0000000..b4b2ed7
--- /dev/null
+++ b/tests/mock_responses/pay_with_card_fail_suspension_card.json
@@ -0,0 +1,22 @@
+{
+ "ERROR_MSG": "",
+ "ERROR_CODE": "0",
+ "outDataSets": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV08324",
+ "strResult": "FAIL",
+ "msgTxt": "거래정지카드"
+ }
+ ]
+ },
+ "dsResultMap": {
+ "dsOutput0": [
+ {
+ "msgCd": "WRTV08324",
+ "strResult": "FAIL",
+ "msgTxt": "거래정지카드"
+ }
+ ]
+ }
+}
diff --git a/tests/mock_responses/pay_with_card_success.json b/tests/mock_responses/pay_with_card_success.json
new file mode 100644
index 0000000..80bc179
--- /dev/null
+++ b/tests/mock_responses/pay_with_card_success.json
@@ -0,0 +1,192 @@
+{
+ "ERROR_MSG": "",
+ "ERROR_CODE": "0",
+ "outDataSets": {
+ "dsOutput3": [
+ {
+ "TKLIST_KEY": 0,
+ "dcntKndNm": null,
+ "DCNTLIST_KEY": 0
+ }
+ ],
+ "dsOutput2": [
+ {
+ "trnNo1": "",
+ "rcvdAmt": "00000036900",
+ "seatNo1": "",
+ "arvDt1": "",
+ "scarNo": 2,
+ "crwPwd": "000000000000000000",
+ "jrnyTpCd": "11",
+ "tkPrc": 36900,
+ "dptRsStnNmEn": "Suseo",
+ "trnGpNm1": "",
+ "arvTm1": "",
+ "dptTm": "053000",
+ "entNm": "",
+ "dptRsStnNm2": "",
+ "TKLIST_KEY": 0,
+ "dptRsStnNm1": "",
+ "arvDt": "20231024",
+ "psrmClCdNm": "일반실",
+ "trnClsfNm": "SRT",
+ "trnGpNm": "SRT",
+ "rmkCont": "신용000000",
+ "trnGpCd": "300",
+ "scarNo1": 0,
+ "psgTpCdNm": "어른",
+ "arvTm": "071100",
+ "dptRsStnNmKo": "수서",
+ "arvRsStnNm1": "",
+ "trnClsfNm1": "",
+ "dcntGroupGridcnt": 0,
+ "tkSqno": 0,
+ "psrmClCdNm1": "",
+ "tkKndCdNm": "스마트티켓",
+ "arvRsStnNmEn": "Dongdaegu",
+ "arvRsStnNm2": "",
+ "dlayDscpFlg": "N",
+ "saleSqno": 10000,
+ "arvRsStnNmKo": "동대구",
+ "stdrPrc": "00000037500",
+ "dptDt1": "",
+ "dptDt": "20231024",
+ "trnNo": "00301",
+ "seatNo": "10A",
+ "tkRetNo": "0000000000000000",
+ "mogoNm": "",
+ "dcntAmt": "00000000600",
+ "dptTm1": "",
+ "saleDt": "20230923"
+ }
+ ],
+ "dsOutput1": [
+ {
+ "pontDvCd": "",
+ "apvDt": "20230923",
+ "stlSqno": "00000000001",
+ "stlCrCrdNo": "0000000000000000",
+ "stlApvTm": "145030",
+ "stlMnsCd": "02",
+ "mnsStlAmt": "36900",
+ "apvNo": "00000000"
+ }
+ ],
+ "dsOutput0": [
+ {
+ "tkNum": 1,
+ "buyPsNm": "김철수",
+ "totPrc": "00000036900",
+ "acmMlgNum": 0,
+ "strResult": "SUCC",
+ "cgPsId": "korail",
+ "stlAmt": "36900",
+ "totNewStlAmt": "00000000036900",
+ "pnrNo": "000000000000000",
+ "msgCd": "IRT000000",
+ "wctNo": "00000",
+ "totRcvdAmt": "0000000000036900",
+ "totDcntAmt": 600,
+ "rmkCont": "",
+ "msgTxt": "정상발매처리,정상발권처리",
+ "stlMnsCd": "02",
+ "uuid": "APP0000000000000000000000000000000",
+ "apvNo": "00000000"
+ }
+ ]
+ },
+ "dsResultMap": {
+ "dsOutput3": [
+ {
+ "TKLIST_KEY": 0,
+ "dcntKndNm": null,
+ "DCNTLIST_KEY": 0
+ }
+ ],
+ "dsOutput2": [
+ {
+ "trnNo1": "",
+ "rcvdAmt": "00000036900",
+ "seatNo1": "",
+ "arvDt1": "",
+ "scarNo": 2,
+ "crwPwd": "000000000000000000",
+ "jrnyTpCd": "11",
+ "tkPrc": 36900,
+ "dptRsStnNmEn": "Suseo",
+ "trnGpNm1": "",
+ "arvTm1": "",
+ "dptTm": "053000",
+ "entNm": "",
+ "dptRsStnNm2": "",
+ "TKLIST_KEY": 0,
+ "dptRsStnNm1": "",
+ "arvDt": "20231024",
+ "psrmClCdNm": "일반실",
+ "trnClsfNm": "SRT",
+ "trnGpNm": "SRT",
+ "rmkCont": "신용000000",
+ "trnGpCd": "300",
+ "scarNo1": 0,
+ "psgTpCdNm": "어른",
+ "arvTm": "071100",
+ "dptRsStnNmKo": "수서",
+ "arvRsStnNm1": "",
+ "trnClsfNm1": "",
+ "dcntGroupGridcnt": 0,
+ "tkSqno": 0,
+ "psrmClCdNm1": "",
+ "tkKndCdNm": "스마트티켓",
+ "arvRsStnNmEn": "Dongdaegu",
+ "arvRsStnNm2": "",
+ "dlayDscpFlg": "N",
+ "saleSqno": 10000,
+ "arvRsStnNmKo": "동대구",
+ "stdrPrc": "00000037500",
+ "dptDt1": "",
+ "dptDt": "20231024",
+ "trnNo": "00301",
+ "seatNo": "10A",
+ "tkRetNo": "0000000000000000",
+ "mogoNm": "",
+ "dcntAmt": "00000000600",
+ "dptTm1": "",
+ "saleDt": "20230923"
+ }
+ ],
+ "dsOutput1": [
+ {
+ "pontDvCd": "",
+ "apvDt": "20230923",
+ "stlSqno": "00000000001",
+ "stlCrCrdNo": "0000000000000000",
+ "stlApvTm": "145030",
+ "stlMnsCd": "02",
+ "mnsStlAmt": "36900",
+ "apvNo": "00000000"
+ }
+ ],
+ "dsOutput0": [
+ {
+ "tkNum": 1,
+ "buyPsNm": "김철수",
+ "totPrc": "00000036900",
+ "acmMlgNum": 0,
+ "strResult": "SUCC",
+ "cgPsId": "korail",
+ "stlAmt": "36900",
+ "totNewStlAmt": "00000000036900",
+ "pnrNo": "000000000000000",
+ "msgCd": "IRT000000",
+ "wctNo": "00000",
+ "totRcvdAmt": "0000000000036900",
+ "totDcntAmt": 600,
+ "rmkCont": "",
+ "msgTxt": "정상발매처리,정상발권처리",
+ "stlMnsCd": "02",
+ "uuid": "APP0000000000000000000000000000000",
+ "apvNo": "00000000"
+ }
+ ]
+ }
+}
diff --git a/tests/test_srt.py b/tests/test_srt.py
index eb42693..7a401fe 100644
--- a/tests/test_srt.py
+++ b/tests/test_srt.py
@@ -93,3 +93,49 @@ def test_reserve_and_cancel(srt, pytestconfig):
pytest.warns(Warning("Empty seat not found, skipping reservation test"))
srt.cancel(reservation)
+
+
+def test_reserve_and_pay_with_card(srt, pytestconfig):
+ pytestconfig.getoption("--full", skip=True)
+ dep = "수서"
+ arr = "대전"
+ time = "000000"
+
+ membership_number = "1234567890"
+ password = "12"
+ card_expire_date = "2209"
+ card_installment = 0
+ card_validation_number = "1234567890123"
+ number = "1234567890123456"
+ card_type = "J"
+
+ # loop until empty seat is found
+ reservation = None
+ for day in range(5, 30):
+ date = (datetime.now() + timedelta(days=day)).strftime("%Y%m%d")
+
+ trains = srt.search_train(dep, arr, date, time)
+
+ assert len(trains) != 0
+
+ for train in trains:
+ if train.general_seat_available():
+ reservation = srt.reserve(train)
+ break
+
+ if reservation is not None:
+ break
+
+ if reservation is None:
+ pytest.warns(Warning("Empty seat not found, skipping reservation test"))
+
+ srt.pay_with_card(
+ reservation,
+ membership_number,
+ password,
+ card_expire_date,
+ card_installment,
+ card_validation_number,
+ number,
+ card_type,
+ )
diff --git a/tests/test_srt_mock.py b/tests/test_srt_mock.py
index c501747..0d5c64e 100644
--- a/tests/test_srt_mock.py
+++ b/tests/test_srt_mock.py
@@ -2,6 +2,8 @@
import pytest
+from SRT.reservation import SRTReservation
+
mock_response_dir = Path(__file__).parent / "mock_responses"
@@ -19,6 +21,7 @@ def mock_server(httpserver, monkeypatch):
"ticket_info": httpserver.url_for("/ticket_info"),
"cancel": httpserver.url_for("/cancel"),
"standby_option": httpserver.url_for("/standby_option"),
+ "payment": httpserver.url_for("/payment"),
}
monkeypatch.setattr(
@@ -75,3 +78,222 @@ def test_logout(mock_server, httpserver):
srt = SRT("010-1234-1234", "password")
srt.logout()
assert not srt.is_login
+
+
+# 결제 테스트를 위한 mock reservation
+mock_reservation = SRTReservation(
+ {
+ "pnrNo": "000000000",
+ "tkSpecNum": "1",
+ "rcvdAmt": "36900",
+ },
+ {
+ "stlbTrnClsfCd": "00",
+ "trnNo": "0000",
+ "dptDt": "20231024",
+ "dptTm": "000000",
+ "dptRsStnCd": "0551",
+ "arvTm": "000000",
+ "arvRsStnCd": "0015",
+ "iseLmtDt": "20231024",
+ "iseLmtTm": "000000",
+ "stlFlg": "N",
+ },
+ [
+ {
+ "scarNo": "1",
+ "seatNo": "1",
+ "psrmClCd": "1",
+ "psgTpCd": "1",
+ "rcvdAmt": "36900",
+ "stdrPrc": "36900",
+ "dcntPrc": "600",
+ }
+ ],
+)
+
+
+# 결제 성공
+def test_pay_with_card_success(mock_server, httpserver):
+ from SRT import SRT
+
+ register_mock_response(httpserver, "/login", "login_success.json")
+ register_mock_response(httpserver, "/payment", "pay_with_card_success.json")
+
+ srt = SRT("010-1234-1234", "password")
+
+ assert srt.pay_with_card(
+ mock_reservation, "12", "1221", 0, "981204", "0000000000000000", "J"
+ )
+
+
+# 결제 실패 - 입력이 잘못되었습니다
+def test_pay_with_card_fail_bad_request(mock_server, httpserver):
+ from SRT import SRT, SRTResponseError
+
+ register_mock_response(httpserver, "/login", "login_success.json")
+ register_mock_response(
+ httpserver, "/payment", "pay_with_card_fail_bad_request.json"
+ )
+
+ srt = SRT("010-1234-1234", "password")
+
+ with pytest.raises(SRTResponseError, match="% 입력이 잘못되었습니다."):
+ srt.pay_with_card(
+ mock_reservation, "12", "1221", 0, "981204", "0000000000000000", "J"
+ )
+
+
+# 결제 실패 - 할부 불가 카드
+def test_pay_with_card_fail_cant_installment(mock_server, httpserver):
+ from SRT import SRT, SRTResponseError
+
+ register_mock_response(httpserver, "/login", "login_success.json")
+ register_mock_response(
+ httpserver, "/payment", "pay_with_card_fail_cant_installment.json"
+ )
+
+ srt = SRT("010-1234-1234", "password")
+
+ with pytest.raises(SRTResponseError, match="할부불가카드"):
+ srt.pay_with_card(
+ mock_reservation, "12", "1221", 0, "981204", "0000000000000000", "J"
+ )
+
+
+# 결제 실패 - 비밀번호 오류
+def test_pay_with_card_fail_card_password(mock_server, httpserver):
+ from SRT import SRT, SRTResponseError
+
+ register_mock_response(httpserver, "/login", "login_success.json")
+ register_mock_response(
+ httpserver, "/payment", "pay_with_card_fail_card_password.json"
+ )
+
+ srt = SRT("010-1234-1234", "password")
+
+ with pytest.raises(
+ SRTResponseError, match="비밀번호오류
사용하신 각 신용카드사의 고객센터로 문의 바랍니다."
+ ):
+ srt.pay_with_card(
+ mock_reservation, "12", "1221", 0, "981204", "0000000000000000", "J"
+ )
+
+
+# 결제 실패 - 유효기간 경과 카드
+def test_pay_with_card_fail_expired_card(mock_server, httpserver):
+ from SRT import SRT, SRTResponseError
+
+ register_mock_response(httpserver, "/login", "login_success.json")
+ register_mock_response(
+ httpserver, "/payment", "pay_with_card_fail_expired_card.json"
+ )
+
+ srt = SRT("010-1234-1234", "password")
+
+ with pytest.raises(SRTResponseError, match="유효기간경과카드"):
+ srt.pay_with_card(
+ mock_reservation, "12", "1221", 0, "981204", "0000000000000000", "J"
+ )
+
+
+# 결제 실패 - 주민번호 또는 사업자번호 오류
+def test_pay_with_card_fail_invalid_auth_number(mock_server, httpserver):
+ from SRT import SRT, SRTResponseError
+
+ register_mock_response(httpserver, "/login", "login_success.json")
+ register_mock_response(
+ httpserver, "/payment", "pay_with_card_fail_invalid_auth_number.json"
+ )
+
+ srt = SRT("010-1234-1234", "password")
+
+ with pytest.raises(SRTResponseError, match="주민번호 또는 사업자번호 오류입니다."):
+ srt.pay_with_card(
+ mock_reservation, "12", "1221", 0, "981204", "0000000000000000", "J"
+ )
+
+
+# 결제 실패 - 카드번호 오류
+def test_pay_with_card_fail_invalid_card_number(mock_server, httpserver):
+ from SRT import SRT, SRTResponseError
+
+ register_mock_response(httpserver, "/login", "login_success.json")
+ register_mock_response(
+ httpserver, "/payment", "pay_with_card_fail_invalid_card_number.json"
+ )
+
+ srt = SRT("010-1234-1234", "password")
+
+ with pytest.raises(SRTResponseError, match="카드번호오류"):
+ srt.pay_with_card(
+ mock_reservation, "12", "1221", 0, "981204", "0000000000000000", "J"
+ )
+
+
+# 결제 실패 - 유효기간 입력 오류
+def test_pay_with_card_fail_invalid_expiration_date(mock_server, httpserver):
+ from SRT import SRT, SRTResponseError
+
+ register_mock_response(httpserver, "/login", "login_success.json")
+ register_mock_response(
+ httpserver, "/payment", "pay_with_card_fail_invalid_expiration_date.json"
+ )
+
+ srt = SRT("010-1234-1234", "password")
+
+ with pytest.raises(SRTResponseError, match="유효기간을 잘못입력하셨습니다."):
+ srt.pay_with_card(
+ mock_reservation, "12", "1221", 0, "981204", "0000000000000000", "J"
+ )
+
+
+# 결제 실패 - 취소된 예약
+def test_pay_with_card_fail_invalid_reservation(mock_server, httpserver):
+ from SRT import SRT, SRTResponseError
+
+ register_mock_response(httpserver, "/login", "login_success.json")
+ register_mock_response(
+ httpserver, "/payment", "pay_with_card_fail_invalid_reservation.json"
+ )
+
+ srt = SRT("010-1234-1234", "password")
+
+ with pytest.raises(
+ SRTResponseError, match="취소된 여정이므로 발매할 수 없습니다.
비회원은 다시 예약하셔야합니다."
+ ):
+ srt.pay_with_card(
+ mock_reservation, "12", "1221", 0, "981204", "0000000000000000", "J"
+ )
+
+
+# 결제 실패 - 한도 초과
+def test_pay_with_card_fail_over_limit(mock_server, httpserver):
+ from SRT import SRT, SRTResponseError
+
+ register_mock_response(httpserver, "/login", "login_success.json")
+ register_mock_response(httpserver, "/payment", "pay_with_card_fail_over_limit.json")
+
+ srt = SRT("010-1234-1234", "password")
+
+ with pytest.raises(SRTResponseError, match="사용한도초과"):
+ srt.pay_with_card(
+ mock_reservation, "12", "1221", 0, "981204", "0000000000000000", "J"
+ )
+
+
+# 결제 실패 - 거래정지 카드
+def test_pay_with_card_fail_suspension_card(mock_server, httpserver):
+ from SRT import SRT, SRTResponseError
+
+ register_mock_response(httpserver, "/login", "login_success.json")
+ register_mock_response(
+ httpserver, "/payment", "pay_with_card_fail_suspension_card.json"
+ )
+
+ srt = SRT("010-1234-1234", "password")
+
+ with pytest.raises(SRTResponseError, match="거래정지카드"):
+ srt.pay_with_card(
+ mock_reservation, "12", "1221", 0, "981204", "0000000000000000", "J"
+ )