83 lines
3.5 KiB
Python
83 lines
3.5 KiB
Python
from django.conf import settings
|
|
from django.db import transaction
|
|
from django.db.models import Q
|
|
from django.utils import timezone
|
|
from rest_framework import decorators, mixins, response, status, viewsets
|
|
|
|
from apps.core.errors import error_response
|
|
from apps.payments.gateways import get_gateway
|
|
from apps.payments.models import Payment
|
|
from apps.payments.serializers import PaymentSerializer
|
|
from apps.rbac.permissions import ActionPermission
|
|
|
|
|
|
class PaymentViewSet(
|
|
mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.CreateModelMixin, viewsets.GenericViewSet
|
|
):
|
|
serializer_class = PaymentSerializer
|
|
permission_classes = [ActionPermission]
|
|
code_map = {
|
|
"list": "invoice.view",
|
|
"retrieve": "invoice.view",
|
|
"create": "reservation.create",
|
|
"verify": "invoice.create",
|
|
}
|
|
|
|
def get_queryset(self):
|
|
user = self.request.user
|
|
qs = Payment.objects.select_related("order", "reservation").all()
|
|
if user.is_superuser:
|
|
return qs
|
|
return qs.filter(Q(order__customer__user=user) | Q(reservation__customer__user=user))
|
|
|
|
def create(self, request, *args, **kwargs):
|
|
serializer = self.get_serializer(data=request.data)
|
|
serializer.is_valid(raise_exception=True)
|
|
order = serializer.validated_data.get("order")
|
|
reservation = serializer.validated_data.get("reservation")
|
|
if not order and not reservation:
|
|
return response.Response(
|
|
error_response("PAYMENT_TARGET_REQUIRED", "An order or reservation is required."),
|
|
status=status.HTTP_400_BAD_REQUEST,
|
|
)
|
|
amount = serializer.validated_data["amount"]
|
|
currency = order.currency if order else getattr(settings, "BASE_CURRENCY", "IRR")
|
|
reference = order.order_number if order else f"RES-{str(reservation.id)[:8]}"
|
|
|
|
gateway = get_gateway()
|
|
callback_url = request.build_absolute_uri("/api/v1/payments/callback/")
|
|
result = gateway.request_payment(str(amount), currency, reference, callback_url)
|
|
payment = Payment.objects.create(
|
|
order=order,
|
|
reservation=reservation,
|
|
amount=amount,
|
|
currency=currency,
|
|
gateway=settings.PAYMENT_GATEWAY_PROVIDER or "mock",
|
|
authority=result["authority"],
|
|
status=Payment.Status.PENDING,
|
|
request_time=timezone.now(),
|
|
)
|
|
return response.Response(
|
|
{"id": str(payment.id), "authority": result["authority"], "redirect_url": result["redirect_url"]},
|
|
status=status.HTTP_201_CREATED,
|
|
)
|
|
|
|
@decorators.action(detail=True, methods=["post"])
|
|
@transaction.atomic
|
|
def verify(self, request, pk=None):
|
|
payment = Payment.objects.select_for_update().get(pk=pk)
|
|
# Idempotent: a duplicate callback must not create a duplicate success.
|
|
if payment.status == Payment.Status.SUCCESS:
|
|
return response.Response(PaymentSerializer(payment).data)
|
|
gateway = get_gateway()
|
|
result = gateway.verify(payment.authority, str(payment.amount))
|
|
payment.verification_time = timezone.now()
|
|
if result.get("status") == "success":
|
|
payment.status = Payment.Status.SUCCESS
|
|
payment.transaction_id = result["transaction_id"]
|
|
else:
|
|
payment.status = Payment.Status.FAILED
|
|
payment.error = result.get("error", "")
|
|
payment.save()
|
|
return response.Response(PaymentSerializer(payment).data)
|