Skip to content

Commit

Permalink
Merge pull request #4168 from open-formulieren/feature/4156-payment-t…
Browse files Browse the repository at this point in the history
…emplate-2

[#4156] Use a template for payment reference
  • Loading branch information
sergei-maertens authored Apr 15, 2024
2 parents 0c4172e + 591c750 commit 52e26a4
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 46 deletions.
2 changes: 1 addition & 1 deletion src/openforms/config/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ class GlobalConfigurationAdmin(TranslationAdmin, SingletonModelAdmin):
"display_sdk_information",
"enable_demo_plugins",
"allow_empty_initiator",
"payment_order_id_prefix",
"payment_order_id_template",
"enable_backend_formio_validation",
),
},
Expand Down
9 changes: 6 additions & 3 deletions src/openforms/config/migrations/0001_initial_to_v250.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@
import openforms.utils.translations


# Function removed from the code, moved here to not break the migration:
def validate_payment_order_id_prefix(value: str):
pass


def load_cookiegroups(*args):
call_command("loaddata", "cookie_consent", stdout=StringIO())

Expand Down Expand Up @@ -608,9 +613,7 @@ class Migration(migrations.Migration):
default="{year}",
help_text="Prefix to apply to generated numerical order IDs. Alpha-numerical only, supports placeholder {year}.",
max_length=16,
validators=[
openforms.payments.validators.validate_payment_order_id_prefix
],
validators=[validate_payment_order_id_prefix],
verbose_name="Payment Order ID prefix",
),
),
Expand Down
9 changes: 6 additions & 3 deletions src/openforms/config/migrations/0002_squashed_to_of_v230.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@
import openforms.utils.translations


# Function removed from the code, moved here to not break the migration:
def validate_payment_order_id_prefix(value: str):
pass


class Migration(migrations.Migration):

dependencies = [
Expand Down Expand Up @@ -50,9 +55,7 @@ class Migration(migrations.Migration):
default="{year}",
help_text="Prefix to apply to generated numerical order IDs. Alpha-numerical only, supports placeholder {year}.",
max_length=16,
validators=[
openforms.payments.validators.validate_payment_order_id_prefix
],
validators=[validate_payment_order_id_prefix],
verbose_name="Payment Order ID prefix",
),
),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Generated by Django 4.2.11 on 2024-04-12 10:12

from django.db import migrations, models
from django.db.migrations.state import StateApps
from django.db.backends.base.schema import BaseDatabaseSchemaEditor
from django.core.cache import caches
import openforms.payments.validators

from django.conf import settings


def migrate_to_order_id_template(
apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
) -> None:
"""Migrate from the payment order ID prefix to a customizable template.
If this migration is run on a new instance, the default value of the model field will take effect.
Otherwise, the default value is adapted to have the ``{uid}`` placeholder included.
"""
GlobalConfiguration = apps.get_model("config", "GlobalConfiguration")
VersionInfo = apps.get_model("upgrades", "VersionInfo")
config = GlobalConfiguration.objects.first()
if config is None:
return

version_info = VersionInfo.objects.first()

is_new_deploy = version_info is None or version_info.current == settings.RELEASE
if is_new_deploy:
# The default value of the `payment_order_id_template` field
# will take effect
return

config.payment_order_id_template = config.payment_order_id_prefix + "{uid}"
config.save()
caches[settings.SOLO_CACHE].clear()


class Migration(migrations.Migration):

dependencies = [
("config", "0056_globalconfiguration_enable_backend_formio_validation"),
("upgrades", "0001_initial"),
]

operations = [
migrations.AddField(
model_name="globalconfiguration",
name="payment_order_id_template",
field=models.CharField(
default="{year}/{public_reference}/{uid}",
help_text="Template to use when generating payment order IDs. It should be alpha-numerical and can contain the '/._-' characters. You can use the placeholder tokens: {year}, {public_reference}, {uid}.",
max_length=48,
validators=[
openforms.payments.validators.validate_payment_order_id_template
],
verbose_name="Payment Order ID template",
),
),
migrations.RunPython(migrate_to_order_id_template, migrations.RunPython.noop),
migrations.RemoveField(
model_name="globalconfiguration",
name="payment_order_id_prefix",
),
]
17 changes: 9 additions & 8 deletions src/openforms/config/models/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

from openforms.data_removal.constants import RemovalMethods
from openforms.emails.validators import URLSanitationValidator
from openforms.payments.validators import validate_payment_order_id_prefix
from openforms.payments.validators import validate_payment_order_id_template
from openforms.template import openforms_backend, render_from_string
from openforms.template.validators import DjangoTemplateValidator
from openforms.translations.utils import ensure_default_language
Expand Down Expand Up @@ -321,15 +321,16 @@ class GlobalConfiguration(SingletonModel):
)

# global payment settings
payment_order_id_prefix = models.CharField(
_("Payment Order ID prefix"),
max_length=16,
default="{year}",
blank=True,
payment_order_id_template = models.CharField(
_("Payment Order ID template"),
# A somewhat arbitrary value, should be around 40 characters after template substitutions:
max_length=48,
default="{year}/{public_reference}/{uid}",
help_text=_(
"Prefix to apply to generated numerical order IDs. Alpha-numerical only, supports placeholder {year}."
"Template to use when generating payment order IDs. It should be alpha-numerical and can contain the '/._-' characters. "
"You can use the placeholder tokens: {year}, {public_reference}, {uid}.",
),
validators=[validate_payment_order_id_prefix],
validators=[validate_payment_order_id_template],
)

# Privacy policy related fields
Expand Down
52 changes: 52 additions & 0 deletions src/openforms/config/tests/test_migrations.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from django.db.migrations.state import StateApps

from openforms.utils.tests.test_migrations import TestMigrations


Expand All @@ -15,3 +17,53 @@ def test_builder_enabled(self):
config = GlobalConfiguration.objects.get()

self.assertTrue(config.enable_react_formio_builder)


class MigrateToOrderIdTemplateExistingMigrationTests(TestMigrations):
app = "config"
migrate_from = "0056_globalconfiguration_enable_backend_formio_validation"
migrate_to = "0057_migrate_to_order_id_template"
setting_overrides = {"RELEASE": "a_new_release"}

def setUpBeforeMigration(self, apps: StateApps) -> None:
GlobalConfiguration = apps.get_model("config", "GlobalConfiguration")
VersionInfo = apps.get_model("upgrades", "VersionInfo")

GlobalConfiguration.objects.create(payment_order_id_prefix="{year}CUSTOM")
version_info = VersionInfo.objects.first()
assert version_info is not None
version_info.current = "some_existing_version"
version_info.save()

def test_template_from_prefix(self) -> None:
GlobalConfiguration = self.apps.get_model("config", "GlobalConfiguration")
config = GlobalConfiguration.objects.get()

self.assertEqual(config.payment_order_id_template, "{year}CUSTOM{uid}")


class MigrateToOrderIdTemplateNewMigrationTests(TestMigrations):
app = "config"
migrate_from = "0056_globalconfiguration_enable_backend_formio_validation"
migrate_to = "0057_migrate_to_order_id_template"
setting_overrides = {"RELEASE": "dev"}

def setUpBeforeMigration(self, apps: StateApps) -> None:
GlobalConfiguration = apps.get_model("config", "GlobalConfiguration")
VersionInfo = apps.get_model("upgrades", "VersionInfo")

GlobalConfiguration.objects.create()

# For some reason, `current` isn't set to `"dev"` on CI:
version_info = VersionInfo.objects.first()
assert version_info is not None
version_info.current = "dev"
version_info.save()

def test_template_default_value(self) -> None:
GlobalConfiguration = self.apps.get_model("config", "GlobalConfiguration")
config = GlobalConfiguration.objects.get()

self.assertEqual(
config.payment_order_id_template, "{year}/{public_reference}/{uid}"
)
14 changes: 7 additions & 7 deletions src/openforms/payments/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,17 @@ def create_public_order_id_for(
# TODO it isn't really clear what the required format/max length is
# for payment providers. Ogone seems to allow up to 40 characters or so,
# So this might fail at some point.
config = GlobalConfiguration.get_solo()
prefix = config.payment_order_id_prefix.format(year=payment.created.year)
if prefix:
prefix = f"{prefix}/"

assert payment.submission.public_registration_reference

pk = pk if pk is not None else payment.pk
config = GlobalConfiguration.get_solo()
template: str = config.payment_order_id_template

return (
f"{prefix}{payment.submission.public_registration_reference}/{payment.pk}"
template.replace("{year}", str(payment.created.year))
.replace(
"{public_reference}", payment.submission.public_registration_reference
)
.replace("{uid}", str(pk if pk is not None else payment.pk))
)


Expand Down
10 changes: 7 additions & 3 deletions src/openforms/payments/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ def test_str(self):

@patch(
"openforms.payments.models.GlobalConfiguration.get_solo",
return_value=GlobalConfiguration(payment_order_id_prefix=""),
return_value=GlobalConfiguration(
payment_order_id_template="{public_reference}/{uid}"
),
)
def test_create_for(self, m: MagicMock):
amount = Decimal("11.25")
Expand Down Expand Up @@ -59,9 +61,11 @@ def test_create_for(self, m: MagicMock):
@freeze_time("2020-01-01")
@patch(
"openforms.payments.models.GlobalConfiguration.get_solo",
return_value=GlobalConfiguration(payment_order_id_prefix="xyz{year}"),
return_value=GlobalConfiguration(
payment_order_id_template="xyz{year}/{public_reference}/{uid}"
),
)
def test_create_for_with_prefix(self, m: MagicMock):
def test_create_for_complete_template(self, m: MagicMock):
amount = Decimal("11.25")
options = {
"foo": 123,
Expand Down
47 changes: 29 additions & 18 deletions src/openforms/payments/tests/test_validators.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,46 @@
from django.core.exceptions import ValidationError
from django.test import TestCase
from django.test import SimpleTestCase

from openforms.payments.validators import validate_payment_order_id_prefix
from openforms.payments.validators import validate_payment_order_id_template


class PaymentOrderIDValidatorTests(TestCase):
def test_valid_prefixes(self):
class PaymentOrderIDValidatorTests(SimpleTestCase):
def test_uid_present(self):
valid = "{uid}"
with self.subTest(value=valid):
validate_payment_order_id_template(valid)

invalid = "other_things"
with self.subTest(value=invalid):
with self.assertRaises(ValidationError):
validate_payment_order_id_template(invalid)

def test_valid_templates(self):
valid = [
"",
"ab",
"12",
"ab12",
# placeholder
"{year}" "aa{year}" "a1{year}a1",
"{uid}",
"{uid}ab",
"{uid}12",
"{uid}ab12",
"{uid}a-.///---_",
# placeholders:
"{uid}{year}",
"{year}{public_reference}{uid}",
]

for value in valid:
with self.subTest(value=value):
validate_payment_order_id_prefix(value)
validate_payment_order_id_template(value)

def test_raises_for_invalid_prefixes(self):
invalid = [
" ",
"aa ",
" aa",
"a-2",
"{yearrr",
"{bad}",
" {uid}",
"aa {uid}",
" aa{uid}",
"{yearrr{uid}",
"{bad}{uid}",
]

for value in invalid:
with self.subTest(value=value):
with self.assertRaises(ValidationError):
validate_payment_order_id_prefix(value)
validate_payment_order_id_template(value)
11 changes: 8 additions & 3 deletions src/openforms/payments/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,16 @@
from django.utils.translation import gettext as _


def validate_payment_order_id_prefix(value: str):
value = value.replace("{year}", "")
def validate_payment_order_id_template(value: str) -> None:
if "{uid}" not in value:
raise ValidationError(_("The template must include the {uid} placeholder."))

for allowed in ["{year}", "{public_reference}", "{uid}", "/", ".", "_", "-"]:
value = value.replace(allowed, "")

if value and not value.isalnum():
raise ValidationError(
_(
"Prefix must be alpha numeric, no spaces or special characters except {year}"
"The template may only consist of alphanumeric, /, ., _ and - characters."
)
)

0 comments on commit 52e26a4

Please sign in to comment.