Skip to content

Commit

Permalink
Some more refactoring.
Browse files Browse the repository at this point in the history
  • Loading branch information
dekoza committed Apr 12, 2020
1 parent 88fe9c4 commit 6b713d7
Show file tree
Hide file tree
Showing 14 changed files with 149 additions and 80 deletions.
5 changes: 3 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Welcome to django-getpaid
:target: https://pypi.org/project/django-getpaid/


django-getpaid is a multi-broker payment processor for Django
django-getpaid is payment processing framework for Django

Documentation
=============
Expand All @@ -26,10 +26,11 @@ Features
========

* support for multiple payment brokers at the same time
* clean but flexible architecture
* very flexible architecture
* support for asynchronous status updates - both push and pull
* support for modern REST-based broker APIs
* support for multiple currencies (but one per payment)
* support for global and per-plugin validators
* easy customization with provided base abstract models and swappable mechanic (same as with Django's User model)


Expand Down
6 changes: 4 additions & 2 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ Welcome to django-getpaid's documentation!
**django-getpaid** is a multi-broker payment processor for Django. Main features include:

* support for multiple payment brokers at the same time
* clean but flexible architecture
* very flexible architecture
* support for asynchronous status updates - both push and pull
* support for using multiple currencies (but one per payment)
* support for modern REST-based broker APIs
* support for multiple currencies (but one per payment)
* support for global and per-plugin validators
* easy customization with provided base abstract models and swappable mechanic (same as with Django's User model)

We would like to provide a :doc:`catalog<catalog>` of plugins for ``django-getpaid`` - if you create a plugin please let us know.
Expand Down
10 changes: 3 additions & 7 deletions docs/roadmap.rst
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
Planned features
================

2.1
---
These features are planned for future versions (in no particular order):

* paywall communication log (all responses and callbacks)
* Subscriptions handling
* Complete type annotations


future
------

* cookiecutter for plugins
* django-rest-framework helpers
* async/await support
* admin actions to PULL payment statuses
19 changes: 19 additions & 0 deletions docs/settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,32 @@ plugins requiring such template. You can also use ``POST_TEMPLATE`` key in
:ref:`backend's config<Backend settings>` to override the template just for one backend.


``POST_FORM_CLASS``
-------------------

Default: None

This setting is used by backends that use POST flow.
This setting can be used to provide a global default for such cases if you use more
plugins requiring such template. You can also use ``POST_FORM_CLASS`` key in
:ref:`backend's config<Backend settings>` to override the template just for one backend.
Use full dotted path name.


``SUCCESS_URL``
---------------

Default: ``"getpaid:payment-success"``

Allows setting custom view name for successful returns from paywall.
Again, this can also be set on a per-backend basis.

If the view requires kwargs to be resolved, you need to override

``FAILURE_URL``
---------------

Default: ``"getpaid:payment-failure"``

Allows setting custom view name for fail returns from paywall.
Again, this can also be set on a per-backend basis.
2 changes: 1 addition & 1 deletion example/orders/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class OrderView(DetailView):
def get_context_data(self, **kwargs):
context = super(OrderView, self).get_context_data(**kwargs)
context["payment_form"] = PaymentMethodForm(
initial={"order": self.object, "currency": self.object.currency,}
initial={"order": self.object, "currency": self.object.currency}
)
return context

Expand Down
2 changes: 1 addition & 1 deletion example/paywall/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
app_name = "paywall"

urlpatterns = [
path("fake_gateway/", views.AuthorizationView.as_view(), name="gateway"),
path("fake_gateway/", views.authorization_view, name="gateway"),
]
5 changes: 4 additions & 1 deletion example/paywall/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,11 @@ def form_valid(self, form):
return super().form_valid(form)


authorization_view = csrf_exempt(AuthorizationView.as_view())


@csrf_exempt
def register_payment(request):
def rest_register_payment(request):
legal_fields = [
"payment",
"value",
Expand Down
21 changes: 3 additions & 18 deletions getpaid/backends/dummy/processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from django.template.response import TemplateResponse
from django.urls import reverse

from getpaid.post_forms import PaymentHiddenInputsPostForm
from getpaid.processor import BaseProcessor


Expand All @@ -17,28 +18,12 @@ class PaymentProcessor(BaseProcessor):
display_name = "Dummy"
accepted_currencies = ["PLN", "EUR"]
method = "POST"
template_name = "getpaid_dummy_backend/payment_post_form.html"
post_form_class = PaymentHiddenInputsPostForm
post_template_name = "getpaid_dummy_backend/payment_post_form.html"

def get_paywall_method(self):
return self.get_setting("method") or self.method

def get_template_names(self, view=None) -> list:
template_name = self.get_setting("POST_TEMPLATE")
if template_name is None:
template_name = self.optional_config.get("POST_TEMPLATE")
if template_name is None:
template_name = self.template_name
if template_name is None and hasattr(view, "get_template_names"):
return view.get_template_names()
if template_name is None:
raise ImproperlyConfigured("Couldn't determine template name!")
return [template_name]

def get_form(self, post_data):
from getpaid.forms import PaymentHiddenInputsPostForm

return PaymentHiddenInputsPostForm(items=post_data)

def prepare_paywall_headers(self, params):
token = uuid.uuid4() # do some authentication stuff
return {"Authorization": f"Bearer {token}"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
This is an intermittent form representing POST flow. Confirming will send
a POST request to broker's paywall.
<form action="{{ paywall_url }}" method="post">
{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="{% trans "Confirm payment" %}">
</form>
17 changes: 4 additions & 13 deletions getpaid/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@

from getpaid.validators import run_getpaid_validators

from .registry import registry

Order = swapper.load_model("getpaid", "Order")


class PaymentMethodForm(forms.ModelForm):
"""
Usable example.
Displays all available payments backends as choice list.
"""

Expand All @@ -28,6 +27,8 @@ class Meta:
}

def __init__(self, *args, **kwargs):
from .registry import registry

super().__init__(*args, **kwargs)
order = self.initial.get("order")
currency = getattr(order, "currency", None) or self.data.get("currency")
Expand All @@ -50,19 +51,9 @@ def __init__(self, *args, **kwargs):
def clean_order(self):
if hasattr(self.cleaned_data["order"], "is_ready_for_payment"):
if not self.cleaned_data["order"].is_ready_for_payment():
raise forms.ValidationError(_("Order cannot be paid"))
raise forms.ValidationError(_("Order is not ready for payment."))
return self.cleaned_data["order"]

def clean(self):
cleaned_data = super().clean()
return run_getpaid_validators(cleaned_data)


class PaymentHiddenInputsPostForm(forms.Form):
def __init__(self, items, *args, **kwargs):
super(PaymentHiddenInputsPostForm, self).__init__(*args, **kwargs)

for key in items:
self.fields[key] = forms.CharField(
initial=items[key], widget=forms.HiddenInput
)
64 changes: 35 additions & 29 deletions getpaid/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

import pendulum
import swapper
from django import forms
from django.conf import settings
from django.db import models
from django.shortcuts import resolve_url
Expand All @@ -13,6 +14,14 @@


class AbstractOrder(models.Model):
"""
Please consider setting either primary or secondary key of your Orders to
UUIDField. This way you will hide your volume which is valuable business
information that should be kept hidden. If you set it as secondary key,
remember to use ``dbindex=True`` (primary keys are indexed by default).
Read more: https://docs.djangoproject.com/en/3.0/ref/models/fields/#uuidfield
"""

class Meta:
abstract = True

Expand All @@ -29,14 +38,22 @@ def get_return_url(self, *args, success=None, **kwargs):
def get_absolute_url(self):
"""
Standard method recommended in Django docs. It should return
the URL to see details of particular Order.
the URL to see details of particular Payment (or usually - Order).
"""
raise NotImplementedError

def is_ready_for_payment(self):
"""Most of the validation is made in PaymentMethodForm using but if you
need any extra validation. For example you most probably want to disable
making another payment for order that is already paid."""
"""Most of the validation is made in PaymentMethodForm but if you need
any extra validation. For example you most probably want to disable
making another payment for order that is already paid.
You can raise :class:`~django.forms.ValidationError` if you want more
verbose error message.
"""
if self.payments.exclude(
status__in=[PaymentStatus.FAILED, PaymentStatus.CANCELLED]
).exists():
raise forms.ValidationError(_("Non-failed Payments exist for this Order."))
return True

def get_items(self):
Expand Down Expand Up @@ -257,29 +274,33 @@ def on_success(self, amount=None):
Returns boolean value whether payment was fully paid.
"""
saved = False
if getattr(settings, "USE_TZ", False):
timezone = getattr(settings, "TIME_ZONE", "local")
self.last_payment_on = pendulum.now(timezone)
else:
self.last_payment_on = pendulum.now("UTC")

if amount is not None:
self.amount_paid = amount
self.amount_paid += amount
else:
self.amount_paid = self.amount_required

if self.fully_paid:
self.change_status(PaymentStatus.PAID)
saved = self.change_status(PaymentStatus.PAID)
else:
self.change_status(PaymentStatus.PARTIAL)
saved = self.change_status(PaymentStatus.PARTIAL)

if not saved:
self.save()

return self.fully_paid

def on_failure(self):
"""
Called when payment has failed. By default changes status to 'failed'
"""
self.change_status(PaymentStatus.FAILED)
return self.change_status(PaymentStatus.FAILED)

def get_form(self, *args, **kwargs):
"""
Expand Down Expand Up @@ -342,20 +363,11 @@ def fetch_and_update_status(self):
self.change_status(status)
return status

def lock(self, amount=None):
def lock(self, request=None, amount=None, **kwargs):
"""
Used to lock payment for future charge. Used in flows with manual charging.
Returns locked amount.
Interfaces processor's :meth:`~getpaid.processor.BaseProcessor.lock`.
"""
if amount is None:
amount = self.amount_required
amount_locked = self.processor.lock(amount)
if amount_locked:
self.amount_locked = amount_locked
self.change_status(PaymentStatus.ACCEPTED)
else:
self.on_failure()
return amount_locked
return self.processor.lock(request=request, amount=amount, **kwargs)

def charge_locked(self, amount=None):
"""
Expand Down Expand Up @@ -425,16 +437,10 @@ def finish_refund(self, amount=None):
return self.amount_refunded

def get_return_redirect_url(self, request, success):
fallback_settings = getattr(settings, "GETPAID", {})

if success:
url = self.processor.get_setting(
"SUCCESS_URL", getattr(fallback_settings, "SUCCESS_URL", None)
)
url = self.processor.get_setting("SUCCESS_URL")
else:
url = self.processor.get_setting(
"FAILURE_URL", getattr(fallback_settings, "FAILURE_URL", None)
)
url = self.processor.get_setting("FAILURE_URL")

if url is not None:
# we may want to return to Order summary or smth
Expand All @@ -443,7 +449,7 @@ def get_return_redirect_url(self, request, success):
return resolve_url(self.order.get_return_url(self, success=success))

def get_return_redirect_kwargs(self, request, success):
return {"pk": self.order_id}
return {"pk": self.id}


class Payment(AbstractPayment):
Expand Down
11 changes: 11 additions & 0 deletions getpaid/post_forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
from django import forms


class PaymentHiddenInputsPostForm(forms.Form):
def __init__(self, items, *args, **kwargs):
super().__init__(*args, **kwargs)

for key in items:
self.fields[key] = forms.CharField(
initial=items[key], widget=forms.HiddenInput
)
Loading

0 comments on commit 6b713d7

Please sign in to comment.