Passed
Push — master ( ff4459...addf59 )
by Messense
06:16 queued 04:27
created

wechatpy.pay.WeChatPay.__init__()   A

Complexity

Conditions 1

Size

Total Lines 13
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 13
nop 10
dl 0
loc 13
rs 9.75
c 0
b 0
f 0
ccs 12
cts 12
cp 1
crap 1

How to fix   Many Parameters   

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

1
# -*- coding: utf-8 -*-
2 10
from __future__ import absolute_import, unicode_literals
3 10
import inspect
4 10
import logging
5
6 10
import requests
7 10
import xmltodict
8 10
from xml.parsers.expat import ExpatError
9 10
from optionaldict import optionaldict
10
11 10
from wechatpy.crypto import WeChatRefundCrypto
12 10
from wechatpy.utils import random_string
13 10
from wechatpy.exceptions import WeChatPayException, InvalidSignatureException
14 10
from wechatpy.pay.utils import (
15
    calculate_signature, calculate_signature_hmac, _check_signature, dict_to_xml
16
)
17 10
from wechatpy.pay.base import BaseWeChatPayAPI
18 10
from wechatpy.pay import api
19
20 10
logger = logging.getLogger(__name__)
21
22
23 10
def _is_api_endpoint(obj):
24 10
    return isinstance(obj, BaseWeChatPayAPI)
25
26
27 10
class WeChatPay(object):
28
    """
29
    微信支付接口
30
31
    :param appid: 微信公众号 appid
32
    :param sub_appid: 当前调起支付的小程序APPID
33
    :param api_key: 商户 key,不要在这里使用小程序的密钥
34
    :param mch_id: 商户号
35
    :param sub_mch_id: 可选,子商户号,受理模式下必填
36
    :param mch_cert: 必填,商户证书路径
37
    :param mch_key: 必填,商户证书私钥路径
38
    :param timeout: 可选,请求超时时间,单位秒,默认无超时设置
39
    :param sandbox: 可选,是否使用测试环境,默认为 False
40
    """
41
42 10
    redpack = api.WeChatRedpack()
43
    """红包接口"""
44 10
    transfer = api.WeChatTransfer()
45
    """企业付款接口"""
46 10
    coupon = api.WeChatCoupon()
47
    """代金券接口"""
48 10
    order = api.WeChatOrder()
49
    """订单接口"""
50 10
    refund = api.WeChatRefund()
51
    """退款接口"""
52 10
    micropay = api.WeChatMicroPay()
53
    """刷卡支付接口"""
54 10
    tools = api.WeChatTools()
55
    """工具类接口"""
56 10
    jsapi = api.WeChatJSAPI()
57
    """公众号网页 JS 支付接口"""
58 10
    withhold = api.WeChatWithhold()
59
    """代扣接口"""
60
61 10
    API_BASE_URL = 'https://api.mch.weixin.qq.com/'
62
63 10
    def __new__(cls, *args, **kwargs):
64 10
        self = super(WeChatPay, cls).__new__(cls)
65 10
        api_endpoints = inspect.getmembers(self, _is_api_endpoint)
66 10
        for name, _api in api_endpoints:
67 10
            api_cls = type(_api)
68 10
            _api = api_cls(self)
69 10
            setattr(self, name, _api)
70 10
        return self
71
72 10
    def __init__(self, appid, api_key, mch_id, sub_mch_id=None,
73
                 mch_cert=None, mch_key=None, timeout=None, sandbox=False, sub_appid=None):
74 10
        self.appid = appid
75 10
        self.sub_appid = sub_appid
76 10
        self.api_key = api_key
77 10
        self.mch_id = mch_id
78 10
        self.sub_mch_id = sub_mch_id
79 10
        self.mch_cert = mch_cert
80 10
        self.mch_key = mch_key
81 10
        self.timeout = timeout
82 10
        self.sandbox = sandbox
83 10
        self._sandbox_api_key = None
84 10
        self._http = requests.Session()
85
86 10
    def _fetch_sandbox_api_key(self):
87
        nonce_str = random_string(32)
88
        sign = calculate_signature({'mch_id': self.mch_id, 'nonce_str': nonce_str}, self.api_key)
89
        payload = dict_to_xml({
90
            'mch_id': self.mch_id,
91
            'nonce_str': nonce_str,
92
        }, sign=sign)
93
        headers = {'Content-Type': 'text/xml'}
94
        api_url = '{base}sandboxnew/pay/getsignkey'.format(base=self.API_BASE_URL)
95
        response = self._http.post(api_url, data=payload, headers=headers)
96
        return xmltodict.parse(response.text)['xml'].get('sandbox_signkey')
97
98 10
    def _request(self, method, url_or_endpoint, **kwargs):
99 10
        if not url_or_endpoint.startswith(('http://', 'https://')):
100 10
            api_base_url = kwargs.pop('api_base_url', self.API_BASE_URL)
101 10
            if self.sandbox:
102
                api_base_url = '{url}sandboxnew/'.format(url=api_base_url)
103 10
            url = '{base}{endpoint}'.format(
104
                base=api_base_url,
105
                endpoint=url_or_endpoint
106
            )
107
        else:
108
            url = url_or_endpoint
109
110 10
        if isinstance(kwargs.get('data', ''), dict):
111 10
            data = kwargs['data']
112 10
            if 'mchid' not in data:
113
                # Fuck Tencent
114 10
                data.setdefault('mch_id', self.mch_id)
115 10
            data.setdefault('sub_mch_id', self.sub_mch_id)
116 10
            data.setdefault('nonce_str', random_string(32))
117 10
            data = optionaldict(data)
118
119 10
            if data.get('sign_type', 'MD5') == 'HMAC-SHA256':
120
                sign = calculate_signature_hmac(data, self.sandbox_api_key if self.sandbox else self.api_key)
121
            else:
122 10
                sign = calculate_signature(data, self.sandbox_api_key if self.sandbox else self.api_key)
123 10
            body = dict_to_xml(data, sign)
124 10
            body = body.encode('utf-8')
125 10
            kwargs['data'] = body
126
127
        # 商户证书
128 10
        if self.mch_cert and self.mch_key:
129
            kwargs['cert'] = (self.mch_cert, self.mch_key)
130
131 10
        kwargs['timeout'] = kwargs.get('timeout', self.timeout)
132 10
        logger.debug('Request to WeChat API: %s %s\n%s', method, url, kwargs)
133 10
        res = self._http.request(
134
            method=method,
135
            url=url,
136
            **kwargs
137
        )
138 10
        try:
139 10
            res.raise_for_status()
140
        except requests.RequestException as reqe:
141
            raise WeChatPayException(
142
                return_code=None,
143
                client=self,
144
                request=reqe.request,
145
                response=reqe.response
146
            )
147
148 10
        return self._handle_result(res)
149
150 10
    def _handle_result(self, res):
151 10
        res.encoding = 'utf-8'
152 10
        xml = res.text
153 10
        logger.debug('Response from WeChat API \n %s', xml)
154 10
        try:
155 10
            data = xmltodict.parse(xml)['xml']
156
        except (xmltodict.ParsingInterrupted, ExpatError):
157
            # 解析 XML 失败
158
            logger.debug('WeChat payment result xml parsing error', exc_info=True)
159
            return xml
160
161 10
        return_code = data['return_code']
162 10
        return_msg = data.get('return_msg')
163 10
        result_code = data.get('result_code')
164 10
        errcode = data.get('err_code')
165 10
        errmsg = data.get('err_code_des')
166 10
        if return_code != 'SUCCESS' or result_code != 'SUCCESS':
167
            # 返回状态码不为成功
168
            raise WeChatPayException(
169
                return_code,
170
                result_code,
171
                return_msg,
172
                errcode,
173
                errmsg,
174
                client=self,
175
                request=res.request,
176
                response=res
177
            )
178 10
        return data
179
180 10
    def get(self, url, **kwargs):
181
        return self._request(
182
            method='get',
183
            url_or_endpoint=url,
184
            **kwargs
185
        )
186
187 10
    def post(self, url, **kwargs):
188 10
        return self._request(
189
            method='post',
190
            url_or_endpoint=url,
191
            **kwargs
192
        )
193
194 10
    def check_signature(self, params):
195
        return _check_signature(params, self.api_key if not self.sandbox else self.sandbox_api_key)
196
197 10
    @classmethod
198
    def get_payment_data(cls, xml):
199
        """
200
        解析微信支付结果通知,获得appid, mch_id, out_trade_no, transaction_id
201
        如果你需要进一步判断,请先用appid, mch_id来生成WeChatPay,
202
        然后用`wechatpay.parse_payment_result(xml)`来校验支付结果
203
204
        使用示例::
205
206
            from wechatpy.pay import WeChatPay
207
            # 假设你已经获取了微信服务器推送的请求中的xml数据并存入xml变量
208
            data = WeChatPay.get_payment_appid(xml)
209
            {
210
                "appid": "公众号或者小程序的id",
211
                "mch_id": "商户id",
212
            }
213
214
        """
215
        try:
216
            data = xmltodict.parse(xml)
217
        except (xmltodict.ParsingInterrupted, ExpatError):
218
            raise ValueError("invalid xml")
219
        if not data or 'xml' not in data:
220
            raise ValueError("invalid xml")
221
        return {
222
            "appid": data["appid"],
223
            "mch_id": data["mch_id"],
224
            "out_trade_no": data["out_trade_no"],
225
            "transaction_id": data["transaction_id"],
226
        }
227
228 10
    def parse_payment_result(self, xml):
229
        """解析微信支付结果通知"""
230
        try:
231
            data = xmltodict.parse(xml)
232
        except (xmltodict.ParsingInterrupted, ExpatError):
233
            raise InvalidSignatureException()
234
235
        if not data or 'xml' not in data:
236
            raise InvalidSignatureException()
237
238
        data = data['xml']
239
        sign = data.pop('sign', None)
240
        real_sign = calculate_signature(data, self.api_key if not self.sandbox else self.sandbox_api_key)
241
        if sign != real_sign:
242
            raise InvalidSignatureException()
243
244
        for key in ('total_fee', 'settlement_total_fee', 'cash_fee', 'coupon_fee', 'coupon_count'):
245
            if key in data:
246
                data[key] = int(data[key])
247
        data['sign'] = sign
248
        return data
249
250 10
    def parse_refund_notify_result(self, xml):
251
        """解析微信退款结果通知"""
252
        refund_crypto = WeChatRefundCrypto(self.api_key if not self.sandbox else self.sandbox_api_key)
253
        data = refund_crypto.decrypt_message(xml, self.appid, self.mch_id)
254
        for key in ('total_fee', 'settlement_total_fee', 'refund_fee', 'settlement_refund_fee'):
255
            if key in data:
256
                data[key] = int(data[key])
257
        return data
258
259 10
    @property
260
    def sandbox_api_key(self):
261 10
        if self.sandbox and self._sandbox_api_key is None:
262
            self._sandbox_api_key = self._fetch_sandbox_api_key()
263
264
        return self._sandbox_api_key
265