From ae89a2038807e9ecf7deb90892bfd76bf81ca68e Mon Sep 17 00:00:00 2001 From: Jaimyn Mayer Date: Wed, 15 Nov 2023 01:03:03 +1000 Subject: [PATCH] added websocket support for debit and balance --- memberportal/api_access/consumers.py | 157 ++++++++++++++++++++++++++- memberportal/memberbucks/urls.py | 40 ------- memberportal/memberbucks/views.py | 124 --------------------- memberportal/membermatters/urls.py | 1 - memberportal/profile/models.py | 14 ++- 5 files changed, 161 insertions(+), 175 deletions(-) delete mode 100644 memberportal/memberbucks/urls.py delete mode 100644 memberportal/memberbucks/views.py diff --git a/memberportal/api_access/consumers.py b/memberportal/api_access/consumers.py index a8c6c844..40ef14d3 100644 --- a/memberportal/api_access/consumers.py +++ b/memberportal/api_access/consumers.py @@ -10,8 +10,11 @@ MemberbucksDevice, AccessControlledDeviceAPIKey, ) -from profile.models import Profile, log_event +from memberbucks.models import MemberBucks +from profile.models import Profile, User from constance import config +from django.core.exceptions import ObjectDoesNotExist +from django.utils import timezone logger = logging.getLogger("app") @@ -86,7 +89,7 @@ def receive_json(self, content=None, **kwargs): Receive message from WebSocket. """ try: - logger.info( + logger.debug( f"Got message from {self.device.type} ({self.device.serial_number}): {json.dumps(content)}", ) self.last_seen = datetime.datetime.now() @@ -411,10 +414,156 @@ class MemberbucksConsumer(AccessDeviceConsumer): def __init__(self, *args, **kwargs): super().__init__(args, kwargs) - self.DeviceClass = MemberbucksConsumer + self.DeviceClass = MemberbucksDevice def handle_other_packet(self, content): + if content.get("command") == "balance": + card_id = content.get("card_id") + + if card_id is None: + self.send_json( + { + "command": "balance", + "reason": "invalid_card_id", + "success": False, + } + ) + return True + + try: + profile = Profile.objects.get(rfid=card_id) + self.send_json( + { + "command": "balance", + "balance": int(profile.memberbucks_balance * 100), + } + ) + return True + + except ObjectDoesNotExist: + self.send_json( + { + "command": "balance", + "reason": "invalid_card_id", + "success": False, + } + ) + return True + if content.get("command") == "debit": - pass # TODO: implement + card_id = content.get("card_id") + amount = int(content.get("amount") or 0) + description = content.get("description", f"{self.device.name} purchase.") + + if card_id is None: + self.send_json( + { + "command": "debit", + "reason": "invalid_card_id", + "success": False, + } + ) + return True + + # stops us accidentally crediting an account if it's negative + if amount <= 0: + self.send_json( + { + "command": "debit", + "reason": "invalid_amount", + "success": False, + } + ) + return True + + try: + profile = Profile.objects.get(rfid=card_id) + + except ObjectDoesNotExist: + self.send_json( + { + "command": "debit", + "reason": "invalid_card_id", + "success": False, + } + ) + return True + + if profile.memberbucks_balance >= amount: + time_dif = ( + timezone.now() - profile.last_memberbucks_purchase + ).total_seconds() + + if time_dif > 5: + transaction = MemberBucks() + transaction.amount = amount * -1.0 + transaction.user = profile.user + transaction.description = description + transaction.transaction_type = "card" + transaction.save() + + profile.last_memberbucks_purchase = timezone.now() + profile.save() + profile.refresh_from_db() + + subject = ( + f"You just made a ${amount} {config.MEMBERBUCKS_NAME} purchase." + ) + message = ( + f"Description: {transaction.description}. Balance Remaining: " + ) + f"${profile.memberbucks_balance}. If this wasn't you, or you believe there " + f"has been an error, please let us know." + + User.objects.get(profile=profile).email_notification( + subject, message + ) + + profile.user.log_event( + f"Debited ${amount} from {config.MEMBERBUCKS_NAME} account.", + "memberbucks", + ) + + self.send_json( + { + "command": "debit", + "balance": int(profile.memberbucks_balance * 100), + } + ) + return True + + else: + self.send_json( + { + "command": "rate_limited", + } + ) + + else: + profile.user.log_event( + f"Not enough funds to debit ${amount} from {config.MEMBERBUCKS_NAME} account by {self.device.name}.", + "memberbucks", + ) + + subject = ( + f"Failed to make a ${amount} {config.MEMBERBUCKS_NAME} purchase." + ) + message = f"We just tried to debit ${amount} from your {config.MEMBERBUCKS_NAME} balance but were not " + f"successful. You currently have ${profile.memberbucks_balance}. If this wasn't you, please let us know " + f"immediately." + + User.objects.get(profile=profile).email_notification(subject, message) + + self.send_json( + { + "command": "debit_declined", + "reason": "insufficient_funds", + "balance": int(profile.memberbucks_balance * 100), + } + ) + return True + + if content.get("command") == "credit": + return False # TODO: implement else: return False diff --git a/memberportal/memberbucks/urls.py b/memberportal/memberbucks/urls.py deleted file mode 100644 index 813bc0bb..00000000 --- a/memberportal/memberbucks/urls.py +++ /dev/null @@ -1,40 +0,0 @@ -from django.urls import path -from . import views - -urlpatterns = [ - path( - "api/memberbucks/debit/", - views.MemberbucksDebit.as_view(), - name="memberbucks_debit", - ), - path( - "api/memberbucks/debit///", - views.MemberbucksDebit.as_view(), - name="memberbucks_debit", - ), - path( - "api/spacebucks/debit///", - views.MemberbucksDebit.as_view(), - name="memberbucks_debit", - ), - path( - "api/memberbucks/debit///", - views.MemberbucksDebit.as_view(), - name="memberbucks_debit", - ), - path( - "api/spacebucks/debit///", - views.MemberbucksDebit.as_view(), - name="memberbucks_debit", - ), - path( - "api/memberbucks/balance//", - views.MemberbucksBalance.as_view(), - name="memberbucks_balance", - ), - path( - "api/spacebucks/balance//", - views.MemberbucksBalance.as_view(), - name="memberbucks_balance", - ), -] diff --git a/memberportal/memberbucks/views.py b/memberportal/memberbucks/views.py deleted file mode 100644 index 33d565eb..00000000 --- a/memberportal/memberbucks/views.py +++ /dev/null @@ -1,124 +0,0 @@ -from django.core.exceptions import ObjectDoesNotExist -from django.utils import timezone -from .models import MemberBucks -from profile.models import Profile, User -from constance import config -import pytz - -from rest_framework import status, permissions -from rest_framework.response import Response -from rest_framework.views import APIView - -utc = pytz.UTC - - -class MemberbucksDebit(APIView): - """ - get: - post: - """ - - permission_classes = [permissions.AllowAny] - - def post(self, request, rfid=None, amount=None, description="No Description"): - if amount is None or rfid is None: - return Response(status=status.HTTP_400_BAD_REQUEST) - - amount = abs( - amount / 100 - ) # the abs() stops us accidentally crediting an account if it's negative - try: - profile = Profile.objects.get(rfid=rfid) - - except ObjectDoesNotExist: - return Response(status=status.HTTP_400_BAD_REQUEST) - - if amount is not None: - if abs(amount / 100) > 10: - return Response( - "A maximum of $10 may be debited with this API.", - status=status.HTTP_400_BAD_REQUEST, - ) - - if profile.memberbucks_balance >= amount: - time_dif = ( - timezone.now() - profile.last_memberbucks_purchase - ).total_seconds() - - if time_dif > 5: - transaction = MemberBucks() - transaction.amount = amount * -1.0 - transaction.user = profile.user - transaction.description = description.replace("+", " ") - transaction.transaction_type = "card" - transaction.save() - - profile.last_memberbucks_purchase = timezone.now() - profile.save() - - subject = ( - f"You just made a ${amount} {config.MEMBERBUCKS_NAME} purchase." - ) - message = ( - "Description: {}. Balance Remaining: ${}. If this wasn't you, or you believe there has been an " - "error, please let us know.".format( - transaction.description, profile.memberbucks_balance - ) - ) - User.objects.get(profile=profile).email_notification(subject, message) - - profile.user.log_event( - f"Successfully debited ${amount} from {config.MEMBERBUCKS_NAME} account.", - "memberbucks", - ) - - return Response( - {"success": True, "balance": round(profile.memberbucks_balance, 2)} - ) - - else: - profile.user.log_event( - f"Not enough funds to debit ${amount} from {config.MEMBERBUCKS_NAME} account.", - "memberbucks", - ) - subject = ( - f"Failed to make a ${amount} {config.MEMBERBUCKS_NAME} purchase." - ) - User.objects.get(profile=profile).email_notification( - subject, - f"We just tried to debit ${amount} from your {config.MEMBERBUCKS_NAME} balance but were not successful. " - f"You currently have ${profile.memberbucks_balance}. If this wasn't you, please let us know " - "immediately.", - ) - - return Response( - {"success": False, "balance": round(profile.memberbucks_balance, 2)} - ) - - else: - return Response( - { - "success": False, - "balance": round(profile.memberbucks_balance, 2), - "message": "Whoa! Not so fast!!", - } - ) - - -class MemberbucksBalance(APIView): - """ - get: returns the member's current memberbucks balance. - """ - - permission_classes = [permissions.AllowAny] - - def get(self, request, rfid=None): - if rfid is None: - return Response(status=status.HTTP_400_BAD_REQUEST) - - try: - profile = Profile.objects.get(rfid=rfid) - return Response({"balance": profile.memberbucks_balance}) - - except ObjectDoesNotExist: - return Response() diff --git a/memberportal/membermatters/urls.py b/memberportal/membermatters/urls.py index ae1d64bd..2b9e0a20 100644 --- a/memberportal/membermatters/urls.py +++ b/memberportal/membermatters/urls.py @@ -39,7 +39,6 @@ def safe_constance_get(fld: str): urlpatterns = [ path("api/openid/", include("oidc_provider.urls", namespace="oidc_provider")), - path("", include("memberbucks.urls")), path("", include("api_spacedirectory.urls")), path("", include("api_general.urls")), path("", include("api_access.urls")), diff --git a/memberportal/profile/models.py b/memberportal/profile/models.py index 6b6a094e..53e4c90a 100644 --- a/memberportal/profile/models.py +++ b/memberportal/profile/models.py @@ -193,7 +193,7 @@ def is_admin(self): "Is the user a admin member?" return self.admin - def log_event(self, description, event_type, data=""): + def log_event(self, description: str, event_type, data=""): UserEventLog( description=description, logtype=event_type, user=self, data=data ).save() @@ -207,7 +207,9 @@ def __send_email(self, subject, template_vars, template_name=None): template_name=template_name, ) - def email_link(self, subject, title, message, link, btn_text): + def email_link( + self, subject: str, title: str, message: str, link: str, btn_text: str + ): template_vars = { "title": title, "message": message, @@ -221,11 +223,11 @@ def email_link(self, subject, title, message, link, btn_text): template_name="email_with_button.html", ) - def email_notification(self, subject, message): + def email_notification(self, subject: str, message: str): template_vars = {"title": subject, "message": message} return self.__send_email(subject, template_vars=template_vars) - def email_password_reset(self, link): + def email_password_reset(self, link: str): template_vars = {"link": link} return self.__send_email( @@ -403,7 +405,7 @@ def generate_digital_id_token(self): return self.digital_id_token - def validate_digital_id_token(self, token): + def validate_digital_id_token(self, token: str): if make_aware( datetime.now() ) < self.digital_id_token_expire and self.digital_id_token == uuid.UUID(token): @@ -473,7 +475,7 @@ def set_account_only(self): self.state = "accountonly" self.save() - def email_profile_to(self, to_email): + def email_profile_to(self, to_email: str): message = ( f"{self.get_full_name()} has just signed up on the portal." f"Their email is {self.user.email}."