[Plugin] balance: Added reverting feature
This commit is contained in:
parent
737dd9d5cf
commit
57930837ac
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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("_")]
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
Loading…
Reference in New Issue