[Plugin] balance: Added reverting feature

This commit is contained in:
Ferdinand Thiessen 2020-11-18 02:55:31 +01:00
parent 737dd9d5cf
commit 57930837ac
5 changed files with 97 additions and 31 deletions

View File

@ -3,17 +3,17 @@
Extends users plugin with balance functions
"""
from http.client import NO_CONTENT
from datetime import datetime, timezone
from flask import Blueprint, jsonify, request
from flask import Blueprint, request
from werkzeug.exceptions import Forbidden, BadRequest
from flaschengeist import logger
from flaschengeist.utils import HTTP
from flaschengeist.models.session import Session
from flaschengeist.utils.datetime import from_iso_format
from flaschengeist.decorator import login_required
from flaschengeist.controller import userController
from flaschengeist.plugins import Plugin, update_user_hook
from flaschengeist.plugins import Plugin, before_update_user
from . import balance_controller, permissions, models
@ -26,7 +26,7 @@ class BalancePlugin(Plugin):
def __init__(self, config):
super().__init__(blueprint=balance_bp, permissions=permissions.permissions)
@update_user_hook
@before_update_user
def set_default_limit(user):
if "limit" in config:
limit = config["limit"]
@ -59,7 +59,7 @@ def get_limit(userid, current_session: Session):
):
raise Forbidden
return jsonify({"limit": balance_controller.get_limit(user)})
return {"limit": balance_controller.get_limit(user)}
@balance_bp.route("/users/<userid>/balance/limit", methods=["PUT"])
@ -88,7 +88,7 @@ def set_limit(userid, current_session: Session):
except (TypeError, KeyError):
raise BadRequest
balance_controller.set_limit(user, limit)
return "", NO_CONTENT
return HTTP.no_content()
@balance_bp.route("/users/<userid>/balance", methods=["GET"])
@ -125,8 +125,8 @@ def get_balance(userid, current_session: Session):
else:
end = datetime.now(tz=timezone.utc)
balance = balance_controller.get(user, start, end)
return jsonify({"credit": balance[0], "debit": balance[1], "balance": balance[2]})
balance = balance_controller.get_balance(user, start, end)
return {"credit": balance[0], "debit": balance[1], "balance": balance[2]}
@balance_bp.route("/users/<userid>/balance", methods=["PUT"])
@ -144,7 +144,7 @@ def change_balance(userid, current_session: Session):
current_session: Session sent with Authorization Header
Returns:
JSON object containing credit, debit and balance or HTTP error
JSON encoded transaction (201) or HTTP error
"""
data = request.get_json()
@ -164,8 +164,7 @@ def change_balance(userid, current_session: Session):
if (sender == current_session._user and sender.has_permission(permissions.SEND)) or (
sender != current_session._user and current_session._user.has_permission(permissions.SEND_OTHER)
):
balance_controller.send(sender, user, data["amount"], current_session._user)
return "", NO_CONTENT
return HTTP.created(balance_controller.send(sender, user, data["amount"], current_session._user))
elif (
amount < 0
@ -174,7 +173,31 @@ def change_balance(userid, current_session: Session):
or current_session._user.has_permission(permissions.DEBIT)
)
) or (amount > 0 and current_session._user.has_permission(permissions.CREDIT)):
balance_controller.change_balance(user, data["amount"], current_session._user)
return "", NO_CONTENT
return HTTP.created(balance_controller.change_balance(user, data["amount"], current_session._user))
raise Forbidden
@balance_bp.route("/balance/<int:transaction_id>", methods=["DELETE"])
@login_required()
def reverse_transaction(transaction_id, current_session: Session):
"""Reverse a transaction
Route: ``/balance/<int:transaction_id>`` | Method: ``DELETE``
Args:
transaction_id: Identifier of the transaction
current_session: Session sent with Authorization Header
Returns:
JSON encoded reversal (transaction) (201) or HTTP error
"""
transaction = balance_controller.get_transaction(transaction_id)
if current_session._user.has_permission(permissions.REVERSAL) or (
transaction.sender_ == current_session._user
and (datetime.now(tz=timezone.utc) - transaction.time).total_seconds() < 10
):
reversal = balance_controller.reverse_transaction(transaction, current_session._user)
return HTTP.created(reversal)
raise Forbidden

View File

@ -5,13 +5,14 @@
from sqlalchemy import func
from datetime import datetime, timezone
from werkzeug.exceptions import BadRequest
from werkzeug.exceptions import BadRequest, NotFound
from flaschengeist.database import db
from flaschengeist.models.user import User
from .models import Transaction
from . import permissions
from ... import logger
__attribute_limit = "balance_limit"
@ -26,7 +27,7 @@ def get_limit(user: User) -> float:
return user.get_attribute(__attribute_limit, default=None)
def get(user, start: datetime = None, end: datetime = None):
def get_balance(user, start: datetime = None, end: datetime = None):
if not start:
start = datetime.fromtimestamp(0, tz=timezone.utc)
if not end:
@ -35,6 +36,7 @@ def get(user, start: datetime = None, end: datetime = None):
credit = (
db.session.query(func.sum(Transaction.amount))
.filter(Transaction.receiver_ == user)
.filter(Transaction._reversal_id.is_(None)) # ignore reverted transactions
.filter(start <= Transaction.time)
.filter(Transaction.time <= end)
.scalar()
@ -43,6 +45,7 @@ def get(user, start: datetime = None, end: datetime = None):
debit = (
db.session.query(func.sum(Transaction.amount))
.filter(Transaction.sender_ == user)
.filter(Transaction._reversal_id.is_(None)) # ignore reverted transactions
.filter(start <= Transaction.time)
.filter(Transaction.time <= end)
.scalar()
@ -59,13 +62,15 @@ def send(sender: User, receiver, amount: float, author: User):
receiver: User who receives the amount
amount: Amount to send
author: User authoring this transaction
Returns:
Transaction that was created
Raises:
BadRequest if amount <= 0
"""
if amount <= 0:
raise BadRequest
if sender.has_attribute(__attribute_limit):
if sender and sender.has_attribute(__attribute_limit):
if (get(sender)[2] - amount) < sender.get_attribute(__attribute_limit) and not author.has_permission(
permissions.EXCEED_LIMIT
):
@ -74,6 +79,7 @@ def send(sender: User, receiver, amount: float, author: User):
transaction = Transaction(sender_=sender, receiver_=receiver, amount=amount, author_=author)
db.session.add(transaction)
db.session.commit()
return transaction
def change_balance(user, amount: float, author):
@ -86,4 +92,25 @@ def change_balance(user, amount: float, author):
"""
sender = user if amount < 0 else None
receiver = user if amount > 0 else None
send(sender, receiver, abs(amount), author)
return send(sender, receiver, abs(amount), author)
def get_transaction(transaction_id) -> Transaction:
transaction = Transaction.query.get(transaction_id)
if not transaction:
raise NotFound
return transaction
def reverse_transaction(transaction: Transaction, author: User):
"""Reverse a transaction
Args:
transaction: Transaction to reverse
author: User that wants the transaction to be reverted
"""
reversal = send(transaction.receiver_, transaction.sender_, transaction.amount, author)
reversal.reversal = transaction
transaction.reversal = reversal
db.session.commit()
return reversal

View File

@ -1,4 +1,5 @@
from datetime import datetime, timezone
from datetime import datetime
from typing import Optional
from flaschengeist.models.user import User
@ -8,19 +9,22 @@ from flaschengeist.models import ModelSerializeMixin, UtcDateTime
class Transaction(db.Model, ModelSerializeMixin):
__tablename__ = "balance_transaction"
id: int = db.Column("id", db.Integer, primary_key=True)
time: datetime = db.Column(UtcDateTime, nullable=False, default=datetime.now(tz=timezone.utc))
amount: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
# Dummy properties used for JSON serialization (userid instead of full user)
sender_id: str = ""
receiver_id: str = ""
author_id: str = ""
# Protected foreign key properties
_receiver_id = db.Column("receiver_id", db.Integer, db.ForeignKey("user.id"))
_sender_id = db.Column("sender_id", db.Integer, db.ForeignKey("user.id"))
_author_id = db.Column("author_id", db.Integer, db.ForeignKey("user.id"), nullable=False)
_reversal_id = db.Column("reversal_id", db.Integer, db.ForeignKey("balance_transaction.id"))
# Public and exported member
id: int = db.Column("id", db.Integer, primary_key=True)
time: datetime = db.Column(UtcDateTime, nullable=False, default=UtcDateTime.current_utc)
amount: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
reversal: Optional["Transaction"] = db.relationship("Transaction", foreign_keys=[_reversal_id])
# Dummy properties used for JSON serialization (userid instead of full user)
sender_id: Optional[str] = ""
receiver_id: Optional[str] = ""
author_id: Optional[str] = ""
# Not exported relationships just in backend only
sender_: User = db.relationship("User", foreign_keys=[_sender_id])
@ -29,12 +33,21 @@ class Transaction(db.Model, ModelSerializeMixin):
@property
def sender_id(self):
return self.sender_.userid
return self.sender_.userid if self.sender_ else None
@property
def receiver_id(self):
return self.receiver_.userid
return self.receiver_.userid if self.receiver_ else None
@property
def author_id(self):
return self.author_.userid
# Override to prevent circular dependencies (endless JSON)
def serialize(self):
d = super().serialize()
if d["reversal"]:
d["reversal"].reversal = None
else:
d["reversal"] = None
return d

View File

@ -21,4 +21,7 @@ SET_LIMIT = "balance_set_limit"
EXCEED_LIMIT = "balance_exceed_limit"
"""Allow sending / sub while exceeding the set limit"""
REVERSAL = "balance_reversal"
"""Allow reverting transactions"""
permissions = [value for key, value in globals().items() if not key.startswith("_")]

View File

@ -7,7 +7,7 @@ from flaschengeist.models.user import User
from flaschengeist.controller import userController
from flaschengeist.controller.messageController import Message
from . import Plugin, send_message_hook
from . import Plugin
class MailMessagePlugin(Plugin):
@ -20,7 +20,7 @@ class MailMessagePlugin(Plugin):
self.crypt = config["CRYPT"]
self.mail = config["MAIL"]
@send_message_hook
# @send_message_hook
def dummy_send(msg):
self.send_mail(msg)