Skip to content

Commit

Permalink
feat: add endpoint for leaving an auction that can be used by sellers
Browse files Browse the repository at this point in the history
  • Loading branch information
sandronadiradze committed Nov 4, 2024
1 parent 39a1958 commit 0708c76
Show file tree
Hide file tree
Showing 5 changed files with 368 additions and 15 deletions.
4 changes: 2 additions & 2 deletions auction/openapi/auction_cancel_openapi_examples.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def examples():
status_codes=[401],
),
OpenApiExample(
"Error: No permission. (GET)",
"Error: No permission. (POST)",
summary="No permission to cancel auction.",
description="This example shows an error response when a user tries "
"to cancel an auction that does not belong to them.",
Expand All @@ -49,7 +49,7 @@ def examples():
status_codes=[403],
),
OpenApiExample(
"Response example 5 (GET)",
"Response example 5 (POST)",
summary="Auction not found",
description="This example demonstrates the response for a scenario where "
"the requested auction ID does not exist.",
Expand Down
162 changes: 162 additions & 0 deletions auction/openapi/auction_leave_openapi_examples.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from drf_spectacular.openapi import OpenApiExample


def examples():
return [
OpenApiExample(
"Successful auction leave (POST)",
summary="Response for successfully leaving an auction",
description="This example demonstrates a successful response after seller "
"successfully leaves an auction.",
value={
"message": "Successfully left the auction",
"user_id": "7ca1as98-c896-4802-8fa6-426220c78cd9",
"cancelled_auction_count": 24,
},
response_only=True,
status_codes=[200],
),
OpenApiExample(
"User trying to leave an auction with a status of Draft or Cancelled (POST)",
summary="leaving an auction with a status of Draft or Cancelled",
description="This example shows response for a scenario where "
"user wants to send a request of leaving an auction with a status of "
"Draft or Cancelled.",
value={
"type": "validation_error",
"errors": [
{
"code": "invalid",
"message": "You can not leave an auction that has been cancelled, drafted.",
"field_name": None,
}
],
},
response_only=True,
status_codes=[400],
),
OpenApiExample(
"Auction has not started yet (POST)",
summary="Auction has not started yet",
description="This example shows response for user that wants "
"to leave an auction that has not started yet.",
value={
"type": "validation_error",
"errors": [
{
"code": "invalid",
"message": "You can not leave an auction that has not started yet.",
"field_name": None,
}
],
},
response_only=True,
status_codes=[400],
),
OpenApiExample(
"Auction has ended (POST)",
summary="Auction has already ended",
description="This example shows response for user that wants "
"to leave an auction that has already ended.",
value={
"type": "validation_error",
"errors": [
{
"code": "invalid",
"message": "You can not leave an auction that has already been completed.",
"field_name": None,
}
],
},
response_only=True,
status_codes=[400],
),
OpenApiExample(
"User is a winner of an auction (POST)",
summary="User is a winner of an auction",
description="This example shows response for user that wants "
"to leave an auction but is a winner of an auction.",
value={
"type": "validation_error",
"errors": [
{
"code": "invalid",
"message": "As a winner of an auction, you can not leave it.",
"field_name": None,
}
],
},
response_only=True,
status_codes=[400],
),
OpenApiExample(
"Unauthorized user trying to leave an auction (POST)",
summary="Unauthorized user",
description="This example shows an unauthorized user trying to leave an "
"auction without authentication.",
value={
"type": "client_error",
"errors": [
{
"code": "not_authenticated",
"message": "Authentication credentials were not provided.",
"field_name": None,
}
],
},
response_only=True,
status_codes=[401],
),
OpenApiExample(
"Error: No permission. (POST)",
summary="No permission to leave an auction.",
description="This example shows an error response when a user tries "
"to leave an auction but they do not have permission(e.g they are Buyer "
"type of user.",
value={
"type": "client_error",
"errors": [
{
"code": "permission_denied",
"message": "You do not have permission to perform this action.",
"field_name": None,
}
],
},
response_only=True,
status_codes=[403],
),
OpenApiExample(
"Response example 5 (POST)",
summary="Auction not found",
description="This example demonstrates the response for a scenario where "
"the requested auction ID does not exist.",
value={
"type": "client_error",
"errors": [
{"code": "not_found", "message": "Not found.", "field_name": None}
],
},
response_only=True,
status_codes=[404],
),
OpenApiExample(
"Response example 6 (POST)",
summary="Active bids not found",
description="This example demonstrates the response for a scenario where "
"the user wants to leave an auction but they do not have no "
"active bids created on that auction.",
value={
"type": "not_found",
"errors": [
{
"code": "not_found",
"message": "No active bids found for this auction",
"field_name": None,
}
],
},
response_only=True,
status_codes=[404],
),
]
2 changes: 2 additions & 0 deletions auction/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
DeclareWinnerView,
DeleteAuctionView,
DeleteBookmarkView,
LeaveAuctionView,
RetrieveAuctionView,
SellerAuctionListView,
SellerDashboardListView,
Expand Down Expand Up @@ -62,4 +63,5 @@
name="declare-winner",
),
path("cancel/<uuid:auction_id>/", CancelAuctionView.as_view(), name="cancel-auction"),
path("leave/<uuid:auction_id>/", LeaveAuctionView.as_view(), name="leave-auction"),
]
170 changes: 170 additions & 0 deletions auction/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
auction_create_openapi_examples,
auction_declare_winner_openapi_examples,
auction_delete_openapi_examples,
auction_leave_openapi_examples,
auction_retrieve_openapi_examples,
auction_update_patch_openapi_examples,
auction_update_put_openapi_examples,
Expand Down Expand Up @@ -1282,3 +1283,172 @@ def notify_auction_group(self, auction):
"message": data,
},
)


@extend_schema(
tags=["Auctions"],
responses={
200: inline_serializer(
name="LeaveAuction",
fields={
"message": serializers.CharField(),
"user_id": serializers.UUIDField(),
"cancelled_auction_count": serializers.IntegerField(),
},
),
400: inline_serializer(
name="LeaveAuctionBadRequest",
fields={
"message": serializers.CharField(),
},
),
401: inline_serializer(
name="LeaveAuctionBadUnauthorized",
fields={
"message": serializers.CharField(help_text="Authentication error message")
},
),
403: inline_serializer(
name="LeaveAuctionForbidden",
fields={
"message": serializers.CharField(help_text="Permission error message")
},
),
404: inline_serializer(
name="LeaveAuctionNotFound",
fields={
"message": serializers.CharField(help_text="Not found error message")
},
),
},
examples=auction_leave_openapi_examples.examples(),
)
class LeaveAuctionView(generics.GenericAPIView):
"""
Leave an auction by cancelling all user's bids.
This view allows authenticated users to leave an auction by cancelling all their active bids.
Users can leave the auction only if it is in progress and they are not the auction's winner.
**Permissions:**
- IsAuthenticated: Requires the user to be authenticated.
- IsSeller: Requires the user to be a seller to leave the auction.
**Response:**
- 200 (OK): Successfully left the auction. The response contains a
success message and details about the cancelled bids.
- 401 (Unauthorized): Authentication credentials are missing or invalid.
- 403 (Forbidden): User does not have permission to make request to this endpoint.
- 404 (Not Found): The specified auction does not exist or no active bids found for the user.
**Notes:**
- Users cannot leave an auction that has already been completed or is in the future.
- Auctions that have been cancelled, drafted, or have the user as the winner cannot be left.
- Upon successful execution:
- All active bids of the user for the auction are cancelled (their status is set to `Cancelled`).
- If the user's cancelled bid was the top bid, the auction's statistics are updated
to reflect the next highest bid.
"""

permission_classes = [IsAuthenticated, IsSeller]
lookup_url_kwarg = "auction_id"
queryset = Auction.objects.all()

def validate_auction_status(self, auction):
if auction.end_date < timezone.now() or auction.status == StatusChoices.COMPLETED:
raise ValidationError(
_("You can not leave an auction that has already been completed."),
)
if auction.start_date > timezone.now():
raise ValidationError(
_("You can not leave an auction that has not started yet."),
)
if auction.status in [StatusChoices.CANCELED, StatusChoices.DRAFT]:
raise ValidationError(
_("You can not leave an auction that has been cancelled, drafted."),
)
if auction.statistics.winner_bid_object is not None and str(
auction.statistics.winner_bid_object.author
) == str(self.request.user.id):
raise ValidationError(_("As a winner of an auction, you can not leave it."))

return True

def post(self, request, auction_id, *args, **kwargs):
auction = self.get_object()
self.validate_auction_status(auction)

try:
with transaction.atomic():
# Get all user's active bids for this auction
user_bids = Bid.objects.filter(
auction=auction,
author=request.user.id,
status__in=[
BidStatusChoices.PENDING,
BidStatusChoices.APPROVED,
BidStatusChoices.REJECTED,
],
)
user_bids_count = user_bids.count()

if not user_bids.exists():
return Response(
{"detail": "No active bids found for this auction"},
status=status.HTTP_404_NOT_FOUND,
)

# Check if any of user's bids is the top bid
auction_stats = auction.statistics
needs_top_bid_update = False

if str(auction_stats.top_bid_author) == str(request.user.id):
needs_top_bid_update = True

# Cancel all user's bids
user_bids.update(status=BidStatusChoices.CANCELLED)

# Update top bid if necessary
if needs_top_bid_update:
# Get the next highest bid
next_top_bid = (
Bid.objects.filter(
auction=auction,
status__in=[
BidStatusChoices.PENDING,
BidStatusChoices.APPROVED,
],
)
.order_by("-offer")
.first()
)

if next_top_bid:
# Update auction statistics with new top bid
auction_stats.top_bid = next_top_bid.offer
auction_stats.top_bid_author = next_top_bid.author
auction_stats.top_bid_object = next_top_bid
auction_stats.save()
else:
# No more active bids
auction_stats.top_bid = None
auction_stats.top_bid_author = None
auction_stats.top_bid_object = None
auction_stats.save()

response_data = {
"message": "Successfully left the auction",
"user_id": str(self.request.user.id),
"cancelled_auction_count": user_bids_count,
}

return Response(response_data, status=status.HTTP_200_OK)

except Exception as e:
return Response(
{"detail": f"An error occurred: {str(e)}"},
status=status.HTTP_500_INTERNAL_SERVER_ERROR,
)
Loading

0 comments on commit 0708c76

Please sign in to comment.