diff --git a/docs/backends.rst b/docs/backends.rst
index 473efb5c..a7f85743 100644
--- a/docs/backends.rst
+++ b/docs/backends.rst
@@ -16,6 +16,11 @@ This can be overridden by defining ``PROCESSOR_ID`` in the settings block.
'PROCESSOR_ID': 1
}
+Flutterwave configuration
+-------------------------
+
+.. automodule:: saas.backends.flutterwave_processor
+
Razorpay configuration
----------------------
diff --git a/saas/backends/flutterwave_processor.py b/saas/backends/flutterwave_processor.py
new file mode 100644
index 00000000..a7156f5d
--- /dev/null
+++ b/saas/backends/flutterwave_processor.py
@@ -0,0 +1,204 @@
+# Copyright (c) 2024, DjaoDjin inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# 2. Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
+# TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
+# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
+# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
+# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+"""
+Beware the Flutterwave backend is currently experimental!
+
+Install Flutterwave pip package
+
+.. code-block:: shell
+
+ $ pip install rave_python
+
+Go to your `Flutterwave `_ dashboard "API Keys",
+then copy/paste the keys into your project settings.py
+
+.. code-block:: python
+
+ SAAS = {
+ 'PROCESSOR': {
+ 'BACKEND': 'saas.backends.flutterwave_processor.FlutterwaveBackend',
+ 'PRIV_KEY': "...",
+ 'PUB_KEY': "...",
+ }
+ }
+
+The backend relies on `Flutterwave Inline `_
+such that credit cards numbers are never posted to the application server
+running djaodjin-saas (PCI compliance).
+"""
+import logging
+
+from rave_python import Rave, RaveExceptions
+
+from . import CardError
+from .. import settings
+from ..compat import six
+from ..utils import datetime_or_now, generate_random_slug
+
+
+LOGGER = logging.getLogger(__name__)
+
+
+class FlutterwaveBackend(object):
+
+ token_id = 'stripeToken'
+
+ def __init__(self):
+ self.pub_key = settings.PROCESSOR.get('PUB_KEY', None)
+ self.priv_key = settings.PROCESSOR.get("PRIV_KEY", None)
+
+
+ def charge_distribution(self, charge,
+ refunded=0, orig_total_broker_fee_amount=0,
+ unit=settings.DEFAULT_UNIT):
+ #pylint:disable=unused-argument
+ # Stripe processing fee associated to a transaction
+ # is 2.9% + 30 cents.
+ # Stripe rounds up so we do the same here. Be careful Python 3.x
+ # semantics are broken and will return a float instead of a int.
+ processor_fee_unit = unit
+ available_amount = charge.amount - refunded
+ if available_amount > 0:
+ # integer division
+ processor_fee_amount = (available_amount * 290 + 5000) // 10000 + 30
+ assert isinstance(processor_fee_amount, six.integer_types)
+ else:
+ processor_fee_amount = 0
+ distribute_amount = available_amount - processor_fee_amount
+ distribute_unit = charge.unit
+ broker_fee_amount = 0
+ broker_fee_unit = charge.unit
+ return (distribute_amount, distribute_unit,
+ processor_fee_amount, processor_fee_unit,
+ broker_fee_amount, broker_fee_unit)
+
+
+ def create_payment(self, amount, unit, token,
+ processor_card_key=None,
+ descr=None, stmt_descr=None, created_at=None,
+ broker_fee_amount=0, provider=None, broker=None):
+ #pylint: disable=too-many-arguments,unused-argument
+ rave = Rave(self.pub_key, self.priv_key,
+ usingEnv=False)
+ LOGGER.debug(
+ "[FlutterwaveBackend.create_payment] create_payment(token=%s)",
+ token)
+ charge_key = None
+ created_at = None
+ receipt_info = {}
+ try:
+ resp = rave.Card.verify(token)
+ LOGGER.info(
+ "[Flutterwave verification response for '%s': %s",
+ token, resp)
+ charge_key = resp['flwRef']
+ created_at = datetime_or_now(resp['meta'][0]['createdAt'])
+ if not resp.transactionComplete:
+ raise CardError("Could not complete transaction",
+ resp['chargecode'],
+ charge_processor_key=charge_key)
+ except RaveExceptions.TransactionVerificationError as err:
+ raise CardError(str(err), err.code,
+ charge_processor_key=err.flwRef,
+ backend_except=err)
+
+ return (charge_key, created_at, receipt_info)
+
+
+ def create_transfer(self, provider, amount, unit, descr=None):
+ """
+ Transfer *amount* into the organization bank account.
+ """
+ raise NotImplementedError()
+
+
+ def create_or_update_card(self, subscriber, token,
+ user=None, provider=None, broker=None):
+ """
+ Create or update a card associated to a subscriber.
+ """
+ #pylint:disable=too-many-arguments
+ raise NotImplementedError()
+
+
+ def delete_card(self, subscriber, broker=None):
+ """
+ Removes a card associated to an subscriber.
+ """
+ raise NotImplementedError()
+
+
+ def get_payment_context(self, subscriber,
+ amount=None, unit=None, broker_fee_amount=0,
+ provider=None, broker=None):
+ #pylint:disable=too-many-arguments,unused-argument
+ context = {
+ 'FLUTTERWAVE_PUB_KEY': self.pub_key,
+ 'flutterwave_invoice_id': generate_random_slug()
+ }
+ return context
+
+
+ def reconcile_transfers(self, provider, created_at, dry_run=False):
+ #pylint:disable=unused-argument
+ raise NotImplementedError(
+ "reconcile_transfers is not implemented on FakeProcessor")
+
+
+
+ def refund_charge(self, charge, amount, broker_amount=0):
+ """
+ Refund a charge on the associated card.
+ """
+ raise NotImplementedError()
+
+
+
+ def retrieve_charge(self, charge):
+ if charge.is_progress:
+ charge.payment_successful()
+ return charge
+
+
+
+ def dispute_fee(self, amount): #pylint: disable=unused-argument
+ """
+ Return processing fee associated to a chargeback (i.e. $15).
+ """
+ raise NotImplementedError()
+
+
+ def prorate_transfer(self, amount, provider):
+ """
+ Return processing fee associated to a transfer (i.e. nothing here).
+ """
+ #pylint: disable=unused-argument
+ raise NotImplementedError()
+
+
+ def retrieve_card(self, subscriber, broker=None):
+ #pylint:disable=unused-argument
+ context = {}
+ return context
diff --git a/saas/templates/saas/_flutterwave_checkout.html b/saas/templates/saas/_flutterwave_checkout.html
new file mode 100644
index 00000000..162cfe2d
--- /dev/null
+++ b/saas/templates/saas/_flutterwave_checkout.html
@@ -0,0 +1,38 @@
+
+
+
+
+ {% if submit_title %}{{submit_title}}{% else %}Pay Now{% endif %}
+
diff --git a/saas/templates/saas/billing/cart.html b/saas/templates/saas/billing/cart.html
index 8f279239..f28234c3 100644
--- a/saas/templates/saas/billing/cart.html
+++ b/saas/templates/saas/billing/cart.html
@@ -41,9 +41,12 @@ {% block order_title %}Place Order{% endblock %}
{% elif STRIPE_PUB_KEY %}
{% include "saas/_card_use.html" %}
+ {% elif FLUTTERWAVE_PUB_KEY %}
+ {% include "saas/_flutterwave_checkout.html" %}
{% else %}
-Either variables RAZORPAY_PUB_KEY or STRIPE_PUB_KEY must be defined.
+Either variables FLUTTERWAVE_PUB_KEY, RAZORPAY_PUB_KEY, or STRIPE_PUB_KEY
+must be defined.
{% endif %}
{% endblock %}
diff --git a/testsite/settings.py b/testsite/settings.py
index 5df6f27d..39cdf37d 100644
--- a/testsite/settings.py
+++ b/testsite/settings.py
@@ -194,22 +194,29 @@ def load_config(confpath):
# Configuration of djaodjin-saas
SAAS = {
- 'BROKER': {
- 'GET_INSTANCE': 'cowork',
- },
- 'PLATFORM_NAME': 'cowork',
- 'PROCESSOR': {
- 'BACKEND': 'saas.backends.stripe_processor.StripeBackend',
- 'MODE': 0, # `LOCAL`
- 'PRIV_KEY': getattr(sys.modules[__name__], "STRIPE_PRIV_KEY", None),
- 'PUB_KEY': getattr(sys.modules[__name__], "STRIPE_PUB_KEY", None),
- 'CLIENT_ID': getattr(sys.modules[__name__], "STRIPE_CLIENT_ID", None),
- 'WEBHOOK_SECRET': getattr(
- sys.modules[__name__], "STRIPE_ENDPOINT_SECRET", None),
-# Comment above and uncomment below to use RazorPay instead.
-# 'BACKEND': 'saas.backends.razorpay_processor.RazorpayBackend',
-# 'PRIV_KEY': getattr(sys.modules[__name__], "RAZORPAY_PRIV_KEY", None),
-# 'PUB_KEY': getattr(sys.modules[__name__], "RAZORPAY_PUB_KEY", None),
+ 'BROKER': {
+ 'GET_INSTANCE': 'cowork',
+ },
+ 'PLATFORM_NAME': 'cowork',
+ 'PROCESSOR': {
+ 'BACKEND': 'saas.backends.stripe_processor.StripeBackend',
+ 'MODE': 0, # `LOCAL`
+ 'PRIV_KEY': getattr(sys.modules[__name__], "STRIPE_PRIV_KEY", None),
+ 'PUB_KEY': getattr(sys.modules[__name__], "STRIPE_PUB_KEY", None),
+ 'CLIENT_ID': getattr(sys.modules[__name__], "STRIPE_CLIENT_ID", None),
+ 'WEBHOOK_SECRET': getattr(
+ sys.modules[__name__], "STRIPE_ENDPOINT_SECRET", None),
+
+ # Comment above and uncomment below to use RazorPay instead.
+# 'BACKEND': 'saas.backends.razorpay_processor.RazorpayBackend',
+# 'PRIV_KEY': getattr(sys.modules[__name__], "RAZORPAY_PRIV_KEY", None),
+# 'PUB_KEY': getattr(sys.modules[__name__], "RAZORPAY_PUB_KEY", None),
+
+ # Comment above and uncomment below to use RazorPay instead.
+# 'BACKEND': 'saas.backends.flutterwave_processor.FlutterwaveBackend',
+# 'PRIV_KEY': getattr(sys.modules[__name__],
+# "FLUTTERWAVE_PRIV_KEY", None),
+# 'PUB_KEY': getattr(sys.modules[__name__], "FLUTTERWAVE_PUB_KEY", None),
},
'EXPIRE_NOTICE_DAYS': [90, 60, 30, 15, 1],
}