diff --git a/.github/workflows/Linter.yml b/.github/workflows/Linter.yml index 0de1852..2b38ed0 100644 --- a/.github/workflows/Linter.yml +++ b/.github/workflows/Linter.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10"] + python-version: ["3.8", "3.9", "3.10", "3.11"] steps: - uses: actions/checkout@v3 - name: Set up Python ${{ matrix.python-version }} @@ -21,9 +21,9 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install pylint + pip install ruff pip install -r requirements.txt - name: Analysing the code with pylint run: | - pylint chapa/* + ruff check diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 361d330..d212862 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -16,10 +16,10 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - name: Set up Python 3.9 + - name: Set up Python 3.10 uses: actions/setup-python@v1 with: - python-version: 3.9 + python-version: "3.10" - name: Version the package run: | diff --git a/README.md b/README.md index 036b33a..b927079 100644 --- a/README.md +++ b/README.md @@ -7,95 +7,165 @@ Unofficial Python SDK for [Chapa API](https://developer.chapa.co/docs). -## Instructions +## Introduction -This is a Python SDK for Chapa API. It is not official and is not supported by Chapa. It is provided as-is. Anyone can contribute to this project. +This document provides a comprehensive guide to integrating and using the Chapa Payment Gateway SDK in your application. Chapa is a powerful payment gateway that supports various payment methods, facilitating seamless transactions for businesses. This SDK simplifies interaction with Chapa’s API, enabling operations such as initiating payments, verifying transactions, and managing subaccounts. ## Installation -``` +To use the Chapa SDK in your project, you need to install it using pip, as it is a dependency for making HTTP requests it will also install `httpx` as a dependency. + +```bash pip install chapa ``` ## Usage +To begin using the SDK, import the `Chapa` class from the module and instantiate it with your secret key. + +### Initializing the SDK + ```python from chapa import Chapa -data = { - 'email': 'abebe@bikila.com', - 'amount': 1000, - 'first_name': 'Abebe', - 'last_name': 'Bikila', - 'tx_ref': '', - # optional - 'callback_url': 'https://www.your-site.com/callback', - 'customization': { - 'title': '', - 'description': 'Payment for your services', - } -} - -chapa = Chapa('') -response = chapa.initialize(**data) -print(response['data']['checkout_url']) - -# Another Implementation -chapa = Chapa('', response_format='obj') -response = chapa.initialize(**data) -# notice how the response is an object -print(response.data.checkout_url) - - -# How to verify a transaction -response = chapa.verify('') +# Replace 'your_secret_key' with your actual Chapa secret key +chapa = Chapa('your_secret_key') ``` -## Contributing +### Async Support -Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. After that free to contribute to this project. Please read the [CONTRIBUTING.md](https://github.com/chapimenge3/chapa/blob/main/CONTRIBUTING.md) file for more information. +The Chapa SDK implements async support using the `AsyncChapa` class. To use the async version of the SDK, import the `AsyncChapa` class from the module and instantiate it with your secret key. -Please make sure to update tests as appropriate. +```python +from chapa import AsyncChapa + +# Replace 'your_secret_key' with your actual Chapa secret key +chapa = AsyncChapa('your_secret') +``` + +All of the below methods are available in the async version of the SDK. So you can just use it as you would use the sync version. + +```python +response = await chapa.initialize( + ... +) +``` + +### Making Payments + +To initiate a payment, use the `initialize` method. This method requires a set of parameters like the customer's email, amount, first name, last name, and a transaction reference. + +```python +response = chapa.initialize( + email="customer@example.com", + amount=1000, + first_name="John", + last_name="Doe", + tx_ref="your_unique_transaction_reference", + callback_url="https://yourcallback.url/here" +) +print(response) +``` + +### Verifying Payments + +After initiating a payment, you can verify the transaction status using the `verify` method. + +```python +transaction_id = "your_transaction_id" +verification_response = chapa.verify(transaction_id) +print(verification_response) +``` + +### Creating Subaccounts + +You can create subaccounts for split payments using the `create_subaccount` method. -## API Reference +```python +subaccount_response = chapa.create_subaccount( + business_name="My Business", + account_name="My Business Account", + bank_code="12345", + account_number="0012345678", + split_value="0.2", + split_type="percentage" +) +print(subaccount_response) +``` -### Create new Transaction +### Bank Transfers -Base endpoint https://api.chapa.co/v1 +To initiate a bank transfer, use the `transfer_to_bank` method. -```http - POST /transaction/initialize +```python +transfer_response = chapa.transfer_to_bank( + account_name="Recipient Name", + account_number="0987654321", + amount="500", + reference="your_transfer_reference", + bank_code="67890", + currency="ETB" +) +print(transfer_response) ``` -| Parameter | Type | Description | -| :---------------------- | :-------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `key` | `string` | **Required**. This will be your public key from Chapa. When on test mode use the test key, and when on live mode use the live key. | -| `email` | `string` | **Required**. A customer’s email. address | -| `amount` | `integer` | **Required**. The amount you will be charging your customer. | -| `first_name` | `string` | **Required**. Your API key | -| `last_name` | `string` | **Required**. A customer’s last name. | -| `tx_ref` | `string` | **Required**. A unique reference given to each transaction. | -| `currency` | `string` | **Required**. The currency in which all the charges are made. Currency allowed is ETB. | -| `callback_url` | `string` | The URL to redirect the customer to after payment is done. | -| `customization[title]` | `string` | The customizations field (optional) allows you to customize the look and feel of the payment modal. You can set a logo, the store name to be displayed (title), and a description for the payment. | - -| HEADER Key | Value | -| :-------------- | :---------------------- | -| `Authorization` | `Bearer ` | - -### Verify Transaction - -```http - GET /transaction/verify/${tx_ref} +### Verifying Webhook + +The reason for verifying a webhook is to ensure that the request is coming from Chapa. You can verify a webhook using the `verify_webhook` method. + +```python +from chapa import verify_webhook + +# request is just an example of a request object +# request.body is the request body +# request.headers.get("Chapa-Signature") is the Chapa-Signature header + +verify_webhook( + secret_key="your_secret_key", + body=request.body, + chapa_signature=request.headers.get("Chapa-Signature") +) ``` -| Parameter | Type | Description | -| :-------- | :------- | :--------------------------------------------------------- | -| `tx_ref` | `string` | **Required**. A unique reference given to each transaction | +### Getting Testing Cards and Mobile Numbers + +For testing purposes, you can retrieve a set of test cards and mobile numbers. + +```python +from chapa import get_testing_cards, get_testing_mobile + +# Get a list of testing cards +test_cards = get_testing_cards() +print(test_cards) + +# Get a list of testing mobile numbers +test_mobiles = get_testing_mobile() +print(test_mobiles) +``` -## FAQ +### Get Webhook Events -#### No Available Questions! +You can get webhook events details with description like below + +```python +from chapa import WEBHOOK_EVENTS, WEBHOOKS_EVENT_DESCRIPTION + +# Get a list of webhook events +print(WEBHOOK_EVENTS) + +# Get a list of webhook events with description +print(WEBHOOKS_EVENT_DESCRIPTION) +``` + +## Conclusion + +The Chapa Payment Gateway SDK is a flexible tool that allows developers to integrate various payment functionalities into their applications easily. By following the steps outlined in this documentation, you can implement features like payment initialization, transaction verification, and sub-account management. Feel free to explore the SDK further to discover all the supported features and functionalities. + +## Contributing + +Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. After that free to contribute to this project. Please read the [CONTRIBUTING.md](https://github.com/chapimenge3/chapa/blob/main/CONTRIBUTING.md) file for more information. + +Please make sure to update tests as appropriate. ## Run Locally @@ -108,7 +178,7 @@ git clone https://github.com/chapimenge3/chapa.git Go to the project directory ```bash - cd chapa +cd chapa ``` Install dependencies diff --git a/chapa/__init__.py b/chapa/__init__.py index af7c00e..231011f 100644 --- a/chapa/__init__.py +++ b/chapa/__init__.py @@ -12,5 +12,15 @@ PyPI: https://pypi.org/project/Chapa/ """ -from .api import * -from .webhook import * +from .api import Chapa, AsyncChapa, get_testing_cards, get_testing_mobile +from .webhook import verify_webhook, WEBHOOK_EVENTS, WEBHOOKS_EVENT_DESCRIPTION + +__all__ = [ + 'Chapa', + 'AsyncChapa', + 'get_testing_cards', + 'get_testing_mobile', + 'verify_webhook', + 'WEBHOOK_EVENTS', + 'WEBHOOKS_EVENT_DESCRIPTION' +] diff --git a/chapa/api.py b/chapa/api.py index 9c9c00e..5d8b1ad 100644 --- a/chapa/api.py +++ b/chapa/api.py @@ -1,12 +1,21 @@ """ API SDK for Chapa Payment Gateway """ + # pylint: disable=too-few-public-methods # pylint: disable=too-many-branches # pylint: disable=too-many-arguments import re import json -import requests +from typing import Dict, Optional +import httpx + + +# TODO: Implement the following methods +# - Direct Charge +# - Initiate Payments +# - Authorize Payments +# - Encryption class Response: @@ -16,6 +25,22 @@ def __init__(self, dict1): self.__dict__.update(dict1) +def convert_response(response: dict) -> Response: + """ + Convert Response data to a Response object + + Args: + response (dict): The response data to convert + + Returns: + Response: The converted response + """ + if not isinstance(response, dict): + return response + + return json.loads(json.dumps(response), object_hook=Response) + + class Chapa: """ Simple SDK for Chapa Payment gateway @@ -37,6 +62,7 @@ def __init__( raise ValueError("response_format must be 'json' or 'obj'") self.headers = {"Authorization": f"Bearer {self._key}"} + self.client = httpx.Client() def send_request(self, url, method, data=None, params=None, headers=None): """ @@ -63,31 +89,16 @@ def send_request(self, url, method, data=None, params=None, headers=None): else: headers = self.headers - func = getattr(requests, method) + func = getattr(self.client, method) response = func(url, data=data, headers=headers) return getattr(response, "json", lambda: response.text)() - def convert_response(self, response): - """ - Convert Response data to a Response object - - Args: - response (dict): The response data to convert - - Returns: - Response: The converted response - """ - if not isinstance(response, dict): - return response - - return json.loads(json.dumps(response), object_hook=Response) - def _construct_request(self, *args, **kwargs): """Construct the request to send to the API""" res = self.send_request(*args, **kwargs) if self.response_format == "obj" and isinstance(res, dict): - return self.convert_response(res) + return convert_response(res) return res @@ -104,7 +115,8 @@ def initialize( return_url=None, customization=None, headers=None, - ): + **kwargs, + ) -> dict | Response: """ Initialize the Transaction @@ -137,6 +149,9 @@ def initialize( "currency": currency, } + if kwargs: + data.update(kwargs) + if not isinstance(amount, int): if str(amount).replace(".", "", 1).isdigit() and float(amount) > 0: pass @@ -178,7 +193,7 @@ def initialize( ) return response - def verify(self, transaction, headers=None): + def verify(self, transaction: str, headers=None) -> dict | Response: """Verify the transaction Args: @@ -195,8 +210,133 @@ def verify(self, transaction, headers=None): ) return response - def get_banks(self, headers=None): + def create_subaccount( + self, + business_name: str, + account_name: str, + bank_code: str, + account_number: str, + split_value: str, + split_type: str, + headers=None, + **kwargs, + ) -> dict | Response: + """ + Create a subaccount for split payment + + Args: + business_name (str): business name + account_name (str): account name + bank_code (str): bank code + account_number (str): account number + split_value (str): split value + split_type (str): split type + headers(dict, optional): header to attach on the request. Default to None + **kwargs: additional data to be sent to the server + + Return: + dict: response from the server + response(Response): response object of the response data return from the Chapa server. + """ + + data = { + "business_name": business_name, + "account_name": account_name, + "bank_code": bank_code, + "account_number": account_number, + "split_value": split_value, + "split_type": split_type, + } + + if kwargs: + data.update(kwargs) + + response = self._construct_request( + url=f"{self.base_url}/{self.api_version}/subaccount", + method="post", + data=data, + headers=headers, + ) + return response + + def initialize_split_payment( + self, + amount: int, + currency: str, + email: str, + first_name: str, + last_name: str, + tx_ref: str, + callback_url: str, + return_url: str, + subaccount_id: str, + headers=None, + **kwargs, + ) -> dict | Response: + """ + Initialize split payment transaction + + Args: + email (str): customer email + amount (int): amount to be paid + first_name (str): first name of the customer + last_name (str): last name of the customer + tx_ref (str): your transaction id + currency (str, optional): currency the transaction. Defaults to 'ETB'. + callback_url (str, optional): url for the customer to redirect after payment. + Defaults to None. + return_url (str, optional): url for the customer to redirect after payment. + Defaults to None. + subaccount_id (str, optional): subaccount id to split payment. + Defaults to None. + headers(dict, optional): header to attach on the request. Default to None + **kwargs: additional data to be sent to the server + + Return: + dict: response from the server + response(Response): response object of the response data return from the Chapa server. + """ + + data = { + "first_name": first_name, + "last_name": last_name, + "tx_ref": tx_ref, + "currency": currency, + "callback_url": callback_url, + "return_url": return_url, + "subaccount_id": subaccount_id, + } + + if kwargs: + data.update(kwargs) + + if not isinstance(amount, int): + if str(amount).replace(".", "", 1).isdigit() and float(amount) > 0: + pass + else: + raise ValueError("invalid amount") + elif isinstance(amount, int): + if amount < 0: + raise ValueError("invalid amount") + + data["amount"] = amount + + if not re.match(r"[^@]+@[^@]+\.[^@]+", email): + raise ValueError("invalid email") + + data["email"] = email + + response = self._construct_request( + url=f"{self.base_url}/{self.api_version}/transaction/initialize", + method="post", + data=data, + headers=headers, + ) + return response + + def get_banks(self, headers=None) -> dict | Response: """Get the list of all banks + Response: dict: response from the server response(Response): response object of the response data return from the Chapa server. @@ -208,31 +348,298 @@ def get_banks(self, headers=None): ) return response - def create_subaccount( + def transfer_to_bank( self, - business_name: str, + *, account_name: str, + account_number: str, + amount: str, + reference: str, + beneficiary_name: Optional[str], + bank_code: str, + currency: str = "ETB", + ) -> dict | Response: + """Initiate a Bank Transfer + + This section describes how to Initiate a transfer with Chapa + + Args: + account_name (str): This is the recipient Account Name matches on their bank account + account_number (str): This is the recipient Account Number. + amount (str): This the amount to be transferred to the recipient. + beneficiary_name (Optional[str]): This is the full name of the Transfer beneficiary (You may use it to match on your required). + currency (float): This is the currency for the Transfer. Expected value is ETB. Default value is ETB. + reference (str): This a merchant’s uniques reference for the transfer, it can be used to query for the status of the transfer + bank_code (str): This is the recipient bank code. You can see a list of all the available banks and their codes from the get banks endpoint. + + Returns: + dict: response from the server + response(Response): response object of the response data return from the Chapa server. + """ + data = { + "account_name": account_name, + "account_number": account_number, + "amount": amount, + "reference": reference, + "bank_code": bank_code, + "currency": currency, + } + if beneficiary_name: + data["beneficiary_name"] = beneficiary_name + + response = self._construct_request( + url=f"{self.base_url}/{self.api_version}/transfer", + method="post", + data=data, + ) + return response + + def verify_transfer(self, reference: str) -> dict | Response: + """Verify the status of a transfer + + This section describes how to verify the status of a transfer with Chapa + + Args: + reference (str): This a merchant’s uniques reference for the transfer, it can be used to query for the status of the transfer + + Returns: + dict: response from the server + - message: str + - status: str + - data: str | None + response(Response): response object of the response data return from the Chapa server. + """ + response = self._construct_request( + url=f"{self.base_url}/{self.api_version}/transfer/verify/{reference}", + method="get", + ) + return response + + +class AsyncChapa: + def __init__( + self, + secret: str, + base_ur: str = "https://api.chapa.co", + api_version: str = "v1", + response_format: str = "json", + ) -> None: + self._key = secret + self.base_url = base_ur + self.api_version = api_version + if response_format and response_format in ["json", "obj"]: + self.response_format = response_format + else: + raise ValueError("response_format must be 'json' or 'obj'") + + self.headers = {"Authorization": f"Bearer {self._key}"} + self.client = httpx.AsyncClient() + + async def send_request( + self, + url: str, + method: str, + data: Optional[Dict] = None, + params: Optional[Dict] = None, + headers: Optional[Dict] = None, + ): + """ + Request sender to the api + + Args: + url (str): url for the request to be sent. + method (str): the method for the request. + data (dict, optional): request body. Defaults to None. + + Returns: + response: response of the server. + """ + if params and not isinstance(params, dict): + raise ValueError("params must be a dict") + + if data and not isinstance(data, dict): + raise ValueError("data must be a dict") + + if headers and isinstance(data, dict): + headers.update(self.headers) + elif headers and not isinstance(data, dict): + raise ValueError("headers must be a dict") + else: + headers = self.headers + + async with self.client as client: + func = getattr(client, method) + response = await func(url, data=data, headers=headers) + return getattr(response, "json", lambda: response.text)() + + async def _construct_request(self, *args, **kwargs): + """Construct the request to send to the API""" + + res = await self.send_request(*args, **kwargs) + if self.response_format == "obj" and isinstance(res, dict): + return convert_response(res) + + return res + + async def initialize( + self, + *, + email: Optional[str] = None, + amount: float, + first_name: Optional[str] = None, + last_name: Optional[str] = None, + phone_number: Optional[str] = None, + tx_ref: str, + currency: str, + callback_url: Optional[str] = None, + return_url: Optional[str] = None, + customization: Optional[Dict] = None, + subaccount_id: Optional[str] = None, + **kwargs, + ): + """Initialize the Transaction and Get a payment link + + Once all the information needed to proceed with the transaction is retrieved, the action taken further would be to associate the following information into the javascript function(chosen language) which will innately display the checkout. + + + Args: + amount (float): A customer’s email. address + tx_ref (str): A unique reference given to each transaction. + currency (str): The currency in which all the charges are made. Currency allowed is ETB and USD. + email (Optional[str], optional): A customer’s email. address. Defaults to None. + first_name (Optional[str], optional): A customer’s first name. Defaults to None. + last_name (Optional[str], optional): A customer’s last name. Defaults to None. + phone_number (Optional[str], optional): The customer’s phone number. Defaults to None. + callback_url (Optional[str], optional): Function that runs when payment is successful. This should ideally be a script that uses the verify endpoint on the Chapa API to check the status of the transaction. Defaults to None. + return_url (Optional[str], optional): Web address to redirect the user after payment is successful. Defaults to None. + customization (Optional[Dict], optional): The customizations field (optional) allows you to customize the look and feel of the payment modal. You can set a logo, the store name to be displayed (title), and a description for the payment. Defaults to None. + subaccount_id (Optional[str], optional): The subaccount id to split payment. Defaults to None. + **kwargs: Additional data to be sent to the server. + + Returns: + dict: response from the server + - message: str + - status: str + - data: dict + - checkout_url: str + response(Response): response object of the response data return from the Chapa server. + + Raises: + ValueError: If the parameters are invalid. + """ + data = { + "first_name": first_name, + "last_name": last_name, + "tx_ref": tx_ref, + "currency": currency, + } + + if subaccount_id: + data["subaccount"] = {"id": subaccount_id} + + if kwargs: + data.update(kwargs) + + if not isinstance(amount, int): + if str(amount).replace(".", "", 1).isdigit() and float(amount) > 0: + pass + else: + raise ValueError("invalid amount") + elif isinstance(amount, int): + if amount < 0: + raise ValueError("invalid amount") + + data["amount"] = amount + + regex = re.compile( + r"([A-Za-z0-9]+[.-_])*[A-Za-z0-9]+@[A-Za-z0-9-]+(\.[A-Z|a-z]{2,})+" + ) + if not regex.match(email): + raise ValueError("invalid email") + + data["email"] = email + + if phone_number: + data["phone_number"] = phone_number + + if callback_url: + data["callback_url"] = callback_url + + if return_url: + data["return_url"] = return_url + + if customization: + if "title" in customization: + data["customization[title]"] = customization["title"] + if "description" in customization: + data["customization[description]"] = customization["description"] + if "logo" in customization: + data["customization[logo]"] = customization["logo"] + + response = await self._construct_request( + url=f"{self.base_url}/{self.api_version}/transaction/initialize", + method="post", + data=data, + ) + return response + + async def verify(self, tx_ref: str, headers: Optional[Dict] = None): + """Verify the transaction + + Args: + tx_ref (str): transaction id + + Returns: + dict: response from the server + response(Response): response object of the response data return from the Chapa server. + """ + response = await self._construct_request( + url=f"{self.base_url}/{self.api_version}/transaction/verify/{tx_ref}", + method="get", + headers=headers, + ) + return response + + async def create_subaccount( + self, bank_code: str, account_number: str, + business_name: str, + account_name: str, split_type: str, - split_value: float, - headers=None, + split_value: str, + headers: Optional[Dict] = None, + **kwargs, ): - """Create a subaccount + """ + Create a subaccount for split payment. + + **Note:** that sub-accounts are working with ETB currency as a default settlement. This means if we get subaccount in your payload regardless of the currency we will convert it to ETB and do the settlement. Args: - business_name (str): The vendor/merchant detail the subaccount for - account_name (str): The vendor/merchant account`s name matches from the bank account - bank_code (str): The bank id (you can get this from the get_banks method) - account_number (str): The bank account number for this subaccount - split_type (str): The type of split you want to use with this subaccount - (percentage or flat) - split_value (float): The amount you want to get as commission on each transaction + bank_code (str): The bank account details for this subaccount. The bank_code is the bank id (you can get this from the get banks endpoint). + account_number (str): The account_number is the bank account number. + business_name (str): The vendor/merchant detail the subaccount for. + account_name (str): The vendor/merchant account’s name matches from the bank account. + split_type (str): The type of split you want to use with this subaccount. + - Use flat if you want to get a flat fee from each transaction, while the subaccount gets the rest. + - Use percentage if you want to get a percentage of each transaction. + split_value (str): The amount you want to get as commission on each transaction. This goes with the split_type. + Example: + - to collect 3% from each transaction, split_type will be percentage and split_value will be 0.03. + - to collect 25 Birr from each transaction, split_type will be flat and split_value will be 25. + headers(dict, optional): header to attach on the request. Default to None + **kwargs: additional data to be sent to the server - Response: + Return: dict: response from the server + - message: str + - status: str + - data: dict + - subaccounts[id]": str response(Response): response object of the response data return from the Chapa server. """ + data = { "business_name": business_name, "account_name": account_name, @@ -241,10 +648,167 @@ def create_subaccount( "split_value": split_value, "split_type": split_type, } - response = self._construct_request( + + if kwargs: + data.update(kwargs) + + response = await self._construct_request( url=f"{self.base_url}/{self.api_version}/subaccount", method="post", data=data, headers=headers, ) return response + + async def get_banks(self, headers: Optional[Dict] = None): + """Get the list of all banks + + Returns: + dict: response from the server + response(Response): response object of the response data return from the Chapa server. + """ + response = await self._construct_request( + url=f"{self.base_url}/{self.api_version}/banks", + method="get", + headers=headers, + ) + return response + + async def transfer_to_bank( + self, + *, + account_name: str, + account_number: str, + amount: str, + reference: str, + beneficiary_name: Optional[str], + bank_code: str, + currency: str = "ETB", + ): + """Initiate a Bank Transfer + + This section describes how to Initiate a transfer with Chapa + + Args: + account_name (str): This is the recipient Account Name matches on their bank account + account_number (str): This is the recipient Account Number. + amount (str): This the amount to be transferred to the recipient. + beneficiary_name (Optional[str]): This is the full name of the Transfer beneficiary (You may use it to match on your required). + currency (float): This is the currency for the Transfer. Expected value is ETB. Default value is ETB. + reference (str): This a merchant’s uniques reference for the transfer, it can be used to query for the status of the transfer + bank_code (str): This is the recipient bank code. You can see a list of all the available banks and their codes from the get banks endpoint. + + Returns: + dict: response from the server + - message: str + - status: str + - data: str | None + response(Response): response object of the response data return from the Chapa server. + """ + data = { + "account_name": account_name, + "account_number": account_number, + "amount": amount, + "reference": reference, + "bank_code": bank_code, + "currency": currency, + } + if beneficiary_name: + data["beneficiary_name"] = beneficiary_name + + response = await self._construct_request( + url=f"{self.base_url}/{self.api_version}/transfer", + method="post", + data=data, + ) + return response + + async def verify_transfer(self, reference: str): + """Verify the status of a transfer + + This section describes how to verify the status of a transfer with Chapa + + Args: + reference (str): This a merchant’s uniques reference for the transfer, it can be used to query for the status of the transfer + + Returns: + dict: response from the server + - message: str + - status: str + - data: str | None + response(Response): response object of the response data return from the Chapa server. + """ + response = await self._construct_request( + url=f"{self.base_url}/{self.api_version}/transfer/verify/{reference}", + method="get", + ) + return response + + +def get_testing_cards(self): + """Get the list of all testing cards + + Returns: + List[dict]: all testing cards + """ + testing_cards = [ + { + "Brand": "Visa", + "Card Number": "4200 0000 0000 0000", + "CVV": "123", + "Expiry": "12/34", + }, + { + "Brand": "Amex", + "Card Number": "3700 0000 0000 0000", + "CVV": "1234", + "Expiry": "12/34", + }, + { + "Brand": "Mastercard", + "Card Number": "5400 0000 0000 0000", + "CVV": "123", + "Expiry": "12/34", + }, + { + "Brand": "Union Pay", + "Card Number": "6200 0000 0000 0000", + "CVV": "123", + "Expiry": "12/34", + }, + { + "Brand": "Diners", + "Card Number": "3800 0000 0000 0000", + "CVV": "123", + "Expiry": "12/34", + }, + ] + + return testing_cards + + +def get_testing_mobile(self): + """ + Get the list of all testing mobile numbers + + Returns: + List[dict]: all testing mobile numbers + """ + testing_mobile = [ + {"Bank": "Awash Bank", "Phone": "0900123456", "OTP": "12345"}, + {"Bank": "Awash Bank", "Phone": "0900112233", "OTP": "12345"}, + {"Bank": "Awash Bank", "Phone": "0900881111", "OTP": "12345"}, + {"Bank": "Amole", "Phone": "0900123456", "OTP": "12345"}, + {"Bank": "Amole", "Phone": "0900112233", "OTP": "12345"}, + {"Bank": "Amole", "Phone": "0900881111", "OTP": "12345"}, + {"Bank": "telebirr", "Phone": "0900123456", "OTP": "12345"}, + {"Bank": "telebirr", "Phone": "0900112233", "OTP": "12345"}, + {"Bank": "telebirr", "Phone": "0900881111", "OTP": "12345"}, + {"Bank": "CBEBirr", "Phone": "0900123456", "OTP": "12345"}, + {"Bank": "CBEBirr", "Phone": "0900112233", "OTP": "12345"}, + {"Bank": "CBEBirr", "Phone": "0900881111", "OTP": "12345"}, + {"Bank": "COOPPay-ebirr", "Phone": "0900123456", "OTP": "12345"}, + {"Bank": "COOPPay-ebirr", "Phone": "0900112233", "OTP": "12345"}, + {"Bank": "COOPPay-ebirr", "Phone": "0900881111", "OTP": "12345"}, + ] + return testing_mobile diff --git a/chapa/webhook.py b/chapa/webhook.py index 02d00bd..9e4291e 100644 --- a/chapa/webhook.py +++ b/chapa/webhook.py @@ -1,6 +1,11 @@ """ -Chapa Webhook API +Chapa Webhook Utilities Module """ +import hmac +import hashlib +import json + + WEBHOOKS_EVENT_DESCRIPTION = { 'charge.dispute.create': 'Dispute against company created.', 'charge.dispute.remind': 'Reminder of an unresolved dispute against company.', @@ -8,11 +13,11 @@ 'charge.success': 'Charged successfully.', 'customeridentification.failed': 'Customer identification failed.', 'customeridentification.success': 'Customer identified successfully.', - 'invoice.create': 'An invoice has been created for a customer\'s subscription.' - 'Usually sent 3 days before the subscription is due.', + 'invoice.create': 'An invoice has been created for a customer\'s subscription.' \ + 'Usually sent 3 days before the subscription is due.', 'invoice.payment_failed': 'Payment for invoice has failed.', - 'invoice.update': 'Customer\'s invoice has been updated. This invoice should' - 'be examined carfeully, and take necessary action.', + 'invoice.update': 'Customer\'s invoice has been updated. This invoice should' \ + 'be examined carfeully, and take necessary action.', 'paymentrequest.pending': 'Payment request has been sent to customer and payment is pending.', 'paymentrequest.success': 'Customer\'s payment is successful.', 'subscription.create': 'Subscription has been created.', @@ -25,4 +30,20 @@ 'issuingauthentication.created': 'An authorization has been created.', } -WEBHOOK_EVENT = WEBHOOKS_EVENT_DESCRIPTION.keys() +WEBHOOK_EVENTS = WEBHOOKS_EVENT_DESCRIPTION.keys() + + +def verify_webhook(secret_key: str, body: dict, chapa_signature: str) -> bool: + """ + Verify the webhook request + + Args: + secret_key (str): The secret key + body (dict): The request body + chapa_signature (str): The signature from the request headers + + Returns: + bool: True if the request is valid, False otherwise + """ + signature = hmac.new(secret_key.encode(), json.dumps(body).encode(), hashlib.sha256).hexdigest() + return signature == chapa_signature \ No newline at end of file diff --git a/docs.md b/docs.md new file mode 100644 index 0000000..6b20459 --- /dev/null +++ b/docs.md @@ -0,0 +1,103 @@ +# API SDK for Chapa Payment Gateway Documentation + +## Introduction + +This document provides a comprehensive guide to integrating and using the Chapa Payment Gateway SDK in your application. Chapa is a powerful payment gateway that supports various payment methods, facilitating seamless transactions for businesses. This SDK simplifies interaction with Chapa’s API, enabling operations such as initiating payments, verifying transactions, and managing subaccounts. + +## Installation + +To use the Chapa SDK in your project, you need to install it using pip, as it is a dependency for making HTTP requests it will also install `httpx` as a dependency. + +```bash +pip install chapa +``` + +## Usage + +To begin using the SDK, import the `Chapa` class from the module and instantiate it with your secret key. + +### Initializing the SDK + +```python +from chapa import Chapa + +# Replace 'your_secret_key' with your actual Chapa secret key +chapa = Chapa('your_secret_key') +``` + +### Making Payments + +To initiate a payment, use the `initialize` method. This method requires a set of parameters like the customer's email, amount, first name, last name, and a transaction reference. + +```python +response = chapa.initialize( + email="customer@example.com", + amount=1000, + first_name="John", + last_name="Doe", + tx_ref="your_unique_transaction_reference", + callback_url="https://yourcallback.url/here" +) +print(response) +``` + +### Verifying Payments + +After initiating a payment, you can verify the transaction status using the `verify` method. + +```python +transaction_id = "your_transaction_id" +verification_response = chapa.verify(transaction_id) +print(verification_response) +``` + +### Creating Subaccounts + +You can create subaccounts for split payments using the `create_subaccount` method. + +```python +subaccount_response = chapa.create_subaccount( + business_name="My Business", + account_name="My Business Account", + bank_code="12345", + account_number="0012345678", + split_value="0.2", + split_type="percentage" +) +print(subaccount_response) +``` + +### Bank Transfers + +To initiate a bank transfer, use the `transfer_to_bank` method. + +```python +transfer_response = chapa.transfer_to_bank( + account_name="Recipient Name", + account_number="0987654321", + amount="500", + reference="your_transfer_reference", + bank_code="67890", + currency="ETB" +) +print(transfer_response) +``` + +### Getting Testing Cards and Mobile Numbers + +For testing purposes, you can retrieve a set of test cards and mobile numbers. + +```python +# Get a list of testing cards +test_cards = chapa.get_testing_cards() +print(test_cards) + +# Get a list of testing mobile numbers +test_mobiles = chapa.get_testing_mobile() +print(test_mobiles) +``` + +## Conclusion + +The Chapa Payment Gateway SDK is a flexible tool that allows developers to integrate various payment functionalities into their applications easily. By following the steps outlined in this documentation, you can implement features like payment initialization, transaction verification, and subaccount management. Feel free to explore the SDK further to discover all the supported features and functionalities. +``` diff --git a/requirements.txt b/requirements.txt index 077c95d..0d769c7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1 @@ -requests==2.31.0 \ No newline at end of file +httpx>=0.27.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 569c5fe..815596c 100644 --- a/setup.py +++ b/setup.py @@ -1,15 +1,14 @@ -from ensurepip import version import setuptools import os with open('README.md', 'r', encoding='utf-8') as fh: long_description = fh.read() -version = os.environ.get('CHAPA_VERSION') +version_number = os.environ.get('CHAPA_VERSION', '0.1.0') setuptools.setup( name='chapa', - version=version, + version=version_number, author='Temkin Mengistu (Chapi)', author_email='chapimenge3@gmail.com', description='Python SDK for Chapa API https://developer.chapa.co', @@ -32,6 +31,6 @@ ], python_requires='>=3.6', install_requires=[ - 'requests', + 'httpx>=0.27.0', ], )