python-payway 0.0.2__py3-none-any.whl → 0.0.4__py3-none-any.whl

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