From 6f08e9c44d2b0308afd1d72fbca453da77498bb3 Mon Sep 17 00:00:00 2001 From: williamotieno Date: Sun, 31 Dec 2023 07:42:45 +0300 Subject: [PATCH] v1.0.0 - charge api --- .github/workflows/test.yml | 2 +- README.md | 136 +------------- python_flutterwave/charge/__init__.py | 5 + python_flutterwave/charge/bank.py | 44 +---- python_flutterwave/charge/card.py | 12 +- python_flutterwave/charge/mobile.py | 233 +++++++++++++++++------- python_flutterwave/charge/validation.py | 30 +++ python_flutterwave/decorators.py | 27 +++ python_flutterwave/exceptions.py | 26 +-- setup.cfg | 2 +- 10 files changed, 246 insertions(+), 271 deletions(-) create mode 100644 python_flutterwave/charge/validation.py create mode 100644 python_flutterwave/decorators.py diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6d11ba5..f63e3c9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10", "3.11", "3.12"] steps: - uses: actions/checkout@v2 diff --git a/README.md b/README.md index 5935843..99c1d89 100644 --- a/README.md +++ b/README.md @@ -1,135 +1 @@ -# Python Flutterwave - -### Description -Python Wrapper for interacting with the Flutterwave API - - -### Contribution - - Before making any further steps (for interested contributors), please refer to the [CONTRIBUTING GUIDE](/CONTRIBUTING.md). - - -## Installation - -- ``pip install python-flutterwave`` - -## Usage - -- Create an account in Flutterwave [here](https://dashboard.flutterwave.com/signup?referrals=RV1284343) and obtain your `Secret Key` only. - -``` -from python_flutterwave import payment - -payment.token = 'YOUR_SECRET_KEY' -``` - -``` -Here are all the possible values for payment options available on Flutterwave: -0: "account" -1: "card" -2: "banktransfer" -3: "mpesa" -4: "mobilemoneyrwanda" -5: "mobilemoneyzambia" -6: "qr" -7: "mobilemoneyuganda" -8: "ussd" -9: "credit" -10: "barter" -11: "mobilemoneyghana" -12: "payattitude" -13: "mobilemoneyfranco" -14: "paga" -15: "1voucher" -16: "mobilemoneytanzania" -``` - -- To trigger a standard payment that returns a redirect uri - -```python -uri = payment.initiate_payment(tx_ref="qwerty", amount=100, redirect_url='your_callback_url', - payment_options='mpesa', customer_email='example@email.com', - customer_phone_number='0123456789', currency='KES', customer_name='John Doe', - title='Demo Payment', description='Just pay me...') -print(uri) -``` -- Redirect the user to that uri where he/she will make the payment. -- After payment is made, the user will be redirected to the `redirect_url` you declared but Flutterwave will append some -info regarding the payment i.e. `transaction_id` and `tx_ref`. If your url is `https://example.com/callback` -then it may be `http://example.com/callback/?status=successful&tx_ref=qwerty&transaction_id=2784792` -- You should save the transaction_id to your DB as it will be used to query the transaction details. - -> There is no need to save the transaction_id as it is also possible to use tx_ref to get the payment details! -> Check here - https://developer.flutterwave.com/reference/endpoints/transactions/ - - -- To check the transaction details e.g. successful or not, grab the transaction_id from the previous step. -```python -details = payment.get_payment_details(transaction_id) -# or -deailts = payment.get_payment_details_via_tx_ref(tx_ref) - -print(details) -``` - -- To trigger an automatic MPESA charge on your customer through STK push, first configure your Webhook url in the dashboard, it may be a -simple server; Flutterwave will post some data regarding your transaction status in that url. This method call will -return a Python dict object. You can decide what to do thereon. -```python -mpesa_trans_details = payment.trigger_mpesa_payment(tx_ref="qwertyuio", amount=100, currency='KES', - email='johndoe@gmail.com', phone_number='1234567890', - full_name='John Doe') -print(mpesa_trans_details) -``` - -- To initiate a USSD payment to your customer, configure your webhook url in the dashboard where Flutterwave will post data -regarding the transaction. This method call will return a Python Dict object from which you can extract the USSD code to -show your customer for payment completion. For other banks, you may also need to extract the `payment_code` from the result. - - - By default, `NGN` is the only supported currency for USSD payments so this method automatically uses `NGN` - - Only a number of banks support `USSD` so you have to refer to the docs to check your bank and its corresponding `account_bank` code. -``` -At the moment, banks available for USSD payments (and their numeric codes) are: - -Access Bank -- 044 -Ecobank -- 050 -Fidelity Bank -- 070 -First Bank of Nigeria -- 011 -First City Monument Bank (FCMB) -- 214 -GTBank -- 058 -Heritage Bank -- 030 -Keystone Bank -- 082 -Stanbic IBTC Bank -- 221 -Sterling Bank -- 232 -Union Bank -- 032 -United Bank for Africa -- 033 -Unity Bank -- 215 -VFD Microfinance Bank -- 090110 -Wema Bank -- 035 -Zenith Bank -- 057 -``` - - -```python -details = payment.initiate_ussd_payment(tx_ref="123erd", amount=100, email='johndoe@gmail.com', - phone_number='789456123', full_name='John Doe', account_bank='057') -print(details) -``` - -- For bank transactions, it is important to first verify the details given to you by the customer before granting incentives -according to the specifications of your application. -- To verify bank details call the function below that returns a Python dictionary with the data... -```python -details = payment.verify_bank_account_details(account_number= "0690000032", account_bank= "044") -print(details) -``` - -- For card transactions, it is advisable to ascertain the customer's card details before granting incentives according -to the specifications of your application. -- This function call takes the `card_bin` (usually the first 4-6 digits in debit/credit cards) and returns info regarding -the card. A Python dict object is returned, thence use it according to your needs. -```python -details = payment.verify_card_details(card_bin=553188) -print(details) -``` - -[![Sponsor Python Flutterwave](https://cdn.buymeacoffee.com/buttons/default-red.png)](https://dashboard.flutterwave.com/donate/zvapzky1ozls) +```New docs under development``` diff --git a/python_flutterwave/charge/__init__.py b/python_flutterwave/charge/__init__.py index e69de29..005461b 100644 --- a/python_flutterwave/charge/__init__.py +++ b/python_flutterwave/charge/__init__.py @@ -0,0 +1,5 @@ +from .bank import * +from .card import * +from .mobile import * +from .validation import * + diff --git a/python_flutterwave/charge/bank.py b/python_flutterwave/charge/bank.py index 60dd460..d9a3adc 100644 --- a/python_flutterwave/charge/bank.py +++ b/python_flutterwave/charge/bank.py @@ -2,13 +2,14 @@ import requests import json -from python_flutterwave.exceptions import TokenException +from python_flutterwave.decorators import handle_api_exceptions token = os.environ.get("SECRET_KEY") base_url = "https://api.flutterwave.com/v3/charges" +@handle_api_exceptions def initiate_bank_charge( amount: int, email: str, @@ -22,9 +23,6 @@ def initiate_bank_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - params = {"type": "bank_transfer"} payload = json.dumps( { @@ -36,16 +34,11 @@ def initiate_bank_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) +@handle_api_exceptions def initiate_nigeria_bank_charge( amount: int, email: str, @@ -59,9 +52,6 @@ def initiate_nigeria_bank_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - params = {"type": "mono"} payload = json.dumps( { @@ -73,16 +63,11 @@ def initiate_nigeria_bank_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) +@handle_api_exceptions def initiate_uk_eu_bank_charge( amount: int, email: str, tx_ref: str, phone_number: str, is_token_io: int ) -> dict: @@ -96,9 +81,6 @@ def initiate_uk_eu_bank_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - params = {"type": "account-ach-uk"} payload = json.dumps( { @@ -112,17 +94,12 @@ def initiate_uk_eu_bank_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) -def initiate_ach_charge( +@handle_api_exceptions +def initiate_ach_bank_charge( amount: int, email: str, tx_ref: str, @@ -139,9 +116,6 @@ def initiate_ach_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - params = {"type": "account-ach-uk"} payload = json.dumps( { @@ -155,11 +129,5 @@ def initiate_ach_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) diff --git a/python_flutterwave/charge/card.py b/python_flutterwave/charge/card.py index b575553..e4c5d49 100644 --- a/python_flutterwave/charge/card.py +++ b/python_flutterwave/charge/card.py @@ -2,13 +2,14 @@ import requests import json -from python_flutterwave.exceptions import TokenException +from python_flutterwave.decorators import handle_api_exceptions token = os.environ.get("SECRET_KEY") base_url = "https://api.flutterwave.com/v3/charges" +@handle_api_exceptions def initiate_card_charge( amount: int, card_number: int, @@ -30,9 +31,6 @@ def initiate_card_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - params = {"type": "card"} payload = json.dumps( { @@ -48,11 +46,5 @@ def initiate_card_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) diff --git a/python_flutterwave/charge/mobile.py b/python_flutterwave/charge/mobile.py index 8e94fae..8ae7767 100644 --- a/python_flutterwave/charge/mobile.py +++ b/python_flutterwave/charge/mobile.py @@ -2,13 +2,14 @@ import requests import json -from python_flutterwave.exceptions import TokenException +from python_flutterwave.decorators import handle_api_exceptions token = os.environ.get("SECRET_KEY") base_url = "https://api.flutterwave.com/v3/charges" +@handle_api_exceptions def initiate_mpesa_charge( amount: int, email: str, tx_ref: str, phone_number: str ) -> dict: @@ -21,9 +22,6 @@ def initiate_mpesa_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - params = {"type": "mpesa"} payload = json.dumps( { @@ -37,16 +35,11 @@ def initiate_mpesa_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) +@handle_api_exceptions def initiate_ghana_mobile_charge( amount: int, email: str, tx_ref: str, phone_number: str, network: str ) -> dict: @@ -60,9 +53,6 @@ def initiate_ghana_mobile_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - params = {"type": "mobile_money_ghana"} payload = json.dumps( { @@ -77,16 +67,11 @@ def initiate_ghana_mobile_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) +@handle_api_exceptions def initiate_uganda_mobile_charge( amount: int, email: str, @@ -102,9 +87,6 @@ def initiate_uganda_mobile_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - params = {"type": "mobile_money_uganda"} payload = json.dumps( { @@ -118,16 +100,11 @@ def initiate_uganda_mobile_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) +@handle_api_exceptions def initiate_franco_mobile_charge( amount: int, email: str, @@ -147,9 +124,6 @@ def initiate_franco_mobile_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - params = {"type": "mobile_money_franco"} payload = json.dumps( { @@ -164,16 +138,11 @@ def initiate_franco_mobile_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) +@handle_api_exceptions def initiate_tanzania_mobile_charge( amount: int, email: str, @@ -189,9 +158,6 @@ def initiate_tanzania_mobile_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - params = {"type": "mobile_money_tanzania"} payload = json.dumps( { @@ -205,16 +171,11 @@ def initiate_tanzania_mobile_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) +@handle_api_exceptions def initiate_rwanda_mobile_charge( amount: int, email: str, tx_ref: str, phone_number: str, order_id: str ) -> dict: @@ -228,10 +189,7 @@ def initiate_rwanda_mobile_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - - params = {"type": "mobile_money_zambia"} + params = {"type": "mobile_money_rwanda"} payload = json.dumps( { "tx_ref": f"{tx_ref}", @@ -245,16 +203,11 @@ def initiate_rwanda_mobile_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) +@handle_api_exceptions def initiate_zambia_mobile_charge( amount: int, email: str, @@ -270,10 +223,7 @@ def initiate_zambia_mobile_charge( :return: dict """ - if token == "" or token is None: - raise TokenException(token=token, message="Authentication token absent") - - params = {"type": "mobile_money_tanzania"} + params = {"type": "mobile_money_zambia"} payload = json.dumps( { "tx_ref": f"{tx_ref}", @@ -286,16 +236,11 @@ def initiate_zambia_mobile_charge( headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} response = requests.post(url=base_url, headers=headers, data=payload, params=params) - if response.status_code == 401: - raise TokenException(token=token, message="Invalid token provided") - if response.status_code == 400: - raise Exception(f"{response.json()['message']}") - if response.status_code >= 400: - raise Exception(response.text) return dict(response.json()) +@handle_api_exceptions def initiate_ussd_charge( tx_ref: str, account_bank: str, @@ -304,6 +249,7 @@ def initiate_ussd_charge( phone_number: str, ) -> dict: """ + Collect USSD payments from customers in Nigeria :param tx_ref: str :param account_bank: str :param amount: int @@ -328,3 +274,158 @@ def initiate_ussd_charge( response = requests.post(base_url, headers=headers, data=payload, params=params) return dict(response.json()) + + +@handle_api_exceptions +def initiate_apple_pay_charge( + tx_ref: str, + amount: int, + email: str, + currency: str, +) -> dict: + """ + Accept payments from your customers with Apple Pay + :param tx_ref: str + :param amount: int + :param email: str + :param currency: str + :return: dict + """ + + params = {"type": "applepay"} + payload = json.dumps( + { + "tx_ref": f"{tx_ref}", + "amount": f"{amount}", + "currency": f"{currency}", + "email": f"{email}", + } + ) + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + response = requests.post(base_url, headers=headers, data=payload, params=params) + + return dict(response.json()) + + +@handle_api_exceptions +def initiate_google_pay_charge( + tx_ref: str, + amount: int, + email: str, + currency: str, +) -> dict: + """ + Accept payments from your customers with Google Pay + :param tx_ref: str + :param amount: int + :param email: str + :param currency: str + :return: dict + """ + + params = {"type": "googlepay"} + payload = json.dumps( + { + "tx_ref": f"{tx_ref}", + "amount": f"{amount}", + "currency": f"{currency}", + "email": f"{email}", + } + ) + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + response = requests.post(base_url, headers=headers, data=payload, params=params) + + return dict(response.json()) + + +@handle_api_exceptions +def initiate_enaira_charge( + tx_ref: str, + amount: int, + email: str, +) -> dict: + """ + Accept payment from eNaira wallets + :param tx_ref: str + :param amount: int + :param email: str + :return: dict + """ + + params = {"type": "enaira"} + payload = json.dumps( + { + "tx_ref": f"{tx_ref}", + "amount": f"{amount}", + "email": f"{email}", + } + ) + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + response = requests.post(base_url, headers=headers, data=payload, params=params) + + return dict(response.json()) + + +@handle_api_exceptions +def initiate_fawry_pay_charge( + tx_ref: str, + amount: int, + email: str, +) -> dict: + """ + Receive Fawry payments from customers in Egypt + :param tx_ref: str + :param amount: int + :param email: str + :return: dict + """ + + params = {"type": "fawry_pay"} + payload = json.dumps( + { + "tx_ref": f"{tx_ref}", + "amount": f"{amount}", + "currency": "EGP", + "email": f"{email}", + } + ) + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + response = requests.post(base_url, headers=headers, data=payload, params=params) + + return dict(response.json()) + + +@handle_api_exceptions +def initiate_paypal_charge( + tx_ref: str, + amount: int, + email: str, + currency: str, +) -> dict: + """ + Collect payments from customers with PayPal + :param tx_ref: str + :param amount: int + :param email: str + :param currency: str + :return: dict + """ + + params = {"type": "paypal"} + payload = json.dumps( + { + "tx_ref": f"{tx_ref}", + "amount": f"{amount}", + "currency": f"{currency}", + "email": f"{email}", + } + ) + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + response = requests.post(base_url, headers=headers, data=payload, params=params) + + return dict(response.json()) diff --git a/python_flutterwave/charge/validation.py b/python_flutterwave/charge/validation.py new file mode 100644 index 0000000..ebb30b0 --- /dev/null +++ b/python_flutterwave/charge/validation.py @@ -0,0 +1,30 @@ +import os +import requests +import json +from python_flutterwave.decorators import handle_api_exceptions + +token = os.environ.get("SECRET_KEY") + + +@handle_api_exceptions +def validate_charge(otp: str, flw_ref: str) -> dict: + """ + Collect Mpesa payments from customers in Kenya + :param flw_ref: str + :param otp: str + :return: dict + """ + + params = {"type": "mpesa"} + payload = json.dumps( + { + "flw_ref": f"{flw_ref}", + "otp": f"{otp}", + } + ) + headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"} + + base_url = "https://api.flutterwave.com/v3/validate-charge" + response = requests.post(url=base_url, headers=headers, data=payload, params=params) + + return dict(response.json()) diff --git a/python_flutterwave/decorators.py b/python_flutterwave/decorators.py new file mode 100644 index 0000000..867ebcf --- /dev/null +++ b/python_flutterwave/decorators.py @@ -0,0 +1,27 @@ +import os +import requests +from .exceptions import TokenException, FlutterwaveAPIException + + +token = os.environ.get("FW_SECRET_KEY") + +def require_token(func): + def wrapper(*args, **kwargs): + if token == "" or token is None: + raise TokenException(token=token, message="Authentication token absent") + return func(*args, **kwargs) + + return wrapper + + +def handle_api_exceptions(func): + @require_token + def wrapper(*args, **kwargs): + try: + return func(*args, **kwargs) + except requests.exceptions.RequestException as e: + raise FlutterwaveAPIException(f"Request Exception: {str(e)}") + except Exception as ex: + raise FlutterwaveAPIException(str(ex)) + + return wrapper diff --git a/python_flutterwave/exceptions.py b/python_flutterwave/exceptions.py index 344159d..f20f40f 100644 --- a/python_flutterwave/exceptions.py +++ b/python_flutterwave/exceptions.py @@ -1,24 +1,10 @@ -class TokenException(Exception): - def __init__(self, token, message): - self.token = token - self.message = message - - def __str__(self): - return f"{self.token} -> {self.message}" - +class FlutterwaveAPIException(Exception): + pass -class CustomerDetailException(Exception): - def __init__(self, message: str): - self.message = message - def __str__(self): - return self.message +class TokenException(FlutterwaveAPIException): + def __init__(self, token, message): + super().__init__(f"Token Error: {message}") + self.token = token -class TransactionDetailException(Exception): - def __init__(self, trans_id, message: str): - self.message = message - self.trans_id = trans_id - - def __str__(self): - return f"{self.trans_id} -> {self.message}" diff --git a/setup.cfg b/setup.cfg index 7d12c81..82a439e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = python-flutterwave -version = 0.9.11 +version = 1.0.0 author = William Otieno author_email = jimmywilliamotieno@gmail.com description = Python Wrapper for interacting with the Flutterwave Payments API