Skip to content

Commit

Permalink
Merge pull request #4921 from open-formulieren/feature/3457-refactor-…
Browse files Browse the repository at this point in the history
…payment-plugins

Refactor payments module to apply plugin mechanism consistently
  • Loading branch information
sergei-maertens authored Dec 16, 2024
2 parents 753691c + e9aa4d7 commit c07912d
Show file tree
Hide file tree
Showing 14 changed files with 132 additions and 108 deletions.
7 changes: 5 additions & 2 deletions pyright.pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ include = [
"src/openforms/dmn/registry.py",
"src/openforms/formio/registry.py",
"src/openforms/formio/rendering/registry.py",
"src/openforms/payments/registry.py",
"src/openforms/pre_requests/base.py",
"src/openforms/pre_requests/registry.py",
"src/openforms/prefill/base.py",
Expand All @@ -28,7 +27,7 @@ include = [
# Core forms app
"src/openforms/forms/api/serializers/logic/action_serializers.py",
# Payments
"src/openforms/payments/models.py",
"src/openforms/payments/",
# Interaction with the outside world
"src/openforms/contrib/zgw/service.py",
"src/openforms/contrib/objects_api/",
Expand Down Expand Up @@ -60,6 +59,10 @@ exclude = [
"src/openforms/contrib/objects_api/tests/",
"src/openforms/contrib/objects_api/json_schema.py",
"src/openforms/formio/formatters/tests/",
"src/openforms/payments/management/commands/checkpaymentemaildupes.py",
"src/openforms/payments/tests/",
"src/openforms/payments/contrib/demo/tests/",
"src/openforms/payments/contrib/ogone/tests/",
"src/openforms/registrations/contrib/zgw_apis/tests/test_backend_partial_failure.py",
"src/openforms/registrations/contrib/zgw_apis/tests/test_utils.py",
]
Expand Down
2 changes: 0 additions & 2 deletions src/openforms/forms/admin/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

from openforms.api.utils import underscore_to_camel
from openforms.emails.models import ConfirmationEmailTemplate
from openforms.payments.admin import PaymentBackendChoiceFieldMixin
from openforms.registrations.admin import RegistrationBackendFieldMixin
from openforms.typing import StrOrPromise
from openforms.utils.expressions import FirstNotBlank
Expand Down Expand Up @@ -127,7 +126,6 @@ def expected_parameters(self):
class FormAdmin(
FormioConfigMixin,
RegistrationBackendFieldMixin,
PaymentBackendChoiceFieldMixin,
OrderedInlineModelAdminMixin,
admin.ModelAdmin,
):
Expand Down
14 changes: 0 additions & 14 deletions src/openforms/payments/admin.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,8 @@
from django.contrib import admin

from .fields import PaymentBackendChoiceField
from .models import SubmissionPayment


class PaymentBackendChoiceFieldMixin:
def formfield_for_dbfield(self, db_field, request, **kwargs):
if isinstance(db_field, PaymentBackendChoiceField):
assert not db_field.choices
_old = db_field.choices
db_field.choices = db_field._get_plugin_choices()
field = super().formfield_for_dbfield(db_field, request, **kwargs)
db_field.choices = _old
return field

return super().formfield_for_dbfield(db_field, request, **kwargs)


@admin.register(SubmissionPayment)
class SubmissionPaymentAdmin(admin.ModelAdmin):
fields = (
Expand Down
7 changes: 5 additions & 2 deletions src/openforms/payments/api/fields.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
from rest_framework import serializers

from openforms.forms.models import Form

from ..registry import register as payment_register
from .serializers import PaymentOptionSerializer

Expand All @@ -20,7 +22,8 @@ def __init__(self, *args, **kwargs):
def to_internal_value(self, data):
raise NotImplementedError("read only")

def to_representation(self, form):
def to_representation(self, value):
assert isinstance(value, Form)
request = self.context["request"]
temp = payment_register.get_options(request, form)
temp = payment_register.get_options(request, value)
return super().to_representation(temp)
4 changes: 2 additions & 2 deletions src/openforms/payments/api/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ class PaymentPluginSerializer(PluginBaseSerializer):
class PaymentOptionSerializer(serializers.Serializer):
# serializer for form
identifier = serializers.CharField(label=_("Identifier"), read_only=True)
label = serializers.CharField(
label = serializers.CharField( # pyright: ignore[reportAssignmentType]
label=_("Button label"), help_text=_("Button label"), read_only=True
)

Expand All @@ -34,7 +34,7 @@ class PaymentInfoSerializer(serializers.Serializer):
label=_("Request type"), choices=PaymentRequestType.choices, read_only=True
)
url = serializers.URLField(label=_("URL"), read_only=True)
data = serializers.DictField(
data = serializers.DictField( # pyright: ignore[reportAssignmentType,reportIncompatibleMethodOverride]
label=_("Data"),
child=serializers.CharField(label=_("Value"), read_only=True),
read_only=True,
Expand Down
29 changes: 20 additions & 9 deletions src/openforms/payments/base.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING, Mapping
from typing import TYPE_CHECKING, Mapping, TypedDict

from django.http import HttpRequest, HttpResponse

from rest_framework import serializers
from rest_framework.request import Request

from openforms.plugins.plugin import AbstractBasePlugin
from openforms.utils.mixins import JsonSchemaSerializerMixin
Expand Down Expand Up @@ -32,45 +35,53 @@ class EmptyOptions(JsonSchemaSerializerMixin, serializers.Serializer):
pass


class BasePlugin(AbstractBasePlugin):
class Options(TypedDict):
pass


class BasePlugin[OptionsT: Options](AbstractBasePlugin):
return_method = "GET"
webhook_method = "POST"
configuration_options = EmptyOptions
configuration_options: type[serializers.Serializer] = EmptyOptions

# override

def start_payment(
self,
request: HttpRequest,
payment: "SubmissionPayment",
payment: SubmissionPayment,
options: OptionsT,
) -> PaymentInfo:
raise NotImplementedError()

def handle_return(
self, request: HttpRequest, payment: "SubmissionPayment"
self,
request: Request,
payment: SubmissionPayment,
options: OptionsT,
) -> HttpResponse:
raise NotImplementedError()

def handle_webhook(self, request: HttpRequest) -> "SubmissionPayment":
def handle_webhook(self, request: Request) -> SubmissionPayment:
raise NotImplementedError()

# helpers

def get_start_url(self, request: HttpRequest, submission: "Submission") -> str:
def get_start_url(self, request: HttpRequest, submission: Submission) -> str:
return reverse_plus(
"payments:start",
kwargs={"uuid": submission.uuid, "plugin_id": self.identifier},
request=request,
)

def get_return_url(self, request: HttpRequest, payment: "SubmissionPayment") -> str:
def get_return_url(self, request: HttpRequest, payment: SubmissionPayment) -> str:
return reverse_plus(
"payments:return",
kwargs={"uuid": payment.uuid},
request=request,
)

def get_webhook_url(self, request: HttpRequest) -> str:
def get_webhook_url(self, request: HttpRequest | None) -> str:
return reverse_plus(
"payments:webhook",
kwargs={"plugin_id": self.identifier},
Expand Down
12 changes: 9 additions & 3 deletions src/openforms/payments/contrib/demo/plugin.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import TypedDict

from django.http import HttpResponseRedirect
from django.utils.translation import gettext_lazy as _

Expand All @@ -11,16 +13,20 @@
from ...registry import register


class NoOptions(TypedDict):
pass


@register("demo")
class DemoPayment(BasePlugin):
class DemoPayment(BasePlugin[NoOptions]):
verbose_name = _("Demo")
is_demo_plugin = True

def start_payment(self, request, payment):
def start_payment(self, request, payment, options):
url = self.get_return_url(request, payment)
return PaymentInfo(url=url, data={})

def handle_return(self, request, payment):
def handle_return(self, request, payment, options):
payment.status = PaymentStatus.completed
payment.save()

Expand Down
77 changes: 36 additions & 41 deletions src/openforms/payments/contrib/ogone/constants.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
from typing import cast

from django.db import models
from django.utils.translation import gettext_lazy as _

Expand Down Expand Up @@ -60,45 +58,42 @@ def as_payment_status(cls, ogone_status: str) -> str:
return OGONE_TO_PAYMENT_STATUS[ogone_status]


OGONE_TO_PAYMENT_STATUS = cast(
dict[str, str],
{
OgoneStatus.invalid_or_incomplete: PaymentStatus.failed.value,
OgoneStatus.cancelled_by_customer: PaymentStatus.failed.value,
OgoneStatus.authorisation_declined: PaymentStatus.failed.value,
OgoneStatus.waiting_for_client_payment: PaymentStatus.processing.value,
OgoneStatus.waiting_authentication: PaymentStatus.processing.value,
OgoneStatus.authorised: PaymentStatus.processing.value,
OgoneStatus.authorised_waiting_external_result: PaymentStatus.processing.value,
OgoneStatus.authorisation_waiting: PaymentStatus.processing.value,
OgoneStatus.authorisation_not_known: PaymentStatus.processing.value,
OgoneStatus.standby: PaymentStatus.processing.value,
OgoneStatus.ok_with_scheduled_payments: PaymentStatus.processing.value,
OgoneStatus.not_ok_with_scheduled_payments: PaymentStatus.failed.value,
OgoneStatus.authorised_and_cancelled: PaymentStatus.failed.value,
OgoneStatus.author_deletion_waiting: PaymentStatus.failed.value,
OgoneStatus.author_deletion_uncertain: PaymentStatus.failed.value,
OgoneStatus.author_deletion_refused: PaymentStatus.failed.value,
OgoneStatus.payment_deleted: PaymentStatus.failed.value,
OgoneStatus.payment_deletion_pending: PaymentStatus.failed.value,
OgoneStatus.payment_deletion_uncertain: PaymentStatus.failed.value,
OgoneStatus.payment_deletion_refused: PaymentStatus.failed.value,
OgoneStatus.payment_deleted2: PaymentStatus.failed, # doubl.valuee
OgoneStatus.refund: PaymentStatus.failed.value,
OgoneStatus.refund_pending: PaymentStatus.failed.value,
OgoneStatus.refund_uncertain: PaymentStatus.failed.value,
OgoneStatus.refund_refused: PaymentStatus.failed.value,
OgoneStatus.payment_declined_by_the_acquirer: PaymentStatus.failed.value,
OgoneStatus.refund_processed_by_merchant: PaymentStatus.failed.value,
OgoneStatus.payment_requested: PaymentStatus.completed.value,
OgoneStatus.payment_processing: PaymentStatus.processing.value,
OgoneStatus.payment_uncertain: PaymentStatus.processing.value,
OgoneStatus.payment_refused: PaymentStatus.failed.value,
OgoneStatus.refund_declined_by_the_acquirer: PaymentStatus.failed.value,
OgoneStatus.payment_processed_by_merchant: PaymentStatus.failed.value,
OgoneStatus.being_processed: PaymentStatus.processing.value,
},
)
OGONE_TO_PAYMENT_STATUS: dict[str, str] = {
OgoneStatus.invalid_or_incomplete.value: PaymentStatus.failed.value,
OgoneStatus.cancelled_by_customer.value: PaymentStatus.failed.value,
OgoneStatus.authorisation_declined.value: PaymentStatus.failed.value,
OgoneStatus.waiting_for_client_payment.value: PaymentStatus.processing.value,
OgoneStatus.waiting_authentication.value: PaymentStatus.processing.value,
OgoneStatus.authorised.value: PaymentStatus.processing.value,
OgoneStatus.authorised_waiting_external_result.value: PaymentStatus.processing.value,
OgoneStatus.authorisation_waiting.value: PaymentStatus.processing.value,
OgoneStatus.authorisation_not_known.value: PaymentStatus.processing.value,
OgoneStatus.standby.value: PaymentStatus.processing.value,
OgoneStatus.ok_with_scheduled_payments.value: PaymentStatus.processing.value,
OgoneStatus.not_ok_with_scheduled_payments.value: PaymentStatus.failed.value,
OgoneStatus.authorised_and_cancelled.value: PaymentStatus.failed.value,
OgoneStatus.author_deletion_waiting.value: PaymentStatus.failed.value,
OgoneStatus.author_deletion_uncertain.value: PaymentStatus.failed.value,
OgoneStatus.author_deletion_refused.value: PaymentStatus.failed.value,
OgoneStatus.payment_deleted.value: PaymentStatus.failed.value,
OgoneStatus.payment_deletion_pending.value: PaymentStatus.failed.value,
OgoneStatus.payment_deletion_uncertain.value: PaymentStatus.failed.value,
OgoneStatus.payment_deletion_refused.value: PaymentStatus.failed.value,
OgoneStatus.payment_deleted2.value: PaymentStatus.failed, # doubl.valuee
OgoneStatus.refund.value: PaymentStatus.failed.value,
OgoneStatus.refund_pending.value: PaymentStatus.failed.value,
OgoneStatus.refund_uncertain.value: PaymentStatus.failed.value,
OgoneStatus.refund_refused.value: PaymentStatus.failed.value,
OgoneStatus.payment_declined_by_the_acquirer.value: PaymentStatus.failed.value,
OgoneStatus.refund_processed_by_merchant.value: PaymentStatus.failed.value,
OgoneStatus.payment_requested.value: PaymentStatus.completed.value,
OgoneStatus.payment_processing.value: PaymentStatus.processing.value,
OgoneStatus.payment_uncertain.value: PaymentStatus.processing.value,
OgoneStatus.payment_refused.value: PaymentStatus.failed.value,
OgoneStatus.refund_declined_by_the_acquirer.value: PaymentStatus.failed.value,
OgoneStatus.payment_processed_by_merchant.value: PaymentStatus.failed.value,
OgoneStatus.being_processed.value: PaymentStatus.processing.value,
}

assert set(OgoneStatus.values) == set(
OGONE_TO_PAYMENT_STATUS.keys()
Expand Down
42 changes: 21 additions & 21 deletions src/openforms/payments/contrib/ogone/plugin.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import logging

from django.http import HttpResponseBadRequest, HttpResponseRedirect
from django.http import HttpRequest, HttpResponseBadRequest, HttpResponseRedirect
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _

import requests
from rest_framework import serializers
from rest_framework.exceptions import ParseError
from rest_framework.request import Request

from openforms.api.fields import PrimaryKeyRelatedAsChoicesField
from openforms.config.data import Entry
Expand All @@ -18,12 +19,13 @@

from ...base import BasePlugin
from ...constants import PAYMENT_STATUS_FINAL, UserAction
from ...contrib.ogone.client import OgoneClient
from ...contrib.ogone.constants import OgoneStatus
from ...contrib.ogone.exceptions import InvalidSignature
from ...contrib.ogone.models import OgoneMerchant
from ...models import SubmissionPayment
from ...registry import register
from .client import OgoneClient
from .constants import OgoneStatus
from .exceptions import InvalidSignature
from .models import OgoneMerchant
from .typing import PaymentOptions

logger = logging.getLogger(__name__)

Expand All @@ -41,18 +43,17 @@ class OgoneOptionsSerializer(JsonSchemaSerializerMixin, serializers.Serializer):


@register("ogone-legacy")
class OgoneLegacyPaymentPlugin(BasePlugin):
class OgoneLegacyPaymentPlugin(BasePlugin[PaymentOptions]):
verbose_name = _("Ogone legacy")
configuration_options = OgoneOptionsSerializer

def start_payment(self, request, payment: SubmissionPayment):
def start_payment(
self, request: HttpRequest, payment: SubmissionPayment, options: PaymentOptions
):
# decimal to cents
amount_cents = int((payment.amount * 100).to_integral_exact())

merchant = get_object_or_404(
OgoneMerchant, id=payment.plugin_options["merchant_id"]
)
client = OgoneClient(merchant)
client = OgoneClient(options["merchant_id"])

return_url = self.get_return_url(request, payment)

Expand All @@ -69,14 +70,13 @@ def start_payment(self, request, payment: SubmissionPayment):
)
return info

def handle_return(self, request, payment: SubmissionPayment):
def handle_return(
self, request: Request, payment: SubmissionPayment, options: PaymentOptions
):
action = request.query_params.get(RETURN_ACTION_PARAM)
payment_id = request.query_params[PAYMENT_ID_PARAM]

merchant = get_object_or_404(
OgoneMerchant, id=payment.plugin_options["merchant_id"]
)
client = OgoneClient(merchant)
client = OgoneClient(options["merchant_id"])

try:
params = client.get_validated_params(request.query_params)
Expand Down Expand Up @@ -107,7 +107,7 @@ def handle_return(self, request, payment: SubmissionPayment):
)
return HttpResponseRedirect(redirect_url)

def handle_webhook(self, request):
def handle_webhook(self, request: Request):
# unvalidated data
order_id = case_insensitive_get(request.data, "orderID")
if not order_id:
Expand All @@ -120,10 +120,10 @@ def handle_webhook(self, request):
raise ParseError("missing PAYID")

payment = get_object_or_404(SubmissionPayment, public_order_id=order_id)
merchant = get_object_or_404(
OgoneMerchant, id=payment.plugin_options["merchant_id"]
)
client = OgoneClient(merchant)
options_serializer = self.configuration_options(data=payment.plugin_options)
options_serializer.is_valid(raise_exception=True)
options: PaymentOptions = options_serializer.validated_data
client = OgoneClient(options["merchant_id"])

try:
params = client.get_validated_params(request.data)
Expand Down
Loading

0 comments on commit c07912d

Please sign in to comment.