diff --git a/billing/views.py b/billing/views.py index cf8227d..4859958 100644 --- a/billing/views.py +++ b/billing/views.py @@ -28,6 +28,20 @@ PAYMENT_BASE_URL = env("PAYMENT_BASE_URL", default="") # type: ignore logger = logging.getLogger(__name__) +@dataclass +class Transaction: + ref: str + sourceBank: str + trxDate: str + + +@dataclass +class PaymentVerificationResponse: + message: str + success: bool + transaction: Optional[Transaction] = None + + class InsufficientFundsError(Exception): pass @@ -110,7 +124,7 @@ class UpdatePaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView): serializer.is_valid(raise_exception=True) self.perform_update(serializer) devices.update( - is_active=True, expiry_date=device_expire_date, has_a_pending_payment=False + is_active=False, expiry_date=device_expire_date, has_a_pending_payment=False ) return Response(serializer.data) @@ -145,7 +159,6 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView): ) devices = payment.devices.all() - payment_status = False if method == "WALLET": if user.wallet_balance < payment.amount: # type: ignore return Response( @@ -153,7 +166,7 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView): status=status.HTTP_400_BAD_REQUEST, ) else: - payment_status = self.process_wallet_payment( + self.process_wallet_payment( user, payment, ) @@ -161,15 +174,12 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView): data = { "benefName": f"{user.first_name} {user.last_name}", # type: ignore "accountNo": user.acc_no, # type: ignore - "absAmount": payment.amount, - "time": localtime(timezone.now() + timedelta(minutes=5)).strftime( - "%Y-%m-%d %H:%M" - ), + "absAmount": "{:.2f}".format(payment.amount), + "time": localtime(payment.created_at).strftime("%Y-%m-%d %H:%M"), } - payment_status = self.verify_transfer_payment(data, payment) + payment_verification_response = self.verify_transfer_payment(data, payment) - if payment_status: - # Update devices + if payment_verification_response.success: expiry_date = timezone.now() + timedelta(days=30 * payment.number_of_months) devices.update( is_active=True, @@ -177,7 +187,7 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView): has_a_pending_payment=False, registered=True, ) - # Need to add to omada if its a new device and not an existing device + # add to omada if its a new device and not an existing device device_list = [] for device in devices: device_list.append( @@ -193,12 +203,22 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView): device.save() return Response( - {"message": f"Payment verified successfully using [{method}]."}, + { + "status": payment_verification_response.success, + "message": payment_verification_response.message, + "transaction": asdict(payment_verification_response.transaction) + if payment_verification_response.transaction + else None, + }, status=status.HTTP_200_OK, ) else: return Response( - {"message": f"Payment verification FAILED using [{method}]."}, + { + "status": payment_verification_response.success, + "message": payment_verification_response.message + or "Topup payment verification failed.", + }, status=status.HTTP_400_BAD_REQUEST, ) @@ -215,7 +235,7 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView): user.save() return True - def verify_transfer_payment(self, data, payment): + def verify_transfer_payment(self, data, payment) -> PaymentVerificationResponse: if not PAYMENT_BASE_URL: raise ValueError( "PAYMENT_BASE_URL is not set. Please set it in your environment variables." @@ -230,41 +250,62 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView): response.raise_for_status() except requests.exceptions.HTTPError as e: logger.error(f"HTTPError: {e}") - return False + return PaymentVerificationResponse( + message="Payment verification failed.", success=False, transaction=None + ) mib_resp = response.json() logger.info("MIB Verification Response ->", mib_resp) if not response.json().get("success"): - return mib_resp["success"] + return PaymentVerificationResponse( + message=mib_resp["message"], + success=mib_resp["success"], + transaction=None, + ) else: payment.paid = True payment.paid_at = timezone.now() payment.method = "TRANSFER" payment.mib_reference = mib_resp["transaction"]["ref"] or "" payment.save() - return True + return PaymentVerificationResponse( + message=mib_resp["message"], + success=mib_resp["success"], + transaction=Transaction( + ref=payment.mib_reference, + sourceBank=mib_resp["transaction"]["sourceBank"], + trxDate=mib_resp["transaction"]["trxDate"], + ), + ) -class DeletePaymentView(StaffEditorPermissionMixin, generics.DestroyAPIView): +class CancelPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView): queryset = Payment.objects.all() serializer_class = PaymentSerializer lookup_field = "pk" - def delete(self, request, *args, **kwargs): + def update(self, request, *args, **kwargs): instance = self.get_object() user = request.user + if instance.status == "CANCELLED": + return Response( + {"message": "Payment has already been cancelled."}, + status=status.HTTP_400_BAD_REQUEST, + ) if instance.user != user and not user.is_superuser: return Response( - {"message": "You are not authorized to delete this payment."}, + {"message": "You are not authorized to cancel this payment."}, status=status.HTTP_403_FORBIDDEN, ) if instance.paid: return Response( - {"message": "Paid payments cannot be deleted."}, + {"message": "Paid payments cannot be cancelled."}, status=status.HTTP_400_BAD_REQUEST, ) devices = instance.devices.all() + instance.status = "CANCELLED" + instance.save() devices.update(is_active=False, expiry_date=None, has_a_pending_payment=False) - return super().delete(request, *args, **kwargs) + return super().update(request, *args, **kwargs) class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView): @@ -308,20 +349,6 @@ class TopupDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView): return queryset.filter(user=self.request.user) -@dataclass -class Transaction: - ref: str - sourceBank: str - trxDate: str - - -@dataclass -class PaymentVerificationResponse: - message: str - success: bool - transaction: Optional[Transaction] = None - - class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView): queryset = Topup.objects.all() serializer_class = TopupSerializer