Skip to content

Commit

Permalink
Merge pull request #6 from Zeit-Labs/shadinaif/payfort
Browse files Browse the repository at this point in the history
feat: Implement PayFort Payment Processor
  • Loading branch information
shadinaif authored Jun 7, 2024
2 parents dc42625 + 6037552 commit 9e7b7f4
Show file tree
Hide file tree
Showing 40 changed files with 2,952 additions and 395 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 }}"
16 changes: 11 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,16 @@ download_ecommerce_requirements:


tests: ## Run unit and integration tests
tox -e py38

unit_tests: ## Run unit tests
tox -e py38 -- tests/unit
tox -e py38-tests

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

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
5 changes: 4 additions & 1 deletion ecommerce_payfort/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
"""
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


__version__ = '0.1.0'
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 ""
93 changes: 71 additions & 22 deletions ecommerce_payfort/processors.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,90 @@
"""
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),
"customer_ip": utils.get_ip_address(request),
"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.")
38 changes: 38 additions & 0 deletions ecommerce_payfort/templates/payfort_payment/form.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{% 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="customer_ip" value="{{ customer_ip }}">
<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 9e7b7f4

Please sign in to comment.