Created
May 17, 2016 12:53
-
-
Save ssut/2c9d1ae97dff769822a11e2e193b7d12 to your computer and use it in GitHub Desktop.
A Simple PayApp Implementation
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding: utf-8 | |
try: | |
import requests | |
except ImportError: | |
raise ImportError(u'requests 패키지가 설치되어있지 않습니다.\n>> pip install requests') | |
from urllib.parse import parse_qsl | |
from collections import namedtuple | |
from datetime import datetime | |
import json | |
__all__ = ['PayApp', ] | |
CMD_LIST = ('payrequest', 'paycancel', 'paycancelreq', ) | |
REST_URL = 'http://api.payapp.kr/oapi/apiLoad.html' | |
class Struct(object): | |
def __init__(self, **entries): | |
self.__dict__.update(entries) | |
def __repr__(self): | |
return str(self.__dict__) | |
class PayAppInternalResult(namedtuple('PayAppInternalResult', | |
['success', 'error', 'content'])): | |
pass | |
class PayType(object): | |
TYPES = (u'신용카드', u'휴대전화', u'해외결제', u'대면결제', | |
u'계좌이체', u'가상계좌', u'문화상품권') | |
def __init__(self, type_id): | |
if type(type_id) is not int: | |
type_id = int(type_id) | |
self.type_id = type_id | |
def __int__(self): | |
return self.type_id | |
def __str__(self): | |
return PayType.TYPES[self.type_id - 1] | |
def __repr__(self): | |
return str(self.type_id) | |
class PayState(object): | |
STATES = ( | |
([1], u'요청', 'req'), ([4], u'결제완료', 'approved'), ([8, 16, 32], u'요청취소', 'cancel'), | |
([9, 64], u'승인취소', 'cancel-approval'), ([10], u'결제대기', 'waiting') | |
) | |
def __init__(self, state_id): | |
if type(state_id) is not int: | |
state_id = int(state_id) | |
self.state_id = state_id | |
self.state_name = next(state[1] for state in PayState.STATES if state_id in state[0]) | |
self.stat = next(state[2] for state in PayState.STATES if state_id in state[0]) | |
@property | |
def code(self): | |
return self.stat | |
def __int__(self): | |
return self.state_id | |
def __str__(self): | |
return self.state_name | |
def __repr__(self): | |
return str(self.state_id) | |
class PayApp(object): | |
"""Payapp API""" | |
TEST_ID = 'payapptest' | |
TEST_KEY = 'ALSL4bhw2kPlZ+NeonwmJ+1DPJnCCRVaOgT+oqg6zaM=' | |
TEST_VALUE = 'ALSL4bhw2kPlZ+NeonwmJ7lHBlJsugxqHSmEy6b48Xk=' | |
def __init__(self, user_id, link_key, link_value): | |
self.user_id = user_id | |
self.link_key = link_key | |
self.link_value = link_value | |
def __post(self, cmd, data={}): | |
assert cmd in CMD_LIST | |
assert self.user_id | |
data.update({ | |
'cmd': cmd, | |
'userid': self.user_id, | |
}) | |
print(data) | |
resp = requests.post(REST_URL, data=data) | |
success = False | |
error = None | |
content = {} | |
if resp.status_code == 200: | |
data = dict(parse_qsl(resp.text)) | |
if data['state'] == '1': | |
success = True | |
elif 'errorMessage' in data: | |
error = data['errorMessage'] | |
content = Struct(**{k: v for k, v in data.items() if k not in ('state', 'errorMessage')}) | |
result = PayAppInternalResult(success=success, error=error, content=content) | |
return result | |
def verify_callback(self, params): | |
"""올바른 콜백 데이터인지 확인합니다. | |
:param params: `dict` 또는 쿼리 스트링 `str`, POST 데이터 | |
""" | |
if type(params) is str: | |
params = Struct(**dict(parse_qsl(params))) | |
elif type(params) is dict: | |
params = Struct(**params) | |
valid = True | |
try: | |
assert self.user_id == params.userid, u'판매자 회원 아이디가 일치하지 않습니다.' | |
assert self.link_key == params.linkkey, u'연동 KEY가 일치하지 않습니다.' | |
assert self.link_value == params.linkval, u'' | |
except (AssertionError, AttributeError) as e: | |
valid = False | |
print(e) | |
return valid | |
def parse_callback(self, params): | |
"""콜백 데이터를 사용히기 쉬운 형태로 파싱합니다. | |
파싱된 정보는 반드시 검증을 거쳐야 합니다. (name, price, contact) | |
:param params: `dict` 또는 쿼리 스트링 `str`, POST 데이터 | |
""" | |
if type(params) is str: | |
params = Struct(**dict(parse_qsl(params))) | |
elif type(params) is dict: | |
params = Struct(**params) | |
assert self.user_id == params.userid, u'판매자 회원 아이디가 일치하지 않습니다.' | |
assert self.link_key == params.linkkey, u'연동 KEY가 일치하지 않습니다.' | |
assert self.link_value == params.linkval, u'' | |
reqdate = datetime.strptime(params.reqdate, '%Y-%m-%d %H:%M:%S') | |
paydate = None | |
if params.pay_date != '': | |
paydate = datetime.strptime(params.pay_date, '%Y-%m-%d %H:%M:%S') | |
paystate = PayState(params.pay_state) | |
paytype = None | |
if paystate.code not in ('cancel'): | |
paytype = PayType(params.pay_type) | |
restructed = { | |
'name': params.goodname, | |
'price': params.price, | |
'contact': params.recvphone, | |
'memo': params.memo, | |
'request_date': reqdate, | |
'user_memo': params.pay_memo, | |
'user_addr': params.pay_addr, | |
'pay_date': paydate, | |
'pay_type': paytype, | |
'pay_state': paystate, | |
'var1': params.var1, | |
'var2': params.var2, | |
'identifier': params.mul_no, | |
'url': params.payurl, | |
'currency': params.currency, | |
} | |
# 신용카드 거래일 경우 | |
if paytype is not None and int(paytype) == 1: | |
restructed.update({ | |
'pay_check_url': params.csturl, | |
'pay_card_name': params.card_name, | |
}) | |
# 가상계좌 거래일 경우 | |
elif paytype is not None and int(paytype) == 6: | |
restructed.update({ | |
'pay_vbank': params.vbank, | |
'pay_vbank_account': params.vbankno, | |
}) | |
return Struct(**restructed) | |
def pay_request(self, name, price, contact, callback, returns, paytype='card,rbank', | |
memo='', var1='', var2=''): | |
"""결제를 요청합니다. | |
:param name: 상품명 | |
:param price: 결제요청 금액 | |
:param contact: (recvphone) 수신 휴대폰번호 | |
:param callback: (feedbackurl) 결제 완료 피드백 URL | |
:param returns: (returnurl) 결제 완료 후 이동할 URL | |
:param paytype: (openpaytype) 결제수단 | |
:param memo: 메모 | |
:param var1: 임의 사용 변수 1 | |
:param var2: 임의 사용 변수 2 | |
""" | |
data = { | |
'goodname': name, | |
'price': price, | |
'recvphone': contact, | |
'memo': memo, | |
'feedbackurl': callback, | |
'var1': var1, | |
'var2': var2, | |
'returnurl': returns, | |
'openpaytype': paytype, | |
} | |
result = self.__post('payrequest', data=data) | |
data = { | |
'success': result.success, | |
'error': result.error, | |
} | |
if result.success: | |
data['identifier'] = result.content.mul_no | |
data['url'] = result.content.payurl | |
return Struct(**data) | |
def pay_cancel(self, identifier, memo='', ready=False): | |
"""결제승인 후 5일이 지나지 않았거나, 정산이 완료되지 않은 경우 결제를 즉시 취소합니다. | |
:param identifier: 결제요청번호 | |
:param memo: 결제요청취소 메모 | |
:param mode: 취소 모드, 'ready'인 경우 실제 취소는 이루어지지 않고 결제요청 상태만 취소됩니다. | |
""" | |
data = { | |
'linkkey': self.link_key, | |
'mul_no': identifier, | |
} | |
if len(memo) > 0: | |
data['cancelmemo'] = memo | |
if ready: | |
data['cancelmode'] = True | |
result = self.__post('paycancel', data=data) | |
data = { | |
'success': result.success, | |
'error': result.error, | |
} | |
return Struct(**data) | |
def pay_cancel_request(self, identifier, memo): | |
"""결제승인 후 5일이 지났거나, 정산이 이미 완료된 경우 결제 취소 요청을 보냅니다. | |
:param identifier: 결제요청번호 | |
:param memo: 결제요청취소 메모 | |
""" | |
data = { | |
'linkkey': self.link_key, | |
'mul_no': identifier, | |
'cancelmemo': memo, | |
} | |
result = self.__post('paycancelreq', data=data) | |
data = { | |
'success': result.success, | |
'error': result.error, | |
} | |
return Struct(**data) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment