├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── iamport ├── __init__.py ├── client.py └── client.pyi ├── pyproject.toml ├── setup.cfg └── tests ├── __init__.py ├── conftest.py ├── test_cancel.py ├── test_certification.py ├── test_customer_create.py ├── test_customer_get.py ├── test_find.py ├── test_find_with_status.py ├── test_is_paid.py ├── test_pay_again.py ├── test_pay_foreign.py ├── test_pay_onetime.py ├── test_pay_schedule.py ├── test_pay_unschedule.py └── test_prepare.py /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug Report 3 | about: 버그 리포트 템플릿 4 | title: '[Bug] ...' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **간단 설명** 11 | 버그가 무엇인지 명확하고 간결하게 설명 부탁드립니다. 12 | 13 | **재현 방법** 14 | 버그를 재현하는 순서: 15 | 1. '..'를 인자로 가지는 `Iamport` 객체 생성 16 | 2. '...'를 인자를 가지는 `payload` 초기화 17 | 3. 해당 `payload` 값을 인자로 가지는 `iamport.pay_schedule(**paylaod)` 함수 호출 18 | 4. Response '....' 에러 확인 19 | 20 | **기대 결과** 21 | 기대하셨던 결과에 대한 명확하고 간결한 설명 부탁드립니다. 22 | 23 | **스크린샷** 24 | 해당되는 경우 문제를 설명하는 데 도움이 되는 스크린샷을 추가해주세요. 25 | 26 | **테스트 환경** 27 | - Updater Version: [e.g. 0.8.2] 28 | - Python Version: [e.g. 3.6.10] 29 | - OS: [e.g. macOS] 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: 기능 추가 또는 개선 요청을 위한 템플릿 4 | title: '[Feature] ...' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **간단 설명** 11 | 문제가 무엇인지 명확하고 간결하게 설명 부탁드립니다. 12 | 13 | **현재 상황** 14 | 요청자 분께서 원하시는 작동 방식에 대한 명확하고 간결한 설명 부탁드립니다. 15 | 16 | **요구 사항** 17 | 요청자 분께서 고려하신 대체 솔루션 또는 기능에 대한 명확하고 간결한 설명 부탁드립니다. 18 | 19 | **스크린샷** 20 | 기능 요청에 대한 다른 컨텍스트 또는 스크린샷을 추가해주세요. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### 작업 개요 2 | 5 | ### 작업 분류 6 | - [ ] 버그 수정 7 | - [ ] 신규 기능 8 | - [ ] 프로젝트 구조 변경 9 | 14 | ### 작업 상세 내용 15 | 20 | ### 생각해볼 문제 21 | 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build Status 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build-ubuntu: 7 | name: Build Test 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.6, 3.7, 3.8, 3.9] 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Set up Python ${{ matrix.python-version }} 15 | uses: actions/setup-python@v2 16 | with: 17 | python-version: ${{ matrix.python-version }} 18 | - name: Install dependencies 19 | run: | 20 | python -m pip install --upgrade pip 21 | pip install -e .[dev] 22 | - name: Run unit tests 23 | run: | 24 | python -m pytest --cov=iamport 25 | - name: Run convention tests 26 | run: | 27 | python -m flake8 iamport tests 28 | - name: Run type check 29 | run: | 30 | python -m mypy iamport 31 | - name: Run import sort check 32 | run: | 33 | python -m isort iamport tests --diff 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | env/ 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *,cover 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | 56 | # Sphinx documentation 57 | docs/_build/ 58 | 59 | # PyBuilder 60 | target/ 61 | ### JetBrains template 62 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio 63 | 64 | *.iml 65 | 66 | ## Directory-based project format: 67 | .idea/ 68 | # if you remove the above rule, at least ignore the following: 69 | 70 | # User-specific stuff: 71 | # .idea/workspace.xml 72 | # .idea/tasks.xml 73 | # .idea/dictionaries 74 | 75 | # Sensitive or high-churn files: 76 | # .idea/dataSources.ids 77 | # .idea/dataSources.xml 78 | # .idea/sqlDataSources.xml 79 | # .idea/dynamic.xml 80 | # .idea/uiDesigner.xml 81 | 82 | # Gradle: 83 | # .idea/gradle.xml 84 | # .idea/libraries 85 | 86 | # Mongo Explorer plugin: 87 | # .idea/mongoSettings.xml 88 | 89 | ## File-based project format: 90 | *.ipr 91 | *.iws 92 | 93 | ## Plugin-specific files: 94 | 95 | # IntelliJ 96 | /out/ 97 | 98 | # mpeltonen/sbt-idea plugin 99 | .idea_modules/ 100 | 101 | # JIRA plugin 102 | atlassian-ide-plugin.xml 103 | 104 | # Crashlytics plugin (for Android Studio and IntelliJ) 105 | com_crashlytics_export_strings.xml 106 | crashlytics.properties 107 | crashlytics-build.properties 108 | 109 | .pytest_cache/ 110 | .mypy_cache/ 111 | .python-version 112 | venv/ 113 | 114 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 PerhapsSPY and other contributors. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # I'mport; REST Client Python 2 | 3 | [![Python Versions](https://img.shields.io/pypi/pyversions/iamport-rest-client)](https://pypi.org/project/iamport-rest-client/) 4 | [![PyPI Release (latest by date)](https://img.shields.io/pypi/v/iamport-rest-client?color=blue)](https://pypi.org/project/iamport-rest-client/) 5 | [![GitHub Workflow Status (Build)](https://img.shields.io/github/workflow/status/iamport/iamport-rest-client-python/Build%20Status)](https://github.com/iamport/iamport-rest-client-python/actions) 6 | [![GitHub LICENSE](https://img.shields.io/github/license/iamport/iamport-rest-client-python)](https://github.com/iamport/iamport-rest-client-python/blob/master/LICENSE) 7 | [![Lines of Code](https://img.shields.io/tokei/lines/github/iamport/iamport-rest-client-python)](https://github.com/iamport/iamport-rest-client-python/tree/master/iamport) 8 | 9 | ## 소개 10 | 11 | > Python 개발자를 위한 [아임포트 REST API](https://api.iamport.kr/) 연동 패키지입니다. 12 | 13 | ### 주의 사항 14 | 15 | * 이용 중 발생한 문제에 대해 책임지지 않습니다. 16 | * `lexifdev`님의 도움을 받아 작성되었습니다[`lexifdev's iamport 모듈](https://github.com/lexifdev/iamport) 17 | * 최초 작성은 `[핑크퐁 북스토어](https://store.pinkfong.com)`에서 쓰기 위해 만들었습니다. 18 | 19 | ### 주요 기능 20 | 21 | 1. 결제 정보 찾기 22 | 2. 가격 확인 23 | 3. 취소 24 | 4. 비 인증 결제 25 | 5. 정기 예약 결제 26 | 6. 본인인증결과 조회 및 삭제 27 | 28 | ### 설치 29 | 30 | ```bash 31 | # mac, linux 32 | pip install iamport-rest-client 33 | 34 | # 아나콘다 35 | conda create --name iamport python=3.6 36 | conda activate iamport 37 | python -m pip install iamport-rest-client --upgrade 38 | 39 | # 개발버전 40 | pip install git+https://github.com/iamport/iamport-rest-client-python.git@master --upgrade # master 41 | pip install git+https://github.com/iamport/iamport-rest-client-python.git@v1.0.0 --upgrade # 특정 버전 42 | ``` 43 | 44 | ### 개발 환경 45 | 46 | ```bash 47 | # venv 등 환경 준비 및 활성화 48 | pip install -e .[dev] 49 | pytest # 테스트 실행 50 | ``` 51 | 52 | ### 기여하기 53 | 54 | [iamport-rest-client-python 프로젝트 보드](https://github.com/iamport/iamport-rest-client-python/projects/1)의 `To do` 탭을 참고해주세요. 55 | 56 | ## 사용법 57 | 58 | ### 설정 59 | 60 | ```python 61 | from iamport import Iamport 62 | 63 | # 아임포트 객체를 테스트용 키와 시크릿을 사용하여 생성합니다 (테스트시 지출된 금액은 매일 자정 이전 환불됩니다). 64 | iamport = Iamport( 65 | imp_key='imp_apikey', 66 | imp_secret=( 67 | 'ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6b' 68 | 'kA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f' 69 | ) 70 | ) 71 | 72 | # 아임포트 객체를 각자 발급받으신 실제 키와 시크릿을 사용하여 생성합니다. 73 | iamport = Iamport(imp_key='{발급받은 키}', imp_secret='{발급받은 시크릿}') 74 | ``` 75 | 76 | ### 예제 77 | 78 | 결제를 진행한 상품 아이디나, 전달받은 IMP 아이디를 이용해 결제 정보를 찾습니다. 79 | 80 | ```python 81 | # 상품 아이디로 조회 82 | response = iamport.find(merchant_uid='{상품 아이디}') 83 | 84 | # I'mport; 아이디로 조회 85 | response = iamport.find(imp_uid='{IMP UID}') 86 | ``` 87 | 88 | 실제 제품 가격과 결제된 가격이 같은지 확인합니다. 89 | 90 | ```python 91 | # 상품 아이디로 확인 92 | iamport.is_paid(product_price, merchant_uid='{상품 아이디}') 93 | 94 | # I'mport; 아이디로 확인 95 | iamport.is_paid(product_price, imp_uid='{IMP UID}') 96 | 97 | # 이미 찾은 response 재활용하여 확인 98 | iamport.is_paid(product_price, response=response) 99 | ``` 100 | 101 | 결제를 취소합니다. 102 | 103 | ```python 104 | # 상품 아이디로 취소 105 | response = iamport.cancel('취소하는 이유', merchant_uid='{상품 아이디}') 106 | 107 | # I'mport; 아이디로 취소 108 | response = iamport.cancel('취소하는 이유', imp_uid='{IMP UID}') 109 | 110 | # 취소시 오류 예외처리(이미 취소된 결제는 에러가 발생함) 111 | try: 112 | response = iamport.cancel('취소하는 이유', imp_uid='{IMP UID}') 113 | except Iamport.ResponseError as e: 114 | print(e.code) 115 | print(e.message) # 에러난 이유를 알 수 있음 116 | except Iamport.HttpError as http_error: 117 | print(http_error.code) 118 | print(http_error.reason) # HTTP not 200 에러난 이유를 알 수 있음 119 | ``` 120 | 121 | 1회성 비인증 결제를 진행합니다. 122 | 123 | ```python 124 | # 테스트용 값 125 | payload = { 126 | 'merchant_uid': '00000000', 127 | 'amount': 5000, 128 | 'card_number': '4092-0230-1234-1234', 129 | 'expiry': '2019-03', 130 | 'birth': '500203', 131 | 'pwd_2digit': '19' 132 | } 133 | try: 134 | response = iamport.pay_onetime(**payload) 135 | except KeyError: 136 | # 필수 값이 없을때 에러 처리 137 | pass 138 | except Iamport.ResponseError as e: 139 | # 응답 에러 처리 140 | pass 141 | except Iamport.HttpError as http_error: 142 | # HTTP not 200 응답 에러 처리 143 | pass 144 | ``` 145 | 146 | 저장된 빌링키로 재결제합니다. 147 | 148 | ```python 149 | # 테스트용 값 150 | payload = { 151 | 'customer_uid': '{고객 아이디}', 152 | 'merchant_uid': '00000000', 153 | 'amount': 5000, 154 | 'name' : '제품명', 155 | } 156 | try: 157 | response = iamport.pay_again(**payload) 158 | except KeyError: 159 | # 필수 값이 없을때 에러 처리 160 | pass 161 | except Iamport.ResponseError as e: 162 | # 응답 에러 처리 163 | pass 164 | except Iamport.HttpError as http_error: 165 | # HTTP not 200 응답 에러 처리 166 | pass 167 | ``` 168 | 169 | 정기 결제를 예약합니다. 170 | 171 | ```python 172 | # 테스트용 값 173 | payload = { 174 | 'customer_uid': '{고객 아이디}', 175 | 'schedules': [ 176 | { 177 | 'merchant_uid': 'test_merchant_01', 178 | 'schedule_at': 1478150985, # UNIX timestamp 179 | 'amount': 1004 180 | }, 181 | { 182 | 'merhcant_uid': 'test_merchant_02', 183 | 'schedule_at': 1478150985, # UNIX timestamp 184 | 'amount': 5000, 185 | 'name': '{주문명}', 186 | 'buyer_name': '{주문자명}', 187 | 'buyer_email': '{주문자 이메일}', 188 | 'buyer_tel': '{주문자 전화번호}', 189 | 'buyer_addr': '{주문자 주소}', 190 | 'buyer_postcode': '{주문자 우편번호}', 191 | }, 192 | ] 193 | } 194 | try: 195 | response = iamport.pay_schedule(**payload) 196 | except KeyError: 197 | # 필수 값이 없을때 에러 처리 198 | pass 199 | except Iamport.ResponseError as e: 200 | # 응답 에러 처리 201 | pass 202 | except Iamport.HttpError as http_error: 203 | # HTTP not 200 응답 에러 처리 204 | pass 205 | ``` 206 | 207 | 정기 결제 예약을 취소합니다. 208 | 209 | ```python 210 | # 테스트용 값 (merchant_uid 가 누락되면 customer_uid 에 대한 결제예약정보 일괄취소) 211 | payload = { 212 | 'customer_uid': '{고객 아이디}', 213 | 'merchant_uid': 'test_merchant_01', 214 | } 215 | try: 216 | response = iamport.pay_unschedule(**payload) 217 | except KeyError: 218 | # 필수 값이 없을때 에러 처리 219 | pass 220 | except Iamport.ResponseError as e: 221 | # 응답 에러 처리 222 | pass 223 | except Iamport.HttpError as http_error: 224 | # HTTP not 200 응답 에러 처리 225 | pass 226 | ``` 227 | 228 | 결제될 내역에 대한 사전정보를 등록합니다 229 | 230 | ```python 231 | # 테스트용 값 232 | amount = 12000 233 | mid = 'merchant_test' 234 | try: 235 | response = iamport.prepare(amount=amount, merchant_uid=mid) 236 | except Iamport.ResponseError as e: 237 | # 응답 에러 처리 238 | pass 239 | except Iamport.HttpError as http_error: 240 | # HTTP not 200 응답 에러 처리 241 | pass 242 | ``` 243 | 244 | 등록된 사전정보를 확인합니다. 245 | 246 | ```python 247 | # 테스트용 값 248 | amount = 12000 249 | mid = 'merchant_test' 250 | try: 251 | result = iamport.prepare_validate(merchant_uid=mid, amount=amount) 252 | except Iamport.ResponseError as e: 253 | # 응답 에러 처리 254 | pass 255 | except Iamport.HttpError as http_error: 256 | # HTTP not 200 응답 에러 처리 257 | pass 258 | ``` 259 | 260 | 본인인증결과를 조회합니다. 261 | 262 | ```python 263 | try: 264 | response = iamport.find_certification(imp_uid='{IMP UID}') 265 | except Iamport.ResponseError as e: 266 | # 응답 에러 처리 267 | pass 268 | except Iamport.HttpError as http_error: 269 | # HTTP not 200 응답 에러 처리 270 | pass 271 | ``` 272 | 273 | 본인인증결과를 아임포트에서 삭제합니다. 274 | 275 | ```python 276 | try: 277 | response = iamport.cancel_certification(imp_uid='{IMP UID}') 278 | except Iamport.ResponseError as e: 279 | # 응답 에러 처리 280 | pass 281 | except Iamport.HttpError as http_error: 282 | # HTTP not 200 응답 에러 처리 283 | pass 284 | ``` 285 | -------------------------------------------------------------------------------- /iamport/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import Iamport 2 | 3 | __all__ = ['Iamport'] 4 | -------------------------------------------------------------------------------- /iamport/client.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import requests 4 | 5 | __all__ = ['IAMPORT_API_URL', 'Iamport'] 6 | 7 | IAMPORT_API_URL = 'https://api.iamport.kr/' 8 | 9 | 10 | class Iamport(object): 11 | def __init__(self, imp_key, imp_secret, imp_url=IAMPORT_API_URL): 12 | self.imp_key = imp_key 13 | self.imp_secret = imp_secret 14 | self.imp_url = imp_url 15 | requests_session = requests.Session() 16 | requests_adapters = requests.adapters.HTTPAdapter(max_retries=3) 17 | requests_session.mount('https://', requests_adapters) 18 | self.requests_session = requests_session 19 | 20 | class ResponseError(Exception): 21 | def __init__(self, code=None, message=None): 22 | self.code = code 23 | self.message = message 24 | 25 | class HttpError(Exception): 26 | def __init__(self, code=None, reason=None): 27 | self.code = code 28 | self.reason = reason 29 | 30 | @staticmethod 31 | def get_response(response): 32 | if response.status_code != requests.codes.ok: 33 | raise Iamport.HttpError(response.status_code, response.reason) 34 | result = response.json() 35 | if result['code'] != 0: 36 | raise Iamport.ResponseError( 37 | result.get('code'), result.get('message') 38 | ) 39 | return result.get('response') 40 | 41 | def _get_token(self): 42 | url = '{}users/getToken'.format(self.imp_url) 43 | payload = { 44 | 'imp_key': self.imp_key, 45 | 'imp_secret': self.imp_secret 46 | } 47 | response = self.requests_session.post( 48 | url, headers={'Content-Type': 'application/json'}, 49 | data=json.dumps(payload) 50 | ) 51 | return self.get_response(response).get('access_token') 52 | 53 | def get_headers(self): 54 | return {'Authorization': self._get_token()} 55 | 56 | def _get(self, url, payload=None): 57 | headers = self.get_headers() 58 | response = self.requests_session.get( 59 | url, headers=headers, params=payload 60 | ) 61 | return self.get_response(response) 62 | 63 | def _post(self, url, payload=None): 64 | headers = self.get_headers() 65 | headers['Content-Type'] = 'application/json' 66 | response = self.requests_session.post( 67 | url, headers=headers, data=json.dumps(payload) 68 | ) 69 | return self.get_response(response) 70 | 71 | def _delete(self, url): 72 | headers = self.get_headers() 73 | response = self.requests_session.delete(url, headers=headers) 74 | return self.get_response(response) 75 | 76 | def find_by_status(self, status, **params): 77 | url = '{}payments/status/{}'.format(self.imp_url, status) 78 | return self._get(url, params=params) 79 | 80 | def find_by_merchant_uid(self, merchant_uid, status=None): 81 | url = '{}payments/find/{}'.format(self.imp_url, merchant_uid) 82 | if status is not None: 83 | url = '{}/{}'.format(url, status) 84 | return self._get(url) 85 | 86 | def find_by_imp_uid(self, imp_uid): 87 | url = '{}payments/{}'.format(self.imp_url, imp_uid) 88 | return self._get(url) 89 | 90 | def find(self, **kwargs): 91 | merchant_uid = kwargs.get('merchant_uid') 92 | if merchant_uid: 93 | return self.find_by_merchant_uid(merchant_uid) 94 | try: 95 | imp_uid = kwargs['imp_uid'] 96 | except KeyError: 97 | raise KeyError('merchant_uid or imp_uid is required') 98 | return self.find_by_imp_uid(imp_uid) 99 | 100 | def _cancel(self, payload): 101 | url = '{}payments/cancel'.format(self.imp_url) 102 | return self._post(url, payload) 103 | 104 | def pay_onetime(self, **kwargs): 105 | url = '{}subscribe/payments/onetime'.format(self.imp_url) 106 | for key in [ 107 | 'merchant_uid', 'amount', 'card_number', 'expiry', 'birth', 108 | 'pwd_2digit' 109 | ]: 110 | if key not in kwargs: 111 | raise KeyError('Essential parameter is missing!: %s' % key) 112 | 113 | return self._post(url, kwargs) 114 | 115 | def pay_again(self, **kwargs): 116 | url = '{}subscribe/payments/again'.format(self.imp_url) 117 | for key in ['customer_uid', 'merchant_uid', 'amount']: 118 | if key not in kwargs: 119 | raise KeyError('Essential parameter is missing!: %s' % key) 120 | 121 | return self._post(url, kwargs) 122 | 123 | def customer_create(self, **kwargs): 124 | customer_uid = kwargs.get('customer_uid') 125 | for key in ['customer_uid', 'card_number', 'expiry', 'birth']: 126 | if key not in kwargs: 127 | raise KeyError('Essential parameter is missing!: %s' % key) 128 | url = '{}subscribe/customers/{}'.format(self.imp_url, customer_uid) 129 | return self._post(url, kwargs) 130 | 131 | def customer_get(self, customer_uid): 132 | url = '{}subscribe/customers/{}'.format(self.imp_url, customer_uid) 133 | return self._get(url) 134 | 135 | def customer_delete(self, customer_uid): 136 | url = '{}subscribe/customers/{}'.format(self.imp_url, customer_uid) 137 | return self._delete(url) 138 | 139 | def pay_foreign(self, **kwargs): 140 | url = '{}subscribe/payments/foreign'.format(self.imp_url) 141 | for key in ['merchant_uid', 'amount', 'card_number', 'expiry']: 142 | if key not in kwargs: 143 | raise KeyError('Essential parameter is missing!: %s' % key) 144 | 145 | return self._post(url, kwargs) 146 | 147 | def pay_schedule(self, **kwargs): 148 | headers = self.get_headers() 149 | headers['Content-Type'] = 'application/json' 150 | url = '{}subscribe/payments/schedule'.format(self.imp_url) 151 | if 'customer_uid' not in kwargs: 152 | raise KeyError('customer_uid is required') 153 | for key in ['merchant_uid', 'schedule_at', 'amount']: 154 | for schedules in kwargs['schedules']: 155 | if key not in schedules: 156 | raise KeyError('Essential parameter is missing!: %s' % key) 157 | 158 | return self._post(url, kwargs) 159 | 160 | def pay_schedule_get(self, merchant_id): 161 | url = '{}subscribe/payments/schedule/{}'.format( 162 | self.imp_url, merchant_id 163 | ) 164 | return self._get(url) 165 | 166 | def pay_schedule_get_between(self, **kwargs): 167 | url = '{}subscribe/payments/schedule'.format(self.imp_url) 168 | for key in ['schedule_from', 'schedule_to']: 169 | if key not in kwargs: 170 | raise KeyError('Essential parameter is missing!: %s' % key) 171 | 172 | return self._get(url, kwargs) 173 | 174 | def pay_unschedule(self, **kwargs): 175 | url = '{}subscribe/payments/unschedule'.format(self.imp_url) 176 | if 'customer_uid' not in kwargs: 177 | raise KeyError('customer_uid is required') 178 | 179 | return self._post(url, kwargs) 180 | 181 | def cancel_by_merchant_uid(self, merchant_uid, reason, **kwargs): 182 | payload = {'merchant_uid': merchant_uid, 'reason': reason} 183 | if kwargs: 184 | payload.update(kwargs) 185 | return self._cancel(payload) 186 | 187 | def cancel_by_imp_uid(self, imp_uid, reason, **kwargs): 188 | payload = {'imp_uid': imp_uid, 'reason': reason} 189 | if kwargs: 190 | payload.update(kwargs) 191 | return self._cancel(payload) 192 | 193 | def cancel(self, reason, **kwargs): 194 | imp_uid = kwargs.pop('imp_uid', None) 195 | if imp_uid: 196 | return self.cancel_by_imp_uid(imp_uid, reason, **kwargs) 197 | 198 | merchant_uid = kwargs.pop('merchant_uid', None) 199 | if merchant_uid is None: 200 | raise KeyError('merchant_uid or imp_uid is required') 201 | return self.cancel_by_merchant_uid(merchant_uid, reason, **kwargs) 202 | 203 | def is_paid(self, amount, **kwargs): 204 | response = kwargs.get('response') 205 | if not response: 206 | response = self.find(**kwargs) 207 | status = response.get('status') 208 | response_amount = response.get('amount') 209 | return status == 'paid' and response_amount == amount 210 | 211 | def prepare(self, merchant_uid, amount): 212 | url = '{}payments/prepare'.format(self.imp_url) 213 | payload = {'merchant_uid': merchant_uid, 'amount': amount} 214 | return self._post(url, payload) 215 | 216 | def prepare_validate(self, merchant_uid, amount): 217 | url = '{}payments/prepare/{}'.format(self.imp_url, merchant_uid) 218 | response = self._get(url) 219 | response_amount = response.get('amount') 220 | return response_amount == amount 221 | 222 | def revoke_vbank_by_imp_uid(self, imp_uid): 223 | url = '{}vbanks/{}'.format(self.imp_url, imp_uid) 224 | return self._delete(url) 225 | 226 | def find_certification(self, imp_uid): 227 | url = '{}certifications/{}'.format(self.imp_url, imp_uid) 228 | return self._get(url) 229 | 230 | def cancel_certification(self, imp_uid): 231 | url = '{}certifications/{}'.format(self.imp_url, imp_uid) 232 | return self._delete(url) 233 | -------------------------------------------------------------------------------- /iamport/client.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, Dict, Optional, Union 2 | 3 | import requests 4 | 5 | IAMPORT_API_URL: str = ... 6 | Amount = Union[int, float] 7 | 8 | class Iamport(object): 9 | requests_session: requests.Session 10 | 11 | def __init__(self, imp_key: str, imp_secret: str, imp_url: str = ...) -> None: ... 12 | 13 | class ResponseError(Exception): 14 | code: Any 15 | message: Any 16 | def __init__(self, code: Optional[Any] = ..., message: Optional[Any] = ...) -> None: ... 17 | 18 | class HttpError(Exception): 19 | code: Any 20 | message: Any 21 | def __init__(self, code: Optional[Any] = ..., message: Optional[Any] = ...) -> None: ... 22 | 23 | @staticmethod 24 | def get_response(response: requests.Response) -> Dict: ... 25 | 26 | def _get_token(self) -> str: ... 27 | 28 | def get_headers(self) -> Dict[str, str]: ... 29 | 30 | def _get(self, url: str, payload: Optional[Dict[str, Any]] = ...) -> Dict: ... 31 | 32 | def _post(self, url: str, payload: Optional[Dict[str, Any]] = ...) -> Dict: ... 33 | 34 | def _delete(self, url: str) -> Dict: ... 35 | 36 | def find_by_status(self, status: str, **params) -> Dict: ... 37 | 38 | def find_by_merchant_uid(self, merchant_uid: str) -> Dict: ... 39 | 40 | def find_by_imp_uid(self, imp_uid: str) -> Dict: ... 41 | 42 | def find(self, **kwargs) -> Dict: ... 43 | 44 | def _cancel(self, payload: Dict[str, Any]) -> Dict: ... 45 | 46 | def pay_onetime(self, **kwargs) -> Dict: ... 47 | 48 | def pay_again(self, **kwargs) -> Dict: ... 49 | 50 | def customer_create(self, **kwargs) -> Dict: ... 51 | 52 | def customer_get(self, customer_uid: str) -> Dict: ... 53 | 54 | def customer_delete(self, customer_uid: str) -> Dict: ... 55 | 56 | def pay_foreign(self, **kwargs) -> Dict: ... 57 | 58 | def pay_schedule(self, **kwargs) -> Dict: ... 59 | 60 | def pay_schedule_get(self, merchant_id : str) -> Dict: ... 61 | 62 | def pay_schedule_get_between(self, **kwargs) -> Dict: ... 63 | 64 | def pay_unschedule(self, **kwargs) -> Dict: ... 65 | 66 | def cancel_by_merchant_uid(self, merchant_uid: str, reason: str, **kwargs) -> Dict: ... 67 | 68 | def cancel_by_imp_uid(self, imp_uid: str, reason: str, **kwargs) -> Dict: ... 69 | 70 | def cancel(self, reason: str, **kwargs) -> Dict: ... 71 | 72 | def is_paid(self, amount: Amount, **kwargs) -> bool: ... 73 | 74 | def prepare(self, merchant_uid: str, amount: Amount) -> Dict: ... 75 | 76 | def prepare_validate(self, merchant_uid: str, amount: Amount) -> Dict: ... 77 | 78 | def revoke_vbank_by_imp_uid(self, imp_uid: str) -> Dict: ... 79 | 80 | def certification_by_imp_uid(self, imp_uid: str) -> Dict: ... 81 | 82 | def find_certification(self, imp_uid: str) -> Dict: ... 83 | 84 | def cancel_certification(self, imp_uid: str) -> Dict: ... 85 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = [ 3 | "setuptools >= 42", 4 | "wheel", 5 | ] 6 | build-backend = "setuptools.build_meta" 7 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | name = iamport-rest-client 3 | version = 0.9.0 4 | author = PerhapsSPY 5 | author_email = perhapsspy@gmail.com 6 | description = REST client for I'mport;(http://www.iamport.kr) 7 | long_description = file: README.md 8 | long_description_content_type = text/markdown 9 | license = MIT 10 | url = https://github.com/iamport/iamport-rest-client-python 11 | project_urls = 12 | Bug Tracker = https://github.com/iamport/iamport-rest-client-python/issues 13 | classifiers = 14 | Programming Language :: Python :: 3 15 | License :: OSI Approved :: MIT License 16 | Operating System :: OS Independent 17 | Programming Language :: Python 18 | Programming Language :: Python :: 3.6 19 | Programming Language :: Python :: 3.7 20 | Programming Language :: Python :: 3.8 21 | Programming Language :: Python :: 3.9 22 | Programming Language :: Python :: Implementation :: CPython 23 | Programming Language :: Python :: Implementation :: PyPy 24 | Topic :: Software Development :: Libraries :: Python Modules 25 | 26 | [options] 27 | zip_safe = False 28 | packages = find: 29 | python_requires = >=3.6 30 | install_requires = 31 | requests==2.26.0 32 | 33 | [options.extras_require] 34 | dev = 35 | pytest==6.2.4 36 | pytest-cov==2.12.1 37 | flake8==4.0.1 38 | isort==5.9.3 39 | mypy==0.910 40 | types-requests==2.26.0 41 | 42 | [options.packages.find] 43 | exclude = tests 44 | 45 | [options.package_data] 46 | * = *.pyi 47 | 48 | [flake8] 49 | max-line-length = 79 50 | exclude = .git,__pycache__,docs,build,dist,*.egg-info,*_cache,venv 51 | max-complexity = 10 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iamport/iamport-rest-client-python/959b1d8d66a97974800a2d5869c6242c048203a9/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pytest import fixture 2 | 3 | from iamport import Iamport 4 | 5 | DEFAULT_TEST_IMP_KEY = 'imp_apikey' 6 | DEFAULT_TEST_IMP_SECRET = ( 7 | 'ekKoeW8RyKuT0zgaZsUtXXTLQ4AhPFW3ZGseDA6b' 8 | 'kA5lamv9OqDMnxyeB9wqOsuO9W3Mx9YSJ4dTqJ3f' 9 | ) 10 | 11 | 12 | def pytest_addoption(parser): 13 | parser.addoption( 14 | '--imp-key', 15 | default=DEFAULT_TEST_IMP_KEY, 16 | help='iamport client key for testing [default: %(default)s]' 17 | ) 18 | parser.addoption( 19 | '--imp-secret', 20 | default=DEFAULT_TEST_IMP_SECRET, 21 | help='iamport secret key for testing [default: %(default)s]' 22 | ) 23 | 24 | 25 | @fixture 26 | def iamport(request): 27 | imp_key = request.config.getoption('--imp-key') 28 | imp_secret = request.config.getoption('--imp-secret') 29 | return Iamport(imp_key=imp_key, imp_secret=imp_secret) 30 | -------------------------------------------------------------------------------- /tests/test_cancel.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_cancel(iamport): 5 | with pytest.raises(TypeError): 6 | iamport.cancel(imp_uid='nothing') 7 | with pytest.raises(iamport.ResponseError): 8 | iamport.cancel('reason', imp_uid='nothing') 9 | try: 10 | iamport.cancel('reason', imp_uid='nothing') 11 | except iamport.ResponseError as e: 12 | assert e.code == 1 13 | assert e.message == u'취소할 결제건이 존재하지 않습니다.' 14 | 15 | 16 | def test_partial_cancel(iamport): 17 | try: 18 | iamport.cancel('reason', imp_uid='nothing', amount=100) 19 | except iamport.ResponseError as e: 20 | assert e.code == 1 21 | assert e.message == u'취소할 결제건이 존재하지 않습니다.' 22 | 23 | 24 | def test_cancel_by_merchant_uid(iamport): 25 | payload = { 26 | 'merchant_uid': 'any-merchant_uid', 27 | 'reason': 'any-reason', 28 | } 29 | 30 | try: 31 | iamport.cancel(**payload) 32 | except iamport.ResponseError as e: 33 | assert e.code == 1 34 | assert e.message == u'취소할 결제건이 존재하지 않습니다.' 35 | 36 | 37 | def test_cancel_without_merchant_uid(iamport): 38 | payload = { 39 | 'merchant_uid': None, 40 | 'reason': 'any-reason', 41 | } 42 | 43 | try: 44 | iamport.cancel(**payload) 45 | except KeyError as e: 46 | assert 'merchant_uid or imp_uid is required' in str(e) 47 | 48 | 49 | def test_cancel_by_merchant_uid_with_kwargs(iamport): 50 | payload = { 51 | 'merchant_uid': 'any-merchant_uid', 52 | 'reason': 'any-reason', 53 | 'amount': 1234, 54 | } 55 | 56 | try: 57 | iamport.cancel(**payload) 58 | except iamport.ResponseError as e: 59 | assert e.code == 1 60 | assert e.message == u'취소할 결제건이 존재하지 않습니다.' 61 | 62 | 63 | def test_cancel_by_imp_uid(iamport): 64 | payload = { 65 | 'imp_uid': 'any-imp_uid', 66 | 'reason': 'any-reason', 67 | } 68 | 69 | try: 70 | iamport.cancel(**payload) 71 | except iamport.ResponseError as e: 72 | assert e.code == 1 73 | assert e.message == u'취소할 결제건이 존재하지 않습니다.' 74 | -------------------------------------------------------------------------------- /tests/test_certification.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_find_certification(iamport): 5 | imp_uid = 'imp_12341234' 6 | 7 | with pytest.raises(iamport.HttpError) as e: 8 | iamport.find_certification(imp_uid) 9 | assert u'인증결과가 존재하지 않습니다.' == e.message 10 | 11 | with pytest.raises(iamport.HttpError) as e: 12 | iamport.cancel_certification(imp_uid) 13 | assert u'인증결과가 존재하지 않습니다.' == e.message 14 | -------------------------------------------------------------------------------- /tests/test_customer_create.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_customer_create(iamport): 5 | # Without 'card_number' 6 | payload_notEnough = { 7 | 'customer_uid': 'customer_1234', 8 | 'expiry': '2019-03', 9 | 'birth': '500203', 10 | } 11 | 12 | with pytest.raises(KeyError) as e: 13 | iamport.customer_create(**payload_notEnough) 14 | assert "Essential parameter is missing!: card_number" in str(e) 15 | 16 | payload_full = { 17 | 'customer_uid': 'customer_1234', 18 | 'expiry': '2019-03', 19 | 'birth': '500203', 20 | 'card_number': '4092-0230-1234-1234', 21 | } 22 | 23 | with pytest.raises(iamport.ResponseError) as e: 24 | iamport.customer_create(**payload_full) 25 | assert e.code == -1 26 | assert u'카드정보 인증 및 빌키 발급에 실패하였습니다.' in e.message 27 | -------------------------------------------------------------------------------- /tests/test_customer_get.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_customer_get(iamport): 5 | customer_uid = '000000' 6 | with pytest.raises(iamport.ResponseError) as e: 7 | iamport.customer_get(customer_uid) 8 | assert u'요청하신 customer_uid(000000)로 등록된 정보를 찾을 수 없습니다.' == e.message 9 | -------------------------------------------------------------------------------- /tests/test_find.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_find(iamport): 5 | with pytest.raises(KeyError): 6 | iamport.find() 7 | with pytest.raises(iamport.HttpError): 8 | iamport.find(imp_uid='test') 9 | with pytest.raises(iamport.HttpError): 10 | iamport.find(merchant_uid='âàáaā') 11 | -------------------------------------------------------------------------------- /tests/test_find_with_status.py: -------------------------------------------------------------------------------- 1 | def test_find_with_status(iamport): 2 | try: 3 | iamport.find_by_merchant_uid(merchant_uid='1234qwer', 4 | status='cancelled') 5 | except iamport.HttpError as e: 6 | assert e.code == 404 7 | 8 | res = iamport.find_by_merchant_uid(merchant_uid='1234qwer') 9 | assert res['merchant_uid'] == '1234qwer' 10 | 11 | res = iamport.find_by_merchant_uid(merchant_uid='1234qwer', status='paid') 12 | assert res['merchant_uid'] == '1234qwer' 13 | -------------------------------------------------------------------------------- /tests/test_is_paid.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def test_is_paid_with_response(iamport): 4 | mocked_response = { 5 | 'status': 'paid', 6 | 'amount': 1000, 7 | } 8 | assert True is iamport.is_paid( 9 | amount=1000, response=mocked_response, merchant_uid='test' 10 | ) 11 | 12 | 13 | def test_is_paid_without_response(iamport): 14 | assert False is iamport.is_paid(amount=1000, merchant_uid='qwer1234') 15 | -------------------------------------------------------------------------------- /tests/test_pay_again.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def test_pay_again(iamport): 4 | # Without 'customer_uid' 5 | payload_notEnough = { 6 | 'merchant_uid': '1234qwer', 7 | 'amount': 5000, 8 | } 9 | 10 | try: 11 | iamport.pay_again(**payload_notEnough) 12 | except KeyError as e: 13 | assert "Essential parameter is missing!: customer_uid" in str(e) 14 | 15 | payload_full = { 16 | 'customer_uid': '00000000', 17 | 'merchant_uid': '1234qwer', 18 | 'amount': 5000, 19 | } 20 | 21 | try: 22 | iamport.pay_again(**payload_full) 23 | except iamport.ResponseError as e: 24 | assert e.code == -1 25 | -------------------------------------------------------------------------------- /tests/test_pay_foreign.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def test_pay_foreign(iamport): 4 | payload = { 5 | 'merchant_uid': 'uid', 6 | 'amount': 100, 7 | 'card_number': 'card-number', 8 | } 9 | try: 10 | iamport.pay_foreign(**payload) 11 | except KeyError as e: 12 | assert "Essential parameter is missing!: expiry" in str(e) 13 | 14 | payload.update({ 15 | 'expiry': '2016-08', 16 | }) 17 | 18 | try: 19 | iamport.pay_foreign(**payload) 20 | except iamport.ResponseError as e: 21 | assert e.code == -1 22 | -------------------------------------------------------------------------------- /tests/test_pay_onetime.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | def test_pay_onetime(iamport): 6 | merchant_uid = ''.join( 7 | random.choice(string.ascii_uppercase + string.digits) 8 | for _ in range(10) 9 | ) 10 | 11 | # Without 'card_number' 12 | payload_not_enough = { 13 | 'merchant_uid': merchant_uid, 14 | 'amount': 5000, 15 | 'expiry': '2019-03', 16 | 'birth': '500203', 17 | 'pwd_2digit': '19' 18 | } 19 | 20 | try: 21 | iamport.pay_onetime(**payload_not_enough) 22 | except KeyError as e: 23 | assert "Essential parameter is missing!: card_number" in str(e) 24 | 25 | merchant_uid = ''.join( 26 | random.choice(string.ascii_uppercase + string.digits) 27 | for _ in range(10) 28 | ) 29 | 30 | payload_full = { 31 | 'merchant_uid': merchant_uid, 32 | 'amount': 5000, 33 | 'card_number': '4092-0230-1234-1234', 34 | 'expiry': '2019-03', 35 | 'birth': '500203', 36 | 'pwd_2digit': '19' 37 | } 38 | 39 | try: 40 | iamport.pay_onetime(**payload_full) 41 | except iamport.ResponseError as e: 42 | assert e.code == -1 43 | assert u'카드정보 인증에 실패하였습니다.' in e.message 44 | -------------------------------------------------------------------------------- /tests/test_pay_schedule.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def test_pay_schedule(iamport): 5 | schedule_at = int(time.time() + 1000) 6 | 7 | payload_without_customer_uid = { 8 | # without 'customer_uid' 9 | 'schedules': [ 10 | { 11 | 'merchant_uid': 'pay_schedule_%s' % str(time.time()), 12 | 'schedule_at': schedule_at, 13 | 'amount': 2001, 14 | 'name': '주문명1', 15 | 'buyer_name': '주문자명', 16 | 'buyer_email': '주문자 Email주소', 17 | 'buyer_tel': '주문자 전화번호', 18 | 'buyer_addr': '주문자 주소', 19 | 'buyer_postcode': '주문자 우편번호' 20 | }, 21 | ], 22 | } 23 | 24 | try: 25 | iamport.pay_schedule(**payload_without_customer_uid) 26 | except KeyError as e: 27 | assert 'customer_uid is required' in str(e) 28 | 29 | payload_without_merchant_uid = { 30 | 'customer_uid': '00000000', 31 | 'schedules': [ 32 | { 33 | # without 'merchant_uid' 34 | 'schedule_at': schedule_at, 35 | 'amount': 10000, 36 | 'name': '주문명2', 37 | 'buyer_name': '주문자명', 38 | 'buyer_email': '주문자 Email주소', 39 | 'buyer_tel': '주문자 전화번호', 40 | 'buyer_addr': '주문자 주소', 41 | 'buyer_postcode': '주문자 우편번호' 42 | }, 43 | ], 44 | } 45 | 46 | try: 47 | iamport.pay_schedule(**payload_without_merchant_uid) 48 | except KeyError as e: 49 | assert 'Essential parameter is missing!: merchant_uid' in str(e) 50 | 51 | payload_full = { 52 | 'customer_uid': '00000000', 53 | 'schedules': [ 54 | { 55 | 'merchant_uid': 'pay_schedule_%s' % str(time.time()), 56 | 'schedule_at': schedule_at, 57 | 'amount': 5000, 58 | 'name': '주문명', 59 | 'buyer_name': '주문자명', 60 | 'buyer_email': '주문자 Email주소', 61 | 'buyer_tel': '주문자 전화번호', 62 | 'buyer_addr': '주문자 주소', 63 | 'buyer_postcode': '주문자 우편번호' 64 | }, 65 | ], 66 | } 67 | 68 | try: 69 | iamport.pay_schedule(**payload_full) 70 | except iamport.ResponseError as e: 71 | assert e.code == 1 72 | assert u'등록된 고객정보가 없습니다.' in e.message 73 | -------------------------------------------------------------------------------- /tests/test_pay_unschedule.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def test_pay_unschedule(iamport): 5 | payload_without_customer_uid = { 6 | # without 'customer_uid' 7 | 'merchant_uid': 'pay_unschedule_%s' % str(time.time()), 8 | } 9 | 10 | try: 11 | iamport.pay_unschedule(**payload_without_customer_uid) 12 | except KeyError as e: 13 | assert 'customer_uid is required' in str(e) 14 | 15 | payload_full = { 16 | 'customer_uid': '00000000', 17 | 'merchant_uid': 'pay_unschedule_%s' % str(time.time()), 18 | } 19 | 20 | try: 21 | iamport.pay_unschedule(**payload_full) 22 | except iamport.ResponseError as e: 23 | assert e.code == 1 24 | assert u'취소할 예약결제 기록이 존재하지 않습니다.' in e.message 25 | -------------------------------------------------------------------------------- /tests/test_prepare.py: -------------------------------------------------------------------------------- 1 | import random 2 | import string 3 | 4 | 5 | def test_prepare(iamport): 6 | amount = 12000 7 | mid = ''.join( 8 | random.choice(string.ascii_uppercase + string.digits) 9 | for _ in range(10) 10 | ) 11 | result = iamport.prepare(merchant_uid=mid, amount=amount) 12 | assert result['amount'] == amount 13 | assert result['merchant_uid'] == mid 14 | 15 | result = iamport.prepare_validate(merchant_uid=mid, amount=amount) 16 | assert result 17 | --------------------------------------------------------------------------------