diff --git a/.gitignore b/.gitignore index 20081512..99f02daa 100644 --- a/.gitignore +++ b/.gitignore @@ -167,3 +167,5 @@ cython_debug/ /.envrc /secrets /cron/ +/.vscode +/.DS_STORE \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore index b9bf98b5..d24a0dbd 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,2 +1,3 @@ node_modules/ static/bundle.js +/.DS_STORE diff --git a/frontend/src/components/RazorpayPayment.js b/frontend/src/components/RazorpayPayment.js index 8b0c61dd..660c1e78 100644 --- a/frontend/src/components/RazorpayPayment.js +++ b/frontend/src/components/RazorpayPayment.js @@ -14,6 +14,11 @@ const RazorpayPayment = props => { props.setStatus( "Razorpay SDK failed to load. please check are you online?" ); + if (props.failureCallback) { + props.failureCallback( + "Razorpay SDK failed to load. please check are you online?" + ); + } } }); @@ -24,7 +29,9 @@ const RazorpayPayment = props => { const event_id = props.event?.id; const team_id = props.team?.id; const data = team_id - ? { team_id, event_id } // Team Registration + ? player_ids + ? { team_id, event_id, player_ids } // Player Registration + : { team_id, event_id } // Team Registration : props.annual ? player_ids ? { player_ids, season_id } // Group Membership @@ -70,11 +77,11 @@ const RazorpayPayment = props => { ); } else { - if (response.status === 422) { + if (response.status >= 400 && response.status < 500) { const error = await response.json(); props.setStatus(`Error: ${error.message}`); if (props.failureCallback) { - props.failureCallback(`Error: ${error.message}`); + props.failureCallback(error.message); } } else { const body = await response.text(); @@ -83,7 +90,7 @@ const RazorpayPayment = props => { ); if (props.failureCallback) { props.failureCallback( - `Error: ${response.statusText} (${response.status}) — ${body}` + `${response.statusText} (${response.status}) — ${body}` ); } } @@ -97,7 +104,7 @@ const RazorpayPayment = props => { ); if (props.failureCallback) { props.failureCallback( - `Error: ${response.error.code}: ${response.error.description}` + `${response.error.code}: ${response.error.description}` ); } }); @@ -105,11 +112,11 @@ const RazorpayPayment = props => { // window.location = data.redirect_url; } else { - if (response.status === 422) { + if (response.status >= 400 && response.status < 500) { const error = await response.json(); props.setStatus(`Error: ${error.message}`); if (props.failureCallback) { - props.failureCallback(`Error: ${error.message}`); + props.failureCallback(`${error.message}`); } } else { const body = await response.text(); @@ -118,7 +125,7 @@ const RazorpayPayment = props => { ); if (props.failureCallback) { props.failureCallback( - `Error: ${response.statusText} (${response.status}) — ${body}` + `${response.statusText} (${response.status}) — ${body}` ); } } @@ -129,7 +136,7 @@ const RazorpayPayment = props => { setLoading(false); props.setStatus(`Error: ${error}`); if (props.failureCallback) { - props.failureCallback(`Error: ${error}`); + props.failureCallback(`${error}`); } }); }; diff --git a/frontend/src/components/roster/AddToRoster.js b/frontend/src/components/roster/AddToRoster.js index eb61f263..275685ed 100644 --- a/frontend/src/components/roster/AddToRoster.js +++ b/frontend/src/components/roster/AddToRoster.js @@ -11,35 +11,29 @@ import { } from "@tanstack/solid-table"; import clsx from "clsx"; import { Icon } from "solid-heroicons"; -import { handRaised } from "solid-heroicons/outline"; -import { - arrowRight, - checkCircle, - plus, - xCircle, - xMark -} from "solid-heroicons/solid"; +import { arrowRight, checkCircle, xCircle, xMark } from "solid-heroicons/solid"; import { createEffect, createSignal, For, Show } from "solid-js"; import { ChevronLeft, ChevronRight, Spinner } from "../../icons"; -import { - addToRoster, - fetchUser, - searchSeriesRosterPlayers -} from "../../queries"; +import { addToRoster, searchSeriesRosterPlayers } from "../../queries"; import Info from "../alerts/Info"; import Modal from "../Modal"; import ErrorPopover from "../popover/ErrorPopover"; import SuccessPopover from "../popover/SuccessPopover"; +import RazorpayPayment from "../RazorpayPayment"; -const AddToRoster = props => { - let modalRef; +const AddToRoster = componentProps => { let successPopoverRef, errorPopoverRef, errorModalRef; const [status, setStatus] = createSignal(""); const [error, setError] = createSignal({}); + const [search, setSearch] = createSignal(""); + const [pagination, setPagination] = createSignal({ + pageIndex: 0, + pageSize: 5 + }); + const [selectedPlayers, setSelectedPlayers] = createSignal([]); const queryClient = useQueryClient(); - const userQuery = createQuery(() => ["me"], fetchUser); const addToRosterMutation = createMutation({ mutationFn: addToRoster, onSuccess: () => @@ -48,8 +42,8 @@ const AddToRoster = props => { createEffect(function onMutationComplete() { if (addToRosterMutation.isSuccess) { - setStatus("Player added to the roster"); - successPopoverRef.showPopover(); + setStatus("Successfully added player to the roster"); + successPopoverRef?.showPopover(); } if (addToRosterMutation.isError) { try { @@ -58,9 +52,9 @@ const AddToRoster = props => { setStatus(mutationError.message); // Show error modal for long errors with a description, possibly an action button also if (mutationError.description) { - errorModalRef.showModal(); + errorModalRef?.showModal(); } else { - errorPopoverRef.showPopover(); + errorPopoverRef?.showPopover(); } } catch (err) { throw new Error(`Couldn't parse error object: ${err}`); @@ -68,131 +62,19 @@ const AddToRoster = props => { } }); - return ( -
- reg.player.id) - .includes(userQuery.data?.player?.id) - } - > - - - - Adding a new player to the roster} - close={() => modalRef.close()} - > - - - - -
- -
{status()}
-
-
- - -
- -
{status()}
-
-
- - errorModalRef.close()} - fullWidth={true} - title={ -
-
- -
-
{status()}
-
+ const handleAddToRoster = player => { + if (componentProps.playerFee > 0) { + setSelectedPlayers([...selectedPlayers(), player]); + } else { + addToRosterMutation.mutate({ + event_id: componentProps.eventId, + team_id: componentProps.teamId, + body: { + player_id: player.id } - > -
-
{error().description}
- -
-
-
- ); -}; - -const AddPlayerRegistrationForm = componentProps => { - const [search, setSearch] = createSignal(""); - const [pagination, setPagination] = createSignal({ - pageIndex: 0, - pageSize: 5 - }); - const [status, setStatus] = createSignal(); - - const queryClient = useQueryClient(); - const addToRosterMutation = createMutation({ - mutationFn: addToRoster, - onSuccess: () => - queryClient.invalidateQueries({ queryKey: ["tournament-roster"] }) - }); - - createEffect(function onMutationComplete() { - if (addToRosterMutation.isSuccess) { - setStatus("Successfully added player to the roster"); + }); } - if (addToRosterMutation.isError) { - setStatus("Adding to the roster failed"); - } - }); - - createEffect(() => { - console.log(componentProps.isPartOfSeries); - }); + }; const dataQuery = createQuery( () => [ @@ -243,20 +125,15 @@ const AddPlayerRegistrationForm = componentProps => { when={ !(componentProps.roster || []) .map(reg => reg.player.id) + .includes(props.getValue()) && + !selectedPlayers() + .map(p => p.id) .includes(props.getValue()) } fallback={Added} > + + + p.id)} + amount={componentProps.playerFee * selectedPlayers().length} + setStatus={msg => { + return msg; + }} + successCallback={() => { + queryClient.invalidateQueries({ + queryKey: ["tournament-roster"] + }); + setStatus("Paid successfully!"); + successPopoverRef?.showPopover(); + setSelectedPlayers([]); + }} + failureCallback={msg => { + console.log(msg, componentProps.errorPopoverRef); + + setStatus(msg); + errorPopoverRef?.showPopover(); + }} + /> + + +

Search

Search players by name or email{" "} { -

{status()}

+ + +
+ +
{status()}
+
+
+ + +
+ +
{status()}
+
+
+ + errorModalRef.close()} + fullWidth={true} + title={ +
+
+ +
+
{status()}
+
+ } + > +
+
{error().description}
+ +
+
); }; diff --git a/frontend/src/components/roster/View.js b/frontend/src/components/roster/View.js index d09ea0ec..80001d8c 100644 --- a/frontend/src/components/roster/View.js +++ b/frontend/src/components/roster/View.js @@ -284,6 +284,7 @@ const Roster = () => { isPartOfSeries={ tournamentQuery.data?.event?.series ? true : false } + playerFee={tournamentQuery.data?.event?.player_fee || 0} /> diff --git a/server/api.py b/server/api.py index 39797e16..780f7275 100644 --- a/server/api.py +++ b/server/api.py @@ -1481,6 +1481,9 @@ def add_team_registration( "action_href": f"/series/{tournament.event.series.slug}/", } + if tournament.event.team_fee > 0: + return 400, {"message": "Team registration can be done only after payment of team fee !"} + tournament.teams.add(team) return 200, tournament diff --git a/server/transaction/api.py b/server/transaction/api.py index deae4946..b6b51f89 100644 --- a/server/transaction/api.py +++ b/server/transaction/api.py @@ -34,6 +34,7 @@ ManualTransactionValidationFormSchema, PhonePeOrderSchema, PhonePeTransactionSchema, + PlayerRegistrationSchema, RazorpayCallbackSchema, RazorpayOrderSchema, RazorpayTransactionSchema, @@ -43,6 +44,7 @@ create_transaction, list_transactions_by_type, update_transaction_player_memberships, + update_transaction_player_registrations, update_transaction_team_registration, ) @@ -54,13 +56,15 @@ # Razorpay Transaction @router.post( - "/razorpay", response={200: RazorpayOrderSchema, 400: Response, 422: Response, 502: str} + "/razorpay", + response={200: RazorpayOrderSchema, 400: Response, 401: Response, 422: Response, 502: str}, ) def create_razorpay_transaction( request: AuthenticatedHttpRequest, order: AnnualMembershipSchema | EventMembershipSchema | GroupMembershipSchema + | PlayerRegistrationSchema | TeamRegistrationSchema, ) -> tuple[int, str | message_response | dict[str, Any]]: return create_transaction(request, order, PaymentGateway.RAZORPAY) @@ -112,6 +116,8 @@ def handle_razorpay_callback( update_transaction_player_memberships(transaction) elif transaction.type == RazorpayTransaction.TransactionTypeChoices.TEAM_REGISTRATION: update_transaction_team_registration(transaction) + elif transaction.type == RazorpayTransaction.TransactionTypeChoices.PLAYER_REGISTRATION: + update_transaction_player_registrations(transaction) return 200, transaction.players.all() @@ -187,6 +193,8 @@ def payment_webhook(request: HttpRequest) -> message_response: update_transaction_player_memberships(transaction) elif transaction.type == RazorpayTransaction.TransactionTypeChoices.TEAM_REGISTRATION: update_transaction_team_registration(transaction) + elif transaction.type == RazorpayTransaction.TransactionTypeChoices.PLAYER_REGISTRATION: + update_transaction_player_registrations(transaction) return {"message": "Webhook processed"} diff --git a/server/transaction/client/razorpay.py b/server/transaction/client/razorpay.py index af81a07a..6c9dd955 100644 --- a/server/transaction/client/razorpay.py +++ b/server/transaction/client/razorpay.py @@ -1,11 +1,11 @@ import datetime +import logging import uuid from typing import Any import razorpay from django.conf import settings from django.utils.timezone import now -from requests.exceptions import RequestException from ..models import RazorpayTransaction from ..schema import RazorpayCallbackSchema @@ -15,6 +15,8 @@ RAZORPAY_NOTES_MAX = 512 RAZORPAY_DESCRIPTION_MAX = 255 +logger = logging.getLogger(__name__) + def create_order( amount: int, @@ -33,8 +35,8 @@ def create_order( } try: response = CLIENT.order.create(data=data) - except (RequestException, razorpay.errors.BadRequestError, razorpay.errors.ServerError) as e: - print(f"ERROR: Failed to connect to Razorpay with {e}") + except Exception as e: + logger.error("Failed to initiate Razorpay payment: %s", e) return None response["key"] = settings.RAZORPAY_KEY_ID diff --git a/server/transaction/schema.py b/server/transaction/schema.py index 1dee5763..3cb8c2c9 100644 --- a/server/transaction/schema.py +++ b/server/transaction/schema.py @@ -139,3 +139,9 @@ class GroupMembershipSchema(Schema): class TeamRegistrationSchema(Schema): team_id: int event_id: int + + +class PlayerRegistrationSchema(Schema): + team_id: int + event_id: int + player_ids: list[int] diff --git a/server/transaction/utils.py b/server/transaction/utils.py index 0c9166b6..ee0b2418 100644 --- a/server/transaction/utils.py +++ b/server/transaction/utils.py @@ -8,8 +8,10 @@ from server.core.models import Player, Team, User from server.membership.models import Membership from server.season.models import Season -from server.tournament.models import Event, Tournament +from server.tournament.models import Event, Registration, Tournament +from server.tournament.utils import can_register_player_to_series_event from server.types import message_response +from server.utils import is_today_in_between_dates from .client import phonepe, razorpay from .models import ( @@ -23,6 +25,7 @@ AnnualMembershipSchema, EventMembershipSchema, GroupMembershipSchema, + PlayerRegistrationSchema, TeamRegistrationSchema, ) @@ -32,6 +35,7 @@ def create_transaction( order: AnnualMembershipSchema | EventMembershipSchema | GroupMembershipSchema + | PlayerRegistrationSchema | TeamRegistrationSchema, gateway: PaymentGateway, transaction_id: str | None = None, @@ -39,7 +43,7 @@ def create_transaction( user = request.user ts = round(time.time()) - if isinstance(order, GroupMembershipSchema): + if isinstance(order, GroupMembershipSchema | PlayerRegistrationSchema): players = Player.objects.filter(id__in=order.player_ids) player_ids = {p.id for p in players} if len(player_ids) != len(order.player_ids): @@ -103,6 +107,17 @@ def create_transaction( except Team.DoesNotExist: return 422, {"message": "Team does not exist!"} + if request.user not in team.admins.all(): + return 401, {"message": "Only team admins can register a team to a tournament !"} + + try: + tournament = Tournament.objects.get(event=event) + except Tournament.DoesNotExist: + return 400, {"message": "Tournament does not exist"} + + if tournament.status != Tournament.Status.REGISTERING: + return 400, {"message": "Team registration has closed, you can't register a team now !"} + if event.series and team not in event.series.teams.all(): return 400, { "message": "Team is not part of the series", @@ -120,6 +135,60 @@ def create_transaction( } receipt = f"team:{event.id}:{team.id}:{ts}" + elif isinstance(order, PlayerRegistrationSchema): + try: + team = Team.objects.get(id=order.team_id) + event = Event.objects.get(id=order.event_id) + tournament = Tournament.objects.get(event=event) + except (Event.DoesNotExist, Team.DoesNotExist, Tournament.DoesNotExist): + return 400, {"message": "Team/Event/Tournament does not exist"} + + if not is_today_in_between_dates( + from_date=tournament.event.player_registration_start_date, + to_date=tournament.event.player_registration_end_date, + ): + return 400, {"message": "Rostering has closed, you can't roster players now !"} + + if team not in tournament.teams.all(): + return 400, {"message": f"{team.name} is not registered for ${event.title} !"} + + if request.user not in team.admins.all(): + return 401, {"message": "Only team admins can roster players to the team"} + + if len(players) == 0: + return 400, {"message": "No players selected !"} + + for player in players: + if event.series: + can_register, error = can_register_player_to_series_event( + event=event, team=team, player=player + ) + if not can_register and error: + return 400, error + + if Registration.objects.filter(event=event, player=player).exists(): + return 400, { + "message": f"Player - {player.user.get_full_name()} already registered for this event in another team !" + } + + start_date = event.start_date + end_date = event.end_date + amount = event.player_fee * len(players) + season = None + + player_names = ", ".join(sorted([player.user.get_full_name() for player in players])) + if len(player_names) > razorpay.RAZORPAY_NOTES_MAX: + player_names = player_names[:500] + "..." + + notes = { + "user_id": user.id, + "team_id": team.id, + "event_id": event.id, + "player_ids": str(player_ids), + "players": player_names, + } + receipt = f"player:{event.id}:{team.id}:{ts}" + else: # NOTE: We should never be here, thanks to request validation! pass @@ -181,13 +250,15 @@ def create_transaction( "players": [] if isinstance(order, TeamRegistrationSchema) else [player] - if not isinstance(order, GroupMembershipSchema) + if not isinstance(order, GroupMembershipSchema | PlayerRegistrationSchema) else players, "event": event, "season": season, "team": team, "type": RazorpayTransaction.TransactionTypeChoices.TEAM_REGISTRATION if isinstance(order, TeamRegistrationSchema) + else RazorpayTransaction.TransactionTypeChoices.PLAYER_REGISTRATION + if isinstance(order, PlayerRegistrationSchema) else RazorpayTransaction.TransactionTypeChoices.ANNUAL_MEMBERSHIP, } ) @@ -195,8 +266,10 @@ def create_transaction( RazorpayTransaction.create_from_order_data(data) transaction_user_name = user.get_full_name() description = ( - f"Team registration payment by {transaction_user_name}" + f"Team registration payment by {transaction_user_name} for {team.name if team is not None else ''}, event: {event.title if event is not None else ''}" if isinstance(order, TeamRegistrationSchema) + else f"Player registration payment by {transaction_user_name} for {player_names}, event: {event.title if event is not None else ''}" + if isinstance(order, PlayerRegistrationSchema) else f"Membership for {player.user.get_full_name()}" if not isinstance(order, GroupMembershipSchema) else f"Membership group payment by {transaction_user_name} for {player_names}" @@ -257,6 +330,19 @@ def update_transaction_team_registration( tournament.teams.add(transaction.team) +def update_transaction_player_registrations( + transaction: RazorpayTransaction, +) -> None: + for player in transaction.players.all(): + registration = Registration( + event=transaction.event, + team=transaction.team, + player=player, + ) + + registration.save() + + def list_transactions_by_type( user: User, payment_type: PaymentGateway, user_only: bool = True, only_invalid: bool = False ) -> QuerySet[Model]: