diff --git a/shift4/blacklist.py b/shift4/blacklist.py index 29fb77b..917cd85 100644 --- a/shift4/blacklist.py +++ b/shift4/blacklist.py @@ -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) diff --git a/shift4/cards.py b/shift4/cards.py index 81dc1e1..b0091af 100644 --- a/shift4/cards.py +++ b/shift4/cards.py @@ -2,14 +2,14 @@ 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)) diff --git a/shift4/charges.py b/shift4/charges.py index 73af10e..c8f7beb 100644 --- a/shift4/charges.py +++ b/shift4/charges.py @@ -2,20 +2,20 @@ 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) diff --git a/shift4/credits.py b/shift4/credits.py index 34bd31a..2d5d6b1 100644 --- a/shift4/credits.py +++ b/shift4/credits.py @@ -2,14 +2,14 @@ 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) diff --git a/shift4/customers.py b/shift4/customers.py index 9dbe210..03087ef 100644 --- a/shift4/customers.py +++ b/shift4/customers.py @@ -2,14 +2,14 @@ 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) diff --git a/shift4/disputes.py b/shift4/disputes.py index 62020dc..6378649 100644 --- a/shift4/disputes.py +++ b/shift4/disputes.py @@ -5,11 +5,11 @@ 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) diff --git a/shift4/payment_methods.py b/shift4/payment_methods.py index 2f3c27e..c8ac81c 100644 --- a/shift4/payment_methods.py +++ b/shift4/payment_methods.py @@ -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) diff --git a/shift4/plans.py b/shift4/plans.py index 2bfe1ea..2f04138 100644 --- a/shift4/plans.py +++ b/shift4/plans.py @@ -2,14 +2,14 @@ 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) diff --git a/shift4/request_options.py b/shift4/request_options.py new file mode 100644 index 0000000..666b2e3 --- /dev/null +++ b/shift4/request_options.py @@ -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 diff --git a/shift4/resource.py b/shift4/resource.py index e3590aa..c6706d7 100644 --- a/shift4/resource.py +++ b/shift4/resource.py @@ -13,8 +13,8 @@ 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) @@ -23,14 +23,14 @@ 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, @@ -51,12 +51,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 diff --git a/shift4/subscriptions.py b/shift4/subscriptions.py index cfa514b..6c33b37 100644 --- a/shift4/subscriptions.py +++ b/shift4/subscriptions.py @@ -2,14 +2,14 @@ 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) diff --git a/shift4/tokens.py b/shift4/tokens.py index 35a011a..455aa2e 100644 --- a/shift4/tokens.py +++ b/shift4/tokens.py @@ -2,8 +2,8 @@ class Tokens(Resource): - def create(self, params): - return self._post("/tokens", params) + def create(self, params, request_options=None): + return self._post("/tokens", params, request_options=request_options) def get(self, token_id): return self._get("/tokens/%s" % token_id) diff --git a/tests/integration/test_idempotency_key_on_charges.py b/tests/integration/test_idempotency_key_on_charges.py new file mode 100644 index 0000000..8790ed1 --- /dev/null +++ b/tests/integration/test_idempotency_key_on_charges.py @@ -0,0 +1,46 @@ +from shift4.request_options import RequestOptions + +from . import random_string +from .data.charges import valid_charge_req + + +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