-
Notifications
You must be signed in to change notification settings - Fork 0
/
client.py
362 lines (331 loc) · 13.3 KB
/
client.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
import json
from logging import getLogger
import requests
from payway.conf import TOKEN_NO_REDIRECT, CUSTOMER_URL, TRANSACTION_URL
from payway.constants import (
CREDIT_CARD_PAYMENT_CHOICE,
BANK_ACCOUNT_PAYMENT_CHOICE,
VALID_PAYMENT_METHOD_CHOICES,
)
from payway.customers import CustomerRequest
from payway.exceptions import PaywayError
from payway.model import (
PayWayCustomer,
PayWayTransaction,
PaymentError,
ServerError,
PaymentSetup,
TokenResponse,
)
from payway.transactions import TransactionRequest
logger = getLogger(__name__)
class Client(CustomerRequest, TransactionRequest):
"""
PayWay Client to connect to PayWay and perform methods given credentials
"""
merchant_id = ""
bank_account_id = ""
secret_api_key = ""
publishable_api_key = ""
def __init__(
self, merchant_id, bank_account_id, secret_api_key, publishable_api_key
):
"""
:param merchant_id : str = PayWay Merchant ID
:param bank_account_id : str = PayWay Bank Account ID
:param secret_api_key : str = PayWay Secret APi Key
:param publishable_api_key : str = PayWay Publishable API Key
"""
self._validate_credentials(
merchant_id, bank_account_id, secret_api_key, publishable_api_key
)
self.merchant_id = merchant_id
self.bank_account_id = bank_account_id
self.secret_api_key = secret_api_key
self.publishable_api_key = publishable_api_key
session = requests.Session()
session.auth = (self.secret_api_key, "")
headers = {"content-type": "application/x-www-form-urlencoded"}
session.headers = headers
self.session = session
session_no_headers = requests.Session()
session_no_headers.auth = session.auth
self.session_no_headers = session_no_headers
def _validate_credentials(
self, merchant_id, bank_account_id, secret_api_key, publishable_api_key
):
if (
not merchant_id
or not bank_account_id
or not secret_api_key
or not publishable_api_key
):
if not secret_api_key or not publishable_api_key:
logger.error("PayWay API keys not found")
raise PaywayError(
message="PayWay API keys not found", code="INVALID_API_KEYS"
)
logger.error(
"Merchant ID, bank account ID, secret API key, publishable API key are "
"invalid"
)
raise PaywayError(
message="Invalid credentials", code="INVALID_API_CREDENTIALS"
)
def get_request(self, endpoint):
return requests.get(url=endpoint, auth=(self.secret_api_key, ""))
def post_request(self, endpoint, data, auth=None, idempotency_key=None):
"""
Supply an idempotency_key to avoid duplicate POSTs
https://www.payway.com.au/docs/rest.html#avoiding-duplicate-posts
"""
if not auth:
auth = (self.secret_api_key, "")
headers = {"content-type": "application/x-www-form-urlencoded"}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
return requests.post(url=endpoint, auth=auth, data=data, headers=headers)
def put_request(self, endpoint, data):
headers = {"content-type": "application/x-www-form-urlencoded"}
return requests.put(
url=endpoint, auth=(self.secret_api_key, ""), data=data, headers=headers
)
def create_token(self, payway_obj, payment_method, idempotency_key=None):
"""
Creates a single use token for a Customer's payment setup (credit card or bank account)
:param payway_obj: object: one of model.PayWayCard or model.BankAccount object
:param payment_method: str: one of `card` or `direct_debit`
:param idempotency_key: str: unique value to avoid duplicate POSTs
"""
data = payway_obj.to_dict()
if payment_method == "card":
payway_payment_method = CREDIT_CARD_PAYMENT_CHOICE
elif payment_method == "direct_debit":
payway_payment_method = BANK_ACCOUNT_PAYMENT_CHOICE
else:
raise PaywayError(
message="Invalid payment method. Must be one of %s"
% ", ".join(VALID_PAYMENT_METHOD_CHOICES),
code="INVALID_PAYMENT_METHOD",
)
data.update(
{
"paymentMethod": payway_payment_method,
}
)
endpoint = TOKEN_NO_REDIRECT
logger.info("Sending Create Token request to PayWay.")
response = self.post_request(
endpoint,
data,
auth=(self.publishable_api_key, ""),
idempotency_key=idempotency_key,
)
logger.info("Response from server: %s" % response)
errors = self._validate_response(response)
if errors:
return None, errors
else:
token_response = TokenResponse().from_dict(response.json())
return token_response, errors
def create_card_token(self, card, idempotency_key=None):
"""
:param card: PayWayCard object represents a customer's credit card details
:param idempotency_key: str: unique value to avoid duplicate POSTs
See model.PayWayCard
"""
return self.create_token(card, "card", idempotency_key=idempotency_key)
def create_bank_account_token(self, bank_account, idempotency_key=None):
"""
:param bank_account: BankAccount object represents a customer's bank account
:param idempotency_key: str: unique value to avoid duplicate POSTs
See model.BankAccount
"""
return self.create_token(
bank_account, "direct_debit", idempotency_key=idempotency_key
)
def create_customer(self, customer, idempotency_key=None):
"""
Create a customer in PayWay system
POST /customers to have PayWay generate the customer number
PUT /customers/{customerNumber} to use your own customer number
:param customer: PayWayCustomer object represents a customer in PayWay
:param idempotency_key: str: unique value to avoid duplicate POSTs
See model.PayWayCustomer
:return:
"""
data = customer.to_dict()
data.update(
{"merchantId": self.merchant_id, "bankAccountId": self.bank_account_id}
)
logger.info("Sending Create Customer request to PayWay.")
if customer.custom_id:
endpoint = "{}/{}".format(CUSTOMER_URL, customer.custom_id)
response = self.put_request(endpoint, data)
else:
endpoint = "{}".format(CUSTOMER_URL)
response = self.post_request(
endpoint, data, idempotency_key=idempotency_key
)
logger.info("Response from server: %s" % response)
errors = self._validate_response(response)
if errors:
return None, errors
else:
customer = PayWayCustomer().from_dict(response.json())
return customer, errors
def process_payment(self, payment, idempotency_key=None):
"""
Process an individual payment against a Customer with active Recurring Billing setup.
:param payment: PayWayPayment object (see model.PayWayPayment)
:param idempotency_key: str: unique value to avoid duplicate POSTs
"""
data = payment.to_dict()
endpoint = TRANSACTION_URL
logger.info("Sending Process Payment request to PayWay.")
response = self.post_request(endpoint, data, idempotency_key=idempotency_key)
logger.info("Response from server: %s" % response)
errors = self._validate_response(response)
if errors:
return None, errors
else:
# convert response to PayWayTransaction object
transaction = PayWayTransaction.from_dict(response.json())
return transaction, errors
def _validate_response(self, response):
"""
Validates all responses from PayWay to catch documented PayWay errors.
:param response: requests response object
"""
if response.status_code in [
400,
401,
403,
405,
406,
407,
409,
410,
415,
429,
501,
503,
]:
http_error_msg = "%s Client Error: %s for url: %s" % (
response.status_code,
response.reason,
response.url,
)
raise PaywayError(code=response.status_code, message=http_error_msg)
elif response.status_code in [404, 422]: # Documented PayWay errors in JSON
# parse error message
errors = response.json()
payway_errors = PaymentError().from_dict(errors)
# instead of raising an exception, return the specific PayWay errors as a list
return payway_errors
elif response.status_code == 500:
try:
errors = response.json()
except json.JSONDecodeError:
raise PaywayError(
code=response.status_code, message="Internal server error"
)
# Documented PayWay server errors in JSON
payway_error = ServerError().from_dict(errors)
message = payway_error.to_message()
raise PaywayError(code=response.status_code, message=message)
else:
return None
def get_transaction(self, transaction_id):
"""
Lookup and return a transaction if found in PayWay
:param transaction_id: str A PayWay transaction ID
"""
endpoint = "%s/%s" % (TRANSACTION_URL, str(transaction_id))
response = self.get_request(endpoint)
logger.info("Response from server: %s" % response)
errors = self._validate_response(response)
if errors:
return None, errors
else:
transaction = PayWayTransaction.from_dict(response.json())
return transaction, errors
def void_transaction(self, transaction_id, idempotency_key=None):
"""
Void a transaction in PayWay
:param transaction_id: str A PayWay transaction ID
:param idempotency_key: str: unique value to avoid duplicate POSTs
"""
endpoint = "%s/%s/void" % (TRANSACTION_URL, transaction_id)
response = self.post_request(endpoint, data={}, idempotency_key=idempotency_key)
errors = self._validate_response(response)
if errors:
return None, errors
else:
transaction = PayWayTransaction.from_dict(response.json())
return transaction, errors
def refund_transaction(
self,
transaction_id,
amount,
order_id=None,
ip_address=None,
idempotency_key=None,
):
"""
Refund a transaction in PayWay
:param transaction_id: str A PayWay transaction ID
:param amount: str amount to refund
:param order_id: str optional reference number
:param ip_address: str optional IP address
:param idempotency_key: str: unique value to avoid duplicate POSTs
"""
endpoint = TRANSACTION_URL
data = {
"transactionType": "refund",
"parentTransactionId": transaction_id,
"principalAmount": amount,
}
if order_id:
data["orderNumber"] = order_id
if ip_address:
data["customerIpAddress"] = ip_address
response = self.post_request(endpoint, data, idempotency_key=idempotency_key)
errors = self._validate_response(response)
if errors:
return None, errors
else:
transaction = PayWayTransaction.from_dict(response.json())
return transaction, errors
def get_customer(self, customer_id):
"""
Returns a PayWay Customer's Payment Setup, [Payment] Schedule, Contact Details, Custom Fields and Notes
:param customer_id str PayWay customer ID in PayWay system
"""
endpoint = "%s/%s" % (CUSTOMER_URL, str(customer_id))
response = self.get_request(endpoint)
errors = self._validate_response(response)
if errors:
return None, errors
else:
customer = PayWayCustomer.from_dict(response.json())
return customer, errors
def update_payment_setup(self, token, customer_id):
"""
Updates the Customer's Payment Setup with a new Credit Card or Bank Account.
:param token: PayWay credit card or bank account token
:param customer_id: PayWay customer ID
"""
endpoint = "%s/%s/payment-setup" % (CUSTOMER_URL, str(customer_id))
data = {
"singleUseTokenId": token,
"merchantId": self.merchant_id,
"bankAccountId": self.bank_account_id,
}
response = self.put_request(endpoint, data)
errors = self._validate_response(response)
if errors:
return None, errors
else:
ps = PaymentSetup.from_dict(response.json())
return ps, errors