diff --git a/.github/helper/install.sh b/.github/helper/install.sh new file mode 100644 index 0000000..2bd468b --- /dev/null +++ b/.github/helper/install.sh @@ -0,0 +1,70 @@ +#!/bin/bash + +set -e + +# Check for merge conflicts before proceeding +python -m compileall -f "${GITHUB_WORKSPACE}" +if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" + then echo "Found merge conflicts" + exit 1 +fi + +cd ~ || exit + +echo "Setting Up System Dependencies..." + +sudo apt update + +sudo apt remove mysql-server mysql-client +sudo apt install libcups2-dev redis-server mariadb-client + +install_whktml() { + wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb + sudo apt install /tmp/wkhtmltox.deb +} +install_whktml & +wkpid=$! + +pip install frappe-bench + +git clone "https://github.com/frappe/frappe" --branch "${FRAPPE_BRANCH}" --depth 1 +bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench + +mkdir ~/frappe-bench/sites/test_site + +cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/ + +# Update MySQL/MariaDB credentials +mariadb --host 127.0.0.1 --port 3306 -u root -ppassword -e " +SET GLOBAL character_set_server = 'utf8mb4'; +SET GLOBAL collation_server = 'utf8mb4_unicode_ci'; + +CREATE USER 'test_resilient'@'localhost' IDENTIFIED BY 'test_resilient'; +CREATE DATABASE test_resilient; +GRANT ALL PRIVILEGES ON \`test_resilient\`.* TO 'test_resilient'@'localhost'; + +FLUSH PRIVILEGES; +" + +cd ~/frappe-bench || exit + +sed -i 's/watch:/# watch:/g' Procfile +sed -i 's/schedule:/# schedule:/g' Procfile +sed -i 's/socketio:/# socketio:/g' Procfile +sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile + +bench get-app erpnext --branch "${ERPNEXT_BRANCH}" --resolve-deps + +# Install the app using SSH +bench get-app "git@github.com:${GITHUB_REPOSITORY}.git" --branch "${PR_BRANCH}" + +bench setup requirements --dev + +wait $wkpid + +bench use test_site +bench start & +bench reinstall --yes + +bench install-app "${APP_NAME}" +bench --site test_site add-to-hosts diff --git a/.github/helper/site_config.json b/.github/helper/site_config.json new file mode 100644 index 0000000..7c17d59 --- /dev/null +++ b/.github/helper/site_config.json @@ -0,0 +1,17 @@ +{ + "db_host": "127.0.0.1", + "db_port": 3306, + "db_name": "test_frappe", + "db_password": "test_frappe", + "db_type": "mariadb", + "auto_email_id": "test@example.com", + "mail_server": "smtp.example.com", + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "root", + "root_password": "password", + "host_name": "http://test_site:8000", + "install_apps": ["erpnext"], + "throttle_user_limit": 100 +} diff --git a/payment_integration_utils/patches.txt b/payment_integration_utils/patches.txt index f15c3a9..dc9d5e0 100644 --- a/payment_integration_utils/patches.txt +++ b/payment_integration_utils/patches.txt @@ -3,4 +3,6 @@ # Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations [post_model_sync] -# Patches added in this section will be executed after doctypes are migrated \ No newline at end of file +# Patches added in this section will be executed after doctypes are migrated +execute:from payment_integration_utils.setup import create_custom_fields; create_custom_fields() +payment_integration_utils.patches.delete_old_custom_fields \ No newline at end of file diff --git a/payment_integration_utils/patches/delete_old_custom_fields.py b/payment_integration_utils/patches/delete_old_custom_fields.py new file mode 100644 index 0000000..88e52fc --- /dev/null +++ b/payment_integration_utils/patches/delete_old_custom_fields.py @@ -0,0 +1,9 @@ +from payment_integration_utils.payment_integration_utils.setup import ( + delete_custom_fields, +) + +FIELDS_TO_DELETE = {"Payment Entry": ["is_auto_generated"]} + + +def execute(): + delete_custom_fields(FIELDS_TO_DELETE) diff --git a/payment_integration_utils/patches/delete_old_property_setters.py b/payment_integration_utils/patches/delete_old_property_setters.py new file mode 100644 index 0000000..8a1b448 --- /dev/null +++ b/payment_integration_utils/patches/delete_old_property_setters.py @@ -0,0 +1,9 @@ +from payment_integration_utils.payment_integration_utils.setup import ( + delete_property_setters, +) + +PROPERTY_SETTERS_TO_DELETE = [] + + +def execute(): + delete_property_setters(PROPERTY_SETTERS_TO_DELETE) diff --git a/payment_integration_utils/payment_integration_utils/constants/custom_fields.py b/payment_integration_utils/payment_integration_utils/constants/custom_fields.py index b6ea667..16e54cf 100644 --- a/payment_integration_utils/payment_integration_utils/constants/custom_fields.py +++ b/payment_integration_utils/payment_integration_utils/constants/custom_fields.py @@ -136,21 +136,11 @@ "print_hide": 1, "permlevel": PERMISSION_LEVEL.SEVEN.value, }, - { - "fieldname": "is_auto_generated", - "label": "Is Auto Generated", - "fieldtype": "Check", - "insert_after": "online_payment_meta_data_section", - "hidden": 1, - "print_hide": 1, - "permlevel": PERMISSION_LEVEL.SEVEN.value, - "no_copy": 1, - }, { "fieldname": "payment_authorized_by", "label": "Payment Authorized By", "fieldtype": "Data", - "insert_after": "is_auto_generated", + "insert_after": "online_payment_meta_data_section", "options": "Email", "description": "User who made the payment", "hidden": 1, diff --git a/payment_integration_utils/payment_integration_utils/setup/__init__.py b/payment_integration_utils/payment_integration_utils/setup/__init__.py index 2f6cd87..7b0ece2 100644 --- a/payment_integration_utils/payment_integration_utils/setup/__init__.py +++ b/payment_integration_utils/payment_integration_utils/setup/__init__.py @@ -208,7 +208,7 @@ def delete_custom_fields(custom_fields: dict): for doctype, fields in custom_fields.items(): fieldnames = [] - if isinstance(fields, list) and fields: + if fields and isinstance(fields, list): if isinstance(fields[0], str): fieldnames = fields elif isinstance(fields[0], dict): diff --git a/payment_integration_utils/payment_integration_utils/tests/test_auth.py b/payment_integration_utils/payment_integration_utils/tests/test_auth.py new file mode 100644 index 0000000..444ac39 --- /dev/null +++ b/payment_integration_utils/payment_integration_utils/tests/test_auth.py @@ -0,0 +1 @@ +# TODO: test : payment_integration_utils/payment_integration_utils/utils/auth.py diff --git a/payment_integration_utils/payment_integration_utils/tests/test_payment_entry.py b/payment_integration_utils/payment_integration_utils/tests/test_payment_entry.py new file mode 100644 index 0000000..04baf5d --- /dev/null +++ b/payment_integration_utils/payment_integration_utils/tests/test_payment_entry.py @@ -0,0 +1 @@ +# TODO: test: payment_integration_utils/payment_integration_utils/server_overrides/doctype/payment_entry.py diff --git a/payment_integration_utils/payment_integration_utils/tests/test_utils.py b/payment_integration_utils/payment_integration_utils/tests/test_utils.py new file mode 100644 index 0000000..c1f8af1 --- /dev/null +++ b/payment_integration_utils/payment_integration_utils/tests/test_utils.py @@ -0,0 +1,27 @@ +from frappe.tests.utils import FrappeTestCase + +from payment_integration_utils.payment_integration_utils.utils import ( + paisa_to_rupees, + rupees_to_paisa, + to_hyphenated, +) + + +class TestUtils(FrappeTestCase): + def test_conversion(self): + # (100, 10000) => 100 Rupees = 10000 Paisa + data = [ + (100, 10000), + (100.0, 10000), + (100.5, 10050), + (0.5, 50), + (79.9, 7990), + (0, 0), + ] + for rupees, paisa in data: + self.assertEqual(rupees_to_paisa(rupees), paisa) + self.assertEqual(paisa_to_rupees(paisa), rupees) + + def test_to_hyphenated(self): + self.assertEqual(to_hyphenated("Hello World"), "Hello-World") + self.assertEqual(to_hyphenated("Hello World!"), "Hello-World-") diff --git a/payment_integration_utils/payment_integration_utils/tests/test_validation.py b/payment_integration_utils/payment_integration_utils/tests/test_validation.py new file mode 100644 index 0000000..b10be85 --- /dev/null +++ b/payment_integration_utils/payment_integration_utils/tests/test_validation.py @@ -0,0 +1,52 @@ +import re +from unittest.mock import patch + +import frappe +from frappe.tests.utils import FrappeTestCase + +from payment_integration_utils.payment_integration_utils.utils.validation import ( + validate_ifsc_code, + validate_payment_mode, +) + + +class TestUtils(FrappeTestCase): + @patch("requests.get") + def test_ifsc_code(self, mock_get): + IN_VALID_CODE = "SBK0000001" + + # Mock a failed response + mock_get.return_value.status_code = 404 + self.assertFalse(validate_ifsc_code(IN_VALID_CODE)) + + # Mock a successful response + mock_get.return_value.status_code = 200 + self.assertTrue(validate_ifsc_code("HDFC0000314")) + + # Test throwing an exception + mock_get.return_value.status_code = 404 + self.assertRaisesRegex( + frappe.exceptions.ValidationError, + re.compile(rf"Invalid IFSC Code:.*{IN_VALID_CODE}.*"), + validate_ifsc_code, + IN_VALID_CODE, + throw=True, + ) + + def test_payment_mode(self): + valid_modes = ["NEFT", "IMPS", "RTGS", "UPI", "Link"] + + for mode in valid_modes: + self.assertTrue(validate_payment_mode(mode)) + + IN_VALID_MODE = "Crypto" + self.assertFalse(validate_payment_mode(IN_VALID_MODE)) + + # Test throwing an exception + self.assertRaisesRegex( + frappe.exceptions.ValidationError, + re.compile(r"Invalid Payment Mode:.* Must be one of:.*"), + validate_payment_mode, + IN_VALID_MODE, + throw=True, + ) diff --git a/payment_integration_utils/payment_integration_utils/utils/__init__.py b/payment_integration_utils/payment_integration_utils/utils/__init__.py index fcaf4c9..00748fc 100644 --- a/payment_integration_utils/payment_integration_utils/utils/__init__.py +++ b/payment_integration_utils/payment_integration_utils/utils/__init__.py @@ -3,12 +3,7 @@ import frappe from frappe import _ -from frappe.utils import ( - DateTimeLikeObject, - add_to_date, - get_timestamp, - getdate, -) +from frappe.utils import DateTimeLikeObject, add_to_date, flt, get_timestamp, getdate from payment_integration_utils.constants import SECONDS_IN_A_DAY @@ -89,13 +84,15 @@ def rupees_to_paisa(amount: float | int) -> int: Convert the given amount in Rupees to Paisa. :param amount: The amount in Rupees to be converted to Paisa. - Example: ``` rupees_to_paisa(100) ==> 10000 + rupees_to_paisa(79.899) ==> 7990 + rupees_to_paisa(79.9) ==> 7990 + ``` """ - return int(amount * 100) + return int(flt(amount, 2) * 100) def paisa_to_rupees(amount: int) -> int | float: @@ -107,9 +104,10 @@ def paisa_to_rupees(amount: int) -> int | float: Example: ``` paisa_to_rupees(10000) ==> 100 + paisa_to_rupees(7990) ==> 79.9 ``` """ - return amount / 100 + return flt(amount / 100, 2) ################# HTML RELATED ################# diff --git a/payment_integration_utils/setup.py b/payment_integration_utils/setup.py index b6cfa86..0115002 100644 --- a/payment_integration_utils/setup.py +++ b/payment_integration_utils/setup.py @@ -1,6 +1,8 @@ import click import frappe -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields +from frappe.custom.doctype.custom_field.custom_field import ( + create_custom_fields as make_custom_fields, +) from payment_integration_utils.payment_integration_utils.constants.custom_fields import ( CUSTOM_FIELDS, @@ -30,17 +32,33 @@ ################### After Install ################### def setup_customizations(): click.secho("Creating Roles and Permissions...", fg="blue") - make_roles_and_permissions(ROLES) + create_roles_and_permissions() click.secho("Creating Custom Fields...", fg="blue") - create_custom_fields(CUSTOM_FIELDS) + create_custom_fields() click.secho("Creating Property Setters...", fg="blue") + create_property_setters() + + click.secho("Creating Workflows...", fg="blue") + create_workflows() + + +# Note: separate functions are required to use in patches +def create_roles_and_permissions(): + make_roles_and_permissions(ROLES) + + +def create_custom_fields(): + make_custom_fields(CUSTOM_FIELDS) + + +def create_property_setters(): for property_setter in PROPERTY_SETTERS: frappe.make_property_setter(property_setter) - click.secho("Creating Workflows...", fg="blue") +def create_workflows(): # create states make_workflow_states(WORKFLOW_STATES) diff --git a/payment_integration_utils/tests/__init__.py b/payment_integration_utils/tests/__init__.py new file mode 100644 index 0000000..e69de29