Skip to content

Commit

Permalink
added support for idempotency key
Browse files Browse the repository at this point in the history
  • Loading branch information
Michal Schielmann committed Aug 8, 2024
1 parent c0158f6 commit 8d79e21
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 43 deletions.
4 changes: 2 additions & 2 deletions shift4/blacklist.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@


class Blacklist(Resource):
def create(self, params):
return self._post("/blacklist", params)
def create(self, params, request_options=None):
return self._post("/blacklist", params, request_options=request_options)

def get(self, blacklist_rule_id):
return self._get("/blacklist/%s" % blacklist_rule_id)
Expand Down
14 changes: 10 additions & 4 deletions shift4/cards.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,20 @@


class Cards(Resource):
def create(self, customer_id, params):
return self._post("/customers/%s/cards" % customer_id, params)
def create(self, customer_id, params, request_options=None):
return self._post(
"/customers/%s/cards" % customer_id, params, request_options=request_options
)

def get(self, customer_id, card_id):
return self._get("/customers/%s/cards/%s" % (customer_id, card_id))

def update(self, customer_id, card_id, params):
return self._post("/customers/%s/cards/%s" % (customer_id, card_id), params)
def update(self, customer_id, card_id, params, request_options=None):
return self._post(
"/customers/%s/cards/%s" % (customer_id, card_id),
params,
request_options=request_options
)

def delete(self, customer_id, card_id):
return self._delete("/customers/%s/cards/%s" % (customer_id, card_id))
Expand Down
22 changes: 14 additions & 8 deletions shift4/charges.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,26 @@


class Charges(Resource):
def create(self, params):
return self._post("/charges", params)
def create(self, params, request_options=None):
return self._post("/charges", params, request_options=request_options)

def get(self, charge_id):
return self._get("/charges/%s" % charge_id)

def update(self, charge_id, params):
return self._post("/charges/%s" % charge_id, params)
def update(self, charge_id, params, request_options=None):
return self._post(
"/charges/%s" % charge_id, params, request_options=request_options
)

def list(self, params=None):
return self._get("/charges", params)

def capture(self, charge_id):
return self._post("/charges/%s/capture" % charge_id)
def capture(self, charge_id, request_options=None):
return self._post(
"/charges/%s/capture" % charge_id, request_options=request_options
)

def refund(self, charge_id, params=None):
return self._post("/charges/%s/refund" % charge_id, params)
def refund(self, charge_id, params=None, request_options=None):
return self._post(
"/charges/%s/refund" % charge_id, params, request_options=request_options
)
10 changes: 6 additions & 4 deletions shift4/credits.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@


class Credits(Resource):
def create(self, params):
return self._post("/credits", params)
def create(self, params, request_options=None):
return self._post("/credits", params, request_options=request_options)

def get(self, credit_id):
return self._get("/credits/%s" % credit_id)

def update(self, credit_id, params):
return self._post("/credits/%s" % credit_id, params)
def update(self, credit_id, params, request_options=None):
return self._post(
"/credits/%s" % credit_id, params, request_options=request_options
)

def list(self, params=None):
return self._get("/credits", params)
10 changes: 6 additions & 4 deletions shift4/customers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@


class Customers(Resource):
def create(self, params):
return self._post("/customers", params)
def create(self, params, request_options=None):
return self._post("/customers", params, request_options=request_options)

def get(self, customer_id):
return self._get("/customers/%s" % customer_id)

def update(self, customer_id, params):
return self._post("/customers/%s" % customer_id, params)
def update(self, customer_id, params, request_options=None):
return self._post(
"/customers/%s" % customer_id, params, request_options=request_options
)

def delete(self, customer_id):
return self._delete("/customers/%s" % customer_id)
Expand Down
12 changes: 8 additions & 4 deletions shift4/disputes.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@ class Disputes(Resource):
def get(self, dispute_id):
return self._get("/disputes/%s" % dispute_id)

def update(self, dispute_id, params):
return self._post("/disputes/%s" % dispute_id, params)
def update(self, dispute_id, params, request_options=None):
return self._post(
"/disputes/%s" % dispute_id, params, request_options=request_options
)

def close(self, dispute_id):
return self._post("/disputes/%s/close" % dispute_id)
def close(self, dispute_id, request_options=None):
return self._post(
"/disputes/%s/close" % dispute_id, request_options=request_options
)

def list(self, params=None):
return self._get("/disputes", params)
4 changes: 2 additions & 2 deletions shift4/payment_methods.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@


class PaymentMethods(Resource):
def create(self, params):
return self._post("/payment-methods", params)
def create(self, params, request_options=None):
return self._post("/payment-methods", params, request_options=request_options)

def get(self, payment_method_id):
return self._get("/payment-methods/%s" % payment_method_id)
Expand Down
10 changes: 6 additions & 4 deletions shift4/plans.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@


class Plans(Resource):
def create(self, params):
return self._post("/plans", params)
def create(self, params, request_options=None):
return self._post("/plans", params, request_options=request_options)

def get(self, plan_id):
return self._get("/plans/%s" % plan_id)

def update(self, plan_id, params):
return self._post("/plans/%s" % plan_id, params)
def update(self, plan_id, params, request_options=None):
return self._post(
"/plans/%s" % plan_id, params, request_options=request_options
)

def delete(self, plan_id):
return self._delete("/plans/%s" % plan_id)
Expand Down
14 changes: 14 additions & 0 deletions shift4/request_options.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
class RequestOptions:
__idempotency_key = None

def name(self):
return self.__class__.__name__.lower()

def set_idempotency_key(self, idempotency_key):
self.__idempotency_key = idempotency_key

def has_idempotency_key(self):
return self.__idempotency_key is not None

def get_idempotency_key(self):
return self.__idempotency_key
27 changes: 20 additions & 7 deletions shift4/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ def name(self):
def _get(self, path, params=None, url=None):
return self.__request("GET", path, params=params, url=url)

def _post(self, path, json=None, url=None):
return self.__request("POST", path, json=json, url=url)
def _post(self, path, json=None, url=None, request_options=None):
return self.__request(
"POST", path, json=json, url=url, request_options=request_options
)

def _multipart(self, path, params=None, files=None, url=None):
return self.__request("POST", path, params=params, files=files, url=url)
Expand All @@ -23,14 +25,23 @@ def _delete(self, path, params=None, url=None):
return self.__request("DELETE", path, params=params, url=url)

@classmethod
def __request(cls, method, path, params=None, json=None, files=None, url=None):
def __request(
cls,
method,
path,
params=None,
json=None,
files=None,
url=None,
request_options=None,
):
if url is None:
url = api.api_url.rstrip("/")
resp = requests.request(
method,
url=url + path,
auth=(api.secret_key, ""),
headers=cls.__create_headers(),
headers=cls.__create_headers(request_options),
files=files,
params=params,
json=json,
Expand All @@ -51,12 +62,14 @@ def __request(cls, method, path, params=None, json=None, files=None, url=None):
)

@classmethod
def __create_headers(cls):
def __create_headers(cls, request_options=None):
user_agent = "Shift4-Python/%s (Python/%s.%s.%s)" % (
__version__,
sys.version_info.major,
sys.version_info.minor,
sys.version_info.micro,
)

return {"User-Agent": user_agent}
headers = {"User-Agent": user_agent}
if request_options is not None and request_options.has_idempotency_key():
headers["Idempotency-Key"] = request_options.get_idempotency_key()
return headers
12 changes: 8 additions & 4 deletions shift4/subscriptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@


class Subscriptions(Resource):
def create(self, params):
return self._post("/subscriptions", params)
def create(self, params, request_options=None):
return self._post("/subscriptions", params, request_options=request_options)

def get(self, subscription_id):
return self._get("/subscriptions/%s" % subscription_id)

def update(self, subscription_id, params):
return self._post("/subscriptions/%s" % subscription_id, params)
def update(self, subscription_id, params, request_options=None):
return self._post(
"/subscriptions/%s" % subscription_id,
params,
request_options=request_options,
)

def cancel(self, subscription_id):
return self._delete("/subscriptions/%s" % subscription_id)
Expand Down
52 changes: 52 additions & 0 deletions tests/integration/test_idempotency_key_on_charges.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
from shift4.request_options import RequestOptions

from . import random_string
from .data.charges import valid_charge_req
from .testcase import TestCase


class TestCharges(TestCase):
def test_charge_is_not_duplicated_when_using_same_idempotency_key_and_same_body(
self, api
):
# given
request_options = RequestOptions()
request_options.set_idempotency_key(random_string())
charge_req = valid_charge_req()

# when
first_charge = api.charges.create(charge_req, request_options=request_options)
second_charge = api.charges.create(charge_req, request_options=request_options)

# then
assert first_charge == second_charge

def test_two_charges_are_created_when_using_different_idempotency_key_with_same_body(
self, api
):
# given
request_options = RequestOptions()
request_options.set_idempotency_key(random_string())
other_request_options = RequestOptions()
other_request_options.set_idempotency_key(random_string())
charge_req = valid_charge_req()

# when
first_charge = api.charges.create(charge_req, request_options=request_options)
second_charge = api.charges.create(
charge_req, request_options=other_request_options
)

# then
assert first_charge != second_charge

def test_two_charges_are_created_when_no_idempotency_key_with_same_body(self, api):
# given
charge_req = valid_charge_req()

# when
first_charge = api.charges.create(charge_req)
second_charge = api.charges.create(charge_req)

# then
assert first_charge != second_charge

0 comments on commit 8d79e21

Please sign in to comment.