├── .github └── FUNDING.yml ├── .gitignore ├── Contributing.md ├── LICENSE.txt ├── MANIFEST ├── README.md ├── assets ├── banner.jpeg ├── banner.png ├── become_a_patron_button.png └── pypesa-banner.png ├── description.md ├── keys.json ├── pypesa ├── __init__.py ├── mpesa.py ├── mpesa_exceptions.py └── service_urls.py ├── requirements.txt ├── setup.py └── tests ├── __init__.py └── test_pypesa.py /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | patreon: kalebujordan 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | __pycache__/ 3 | dist/ 4 | build/ 5 | *.egg-info/ 6 | *.py[cod] -------------------------------------------------------------------------------- /Contributing.md: -------------------------------------------------------------------------------- 1 | ## Guide to Contributing to Pypesa Package 2 | # Contributing 3 | 4 | When contributing to this repository, please first discuss the change you wish to make via issue, 5 | email, or any other method with the owners of this repository before making a change. 6 | 7 | Please note we have a code of conduct, please follow it in all your interactions with the project. 8 | 9 | ## Pull Request Process 10 | 11 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 12 | build. 13 | 2. Update the README.md with details of changes to the interface, this includes new environment 14 | variables, exposed ports, useful file locations and container parameters. 15 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 16 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 17 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 18 | do not have permission to do that, you may request the second reviewer to merge it for you. 19 | 20 | ## Code of Conduct 21 | 22 | ### Our Pledge 23 | 24 | In the interest of fostering an open and welcoming environment, we as 25 | contributors and maintainers pledge to making participation in our project and 26 | our community a harassment-free experience for everyone, regardless of age, body 27 | size, disability, ethnicity, gender identity and expression, level of experience, 28 | nationality, personal appearance, race, religion, or sexual identity and 29 | orientation. 30 | 31 | ### Our Standards 32 | 33 | Examples of behavior that contributes to creating a positive environment 34 | include: 35 | 36 | * Using welcoming and inclusive language 37 | * Being respectful of differing viewpoints and experiences 38 | * Gracefully accepting constructive criticism 39 | * Focusing on what is best for the community 40 | * Showing empathy towards other community members 41 | 42 | Examples of unacceptable behavior by participants include: 43 | 44 | * The use of sexualized language or imagery and unwelcome sexual attention or 45 | advances 46 | * Trolling, insulting/derogatory comments, and personal or political attacks 47 | * Public or private harassment 48 | * Publishing others' private information, such as a physical or electronic 49 | address, without explicit permission 50 | * Other conduct which could reasonably be considered inappropriate in a 51 | professional setting 52 | 53 | ### Our Responsibilities 54 | 55 | Project maintainers are responsible for clarifying the standards of acceptable 56 | behavior and are expected to take appropriate and fair corrective action in 57 | response to any instances of unacceptable behavior. 58 | 59 | Project maintainers have the right and responsibility to remove, edit, or 60 | reject comments, commits, code, wiki edits, issues, and other contributions 61 | that are not aligned to this Code of Conduct, or to ban temporarily or 62 | permanently any contributor for other behaviors that they deem inappropriate, 63 | threatening, offensive, or harmful. 64 | 65 | ### Scope 66 | 67 | This Code of Conduct applies both within project spaces and in public spaces 68 | when an individual is representing the project or its community. Examples of 69 | representing a project or community include using an official project e-mail 70 | address, posting via an official social media account, or acting as an appointed 71 | representative at an online or offline event. Representation of a project may be 72 | further defined and clarified by project maintainers. 73 | 74 | ### Enforcement 75 | 76 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 77 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 78 | complaints will be reviewed and investigated and will result in a response that 79 | is deemed necessary and appropriate to the circumstances. The project team is 80 | obligated to maintain confidentiality with regard to the reporter of an incident. 81 | Further details of specific enforcement policies may be posted separately. 82 | 83 | Project maintainers who do not follow or enforce the Code of Conduct in good 84 | faith may face temporary or permanent repercussions as determined by other 85 | members of the project's leadership. 86 | 87 | ### Attribution 88 | 89 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 90 | available at [http://contributor-covenant.org/version/1/4][version] 91 | 92 | [homepage]: http://contributor-covenant.org 93 | [version]: http://contributor-covenant.org/version/1/4/ -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Kalebu Gwalugano 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST: -------------------------------------------------------------------------------- 1 | # file GENERATED by distutils, do NOT edit 2 | setup.py 3 | pypesa/__init__.py 4 | pypesa/mpesa_exceptions.py 5 | pypesa/service_urls.py 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # [pypesa](http://kalebu.github.io/pypesa) 4 | 5 | [![Pypesa banner](assets/pypesa-banner.png)](https://kalebu.github.io/pypesa/) 6 | [![PyPI version](https://badge.fury.io/py/python-pesa.svg)](https://badge.fury.io/py/python-pesa) 7 | [![Releases](https://badgen.net/github/releases/kalebu/pypesa)](https://github.com/Kalebu/pypesa) 8 | [![Downloads](https://pepy.tech/badge/python-pesa)](https://pepy.tech/project/python-pesa) 9 | [![Downloads](https://pepy.tech/badge/python-pesa/month)](https://pepy.tech/project/python-pesa) 10 | [![Downloads](https://pepy.tech/badge/python-pesa/week)](https://pepy.tech/project/python-pesa) 11 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 12 | 13 | Python wrapper on **Mpesa public API** for mobile Payment Integration made with care to offer easy and elegant integration made by [kalebu](https://github.com/kalebu) 14 | 15 | [![Become a patron](assets/become_a_patron_button.png)](https://www.patreon.com/kalebujordan) 16 | 17 | ## Implemented Features 18 | 19 | The following are the features that are supported by the **Mpesa** public API and require the **python** implementation. 20 | 21 | - [x] Customer to Bussiness (C2B) Single Payment 22 | - [x] Bussiness to Customer (B2C) 23 | - [x] Bussiness to Bussiness (B2B) 24 | - [x] Payment Reversal 25 | - [x] Query Transaction status 26 | - [x] Direct debit creation and Payment 27 | 28 | ## Documentation 29 | 30 | Full documentation can be found on [pypesa](http://kalebu.github.io/pypesa) 31 | 32 | ## Getting started 33 | 34 | Getting started with **pypesa** is pretty straight forward and can be categorized 35 | 36 | into steps shown below. 37 | 38 | - Sign up for Mpesa Developer portal 39 | 40 | - Install the [pypesa](http://kalebu.github.io/pypesa) package using **pip** 41 | 42 | - Build your services with **pypesa** 43 | 44 | ## Signing up 45 | 46 | To sign up for Mpesa public API visit [Mpesa-API](https://openapiportal.m-pesa.com/sign-up) and then 47 | 48 | you can go through [getting started Mpesa Developer portal](https://dev.to/alphaolomi/getting-started-with-mpesa-developer-portal-46a4) 49 | by [alphaolomi](https://github.com/alphaolomi) to see how. 50 | 51 | ## Installation 52 | 53 | To install the **pypesa** package to your machine you can either 54 | 55 | install directly from github or use pip to install it. 56 | 57 | - Using github 58 | 59 | ```bash 60 | $~ git clone https://github.com/Kalebu/pypesa 61 | $~ cd pypesa 62 | $ pypesa ~ python setup.py install 63 | ``` 64 | 65 | - Using pip 66 | 67 | ```bash 68 | 69 | pip install python-pesa 70 | 71 | ``` 72 | 73 | ## Usage 74 | 75 | To begin using the package is pretty straight forward 76 | 77 | 1. You need to have a json file named **keys.json** on your project directly 78 | and then fill put in it your public and api keys in json format shown below 79 | 80 | ```python 81 | 82 | { 83 | 'api_key': 'xxx', 84 | 'public_key': 'xxxxxxxxxxxxxx' 85 | } 86 | ``` 87 | 88 | 2.Once done you're ready to go, just make sure you have active internet connection 89 | 90 | ### Example of Usage (Customer to Bussiness Transaction) 91 | 92 | ```python 93 | >>> from pypesa import Mpesa 94 | >>> mpesa = Mpesa() 95 | >>> transaction_query = {"input_Amount": "10", 96 | "input_Country": "TZN", 97 | "input_Currency": "TZS", 98 | "input_CustomerMSISDN": "000000000001", 99 | "input_ServiceProviderCode": "000000", 100 | "input_ThirdPartyConversationID":'2edf7a0206d848f6b6fedea26accdc3a', 101 | "input_TransactionReference": 'T23434ZE5', 102 | "input_PurchasedItemsDesc": "Python Book" 103 | } 104 | >>> mpesa.customer_to_bussiness(transaction_query) 105 | 106 | Request processed successfully INS-0 107 | {'output_ResponseCode': 'INS-0', 'output_ResponseDesc': 'Request processed successfully', 108 | 'output_TransactionID': 'uGnPxFoXT2W0', 'output_ConversationID': '1d1e38495dc946729a8cffb136ab8391', 'output_ThirdPartyConversationID': '2edf7a0206d848f6b6fedea26accdc3a'} 109 | 110 | ``` 111 | 112 | ### Naming the authentication json 113 | 114 | If you named your authentication json in other name than **keys** you might to 115 | specify it while creating an instance for mpesa just as shown below; 116 | 117 | ```python 118 | >>> from pypesa import Mpesa 119 | >>> mpesa = Mpesa(auth_path = filename) 120 | ``` 121 | 122 | ## production environment 123 | 124 | The package run by default using sandbox environment, If you wanna use it to real production 125 | environment you can specify it while creating an instance as shown below 126 | 127 | ```python 128 | >>> from pypesa import Mpesa 129 | >>> mpesa = Mpesa(environment="production") 130 | ``` 131 | 132 | ## To do list 133 | 134 | - [x] Adding a well structured documentation 135 | - [ ] Adding a detailed test case to the implementation 136 | - [ ] Fixing rising bugs 137 | 138 | ## Contributing 139 | 140 | Wanna contribute to Pypesa ? then please [contributing.md](https://github.com/Kalebu/pypesa/blob/main/Contributing.md) to see how 141 | 142 | ## Give it a star 143 | 144 | If you found this repository useful, give it a star, You can also keep in touch with on [twitter](https://twitter.com/j_kalebu). 145 | 146 | ## Bug bounty? 147 | 148 | If you encounter **issue** with the usage of the package, feel free raise an **issue** so as 149 | we can fix it as soon as possible(ASAP) or just reach me directly through my email isaackeinstein(at)gmail.com 150 | 151 | ## Credit 152 | 153 | All the credits to 154 | 1. [kalebu](https://github.com/Kalebu/) 155 | 2. [SentielWarren](https://github.com/SentinelWarren) 156 | 3. and all the future contributors 157 | 158 | 159 | -------------------------------------------------------------------------------- /assets/banner.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neurotech-HQ/pypesa/3b787157b12ff1f176724e3d55fc08d59412a9fa/assets/banner.jpeg -------------------------------------------------------------------------------- /assets/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neurotech-HQ/pypesa/3b787157b12ff1f176724e3d55fc08d59412a9fa/assets/banner.png -------------------------------------------------------------------------------- /assets/become_a_patron_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neurotech-HQ/pypesa/3b787157b12ff1f176724e3d55fc08d59412a9fa/assets/become_a_patron_button.png -------------------------------------------------------------------------------- /assets/pypesa-banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neurotech-HQ/pypesa/3b787157b12ff1f176724e3d55fc08d59412a9fa/assets/pypesa-banner.png -------------------------------------------------------------------------------- /description.md: -------------------------------------------------------------------------------- 1 | # [pypesa](http://kalebu.github.io/pypesa) 2 | 3 | Python wrapper on **Mpesa public API** for mobile Payment Integration made with care to offer easy and elegant integration made by [kalebu](https://github.com/kalebu) 4 | 5 | ## Implemented Features 6 | 7 | The following are the features that are supported by the **Mpesa** public API and require the **python** implementation. 8 | 9 | - Customer to Bussiness (C2B) Single Payment 10 | - Bussiness to Customer (B2C) 11 | - Bussiness to Bussiness (B2B) 12 | - Payment Reversal 13 | - Query Transaction status 14 | - Direct debit creation and Payment 15 | 16 | ## Documentation 17 | 18 | Full documentation can be found on [pypesa](http://kalebu.github.io/pypesa) 19 | 20 | ## Getting started 21 | 22 | Getting started with **pypesa** is pretty straight forward and can be categorized 23 | 24 | into steps shown below. 25 | 26 | - Sign up for Mpesa Developer portal 27 | 28 | - Install the [pypesa](http://kalebu.github.io/pypesa) package using **pip** 29 | 30 | - Build your services with **pypesa** 31 | 32 | ## Signing up 33 | 34 | To sign up for Mpesa public API visit [Mpesa-API](https://openapiportal.m-pesa.com/sign-up) and then 35 | 36 | you can go through [getting started Mpesa Developer portal](https://dev.to/alphaolomi/getting-started-with-mpesa-developer-portal-46a4) 37 | by [alphaolomi](https://github.com/alphaolomi) to see how. 38 | 39 | ## Installation 40 | 41 | To install the **pypesa** package to your machine you can either 42 | 43 | install directly from github or use pip to install it. 44 | 45 | - Using github 46 | 47 | ```bash 48 | $~ git clone https://github.com/Kalebu/pypesa 49 | $~ cd pypesa 50 | $ pypesa ~ python setup.py install 51 | ``` 52 | 53 | - Using pip 54 | 55 | ```bash 56 | 57 | pip install python-pesa 58 | 59 | ``` 60 | 61 | ## Usage 62 | 63 | To begin using the package is pretty straight forward 64 | 65 | 1. You need to have a json file named **keys.json** on your project directly 66 | and then fill put in it your public and api keys in json format shown below 67 | 68 | ```python 69 | 70 | { 71 | 'api_key': 'xxx', 72 | 'public_key': 'xxxxxxxxxxxxxx' 73 | } 74 | ``` 75 | 76 | 2.Once done you're ready to go, just make sure you have active internet connection 77 | 78 | ### Example of Usage (Customer to Bussiness Transaction) 79 | 80 | ```python 81 | >>> from pypesa import Mpesa 82 | >>> mpesa = Mpesa() 83 | >>> transaction_query ={"input_Amount": "10", 84 | "input_Country": "TZN", 85 | "input_Currency": "TZS", 86 | "input_CustomerMSISDN": "000000000001", 87 | "input_ServiceProviderCode": "000000", 88 | "input_ThirdPartyConversationID":'2edf7a0206d848f6b6fedea26accdc3a', 89 | "input_TransactionReference": 'T23434ZE5', 90 | "input_PurchasedItemsDesc": "Python Book" 91 | } 92 | >>> mpesa.customer_to_bussiness(transaction_query) 93 | 94 | Request processed successfully INS-0 95 | {'output_ResponseCode': 'INS-0', 'output_ResponseDesc': 'Request processed successfully', 96 | 'output_TransactionID': 'uGnPxFoXT2W0', 'output_ConversationID': '1d1e38495dc946729a8cffb136ab8391', 'output_ThirdPartyConversationID': '2edf7a0206d848f6b6fedea26accdc3a'} 97 | 98 | ``` 99 | 100 | ### Naming the authentication json 101 | 102 | If you named your authentication json in other name than **keys** you might to 103 | specify it while creating an instance for mpesa just as shown below; 104 | 105 | ```python 106 | >>> from pypesa import Mpesa 107 | >>> mpesa = Mpesa(auth_path = filename) 108 | ``` 109 | 110 | ## production environment 111 | 112 | The package run by default using sandbox environment, If you wanna use it to real production 113 | environment you can specify it while creating an instance as shown below 114 | 115 | ```python 116 | >>> from pypesa import Mpesa 117 | >>> mpesa = Mpesa(environment="production") 118 | ``` 119 | 120 | ## Contributing 121 | 122 | Wanna contribute to Pypesa ? then please [contributing.md](https://github.com/Kalebu/pypesa/blob/main/Contributing.md) to see how. 123 | 124 | ## Bug bounty? 125 | 126 | If you encounter **issue** with the usage of the package, feel free raise an **issue** so as we can fix it as soon as possible(ASAP) or just reach me directly through my email isaackeinstein(at)gmail.com. 127 | 128 | ## Credit 129 | 130 | All the credits to [kalebu](https://github.com/Kalebu/) and all the future contributors. 131 | -------------------------------------------------------------------------------- /keys.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "api_key" : "tdWtOTaKSCVGDCoqKM4zqPgnM70IyVe6", 4 | "public_key" : "MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEArv9yxA69XQKBo24BaF/D+fvlqmGdYjqLQ5WtNBb5tquqGvAvG3WMFETVUSow/LizQalxj2ElMVrUmzu5mGGkxK08bWEXF7a1DEvtVJs6nppIlFJc2SnrU14AOrIrB28ogm58JjAl5BOQawOXD5dfSk7MaAA82pVHoIqEu0FxA8BOKU+RGTihRU+ptw1j4bsAJYiPbSX6i71gfPvwHPYamM0bfI4CmlsUUR3KvCG24rB6FNPcRBhM3jDuv8ae2kC33w9hEq8qNB55uw51vK7hyXoAa+U7IqP1y6nBdlN25gkxEA8yrsl1678cspeXr+3ciRyqoRgj9RD/ONbJhhxFvt1cLBh+qwK2eqISfBb06eRnNeC71oBokDm3zyCnkOtMDGl7IvnMfZfEPFCfg5QgJVk1msPpRvQxmEsrX9MQRyFVzgy2CWNIb7c+jPapyrNwoUbANlN8adU1m6yOuoX7F49x+OjiG2se0EJ6nafeKUXw/+hiJZvELUYgzKUtMAZVTNZfT8jjb58j8GVtuS+6TM2AutbejaCV84ZK58E2CRJqhmjQibEUO6KPdD7oTlEkFy52Y1uOOBXgYpqMzufNPmfdqqqSM4dU70PO8ogyKGiLAIxCetMjjm6FCMEA3Kc8K0Ig7/XtFm9By6VxTJK1Mg36TlHaZKP6VzVLXMtesJECAwEAAQ==" 5 | } -------------------------------------------------------------------------------- /pypesa/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pypesa: A Python wrapper on Mpesa public API for mobile payments Integration. 3 | ============================================================================= 4 | 5 | Documentation is available in the docstrings 6 | and online at http://kalebu.github.io/pypesa 7 | 8 | Contents 9 | -------- 10 | pypesa contents goes here! 11 | 12 | Subpackages 13 | ----------- 14 | Using any of the pypesa subpackages requires either 15 | an explicit or implicit import. For example, 16 | ``import pypesa`` | ``import pypesa.mpesa`` | ``from pypesa import Mpesa``. 17 | 18 | :: 19 | 20 | mpesa --- Mpesa class 21 | mpesa_exceptions --- Exception classes 22 | service_urls --- Services urls 23 | 24 | 25 | Utility tools 26 | ------------- 27 | :: 28 | 29 | tests --- Run pypesa unittests 30 | 31 | Changelog 32 | --------- 33 | **Version 0.4** - 2021 April 30 34 | Improved auth decorator with wraps 35 | Added example in methods docstring to ease the usage 36 | 37 | **Version 0.3** - 2021 February 14 38 | Fixed Query transaction status bug 39 | Improved docstring documentation 40 | Added json response to debit creation and payments 41 | 42 | **Version 0.2** - 2021 February 13 43 | Fixed bug | added doc string | changed import format 44 | 45 | **Version 0.1** - 2021 February 12 46 | Initial release. 47 | 48 | """ 49 | 50 | __version__ = '0.4' 51 | __all__ = ['Mpesa',] 52 | 53 | from pypesa.mpesa import Mpesa 54 | -------------------------------------------------------------------------------- /pypesa/mpesa.py: -------------------------------------------------------------------------------- 1 | """Python package to easy integration with Vodacom's Mpesa Public API. 2 | """ 3 | 4 | import os 5 | import json 6 | import base64 7 | import requests 8 | 9 | from pathlib import Path 10 | from functools import wraps 11 | from typing import Dict, Set, Optional, Union 12 | from Crypto.PublicKey import RSA 13 | from Crypto.Cipher import PKCS1_v1_5 as rsa_cipher 14 | 15 | from pypesa.service_urls import Sandbox, Production 16 | from pypesa.mpesa_exceptions import AuthenticationError, LoadingKeyError, MpesaConnectionError 17 | 18 | 19 | class Mpesa: 20 | """Mpesa API client. 21 | """ 22 | 23 | __slots__ = [ 24 | "auth_path", 25 | "auth_keys", 26 | "_encrypted_api_key", 27 | "_origin_ip", 28 | "urls", 29 | ] 30 | 31 | def __init__( 32 | self, 33 | auth_path: Optional[str] = "keys.json", 34 | environment: Optional[str] = "sandbox", 35 | ) -> None: 36 | self.auth_path = auth_path 37 | self.auth_keys: Dict = dict() 38 | self._encrypted_api_key = None 39 | self._origin_ip = "*" 40 | self.urls = Production() if environment == "production" else Sandbox() 41 | print(self.urls) 42 | 43 | @property 44 | def authenticate(self) -> bool: 45 | """Property to check If the user is has has included auth keys 46 | either manually or through auth json file(keys.json). 47 | 48 | >> import pypesa 49 | >> mpesa = pypesa() 50 | >> mpesa.authenticate 51 | 52 | Return True if the environment has auth keys else 53 | False if auth are not included. 54 | """ 55 | 56 | if self.auth_keys.get("public_key") and self.auth_keys.get("api_key"): 57 | self._encrypted_api_key = self.__generate_encrypted_key() 58 | return True 59 | 60 | elif os.path.isfile(self.auth_path): 61 | # print("loading from file") 62 | self.auth_keys = self.load_keys(self.auth_path) 63 | if self.auth_keys: 64 | self._encrypted_api_key = self.__generate_encrypted_key() 65 | return True 66 | return False 67 | 68 | else: 69 | return False 70 | 71 | def authenticated(method): 72 | @wraps(method) 73 | def authorized_method(self, *args, **kwargs): 74 | if self.authenticate: 75 | return method(self, *args, **kwargs) 76 | else: 77 | raise AuthenticationError 78 | 79 | return authorized_method 80 | 81 | @staticmethod 82 | def load_keys(keys_filename: Union[str, Path]) -> Dict: 83 | """Pypesa internal method to load the auth file 84 | >> import pypesa 85 | >> mpesa = pypesa() 86 | >> pypesa.load_keys() 87 | 88 | Return keys Dict if auth file is present else, 89 | Raise FileNotFoundError if auth file is absent. 90 | """ 91 | try: 92 | 93 | with open(keys_filename, "r") as auth: 94 | return json.load(auth) 95 | 96 | except FileNotFoundError: 97 | raise FileNotFoundError( 98 | f"{keys_filename} is not found on the current directory" 99 | ) 100 | 101 | except Exception as bug: 102 | print(bug) 103 | raise LoadingKeyError 104 | 105 | def __generate_encrypted_key(self, session: Optional[bool] = False) -> str: 106 | """Method use to Encrypt the api key and public key 107 | so as to get a secure RSA Encrypted key. 108 | """ 109 | try: 110 | pub_key = self.auth_keys.get("public_key") 111 | raw_key = self.auth_keys.get("api_key") 112 | 113 | if session: 114 | raw_key = self.get_session_id() 115 | 116 | public_key_string = base64.b64decode(pub_key) 117 | rsa_public_key = RSA.importKey(public_key_string) 118 | raw_cipher = rsa_cipher.new(rsa_public_key) 119 | encrypted_key = raw_cipher.encrypt(raw_key.encode()) 120 | return base64.b64encode(encrypted_key).decode("utf-8") 121 | 122 | except Exception as bug: 123 | print(bug) 124 | """raise AuthenticationError( 125 | "Exceptions thrown while generating encrypted key\nPlease make sure you have the right public key" 126 | )""" 127 | 128 | @property 129 | def path_to_auth(self) -> str: 130 | """ 131 | Return Path to Authentication file 132 | """ 133 | return self.auth_path 134 | 135 | @path_to_auth.setter 136 | def path_to_auth(self, auth_path: Union[str, Path]) -> str: 137 | """Setting new path to the authentication file. 138 | """ 139 | if isinstance(auth_path, str): 140 | self.auth_path = auth_path 141 | return self.auth_path 142 | raise TypeError( 143 | f"Path to auth file must of type Path or String not {type(auth_path)}" 144 | ) 145 | 146 | @property 147 | def environment(self) -> Union[Sandbox, Production]: 148 | """Return the Environment the Pypesa is running 149 | whether its Sandbox | Production. 150 | """ 151 | return self.urls 152 | 153 | @environment.setter 154 | def environment(self, env: str) -> Union[Sandbox, Production]: 155 | """Set new pypesa environment 156 | 157 | Eg. changing env to production; 158 | 159 | >> import pypesa 160 | >> mpesa = pypesa() 161 | >> mpesa.environment = "production" 162 | 163 | """ 164 | if isinstance(env): 165 | if env in ["Sandbox", "sandbox", "Production", "production"]: 166 | if env == "Sandbox" or "sandbox": 167 | self.urls = Sandbox() 168 | elif env == "Production" or "production": 169 | self.urls = Production() 170 | print(self.urls) 171 | return self.urls 172 | raise ValueError( 173 | "Environment must be either sandbox or production") 174 | raise TypeError( 175 | f"environment must be of type string not {type(env)}") 176 | 177 | @property 178 | def api_key(self) -> str: 179 | """Return current api key. 180 | """ 181 | return self.auth_keys.get("api_key") 182 | 183 | @api_key.setter 184 | def api_key(self, Api_key: str) -> str: 185 | """Use this property to explicit set a api_key 186 | 187 | >> import pypesa 188 | >> wallet = pypesa() 189 | >> wallet.api_key = " Your api key" #here 190 | """ 191 | if isinstance(Api_key, str): 192 | self.auth_keys["api_key"] = Api_key 193 | return self.auth_keys["api_key"] 194 | raise TypeError( 195 | f"API key must be a of type String not {type(Api_key)}") 196 | 197 | @property 198 | def public_key(self) -> str: 199 | """Return the current Public key. 200 | """ 201 | return self.auth_keys.get("public_key") 202 | 203 | @public_key.setter 204 | def public_key(self, pb_key: str) -> str: 205 | """Set a new public key. 206 | """ 207 | if isinstance(pb_key, str): 208 | self.auth_keys["public_key"] = pb_key 209 | return self.auth_keys["public_key"] 210 | raise TypeError(f"Public key must be a string not a {type(pb_key)}") 211 | 212 | @property 213 | def origin_address(self) -> str: 214 | """Return the current origin address. 215 | """ 216 | return self._origin_ip 217 | 218 | @origin_address.setter 219 | def origin_address(self, ip_address: str) -> str: 220 | """Set a new origin address. 221 | """ 222 | if isinstance(ip_address, str): 223 | self._origin_ip = ip_address 224 | return self._origin_ip 225 | raise TypeError( 226 | f"Address must be of type string not {type(ip_address)}") 227 | 228 | @authenticated 229 | def default_headers(self, auth_key: Optional[str] = "") -> Dict: 230 | """Generate Default header to be used during a Request. 231 | """ 232 | if not auth_key: 233 | auth_key = self.__generate_encrypted_key(session=True) 234 | return { 235 | "Content-Type": "application/json", 236 | "Authorization": "Bearer {}".format(auth_key), 237 | "Host": "openapi.m-pesa.com", 238 | "Origin": self.origin_address, 239 | } 240 | 241 | @authenticated 242 | def get_session_id(self) -> str: 243 | try: 244 | headers = self.default_headers(auth_key=self._encrypted_api_key) 245 | response = requests.get(self.urls.session_id, headers=headers) 246 | response = response.json() 247 | session_id = response["output_SessionID"] 248 | response_code = response["output_ResponseCode"] 249 | description = response["output_ResponseDesc"] 250 | # print(description, " ", response_code) 251 | if response_code == "INS-989": 252 | # print("Session creation failed!!") 253 | raise AuthenticationError 254 | return session_id 255 | except Exception as bug: 256 | print(bug) 257 | raise AuthenticationError 258 | 259 | @staticmethod 260 | def verify_query(transaction_query: Dict, required_fields: Set) -> bool: 261 | """Raise KeyError if transaction query has a missing key. 262 | """ 263 | query_keys = set(transaction_query.keys()) 264 | missing_keys = required_fields.difference(query_keys) 265 | if missing_keys: 266 | raise KeyError( 267 | "These keys {} are missing in your transaction query".format( 268 | missing_keys 269 | ) 270 | ) 271 | return True 272 | 273 | @authenticated 274 | def customer_to_bussiness(self, transaction_query: Dict) -> Dict: 275 | """Customer to bussiness method. 276 | 277 | Example; 278 | >> import pypesa 279 | >> mpesa = pypesa() 280 | >> transaction_query = { 281 | "input_Amount": "10", 282 | "input_Country": "TZN", 283 | "input_Currency": "TZS", 284 | "input_CustomerMSISDN": "000000000001", 285 | "input_ServiceProviderCode": "000000", 286 | "input_ThirdPartyConversationID":'2edf7a0206d848f6b6fedea26accdc3a', 287 | "input_TransactionReference": 'T23434ZE5', 288 | "input_PurchasedItemsDesc": "Python Book" 289 | } 290 | >> mpesa.customer_to_bussiness(transaction_query) 291 | """ 292 | 293 | self.verify_query(transaction_query, 294 | self.urls.re_customer_to_bussiness) 295 | 296 | try: 297 | return requests.post( 298 | self.urls.single_stage_c2b, 299 | json=transaction_query, 300 | headers=self.default_headers(), 301 | verify=True, 302 | ).json() 303 | 304 | except (requests.ConnectTimeout, requests.ConnectionError): 305 | raise MpesaConnectionError 306 | 307 | @authenticated 308 | def bussiness_to_customer(self, transaction_query: Dict) -> Dict: 309 | """Bussiness to customer method. 310 | 311 | Example; 312 | >> import pypesa 313 | >> mpesa = pypesa() 314 | >> transaction_query = { 315 | 'input_Amount': 250, 316 | 'input_Country': 'TZN', 317 | 'input_Currency': 'TZS', 318 | 'input_CustomerMSISDN': '000000000001', 319 | 'input_ServiceProviderCode': '000000', 320 | 'input_ThirdPartyConversationID':'f5e420e99594a9c496d8600', 321 | 'input_TransactionReference': 'T12345C', 322 | 'input_PaymentItemsDesc': 'Donation', 323 | } 324 | 325 | >> mpesa.bussiness_to_customer(transaction_query) 326 | """ 327 | 328 | self.verify_query(transaction_query, 329 | self.urls.re_bussiness_to_customer) 330 | 331 | try: 332 | 333 | return requests.post( 334 | self.urls.single_stage_b2c, 335 | json=transaction_query, 336 | headers=self.default_headers(), 337 | verify=True, 338 | ).json() 339 | 340 | except (requests.ConnectTimeout, requests.ConnectionError): 341 | raise MpesaConnectionError 342 | 343 | @authenticated 344 | def bussiness_to_bussiness(self, transaction_query: Dict) -> Dict: 345 | """Bussiness to bussiness method 346 | 347 | Example; 348 | >> import pypesa 349 | >> mpesa = pypesa() 350 | >> transaction_query = { 351 | 'input_Amount': 10, 352 | 'input_Country': 'TZN', 353 | 'input_Currency': 'TZS', 354 | 'input_PrimaryPartyCode':'000000', 355 | 'input_ReceiverPartyCode':'000001', 356 | 'input_ServiceProviderCode': '000000', 357 | 'input_ThirdPartyConversationID': '8a89835c71f15e99396', 358 | 'input_TransactionReference': 'T1234C', 359 | 'input_PurchasedItemsDesc': 'Shoes', 360 | } 361 | >> mpesa.bussiness_to_bussiness(transaction_query) 362 | """ 363 | 364 | self.verify_query(transaction_query, 365 | self.urls.re_bussiness_to_bussiness) 366 | 367 | try: 368 | return requests.post( 369 | self.urls.single_stage_b2b, 370 | json=transaction_query, 371 | headers=self.default_headers(), 372 | verify=True, 373 | ).json() 374 | 375 | except (requests.ConnectTimeout, requests.ConnectionError): 376 | raise MpesaConnectionError 377 | 378 | @authenticated 379 | def payment_reversal(self, transaction_query: Dict) -> Dict: 380 | """Payment reversal method. 381 | 382 | Example; 383 | >> import pypesa 384 | >> mpesa = pypesa() 385 | >> transaction_query = { 386 | 'input_ReversalAmount':10, 387 | 'input_Country': 'TZN', 388 | 'input_ServiceProviderCode': '000000', 389 | 'input_ThirdPartyConversationID':'asvf7ba228d83d0d689761', 390 | 'input_TransactionID':'4iUThBRRWXMG' 391 | } 392 | >> mpesa.payment_reversal(transaction_query) 393 | """ 394 | 395 | self.verify_query(transaction_query, self.urls.re_payment_reversal) 396 | 397 | try: 398 | return requests.put( 399 | self.urls.payment_reversal, 400 | json=transaction_query, 401 | headers=self.default_headers(), 402 | verify=True, 403 | ).json() 404 | 405 | except (requests.ConnectTimeout, requests.ConnectionError): 406 | raise MpesaConnectionError 407 | 408 | @authenticated 409 | def query_transaction_status(self, transaction_query: Dict) -> Dict: 410 | """Query transaction status method. 411 | 412 | Example; 413 | >> import pypesa 414 | >> mpesa = pypesa() 415 | >> transaction_query = { 416 | 'input_QueryReference': '000000000000000000001', 417 | 'input_ServiceProviderCode': '000000', 418 | 'input_ThirdPartyConversationID': 'asv02e5958774f7ba228d83d0d689761', 419 | 'input_Country': 'TZN', 420 | } 421 | >> mpesa.query_transaction_status(transaction_query) 422 | """ 423 | 424 | self.verify_query(transaction_query, self.urls.re_transaction_status) 425 | 426 | try: 427 | return requests.get( 428 | self.urls.transaction_status, 429 | json=transaction_query, 430 | headers=self.default_headers(), 431 | verify=True, 432 | ).json() 433 | 434 | except (requests.ConnectTimeout, requests.ConnectionError): 435 | raise MpesaConnectionError 436 | 437 | @authenticated 438 | def create_direct_debit(self, transaction_query: Dict) -> Dict: 439 | """Create direct Debit method. 440 | 441 | Example; 442 | >> import pypesa 443 | >> mpesa = pypesa() 444 | >> transaction_query = { 445 | "input_AgreedTC": "1", 446 | "input_Country": "TZN", 447 | "input_CustomerMSISDN": "000000000001", 448 | "input_EndRangeOfDays": "22", 449 | "input_ExpiryDate": "20211126", 450 | "input_FirstPaymentDate": "20160324", 451 | "input_Frequency": "06", 452 | "input_ServiceProviderCode": "000000", 453 | "input_StartRangeOfDays": "01", 454 | "input_ThirdPartyConversationID": "5334a912jbsj1j2kk1", 455 | "input_ThirdPartyReference": "3333", 456 | } 457 | >> mpesa.create_direct_debit(transaction_query) 458 | """ 459 | 460 | self.verify_query(transaction_query, self.urls.re_create_direct_debit) 461 | 462 | try: 463 | return requests.post( 464 | self.urls.direct_debit, 465 | json=transaction_query, 466 | headers=self.default_headers(), 467 | verify=True, 468 | ).json() 469 | 470 | except (requests.ConnectTimeout, requests.ConnectionError): 471 | raise MpesaConnectionError 472 | 473 | @authenticated 474 | def direct_debit_payment(self, transaction_query: Dict) -> Dict: 475 | """Direct debit payment method. 476 | 477 | Example; 478 | >> import pypesa 479 | >> mpesa = pypesa() 480 | >> transaction_query = { 481 | "input_Amount": "10", 482 | "input_Country": "TZN", 483 | "input_Currency": "TZS", 484 | "input_CustomerMSISDN": "000000000001", 485 | "input_ServiceProviderCode": "000000", 486 | "input_ThirdPartyConversationID": "v2de053v4912jbasdj1j2kk", 487 | "input_ThirdPartyReference": "5db410b459bd433ca8e5" 488 | } 489 | >> mpesa.direct_debit_payment(transaction_query) 490 | """ 491 | self.verify_query(transaction_query, self.urls.re_direct_debit_payment) 492 | 493 | try: 494 | return requests.post( 495 | self.urls.direct_debit_payment, 496 | json=transaction_query, 497 | headers=self.default_headers(), 498 | verify=True, 499 | ).json() 500 | except (requests.ConnectTimeout, requests.ConnectionError): 501 | raise MpesaConnectionError 502 | 503 | #sys.modules[__name__] = Mpesa -------------------------------------------------------------------------------- /pypesa/mpesa_exceptions.py: -------------------------------------------------------------------------------- 1 | key_format = {"api_key": "xxxxxxxxx", "public_key": "xxxxxxx"} 2 | 3 | 4 | class AuthenticationError(Exception): 5 | """Exception class that throws exception for invalid keys. 6 | 7 | Please try entering again carefully. 8 | """ 9 | 10 | error_message = f""" 11 | Could not verify your authentication keys 12 | 13 | keys.json should be formatted as shown below 14 | 15 | {key_format} 16 | """ 17 | 18 | def __init__(self, error_message=error_message): 19 | super().__init__(error_message) 20 | 21 | 22 | class LoadingKeyError(Exception): 23 | """Exception thrown while loading authentication keys. 24 | 25 | """ 26 | 27 | error_message = f""" 28 | 29 | Exception thrown while loading authentication keys from file 30 | 31 | Please make sure you format correctly in json format as shown below 32 | 33 | {key_format} 34 | """ 35 | 36 | def __init__(self, error_message=error_message): 37 | super().__init__(error_message) 38 | 39 | 40 | class MpesaConnectionError(Exception): 41 | """This is exception will be thrown where there is no or slow internet connection 42 | """ 43 | 44 | error_message = f""" 45 | 46 | Transaction couldn\'t be processed due to connection timeout 47 | 48 | Please make sure you have a stable internet Connection 49 | """ 50 | 51 | def __init__(self, error_message=error_message): 52 | super().__init__(error_message) 53 | -------------------------------------------------------------------------------- /pypesa/service_urls.py: -------------------------------------------------------------------------------- 1 | class Required: 2 | """A class holding dictionaries of required fields 3 | for variety of transaction supported by pypesa. 4 | """ 5 | re_customer_to_bussiness = { 6 | "input_Amount", 7 | "input_Country", 8 | "input_Currency", 9 | "input_CustomerMSISDN", 10 | "input_ServiceProviderCode", 11 | "input_ThirdPartyConversationID", 12 | "input_TransactionReference", 13 | "input_PurchasedItemsDesc", 14 | } 15 | 16 | re_bussiness_to_customer = { 17 | "input_Amount", 18 | "input_Country", 19 | "input_Currency", 20 | "input_CustomerMSISDN", 21 | "input_ServiceProviderCode", 22 | "input_ThirdPartyConversationID", 23 | "input_TransactionReference", 24 | "input_PaymentItemsDesc", 25 | } 26 | 27 | re_bussiness_to_bussiness = { 28 | "input_Amount", 29 | "input_Country", 30 | "input_Currency", 31 | "input_PrimaryPartyCode", 32 | "input_ReceiverPartyCode", 33 | "input_ThirdPartyConversationID", 34 | "input_TransactionReference", 35 | "input_PurchasedItemsDesc", 36 | } 37 | 38 | re_payment_reversal = { 39 | "input_Country", 40 | "input_ReversalAmount", 41 | "input_ServiceProviderCode", 42 | "input_ThirdPartyConversationID", 43 | "input_TransactionID", 44 | } 45 | 46 | re_transaction_status = { 47 | "input_Country", 48 | "input_QueryReference", 49 | "input_ServiceProviderCode", 50 | "input_ThirdPartyConversationID", 51 | } 52 | 53 | re_create_direct_debit = { 54 | "input_AgreedTC", 55 | "input_Country", 56 | "input_CustomerMSISDN", 57 | "input_EndRangeOfDays", 58 | "input_ExpiryDate", 59 | "input_FirstPaymentDate", 60 | "input_Frequency", 61 | "input_ServiceProviderCode", 62 | "input_StartRangeOfDays", 63 | "input_ThirdPartyConversationID", 64 | "input_ThirdPartyReference", 65 | } 66 | 67 | re_direct_debit_payment = { 68 | "input_Amount", 69 | "input_Country", 70 | "input_Currency", 71 | "input_CustomerMSISDN", 72 | "input_ServiceProviderCode", 73 | "input_ThirdPartyConversationID", 74 | "input_ThirdPartyReference", 75 | } 76 | 77 | 78 | class Sandbox(Required): 79 | """Service URL to be used during sandbox Development. 80 | """ 81 | 82 | def __init__(self) -> None: 83 | self.base_url = "https://openapi.m-pesa.com:443/sandbox/ipg/v2/vodacomTZN" 84 | 85 | self.session_id = f"{self.base_url}/getSession/" 86 | self.single_stage_c2b = f"{self.base_url}/c2bPayment/singleStage/" 87 | self.single_stage_b2c = f"{self.base_url}/b2cPayment/" 88 | self.single_stage_b2b = f"{self.base_url}/b2bPayment/" 89 | 90 | self.payment_reversal = f"{self.base_url}/reversal/" 91 | self.transaction_status = f"{self.base_url}/queryTransactionStatus/" 92 | 93 | self.direct_debit = f"{self.base_url}/directDebitCreation/" 94 | self.direct_debit_payment = f"{self.base_url}/directDebitPayment/" 95 | 96 | def __str__(self) -> str: 97 | return '' 98 | 99 | def __repr__(self) -> str: 100 | return '' 101 | 102 | 103 | class Production(Required): 104 | """Service URL to be used for Production Development 105 | """ 106 | 107 | def __init__(self) -> None: 108 | self.base_url = "https://openapi.m-pesa.com:443/openapi/ipg/v2/vodacomTZN" 109 | 110 | self.session_id = f"{self.base_url}/getSession/" 111 | self.single_stage_c2b = f"{self.base_url}/c2bPayment/singleStage/" 112 | self.single_stage_b2c = f"{self.base_url}/b2cPayment/" 113 | self.single_stage_b2b = f"{self.base_url}/b2bPayment/" 114 | 115 | self.payment_reversal = f"{self.base_url}/reversal/" 116 | self.transaction_status = f"{self.base_url}/queryTransactionStatus/" 117 | 118 | self.direct_debit = f"{self.base_url}/directDebitCreation/" 119 | self.direct_debit_payment = f"{self.base_url}/directDebitPayment/" 120 | 121 | def __str__(self) -> str: 122 | return '' 123 | 124 | def __repr__(self) -> str: 125 | return '' 126 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs==1.4.3 2 | bleach==3.3.0 3 | CacheControl==0.12.6 4 | certifi==2019.11.28 5 | cffi==1.14.5 6 | chardet==3.0.4 7 | colorama==0.4.3 8 | contextlib2==0.6.0 9 | cryptography==3.4.4 10 | distlib==0.3.0 11 | distro==1.4.0 12 | docutils==0.16 13 | html5lib==1.0.1 14 | idna==2.8 15 | ipaddr==2.2.0 16 | jeepney==0.6.0 17 | keyring==22.0.1 18 | lockfile==0.12.2 19 | msgpack==0.6.2 20 | packaging==20.3 21 | pep517==0.8.2 22 | pkginfo==1.7.0 23 | progress==1.5 24 | pycparser==2.20 25 | pycrypto==2.6.1 26 | Pygments==2.7.4 27 | pyparsing==2.4.6 28 | pytoml==0.1.21 29 | readme-renderer==28.0 30 | requests==2.22.0 31 | requests-toolbelt==0.9.1 32 | retrying==1.3.3 33 | rfc3986==1.4.0 34 | SecretStorage==3.3.1 35 | six==1.14.0 36 | tqdm==4.56.2 37 | twine==3.3.0 38 | urllib3==1.26.5 39 | webencodings==0.5.1 40 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from os import path 2 | from setuptools import setup 3 | 4 | # read the contents of your description file 5 | 6 | this_directory = path.abspath(path.dirname(__file__)) 7 | with open(path.join(this_directory, 'description.md'), encoding='utf-8') as f: 8 | long_description = f.read() 9 | 10 | 11 | setup( 12 | name="python-pesa", 13 | version="0.6", 14 | description='Python package for Vodacom Mpesa API Integration', 15 | long_description=long_description, 16 | long_description_content_type='text/markdown', 17 | url='https://github.com/Kalebu/pypesa', 18 | download_url="https://github.com/Kalebu/pypesa/archive/0.4.tar.gz", 19 | author="Jordan Kalebu", 20 | author_email="isaackeinstein@gmail.com", 21 | license="MIT", 22 | packages=["pypesa"], 23 | keywords=[ 24 | "pypesa", 25 | "python-pesa", 26 | "mpesa python", 27 | "vodacom python", 28 | "pypesa package", 29 | "pypesa python package", 30 | "vodacom python package", 31 | "pypesa github", 32 | "python pypesa", 33 | ], 34 | 35 | install_requires=[ 36 | 'requests', 37 | 'pycryptodome' 38 | ], 39 | 40 | include_package_data=True, 41 | python_requires=">=3.6", 42 | classifiers=[ 43 | "Development Status :: 3 - Alpha", 44 | "Intended Audience :: Developers", 45 | "Topic :: Software Development :: Build Tools", 46 | "License :: OSI Approved :: MIT License", 47 | "Programming Language :: Python :: 3.6", 48 | "Programming Language :: Python :: 3.7", 49 | "Programming Language :: Python :: 3.8", 50 | "Programming Language :: Python :: 3.9", 51 | ], 52 | ) 53 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Neurotech-HQ/pypesa/3b787157b12ff1f176724e3d55fc08d59412a9fa/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_pypesa.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | import unittest 3 | from pypesa import Mpesa 4 | 5 | class MpesaTest(unittest.TestCase): 6 | def setUp(self): 7 | self.mpesa = Mpesa() 8 | self.expected_output_keys = { 9 | 'output_ResponseCode', 10 | 'output_ResponseDesc', 11 | 'output_TransactionID', 12 | 'output_ConversationID', 13 | 'output_ThirdPartyConversationID' 14 | } 15 | self.c2b_transaction_query = { 16 | "input_Amount": "10", 17 | "input_Country": "TZN", 18 | "input_Currency": "TZS", 19 | "input_CustomerMSISDN": "000000000001", 20 | "input_ServiceProviderCode": "000000", 21 | "input_ThirdPartyConversationID":self.random_id, 22 | "input_TransactionReference": 'T23434ZE5', 23 | "input_PurchasedItemsDesc": "Python Book" 24 | } 25 | 26 | self.b2c_transaction_query = { 27 | "input_Amount": "10", 28 | "input_Country": "TZN", 29 | "input_Currency": "TZS", 30 | "input_CustomerMSISDN": "000000000001", 31 | "input_ServiceProviderCode": "000000", 32 | "input_ThirdPartyConversationID": self.random_id, 33 | "input_TransactionReference": "T12344C", 34 | "input_PaymentItemsDesc": "Salary payment" 35 | } 36 | 37 | self.b2b_transaction_query = { 38 | "input_Amount": "10", 39 | "input_Country": "TZN", 40 | "input_Currency": "TZS", 41 | "input_PrimaryPartyCode": "000000", 42 | "input_ReceiverPartyCode": "000001", 43 | "input_ThirdPartyConversationID": self.random_id, 44 | "input_TransactionReference": "T12344C", 45 | "input_PurchasedItemsDesc": "Apartment" 46 | } 47 | 48 | self.payment_reversal_query = { 49 | "input_Country": "TZN", 50 | "input_ReversalAmount": "25", 51 | "input_ServiceProviderCode": "000000", 52 | "input_ThirdPartyConversationID": self.random_id, 53 | "input_TransactionID": "0000000000001" 54 | } 55 | 56 | self.transaction_status_query = { 57 | "input_Country": "TZN", 58 | "input_QueryReference":"000000000000000000001" , 59 | "input_ServiceProviderCode": "000000", 60 | "input_ThirdPartyConversationID": self.random_id 61 | } 62 | 63 | @property 64 | def random_id(self): 65 | unique_id = str(uuid.uuid4()) 66 | unique_id = unique_id.replace('-', '') 67 | return unique_id 68 | 69 | def test_customer_to_bussiness(self): 70 | print('Testing customer_to_bussiness Feature') 71 | response = self.mpesa.customer_to_bussiness(self.c2b_transaction_query) 72 | response_keys = set(response.json().keys()) 73 | self.assertEqual(response_keys, self.expected_output_keys) 74 | 75 | def test_bussiness_to_customer(self): 76 | print('Testing bussiness_to_customer Feature') 77 | response = self.mpesa.bussiness_to_customer(self.b2c_transaction_query) 78 | response_keys = set(response.json().keys()) 79 | self.assertEqual(response_keys, self.expected_output_keys) 80 | print('Finished testing bussiness_to_customer Feature') 81 | 82 | def test_bussiness_to_bussiness(self): 83 | print('Testing bussiness_to_bussiness Feature') 84 | response = self.mpesa.bussiness_to_bussiness(self.b2b_transaction_query) 85 | response_keys = set(response.json().keys()) 86 | self.assertEqual(response_keys, self.expected_output_keys) 87 | 88 | def test_payment_reversal(self): 89 | print('Testing payment Reversal Feature') 90 | response = self.mpesa.payment_reversal(self.payment_reversal_query) 91 | response_keys = set(response.json().keys()) 92 | self.assertEqual(response_keys, self.expected_output_keys) 93 | print('Finished testing Payment reversal') 94 | 95 | def test_query_transaction_status(self): 96 | print('Testing query transaction status Feature') 97 | response = self.mpesa.query_transaction_status(self.transaction_status_query) 98 | response_keys = set(response.json().keys()) 99 | self.assertEqual(response_keys, self.expected_output_keys) 100 | print('Finished Testing transaction status Feature') 101 | 102 | 103 | if __name__ == "__main__": 104 | unittest.main() --------------------------------------------------------------------------------