From 2009c1ab659c7b0a3d3bed8a6bd16ee6ba5258aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20D=C3=BCster?= Date: Fri, 20 Oct 2023 14:21:59 +0200 Subject: [PATCH 1/5] Enable submission of repayment requests --- pycroft/lib/finance/__init__.py | 3 +++ pycroft/lib/finance/repayment/fields.py | 14 ++++++++++++++ pyproject.toml | 1 + web/api/v0/__init__.py | 24 +++++++++++++++++++++++- 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 pycroft/lib/finance/repayment/fields.py diff --git a/pycroft/lib/finance/__init__.py b/pycroft/lib/finance/__init__.py index 0287fef5b..985e00f29 100644 --- a/pycroft/lib/finance/__init__.py +++ b/pycroft/lib/finance/__init__.py @@ -55,6 +55,9 @@ process_transactions, ImportedTransactions, ) +from .repayment.fields import ( + IBANField, +) def user_has_paid(user: User) -> bool: diff --git a/pycroft/lib/finance/repayment/fields.py b/pycroft/lib/finance/repayment/fields.py new file mode 100644 index 000000000..5b5eb15d9 --- /dev/null +++ b/pycroft/lib/finance/repayment/fields.py @@ -0,0 +1,14 @@ +from marshmallow import fields, ValidationError +from schwifty import IBAN + + +class IBANField(fields.Field): + """Field that serializes to a IBAN and deserializes + to a string. + """ + + def _deserialize(self, value, attr, data, **kwargs) -> IBAN: + try: + return IBAN(value) + except ValueError as error: + raise ValidationError("Pin codes must contain only digits.") from error diff --git a/pyproject.toml b/pyproject.toml index 188535bdb..1707a2461 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -53,6 +53,7 @@ dependencies = [ "pydantic ~= 2.4.0", "python-dotenv ~= 0.21.0", "reportlab ~= 3.6.13", # usersheet generation + "schwifty ~= 2024.5.4", "sentry-sdk[Flask] ~= 1.29.2", "simplejson ~= 3.11.1", # decimal serialization "SQLAlchemy >= 2.0.1", diff --git a/web/api/v0/__init__.py b/web/api/v0/__init__.py index 70b036c07..d6e0e9679 100644 --- a/web/api/v0/__init__.py +++ b/web/api/v0/__init__.py @@ -7,6 +7,7 @@ from flask import jsonify, current_app, Response from flask.typing import ResponseReturnValue from flask_restful import Api, Resource as FlaskRestfulResource, abort +from schwifty import IBAN from sqlalchemy.exc import IntegrityError from sqlalchemy import select from sqlalchemy.orm import joinedload, selectinload, undefer, with_polymorphic @@ -16,7 +17,7 @@ from pycroft.helpers import utc from pycroft.helpers.i18n import Message -from pycroft.lib.finance import estimate_balance, get_last_import_date +from pycroft.lib.finance import estimate_balance, get_last_import_date, IBANField from pycroft.lib.host import change_mac, host_create, interface_create, host_edit from pycroft.lib.net import SubnetFullException from pycroft.lib.swdd import get_swdd_person_id, get_relevant_tenancies, \ @@ -796,3 +797,24 @@ def patch(self, token: str, password: str) -> ResponseReturnValue: api.add_resource(ResetPasswordResource, '/user/reset-password') + + +class RequestRepaymentResource(Resource): + def get(self, user_id: int): + current_app.logger.warning("RECEIVED GET FOR REQUEST_REPAYMENT.") + return jsonify(False) + + @use_kwargs( + { + "beneficiary": fields.Str(required=True), + "iban": IBANField(required=True), + "amount": fields.Decimal(required=True), + }, + location="form", + ) + def post(self, user_id: int, beneficiary: str, iban: str, amount: Decimal): + current_app.logger.warning({beneficiary, iban, amount}) + return jsonify({"success": True}) + + +api.add_resource(RequestRepaymentResource, "/user//request-repayment") From d95b566dd18329446bcca318bbba118b67ff0f0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20D=C3=BCster?= Date: Fri, 20 Oct 2023 15:23:55 +0200 Subject: [PATCH 2/5] fixup! Enable submission of repayment requests --- pycroft/lib/finance/repayment/fields.py | 4 ++-- web/api/v0/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pycroft/lib/finance/repayment/fields.py b/pycroft/lib/finance/repayment/fields.py index 5b5eb15d9..5c9aecc7e 100644 --- a/pycroft/lib/finance/repayment/fields.py +++ b/pycroft/lib/finance/repayment/fields.py @@ -9,6 +9,6 @@ class IBANField(fields.Field): def _deserialize(self, value, attr, data, **kwargs) -> IBAN: try: - return IBAN(value) + return IBAN(value, validate_bban=True) except ValueError as error: - raise ValidationError("Pin codes must contain only digits.") from error + raise ValidationError("Field must be a valid IBAN.") from error diff --git a/web/api/v0/__init__.py b/web/api/v0/__init__.py index d6e0e9679..9cf535174 100644 --- a/web/api/v0/__init__.py +++ b/web/api/v0/__init__.py @@ -800,7 +800,7 @@ def patch(self, token: str, password: str) -> ResponseReturnValue: class RequestRepaymentResource(Resource): - def get(self, user_id: int): + def get(self, user_id: int) -> Response: current_app.logger.warning("RECEIVED GET FOR REQUEST_REPAYMENT.") return jsonify(False) @@ -812,7 +812,7 @@ def get(self, user_id: int): }, location="form", ) - def post(self, user_id: int, beneficiary: str, iban: str, amount: Decimal): + def post(self, user_id: int, beneficiary: str, iban: str, amount: Decimal) -> Response: current_app.logger.warning({beneficiary, iban, amount}) return jsonify({"success": True}) From 62320f8db93d43dafadb3f8f05b1eeb9f3d05e40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20D=C3=BCster?= Date: Fri, 20 Oct 2023 15:36:28 +0200 Subject: [PATCH 3/5] fixup! Enable submission of repayment requests --- pycroft/lib/finance/repayment/fields.py | 2 +- web/api/v0/__init__.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pycroft/lib/finance/repayment/fields.py b/pycroft/lib/finance/repayment/fields.py index 5c9aecc7e..e890ecabd 100644 --- a/pycroft/lib/finance/repayment/fields.py +++ b/pycroft/lib/finance/repayment/fields.py @@ -7,7 +7,7 @@ class IBANField(fields.Field): to a string. """ - def _deserialize(self, value, attr, data, **kwargs) -> IBAN: + def _deserialize(self, value, attr, data, **kwargs): try: return IBAN(value, validate_bban=True) except ValueError as error: diff --git a/web/api/v0/__init__.py b/web/api/v0/__init__.py index 9cf535174..6e3ceccca 100644 --- a/web/api/v0/__init__.py +++ b/web/api/v0/__init__.py @@ -812,7 +812,9 @@ def get(self, user_id: int) -> Response: }, location="form", ) - def post(self, user_id: int, beneficiary: str, iban: str, amount: Decimal) -> Response: + def post( + self, user_id: int, beneficiary: str, iban: str, amount: Decimal + ) -> Response: current_app.logger.warning({beneficiary, iban, amount}) return jsonify({"success": True}) From 196ef69490eac2e412952a2880b1a2ba1b94e2de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20D=C3=BCster?= Date: Fri, 20 Oct 2023 18:18:48 +0200 Subject: [PATCH 4/5] fixup! Enable submission of repayment requests --- pycroft/lib/finance/repayment/fields.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pycroft/lib/finance/repayment/fields.py b/pycroft/lib/finance/repayment/fields.py index e890ecabd..4761dcc77 100644 --- a/pycroft/lib/finance/repayment/fields.py +++ b/pycroft/lib/finance/repayment/fields.py @@ -1,3 +1,5 @@ +from typing import Any, Mapping + from marshmallow import fields, ValidationError from schwifty import IBAN @@ -7,7 +9,7 @@ class IBANField(fields.Field): to a string. """ - def _deserialize(self, value, attr, data, **kwargs): + def _deserialize(self, value: Any, attr: str | None, data: Mapping[str, Any], **kwargs) -> IBAN: try: return IBAN(value, validate_bban=True) except ValueError as error: From 495eb8753ec5f22de744de545ad6cd47b9e137b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gregor=20D=C3=BCster?= Date: Thu, 4 Jan 2024 20:29:08 +0100 Subject: [PATCH 5/5] WIP --- pycroft/lib/finance/repayment/fields.py | 4 +++- pycroft/model/repayment.py | 16 ++++++++++++++++ web/blueprints/finance/__init__.py | 5 +++++ 3 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 pycroft/model/repayment.py diff --git a/pycroft/lib/finance/repayment/fields.py b/pycroft/lib/finance/repayment/fields.py index 4761dcc77..eca3ed324 100644 --- a/pycroft/lib/finance/repayment/fields.py +++ b/pycroft/lib/finance/repayment/fields.py @@ -9,7 +9,9 @@ class IBANField(fields.Field): to a string. """ - def _deserialize(self, value: Any, attr: str | None, data: Mapping[str, Any], **kwargs) -> IBAN: + def _deserialize( + self, value: Any, attr: str | None, data: Mapping[str, Any], **kwargs + ) -> IBAN: try: return IBAN(value, validate_bban=True) except ValueError as error: diff --git a/pycroft/model/repayment.py b/pycroft/model/repayment.py new file mode 100644 index 000000000..59b4a3deb --- /dev/null +++ b/pycroft/model/repayment.py @@ -0,0 +1,16 @@ +from sqlalchemy import String, ForeignKey +from sqlalchemy.orm import relationship, Mapped, mapped_column + +from pycroft.model.base import IntegerIdModel + + +class RepaymentRequest(IntegerIdModel): + """A request for transferring back excess membership contributions""" + + id: Mapped[int] = mapped_column(primary_key=True) + user_id: Mapped[int] = mapped_column( + ForeignKey("user.id", ondelete="CASCADE", onupdate="CASCADE") + ) + beneficiary: Mapped[str] = mapped_column(nullable=False) + iban: Mapped[str] = mapped_column(String, nullable=False) + amount: Mapped[Decimal] = mapped_column(Decimal, nullable=False) diff --git a/web/blueprints/finance/__init__.py b/web/blueprints/finance/__init__.py index 9e5e320c8..1969ff72d 100644 --- a/web/blueprints/finance/__init__.py +++ b/web/blueprints/finance/__init__.py @@ -1621,3 +1621,8 @@ def payment_reminder_mail() -> ResponseReturnValue: page_title="Zahlungserinnerungen per E-Mail versenden", form_args=form_args, form=form) + +@bp.route("/repayment_requests", methods=("GET", "POST")) +@access.require("finance_change") +def handle_repayment_requests() -> ResponseReturnValue: + return render_template()