diff --git a/docs/configuration/general/index.rst b/docs/configuration/general/index.rst index 63293c8898..0eb1ca0d40 100644 --- a/docs/configuration/general/index.rst +++ b/docs/configuration/general/index.rst @@ -12,3 +12,4 @@ General configuration oidc cms_integration virus_scan + payment_flow diff --git a/docs/configuration/general/payment_flow.rst b/docs/configuration/general/payment_flow.rst new file mode 100644 index 0000000000..bad34fb942 --- /dev/null +++ b/docs/configuration/general/payment_flow.rst @@ -0,0 +1,20 @@ +.. _configuration_general_payment_flow: + +============ +Payment flow +============ + +For forms where a payment is required, it is possible to configure when the submission should be registered. +The setting **Wait for payment to register** can be found in the admin in the **General configuration**, +under the header **Registration**. + +If **Wait for payment to register** is checked, then a submission will only be sent to the registration backend when +the payment is completed. Note that if the submission should also be co-signed, then it will be registered only when +both the payment and the co-sign are completed. + +If **Wait for payment to register** is NOT checked, then the submission is registered as soon as it is completed (unless +co-sign is also needed). Then, once payment is completed, the payment status will be updated in the registration backend. +This means, that for registration backends that create a zaak (StUF-ZDS and ZGW registration backends), the +status of the zaak will be updated. In the case of the email registration backend, a second email will be sent to notify +that the payment has been received. For the MSGraph backend, the ``payment_status.txt`` file will be updated to say that +the payment was received. diff --git a/docs/manual/forms/basics.rst b/docs/manual/forms/basics.rst index 672d38fd61..a4788fd3ff 100644 --- a/docs/manual/forms/basics.rst +++ b/docs/manual/forms/basics.rst @@ -207,6 +207,19 @@ dezelfde regels als reguliere **Logica**. Zie ook: :ref:`configuration_payment_index` +Er zijn twee mogelijke flows om inzendingen te registreren voor formulieren waar een betaling nodig is. + +1. Zodra de inzending voltooid is, wordt de inzending naar de registratiebackend gestuurd. Als het wordt betaald, wordt + de status van de betaling in de registratiebackend aangepast. In het geval van de e-mailregistratiebackend, gebeurt + dit door een extra (update-)e-mail te sturen, terwijl voor StUF-ZDS en ZGW registratiebackends, de betaalstatus van de zaak + wordt aangepast. +2. De inzending wordt naar de registratiebackend gestuurd pas nĂ¡ dat de betaling voltooid is. + +De flow kan ingesteld worden in de **Algemene Configuratie**. + +Zie ook: :ref:`configuration_general_payment_flow` + + Gegevens opschonen ------------------ diff --git a/src/openforms/config/admin.py b/src/openforms/config/admin.py index 4aac2b5c26..b1ebe2fd7c 100644 --- a/src/openforms/config/admin.py +++ b/src/openforms/config/admin.py @@ -139,7 +139,10 @@ class GlobalConfigurationAdmin( ), (_("Search engines"), {"fields": ("allow_indexing_form_detail",)}), (_("Plugin configuration"), {"fields": ("plugin_configuration",)}), - (_("Registration"), {"fields": ("registration_attempt_limit",)}), + ( + _("Registration"), + {"fields": ("registration_attempt_limit", "wait_for_payment_to_register")}, + ), ( _("Virus scan"), { diff --git a/src/openforms/config/migrations/0066_globalconfiguration_wait_for_payment_to_register.py b/src/openforms/config/migrations/0066_globalconfiguration_wait_for_payment_to_register.py new file mode 100644 index 0000000000..9b320008ce --- /dev/null +++ b/src/openforms/config/migrations/0066_globalconfiguration_wait_for_payment_to_register.py @@ -0,0 +1,22 @@ +# Generated by Django 3.2.23 on 2023-12-12 08:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("config", "0065_theme_uuid"), + ] + + operations = [ + migrations.AddField( + model_name="globalconfiguration", + name="wait_for_payment_to_register", + field=models.BooleanField( + default=False, + help_text="Should a submission be processed (sent to the registration backend) only after payment has been received?", + verbose_name="wait for payment to register", + ), + ), + ] diff --git a/src/openforms/config/models/config.py b/src/openforms/config/models/config.py index 3cca69fb18..53160d3632 100644 --- a/src/openforms/config/models/config.py +++ b/src/openforms/config/models/config.py @@ -528,6 +528,13 @@ class GlobalConfiguration(SingletonModel): blank=True, default=list, ) + wait_for_payment_to_register = models.BooleanField( + verbose_name=_("wait for payment to register"), + help_text=_( + "Should a submission be processed (sent to the registration backend) only after payment has been received?" + ), + default=False, + ) objects = GlobalConfigurationManager() diff --git a/src/openforms/logging/logevent.py b/src/openforms/logging/logevent.py index 642c0b7ecc..2da10c09ba 100644 --- a/src/openforms/logging/logevent.py +++ b/src/openforms/logging/logevent.py @@ -258,6 +258,10 @@ def registration_payment_update_skip(submission: "Submission"): _create_log(submission, "registration_payment_update_skip") +def registration_skipped_not_yet_paid(submission: "Submission"): + _create_log(submission, "registration_skipped_not_yet_paid") + + # - - - diff --git a/src/openforms/logging/templates/logging/events/registration_skipped_not_yet_paid.txt b/src/openforms/logging/templates/logging/events/registration_skipped_not_yet_paid.txt new file mode 100644 index 0000000000..be68196fea --- /dev/null +++ b/src/openforms/logging/templates/logging/events/registration_skipped_not_yet_paid.txt @@ -0,0 +1,4 @@ +{% load i18n %} +{% blocktrans trimmed with lead=log.fmt_lead %} + {{ lead }}: Skipped registration, because this submission is waiting for a payment. +{% endblocktrans %} diff --git a/src/openforms/payments/tasks.py b/src/openforms/payments/tasks.py index 57406b4c61..c0d6d5b856 100644 --- a/src/openforms/payments/tasks.py +++ b/src/openforms/payments/tasks.py @@ -30,7 +30,7 @@ def update_submission_payment_status( submission = Submission.objects.select_related("auth_info").get(id=submission_id) config = GlobalConfiguration.get_solo() - should_skip = any( + should_skip = config.wait_for_payment_to_register or any( ( submission.registration_status != RegistrationStatuses.success, not submission.payment_required, diff --git a/src/openforms/registrations/tasks.py b/src/openforms/registrations/tasks.py index bf85325eed..53f5b8d66a 100644 --- a/src/openforms/registrations/tasks.py +++ b/src/openforms/registrations/tasks.py @@ -152,6 +152,18 @@ def register_submission(submission_id: int, event: PostSubmissionEvents) -> None return config = GlobalConfiguration.get_solo() + if ( + config.wait_for_payment_to_register + and submission.payment_required + and not submission.payment_user_has_paid + ): + logger.debug( + "Skipping registration for submission '%s' as the payment hasn't been received yet.", + submission, + ) + logevent.registration_skipped_not_yet_paid(submission) + return + if submission.registration_attempts >= config.registration_attempt_limit: # if it fails after this many attempts we give up logevent.registration_attempts_limited(submission) diff --git a/src/openforms/submissions/tests/test_post_submission_event.py b/src/openforms/submissions/tests/test_post_submission_event.py index 523dd68456..1d7a1bd988 100644 --- a/src/openforms/submissions/tests/test_post_submission_event.py +++ b/src/openforms/submissions/tests/test_post_submission_event.py @@ -5,10 +5,12 @@ from django.utils.translation import gettext_lazy as _ from privates.test import temp_private_root +from testfixtures import LogCapture from openforms.appointments.exceptions import AppointmentRegistrationFailed from openforms.appointments.tests.utils import setup_jcc from openforms.authentication.constants import AuthAttribute +from openforms.config.models import GlobalConfiguration from openforms.emails.tests.factories import ConfirmationEmailTemplateFactory from openforms.forms.tests.factories import FormDefinitionFactory from openforms.payments.constants import PaymentStatus @@ -211,6 +213,10 @@ def test_submission_completed_payment_needed(self): patch( "openforms.registrations.contrib.email.plugin.EmailRegistration.update_payment_status" ) as mock_payment_status_update, + patch( + "openforms.registrations.tasks.GlobalConfiguration.get_solo", + return_value=GlobalConfiguration(wait_for_payment_to_register=False), + ), ): on_post_submission_event(submission.id, PostSubmissionEvents.on_completion) @@ -447,6 +453,10 @@ def test_cosign_done_payment_needed_not_done(self): patch( "openforms.registrations.contrib.email.plugin.EmailRegistration.update_payment_status" ) as mock_payment_status_update, + patch( + "openforms.registrations.tasks.GlobalConfiguration.get_solo", + return_value=GlobalConfiguration(wait_for_payment_to_register=False), + ), ): with self.captureOnCommitCallbacks(execute=True): on_post_submission_event( @@ -597,6 +607,10 @@ def test_payment_done_cosign_not_needed(self): patch( "openforms.registrations.contrib.email.plugin.EmailRegistration.update_payment_status" ) as mock_payment_status_update, + patch( + "openforms.registrations.tasks.GlobalConfiguration.get_solo", + return_value=GlobalConfiguration(wait_for_payment_to_register=False), + ), ): with self.captureOnCommitCallbacks(execute=True): on_post_submission_event( @@ -967,3 +981,175 @@ def test_cosign_not_required_but_filled_in_does_not_proceed_with_registration(se self.assertTrue(submission.cosign_request_email_sent) self.assertTrue(submission.confirmation_email_sent) self.assertEqual(submission.auth_info.value, "111222333") + + +@temp_private_root() +@override_settings(CELERY_TASK_ALWAYS_EAGER=True) +class PaymentFlowTests(TestCase): + def test_payment_required_and_not_should_wait_for_registration(self): + """ + The payment is required and has not been completed, and the general configuration says to NOT skip registration if + the payment has not been completed. + """ + submission = SubmissionFactory.from_components( + components_list=[ + { + "key": "email", + "type": "email", + "label": "Email", + "confirmationRecipient": True, + } + ], + completed=True, + form__registration_backend="email", + form__registration_backend_options={"to_emails": ["test@registration.nl"]}, + form__product__price=10, + form__payment_backend="demo", + ) + SubmissionPaymentFactory.create( + submission=submission, amount=10, status=PaymentStatus.started + ) + + with ( + patch( + "openforms.registrations.contrib.email.plugin.EmailRegistration.register_submission" + ) as mock_registration, + patch( + "openforms.registrations.tasks.GlobalConfiguration.get_solo", + return_value=GlobalConfiguration(wait_for_payment_to_register=True), + ), + LogCapture() as logs, + ): + on_post_submission_event(submission.id, PostSubmissionEvents.on_completion) + + mock_registration.assert_not_called() + logs.check_present( + ( + "openforms.registrations.tasks", + "DEBUG", + f"Skipping registration for submission '{submission}' as the payment hasn't been received yet.", + ) + ) + + def test_payment_done_and_should_wait_for_payment( + self, + ): + """ + The payment is required and has been completed, so registration should not be skipped regardless of the general + configuration setting. + """ + submission = SubmissionFactory.from_components( + components_list=[ + { + "key": "email", + "type": "email", + "label": "Email", + "confirmationRecipient": True, + } + ], + form__registration_backend="email", + form__registration_backend_options={"to_emails": ["test@registration.nl"]}, + form__product__price=10, + form__payment_backend="demo", + completed=True, + with_completed_payment=True, + ) + + with ( + patch( + "openforms.registrations.contrib.email.plugin.EmailRegistration.register_submission" + ) as mock_registration, + patch( + "openforms.registrations.tasks.GlobalConfiguration.get_solo", + return_value=GlobalConfiguration(wait_for_payment_to_register=True), + ), + patch( + "openforms.payments.tasks.update_submission_payment_registration" + ) as mock_update_payment_status, + ): + on_post_submission_event(submission.id, PostSubmissionEvents.on_completion) + + mock_registration.assert_called_once() + mock_update_payment_status.assert_not_called() + + def test_payment_done_and_not_should_wait_for_payment( + self, + ): + """ + The payment is required and has been completed, so registration should not be skipped regardless of the general + configuration setting. + """ + + submission = SubmissionFactory.create( + form__registration_backend="email", + form__registration_backend_options={"to_emails": ["test@registration.nl"]}, + form__product__price=10, + form__payment_backend="demo", + completed=True, + with_completed_payment=True, + ) + + with ( + patch( + "openforms.registrations.contrib.email.plugin.EmailRegistration.register_submission" + ) as mock_registration, + patch( + "openforms.registrations.tasks.GlobalConfiguration.get_solo", + return_value=GlobalConfiguration(wait_for_payment_to_register=False), + ), + ): + on_post_submission_event(submission.id, PostSubmissionEvents.on_completion) + + mock_registration.assert_called_once() + + def test_payment_not_required_and_should_wait_for_payment( + self, + ): + """ + The payment is NOT required, so registration should not be skipped regardless of the general + configuration setting. + """ + submission = SubmissionFactory.create( + form__registration_backend="email", + form__registration_backend_options={"to_emails": ["test@registration.nl"]}, + completed=True, + ) + + with ( + patch( + "openforms.registrations.contrib.email.plugin.EmailRegistration.register_submission" + ) as mock_registration, + patch( + "openforms.registrations.tasks.GlobalConfiguration.get_solo", + return_value=GlobalConfiguration(wait_for_payment_to_register=True), + ), + ): + on_post_submission_event(submission.id, PostSubmissionEvents.on_completion) + + mock_registration.assert_called_once() + + def test_payment_not_required_and_not_should_wait_for_payment( + self, + ): + """ + The payment is NOT required, so registration should not be skipped regardless of the general + configuration setting. + """ + submission = SubmissionFactory.create( + form__registration_backend="email", + form__registration_backend_options={"to_emails": ["test@registration.nl"]}, + completed=True, + ) + + with ( + patch( + "openforms.registrations.contrib.email.plugin.EmailRegistration.register_submission" + ) as mock_registration, + patch( + "openforms.registrations.tasks.GlobalConfiguration.get_solo", + return_value=GlobalConfiguration(wait_for_payment_to_register=False), + ), + ): + on_post_submission_event(submission.id, PostSubmissionEvents.on_completion) + + mock_registration.assert_called_once()