Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor payments module to apply plugin mechanism consistently #4921

Merged
merged 2 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

irrelevant/unused because we only support the React-based change form page.

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,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this stays HttpRequest because it's called outside of DRF views too

payment: "SubmissionPayment",
payment: SubmissionPayment,
options: OptionsT,
) -> PaymentInfo:
raise NotImplementedError()

def handle_return(
self, request: HttpRequest, payment: "SubmissionPayment"
self,
request: Request,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed to DRF request because plugin code is accessing DRF-specific properties that don't exist on HttpRequest

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
Loading