Skip to content

Commit

Permalink
Completed transfer of mpesa b2c integration application to new reposi…
Browse files Browse the repository at this point in the history
…tory.
  • Loading branch information
GichanaMayaka committed Nov 15, 2023
1 parent 548aac5 commit fe4209b
Show file tree
Hide file tree
Showing 25 changed files with 2,117 additions and 6 deletions.
12 changes: 10 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
*.pyc
*.egg-info
*.swp
*.rest

*.pem
*.cer

logfile.txt
tags
node_modules
__pycache__

csf_ke/docs/current
/.vscode
/__pycache__/
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
## MPesa B2C
## EXPNext MPesa B2C Integration

MPesa B2C Integration for ERPNext
### Installation

#### License
Using bench, [install ERPNext](https://github.com/frappe/bench#installation) as mentioned here.

mit
Once ERPNext is installed, add MPesa B2c app to your bench by running

```sh
$ bench get-app https://github.com/navariltd/navari-mpesa-b2c.git
```

After that, you can install MPesa B2C app on required site by running

```sh
$ bench --site [site.name] install-app navari_mpesa_b2c
```

### License

MIT. See [license.txt](https://github.com/navariltd/navari-mpesa-b2c/blob/develop/license.txt) for more information.
5 changes: 5 additions & 0 deletions navari_mpesa_b2c/mpesa_b2c/doctype/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import frappe
from frappe.utils import logger

logger.set_log_level("DEBUG")
api_logger = frappe.logger("api", allow_site=True, file_count=50)
40 changes: 40 additions & 0 deletions navari_mpesa_b2c/mpesa_b2c/doctype/csf_ke_custom_exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Custom Exceptions and Errors raised by modules in the CSF_KE application"""


class InvalidReceiverMobileNumberError(Exception):
"""Raised when receiver's mobile number fails validation"""


class InsufficientPaymentAmountError(Exception):
"""Raised when the payment amount is less than the required KShs. 10"""


class IncorrectStatusError(Exception):
"""Raised when status is Errored but no errod description or error code has been supplied"""


class InvalidTokenExpiryTime(Exception):
"""
Raised when the access token's expiry time is earlier
or the same as the access token's fetch time.
It should always be 1 hour after the fetch time.
"""


class InvalidURLError(Exception):
"""Raised when URLs fail validation"""


class InvalidAuthenticationCertificateFileError(Exception):
"""Raised when an invalid certificate file, i.e. not a .cer or .pem, is uploaded"""


class UnExistentB2CPaymentRecordError(Exception):
"""Raised when referencing a B2C Payment that does not exist"""


class InformationMismatchError(Exception):
"""
Raised when there's a mismatch in any of the B2C Payment's records
and the corresponding B2C Payments Transaction's records
"""
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright (c) 2023, Navari Limited and contributors
// For license information, please see license.txt

frappe.ui.form.on("Daraja Access Tokens", {
validate: function (frm) {
if (frm.doc.expiry_time && frm.doc.token_fetch_time) {
expiryTime = new Date(frm.doc.expiry_time);
fetchTime = new Date(frm.doc.token_fetch_time);

if (expiryTime <= fetchTime) {
frappe.msgprint({
message: __(
"Token Expiry Time cannot be earlier than or the same as Token Fetch Time"
),
indicator: "red",
title: "Validation Error",
});
frappe.validated = false;
}
}
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
{
"actions": [],
"autoname": "autoincrement",
"creation": "2023-11-01 11:56:09.082783",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"access_token",
"token_fetch_time",
"column_break_snir",
"expiry_time"
],
"fields": [
{
"fieldname": "access_token",
"fieldtype": "Password",
"in_list_view": 1,
"label": "Access Token",
"reqd": 1
},
{
"fieldname": "token_fetch_time",
"fieldtype": "Datetime",
"label": "Token Fetch Time",
"reqd": 1
},
{
"fieldname": "column_break_snir",
"fieldtype": "Column Break"
},
{
"fieldname": "expiry_time",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Expiry",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2023-11-15 10:42:44.625615",
"modified_by": "Administrator",
"module": "MPesa B2C",
"name": "Daraja Access Tokens",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright (c) 2023, Navari Limited and contributors
# For license information, please see license.txt

from frappe.model.document import Document

from ..csf_ke_custom_exceptions import InvalidTokenExpiryTime
from .. import api_logger


class DarajaAccessTokens(Document):
"""Daraja Access Tokens controller class"""

def validate(self) -> None:
"""Run validations before saving document"""
if self.expiry_time and self.expiry_time <= self.token_fetch_time:
api_logger.error(
"Access Token Expiry time cannot be same or early than the fetch time"
)
raise InvalidTokenExpiryTime(
"Access Token Expiry time cannot be same or early than the fetch time"
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Copyright (c) 2023, Navari Limited and Contributors
# See license.txt

import datetime
from unittest.mock import patch

import frappe
import requests
from frappe.tests.utils import FrappeTestCase
from frappe.utils.password import get_decrypted_password

from ..csf_ke_custom_exceptions import InvalidTokenExpiryTime
from ..mpesa_b2c_payment import mpesa_b2c_payment
from ..mpesa_b2c_payment.mpesa_b2c_payment import get_hashed_token

TOKEN_ACCESS_TIME = datetime.datetime.now()


def create_access_token() -> None:
"""Creates a valid access token record for testing"""
if frappe.flags.test_events_created:
return

frappe.set_user("Administrator")

expiry_time = TOKEN_ACCESS_TIME + datetime.timedelta(hours=1)
frappe.get_doc(
{
"doctype": "Daraja Access Tokens",
"access_token": "123456789",
"token_fetch_time": TOKEN_ACCESS_TIME,
"expiry_time": expiry_time,
}
).insert()

frappe.flags.test_events_created = True


class TestDarajaAccessTokens(FrappeTestCase):
"""Testing the Daraja Access Tokens doctype"""

def setUp(self) -> None:
create_access_token()

def tearDown(self) -> None:
frappe.set_user("Administrator")

def test_valid_access_token(self) -> None:
"""Attempt to access an existing token"""
token = frappe.db.get_value(
"Daraja Access Tokens",
{"token_fetch_time": TOKEN_ACCESS_TIME},
["name", "token_fetch_time", "expiry_time"],
as_dict=True,
)
access_token = get_decrypted_password(
"Daraja Access Tokens", token.name, "access_token"
)

self.assertEqual(access_token, "123456789")
self.assertEqual(token.token_fetch_time, TOKEN_ACCESS_TIME)
self.assertEqual(
token.expiry_time, TOKEN_ACCESS_TIME + datetime.timedelta(hours=1)
)

def test_create_incomplete_access_token(self) -> None:
"""Attemp to create a record from incomplete data"""
with self.assertRaises(frappe.exceptions.MandatoryError):
frappe.get_doc(
{
"doctype": "Daraja Access Tokens",
"access_token": "123456789",
"token_fetch_time": TOKEN_ACCESS_TIME,
}
).insert()

def test_incorrect_datetime_type(self) -> None:
"""Test passing strings to datetime fields"""
with self.assertRaises(TypeError):
frappe.get_doc(
{
"doctype": "Daraja Access Tokens",
"access_token": TOKEN_ACCESS_TIME + datetime.timedelta(hours=1),
"token_fetch_time": TOKEN_ACCESS_TIME,
"expiry_time": "123456789",
}
).insert()

def test_expiry_time_earlier_than_fetch_time(self) -> None:
"""Test expiry time being early than fetch time"""
with self.assertRaises(InvalidTokenExpiryTime):
frappe.get_doc(
{
"doctype": "Daraja Access Tokens",
"access_token": "123456789",
"token_fetch_time": TOKEN_ACCESS_TIME,
"expiry_time": TOKEN_ACCESS_TIME - datetime.timedelta(hours=1),
}
).insert()

def test_get_hashed_token(self) -> None:
"""Tests the get_hashed_token() function from the b2c_payment module"""
hashed_token = get_hashed_token()
token = frappe.db.get_value(
"Daraja Access Tokens",
{"token_fetch_time": TOKEN_ACCESS_TIME},
["name"],
)

self.assertEqual(hashed_token, token)

@patch.object(mpesa_b2c_payment.requests, "get")
def test_get_access_tokens(self, mock_response) -> None:
"""Tests the get_access_tokens() function from the b2c_payment module"""
mock_response.return_value.status_code = 200
mock_response.return_value.text = {
"access_token": "987654321",
"expires_in": "3599",
}

token, status_code = mpesa_b2c_payment.get_access_tokens(
"123456789", "secret", "https://example.com/authorise"
)
self.assertIsInstance(token, dict)
self.assertEqual(token["access_token"], "987654321")
self.assertEqual(token["expires_in"], "3599")
self.assertEqual(status_code, 200)

def test_get_access_tokens_error_response(self) -> None:
"""Tests instances the get_access_tokens() function from the b2c_payment module fails"""
with self.assertRaises(requests.HTTPError):
mpesa_b2c_payment.get_access_tokens(
"123456789", "secret", "https://example.com/authorise"
)
Empty file.
Loading

0 comments on commit fe4209b

Please sign in to comment.