Skip to content

Commit

Permalink
feat: Implement PayFort Payment Processor
Browse files Browse the repository at this point in the history
  • Loading branch information
shadinaif committed May 20, 2024
1 parent dc42625 commit 02b3eb1
Show file tree
Hide file tree
Showing 40 changed files with 2,743 additions and 391 deletions.
11 changes: 6 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ jobs:
matrix:
include:
- python-version: 3.8
tox-env: py38
tox-env: tests
- python-version: 3.8
tox-env: flake8
tox-env: quality

name: "Python ${{ matrix.python-version }} - ${{ matrix.tox-env }}"
steps:
Expand All @@ -27,14 +27,15 @@ jobs:
python-version: ${{ matrix.python-version }}

- name: Cache tox environments
uses: actions/cache@v3
uses: actions/cache@v4
with:
save-always: true
path: .tox
# Refresh the cache if the following files change
key: "tox-${{ matrix.python-version }}-${{ matrix.tox-env }}-${{ hashFiles('tox.ini', 'setup.py', 'scripts/tox_install_ecommerce_run_pytest.sh', 'requirements/ecommerce-maple.master.txt', 'payfort-test.txt') }}"
key: "tox-${{ matrix.python-version }}-${{ matrix.tox-env }}-${{ hashFiles('tox.ini', 'setup.py', 'scripts/tox_install_ecommerce_run_pytest.sh', 'requirements/ecommerce-palm.master.txt', 'quality.txt') }}"

- name: Install Dependencies
run: |
pip install tox
- name: "Python ${{ matrix.python-version }} - ${{ matrix.tox-env }}"
run: "tox -e ${{ matrix.tox-env }}"
run: "tox -e py38-${{ matrix.tox-env }}"
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,16 @@ tests: ## Run unit and integration tests
tox -e py38

unit_tests: ## Run unit tests
tox -e py38 -- tests/unit
tox -e py38 -- $(shell pwd)/ecommerce_payfort/tests/unit

quality: ## Run code quality checks
tox -e flake8

translation.requirements:
pip install -r requirements/translation.txt

translation.extract:
i18n_tool extract --no-segment

translation.compile:
i18n_tool generate
1 change: 1 addition & 0 deletions conf/locale
2 changes: 1 addition & 1 deletion ecommerce_payfort/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""
The PayFort payment processor pluggable application for Open edX ecommerce.
"""
default_app_config = 'ecommerce_payfort.apps.PayFortConfig'
default_app_config = 'ecommerce_payfort.apps.PayFortConfig' # pylint: disable=invalid-name
4 changes: 1 addition & 3 deletions ecommerce_payfort/apps.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
"""
PayFort payment processor Django application initialization.
"""
"""PayFort payment processor Django application initialization."""
from django.apps import AppConfig


Expand Down
48 changes: 48 additions & 0 deletions ecommerce_payfort/locale/ar/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,51 @@ msgstr ""
"Content-Transfer-Encoding: 8bit\n"
"Language: ar\n"
"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5;\n"

#: ecommerce_payfort/processors.py:22
msgid "Checkout with credit card"
msgstr "الدفع ببطاقة الائتمان"

#: ecommerce_payfort/templates/payfort_payment/form.html:12
msgid "Redirecting to the Payment Gateway..."
msgstr "إعادة توجيه إلى بوابة الدفع..."

#: ecommerce_payfort/templates/payfort_payment/handle_format_error.html:12
#: ecommerce_payfort/templates/payfort_payment/handle_internal_error.html:12
msgid "This is unfortunate and not expected!"
msgstr "هذا مؤسف وغير متوقع!"

#: ecommerce_payfort/templates/payfort_payment/handle_format_error.html:14
msgid ""
"The response came from the payment gateway is malformed but flagged as "
"succeeded! We're are not sure if your account has been charged or not! The "
"administrator has been notified and will investigate the issue shortly"
msgstr ""
"الرد الذي جاء من بوابة الدفع غير صحيح ولكنه يحتوي علامة الدفع الناجح! "
"لسنا متأكدين مما إذا تم اقتصاص المبلغ من حسابك أم لا! "
"تم إخطار الآدمن وسيقوم بالتحقيق في القضية قريباً"

#: ecommerce_payfort/templates/payfort_payment/handle_format_error.html:16
#: ecommerce_payfort/templates/payfort_payment/handle_internal_error.html:16
msgid ""
"Please do not submit a purchase again before contacting the administrator"
msgstr "الرجاء عدم إعادة تقديم طلب شراء مرة أخرى قبل الاتصال بالآدمن"

#: ecommerce_payfort/templates/payfort_payment/handle_format_error.html:19
#: ecommerce_payfort/templates/payfort_payment/handle_internal_error.html:18
#: ecommerce_payfort/templates/payfort_payment/wait_feedback.html:14
msgid "For your reference, the payment ID is:"
msgstr "لمعلوماتك، رقم الدفع هو:"

#: ecommerce_payfort/templates/payfort_payment/handle_internal_error.html:14
msgid ""
"The payment has been processed successfully, but we had an unexpected error "
"while submitting the payment information to the server. The administrator "
"has been notified and will investigate the issue."
msgstr ""
"الدفع تم معالجته بنجاح، ولكن واجهنا خطأ غير متوقع أثناء تثبيت معلومات الدفع في الخادم. "
"تم إخطار الآدمن وسيقوم بالتحقيق في القضية."

#: ecommerce_payfort/templates/payfort_payment/wait_feedback.html:12
msgid "Payment succeeded. Processing enrollment.. please wait.."
msgstr "الدفع نجح. جاري معالجة التسجيل.. الرجاء الانتظار.."
61 changes: 61 additions & 0 deletions ecommerce_payfort/locale/en/LC_MESSAGES/django.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# edX translation file.
# Copyright (C) 2024 EdX
# This file is distributed under the GNU AFFERO GENERAL PUBLIC LICENSE.
# EdX Team <[email protected]>, 2024.
#
msgid ""
msgstr ""
"Project-Id-Version: 0.1a\n"
"Report-Msgid-Bugs-To: [email protected]\n"
"POT-Creation-Date: 2023-06-13 08:00+0000\n"
"PO-Revision-Date: 2023-06-13 09:00+0000\n"
"Last-Translator: \n"
"Language-Team: openedx-translation <[email protected]>\n"
"Language: en\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"

#: ecommerce_payfort/processors.py:22
msgid "Checkout with credit card"
msgstr ""

#: ecommerce_payfort/templates/payfort_payment/form.html:12
msgid "Redirecting to the Payment Gateway..."
msgstr ""

#: ecommerce_payfort/templates/payfort_payment/handle_format_error.html:12
#: ecommerce_payfort/templates/payfort_payment/handle_internal_error.html:12
msgid "This is unfortunate and not expected!"
msgstr ""

#: ecommerce_payfort/templates/payfort_payment/handle_format_error.html:14
msgid ""
"The response came from the payment gateway is malformed but flagged as "
"succeeded! We're are not sure if your account has been charged or not! The "
"administrator has been notified and will investigate the issue shortly"
msgstr ""

#: ecommerce_payfort/templates/payfort_payment/handle_format_error.html:16
#: ecommerce_payfort/templates/payfort_payment/handle_internal_error.html:16
msgid ""
"Please do not submit a purchase again before contacting the administrator"
msgstr ""

#: ecommerce_payfort/templates/payfort_payment/handle_format_error.html:19
#: ecommerce_payfort/templates/payfort_payment/handle_internal_error.html:18
#: ecommerce_payfort/templates/payfort_payment/wait_feedback.html:14
msgid "For your reference, the payment ID is:"
msgstr ""

#: ecommerce_payfort/templates/payfort_payment/handle_internal_error.html:14
msgid ""
"The payment has been processed successfully, but we had an unexpected error "
"while submitting the payment information to the server. The administrator "
"has been notified and will investigate the issue."
msgstr ""

#: ecommerce_payfort/templates/payfort_payment/wait_feedback.html:12
msgid "Payment succeeded. Processing enrollment.. please wait.."
msgstr ""
92 changes: 70 additions & 22 deletions ecommerce_payfort/processors.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,89 @@
"""
PayFort payment processor.
"""

"""PayFort payment processor."""
import logging
from urllib.parse import urljoin

from django.middleware.csrf import get_token
from django.urls import reverse
from django.utils.translation import ugettext_lazy as _
from oscar.apps.payment.exceptions import GatewayError
from ecommerce.extensions.payment.processors import BasePaymentProcessor, HandledProcessorResponse

from ecommerce.extensions.payment.processors import BasePaymentProcessor
from ecommerce_payfort import utils

logger = logging.getLogger(__name__)


def format_price(price):
"""
Return the price in the expected format.
"""
return '{:0.2f}'.format(price)


class PayFortException(GatewayError):
"""
An umbrella exception to catch all errors from PayFort.
"""
pass # pylint: disable=unnecessary-pass


class PayFort(BasePaymentProcessor):
"""
PayFort payment processor.
For reference, see https://paymentservices-reference.payfort.com/docs/api/build/index.html
Scroll through the page, it's a very long single-page documentation.
"""

NAME = 'payfort'
CHECKOUT_TEXT = _("Checkout with credit card")
NAME = "payfort"

def __init__(self, site):
"""Initialize the PayFort processor."""
super().__init__(site)
self.site = site

self.access_code = self.configuration.get("access_code")
self.merchant_identifier = self.configuration.get("merchant_identifier")
self.request_sha_phrase = self.configuration.get("request_sha_phrase")
self.response_sha_phrase = self.configuration.get("response_sha_phrase")
self.sha_method = self.configuration.get("sha_method")
self.ecommerce_url_root = self.configuration.get("ecommerce_url_root")

def get_transaction_parameters(self, basket, request=None, use_client_side_checkout=False, **kwargs):
"""Return the transaction parameters needed for this processor."""
transaction_parameters = {
"command": "PURCHASE",
"access_code": self.access_code,
"merchant_identifier": self.merchant_identifier,
"language": utils.get_language(request),
"merchant_reference": utils.get_merchant_reference(self.site.id, basket),
"amount": utils.get_amount(basket),
"currency": utils.get_currency(basket),
"customer_email": utils.get_customer_email(basket),
"order_description": utils.get_order_description(basket),
"customer_name": utils.get_customer_name(basket),
"return_url": urljoin(
self.ecommerce_url_root,
reverse("payfort:response")
),
}

signature = utils.get_signature(
self.request_sha_phrase,
self.sha_method,
transaction_parameters,
)
transaction_parameters.update({
"signature": signature,
"payment_page_url": reverse("payfort:form"),
"csrfmiddlewaretoken": get_token(request),
})

return transaction_parameters

def handle_processor_response(self, response, basket=None):
"""Handle the payment processor response and record the relevant details."""
currency = response["currency"]
total = int(response["amount"]) / 100
transaction_id = utils.get_transaction_id(response)
card_number = response.get("card_number")
card_type = response.get("payment_option")

return HandledProcessorResponse(
transaction_id=transaction_id,
total=total,
currency=currency,
card_number=card_number,
card_type=card_type
)

def issue_credit(
self, order_number, basket, reference_number, amount, currency
): # pylint: disable=too-many-arguments
"""Not available."""
raise NotImplementedError("PayFort processor cannot issue credits or refunds from Open edX ecommerce.")
37 changes: 37 additions & 0 deletions ecommerce_payfort/templates/payfort_payment/form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{% extends "edx/base.html" %}
{% load i18n %}

{% block content %}
<style>
.centered-content {
text-align: center;
margin-top: 2em;
}
</style>
<div class="centered-content">
<h1>{% trans "Redirecting to the Payment Gateway..." %}</h1>
</div>

<form action="https://sbcheckout.payfort.com/FortAPI/paymentPage" method="post" name="payment_form">
<input type="hidden" name="command" value="{{ command }}">
<input type="hidden" name="access_code" value="{{ access_code }}">
<input type="hidden" name="merchant_identifier" value="{{ merchant_identifier }}">
<input type="hidden" name="merchant_reference" value="{{ merchant_reference }}">
<input type="hidden" name="amount" value="{{ amount }}">
<input type="hidden" name="currency" value="{{ currency }}">
<input type="hidden" name="language" value="{{ language }}">
<input type="hidden" name="customer_email" value="{{ customer_email }}">
<input type="hidden" name="order_description" value="{{ order_description }}">
<input type="hidden" name="signature" value="{{ signature }}">
<input type="hidden" name="customer_name" value="{{ customer_name }}">
<input type="hidden" name="return_url" value="{{ return_url }}">
</form>
{% endblock %}

{% block javascript %}
<script type="text/javascript">
window.onload = function() {
document.payment_form.submit();
};
</script>
{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{% extends "edx/base.html" %}
{% load i18n %}

{% block content %}
<style>
.centered-content {
text-align: center;
margin-top: 2em;
}
</style>
<div class="centered-content">
<h1>{% trans "This is unfortunate and not expected!" %}</h1>

<p>{% trans "The response came from the payment gateway is malformed but flagged as succeeded! We're are not sure if your account has been charged or not! The administrator has been notified and will investigate the issue shortly" %}</p>

<p><strong>{% trans "Please do not submit a purchase again before contacting the administrator" %}</strong></p>

{% if reference != "none" %}
<p>{% trans "For your reference, the payment ID is:" %} <strong>{{ reference }}</strong></p>
{% endif %}
</div>

{% endblock %}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{% extends "edx/base.html" %}
{% load i18n %}

{% block content %}
<style>
.centered-content {
text-align: center;
margin-top: 2em;
}
</style>
<div class="centered-content">
<h1>{% trans "This is unfortunate and not expected!" %}</h1>

<p>{% trans "The payment has been processed successfully, but we had an unexpected error while submitting the payment information to the server. The administrator has been notified and will investigate the issue." %}</p>

<p><strong>{% trans "Please do not submit a purchase again before contacting the administrator" %}</strong></p>

<p>{% trans "For your reference, the payment ID is:" %} <strong>{{ merchant_reference }}</strong></p>
</div>

{% endblock %}
Loading

0 comments on commit 02b3eb1

Please sign in to comment.