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..eca3ed324 --- /dev/null +++ b/pycroft/lib/finance/repayment/fields.py @@ -0,0 +1,18 @@ +from typing import Any, Mapping + +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: Any, attr: str | None, data: Mapping[str, Any], **kwargs + ) -> IBAN: + try: + return IBAN(value, validate_bban=True) + except ValueError as error: + raise ValidationError("Field must be a valid IBAN.") from 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/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..6e3ceccca 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,26 @@ def patch(self, token: str, password: str) -> ResponseReturnValue: api.add_resource(ResetPasswordResource, '/user/reset-password') + + +class RequestRepaymentResource(Resource): + def get(self, user_id: int) -> Response: + 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 + ) -> Response: + current_app.logger.warning({beneficiary, iban, amount}) + return jsonify({"success": True}) + + +api.add_resource(RequestRepaymentResource, "/user//request-repayment") 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()