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__)
@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