refactor(payment): enhance payment verification with detailed response structure and transaction data 🔨

This commit is contained in:
2025-07-06 21:22:36 +05:00
parent 60e394fffa
commit f10fa74fbb

View File

@ -28,6 +28,20 @@ PAYMENT_BASE_URL = env("PAYMENT_BASE_URL", default="") # type: ignore
logger = logging.getLogger(__name__) 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): class InsufficientFundsError(Exception):
pass pass
@ -110,7 +124,7 @@ class UpdatePaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
serializer.is_valid(raise_exception=True) serializer.is_valid(raise_exception=True)
self.perform_update(serializer) self.perform_update(serializer)
devices.update( 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) return Response(serializer.data)
@ -145,7 +159,6 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
) )
devices = payment.devices.all() devices = payment.devices.all()
payment_status = False
if method == "WALLET": if method == "WALLET":
if user.wallet_balance < payment.amount: # type: ignore if user.wallet_balance < payment.amount: # type: ignore
return Response( return Response(
@ -153,7 +166,7 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
else: else:
payment_status = self.process_wallet_payment( self.process_wallet_payment(
user, user,
payment, payment,
) )
@ -161,15 +174,12 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
data = { data = {
"benefName": f"{user.first_name} {user.last_name}", # type: ignore "benefName": f"{user.first_name} {user.last_name}", # type: ignore
"accountNo": user.acc_no, # type: ignore "accountNo": user.acc_no, # type: ignore
"absAmount": payment.amount, "absAmount": "{:.2f}".format(payment.amount),
"time": localtime(timezone.now() + timedelta(minutes=5)).strftime( "time": localtime(payment.created_at).strftime("%Y-%m-%d %H:%M"),
"%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: if payment_verification_response.success:
# Update devices
expiry_date = timezone.now() + timedelta(days=30 * payment.number_of_months) expiry_date = timezone.now() + timedelta(days=30 * payment.number_of_months)
devices.update( devices.update(
is_active=True, is_active=True,
@ -177,7 +187,7 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
has_a_pending_payment=False, has_a_pending_payment=False,
registered=True, 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 = [] device_list = []
for device in devices: for device in devices:
device_list.append( device_list.append(
@ -193,12 +203,22 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
device.save() device.save()
return Response( 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, status=status.HTTP_200_OK,
) )
else: else:
return Response( 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, status=status.HTTP_400_BAD_REQUEST,
) )
@ -215,7 +235,7 @@ class VerifyPaymentView(StaffEditorPermissionMixin, generics.UpdateAPIView):
user.save() user.save()
return True return True
def verify_transfer_payment(self, data, payment): def verify_transfer_payment(self, data, payment) -> PaymentVerificationResponse:
if not PAYMENT_BASE_URL: if not PAYMENT_BASE_URL:
raise ValueError( raise ValueError(
"PAYMENT_BASE_URL is not set. Please set it in your environment variables." "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() response.raise_for_status()
except requests.exceptions.HTTPError as e: except requests.exceptions.HTTPError as e:
logger.error(f"HTTPError: {e}") logger.error(f"HTTPError: {e}")
return False return PaymentVerificationResponse(
message="Payment verification failed.", success=False, transaction=None
)
mib_resp = response.json() mib_resp = response.json()
logger.info("MIB Verification Response ->", mib_resp) logger.info("MIB Verification Response ->", mib_resp)
if not response.json().get("success"): if not response.json().get("success"):
return mib_resp["success"] return PaymentVerificationResponse(
message=mib_resp["message"],
success=mib_resp["success"],
transaction=None,
)
else: else:
payment.paid = True payment.paid = True
payment.paid_at = timezone.now() payment.paid_at = timezone.now()
payment.method = "TRANSFER" payment.method = "TRANSFER"
payment.mib_reference = mib_resp["transaction"]["ref"] or "" payment.mib_reference = mib_resp["transaction"]["ref"] or ""
payment.save() 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() queryset = Payment.objects.all()
serializer_class = PaymentSerializer serializer_class = PaymentSerializer
lookup_field = "pk" lookup_field = "pk"
def delete(self, request, *args, **kwargs): def update(self, request, *args, **kwargs):
instance = self.get_object() instance = self.get_object()
user = request.user 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: if instance.user != user and not user.is_superuser:
return Response( 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, status=status.HTTP_403_FORBIDDEN,
) )
if instance.paid: if instance.paid:
return Response( return Response(
{"message": "Paid payments cannot be deleted."}, {"message": "Paid payments cannot be cancelled."},
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
devices = instance.devices.all() devices = instance.devices.all()
instance.status = "CANCELLED"
instance.save()
devices.update(is_active=False, expiry_date=None, has_a_pending_payment=False) 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): class ListCreateTopupView(StaffEditorPermissionMixin, generics.ListCreateAPIView):
@ -308,20 +349,6 @@ class TopupDetailAPIView(StaffEditorPermissionMixin, generics.RetrieveAPIView):
return queryset.filter(user=self.request.user) 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): class VerifyTopupPaymentAPIView(StaffEditorPermissionMixin, generics.UpdateAPIView):
queryset = Topup.objects.all() queryset = Topup.objects.all()
serializer_class = TopupSerializer serializer_class = TopupSerializer