Tournament System Documentation
Overview
The tournament system allows verified organizers to create fishing tournaments, collect entry fees, manage registrations, track results, and link sponsors for advertising. The system integrates with Stripe for payment processing and includes comprehensive verification for organizers handling money.
Core Entities
TournamentOrganizer
Verified users who can create tournaments. Required for tournaments that collect entry fees.
Verification Process:
- User submits organizer application
- Admin reviews identity documents, business licenses, tax IDs
- Stripe Connect account setup for payment processing
- Verification status updated (Pending → UnderReview → Verified)
Key Features:
- Organization information (name, type, tax ID)
- Stripe Connect account for receiving payments
- Verification documents (ID, business license, tax docs)
- Limits and restrictions (max tournaments, max entry fees)
- Statistics tracking
Tournament
Main tournament entity representing a fishing competition.
Key Features:
- Tournament Types: Single Day, Multi-Day, Series, Virtual, Hybrid
- Formats: Individual, Team, Individual+Team, Pro-Am, Youth, Senior, Mixed
- Entry Fees: Configurable entry fees with Stripe integration
- Scoring: Weight, Length, Count, Points, Biggest Fish, Combined, Custom
- Categories: Support for multiple categories/divisions
- Verification: Optional catch verification requirements
- Privacy: Public, Private, Circle, Unlisted visibility
TournamentRegistration
User registration for a tournament.
Key Features:
- Registration status (Pending, Confirmed, Waitlisted, Cancelled, Refunded, Disqualified)
- Payment tracking (Stripe payment intent/charge IDs)
- Team membership (if team format)
- Category selection
- Waiver and rules acknowledgment
- Emergency contact information
TournamentTeam
Team entity for team-format tournaments.
Key Features:
- Team name and captain
- Team members (via TournamentRegistration)
- Team statistics and rankings
TournamentSponsor
Sponsors linked to tournaments for advertising.
Key Features:
- Sponsor information (name, type, contact)
- Link to Brand entity (if applicable)
- Sponsorship amount/type
- Advertising assets (logo, banner, ad copy)
- Display settings (order, featured status)
- Prizes provided
TournamentCategory
Categories/divisions within a tournament.
Key Features:
- Category name and description
- Species restrictions
- Age restrictions
- Skill level restrictions
- Category-specific rules (max fish, size limits)
- Separate entry fees per category
- Category-specific prize pools
TournamentResult
Tournament results linking catches to scoring.
Key Features:
- Links to FishingLogEntry and CatchDetail
- Scoring (weight, length, points)
- Ranking (overall, category, team)
- Verification (catch verification, weigh-in)
- Disqualification tracking
TournamentPrize
Prizes/awards for tournaments.
Key Features:
- Prize types (Cash, Product, Trophy, Gift Card, Service, Mixed)
- Prize values
- Sponsor links
- Winner tracking
- Award status
TournamentRule
Detailed rules and regulations for tournaments.
Key Features:
- Rule title and text
- Rule categories
- Required rules (must be acknowledged)
- Display order
Organizer Verification Flow
Step 1: User Applies to Become Organizer
var organizer = new TournamentOrganizer
{
UserId = userId,
OrganizationName = "Great Lakes Fishing Association",
OrganizationType = "Non-Profit",
TaxId = "12-3456789",
ContactEmail = "contact@glfa.org",
ContactPhone = "555-1234",
Address = "123 Main St",
City = "Green Bay",
State = "WI",
ZipCode = "54301",
Country = "USA",
VerificationStatus = OrganizerVerificationStatus.Pending
};
context.TournamentOrganizers.Add(organizer);
await context.SaveChangesAsync();
Step 2: Admin Reviews and Verifies
var organizer = await context.TournamentOrganizers
.FirstOrDefaultAsync(to => to.UserId == userId);
// Review documents, verify identity, check business license
organizer.VerificationStatus = OrganizerVerificationStatus.UnderReview;
organizer.VerifiedByUserId = adminUserId;
// Setup Stripe Connect account
var stripeAccount = await stripeService.CreateConnectAccount(organizer);
organizer.StripeAccountId = stripeAccount.Id;
organizer.StripeAccountStatus = stripeAccount.Status;
// Approve
organizer.VerificationStatus = OrganizerVerificationStatus.Verified;
organizer.VerifiedAt = DateTime.UtcNow;
organizer.CanCreatePaidTournaments = true;
organizer.StripeAccountVerified = true;
await context.SaveChangesAsync();
Creating a Tournament
Basic Tournament Creation
// Check if user is verified organizer
var organizer = await context.TournamentOrganizers
.FirstOrDefaultAsync(to => to.UserId == userId
&& to.VerificationStatus == OrganizerVerificationStatus.Verified
&& to.CanCreatePaidTournaments);
if (organizer == null)
throw new UnauthorizedException("User must be verified organizer");
var tournament = new Tournament
{
OrganizerId = userId,
Name = "Lake Michigan Salmon Classic 2024",
Description = "Annual salmon fishing tournament",
Location = "Lake Michigan, Green Bay",
Latitude = 44.5,
Longitude = -88.0,
TournamentType = TournamentType.SingleDay,
Format = TournamentFormat.Individual,
RegistrationStartDate = DateTime.UtcNow.AddDays(30),
RegistrationEndDate = DateTime.UtcNow.AddDays(60),
StartDate = DateTime.UtcNow.AddDays(60),
EndDate = DateTime.UtcNow.AddDays(61),
EntryFee = 50.00m,
Currency = "USD",
RequiresPayment = true,
MaxParticipants = 200,
ScoringType = TournamentScoringType.Weight,
MaxFishPerAngler = 5,
MinFishLengthInches = 20,
RequiresCatchVerification = true,
RequiresPhoto = true,
Visibility = TournamentVisibility.Public,
IsPublic = true,
Status = TournamentStatus.Draft
};
// Create Stripe product/price for entry fee
if (tournament.RequiresPayment)
{
var stripeProduct = await stripeService.CreateProduct(tournament.Name);
var stripePrice = await stripeService.CreatePrice(
stripeProduct.Id,
tournament.EntryFee,
tournament.Currency);
tournament.StripeProductId = stripeProduct.Id;
tournament.StripePriceId = stripePrice.Id;
}
context.Tournaments.Add(tournament);
await context.SaveChangesAsync();
Adding Categories
var chinookCategory = new TournamentCategory
{
TournamentId = tournament.Id,
Name = "Chinook Salmon",
Description = "Chinook/King Salmon category",
SpeciesId = chinookSpeciesId,
MaxFishPerAngler = 5,
PrizePool = 5000.00m,
PrizePlaces = 5,
DisplayOrder = 1
};
var cohoCategory = new TournamentCategory
{
TournamentId = tournament.Id,
Name = "Coho Salmon",
Description = "Coho/Silver Salmon category",
SpeciesId = cohoSpeciesId,
MaxFishPerAngler = 5,
PrizePool = 3000.00m,
PrizePlaces = 3,
DisplayOrder = 2
};
context.TournamentCategories.AddRange(chinookCategory, cohoCategory);
await context.SaveChangesAsync();
Adding Sponsors
var sponsor = new TournamentSponsor
{
TournamentId = tournament.Id,
SponsorName = "Shimano",
SponsorType = "Title Sponsor",
BrandId = shimanoBrandId,
SponsorshipAmount = 10000.00m,
SponsorshipType = "Cash",
LogoUrl = "https://cdn.example.com/sponsors/shimano-logo.png",
AdvertisementText = "Official Reel Sponsor",
DisplayOrder = 1,
IsFeatured = true,
ShowLogo = true,
IsApproved = true,
ApprovedAt = DateTime.UtcNow
};
context.TournamentSponsors.Add(sponsor);
await context.SaveChangesAsync();
Registration Flow
User Registers for Tournament
// Check if tournament is accepting registrations
var tournament = await context.Tournaments
.FirstOrDefaultAsync(t => t.Id == tournamentId
&& t.Status == TournamentStatus.Published
&& DateTime.UtcNow >= t.RegistrationStartDate
&& DateTime.UtcNow <= t.RegistrationEndDate);
if (tournament == null)
throw new NotFoundException("Tournament not found or registration closed");
// Check capacity
if (tournament.MaxParticipants.HasValue
&& tournament.CurrentParticipantCount >= tournament.MaxParticipants.Value)
{
// Add to waitlist
var registration = new TournamentRegistration
{
TournamentId = tournamentId,
UserId = userId,
Status = RegistrationStatus.Waitlisted,
EntryFee = tournament.EntryFee,
PaymentRequired = tournament.RequiresPayment
};
}
else
{
// Create payment intent if payment required
string? paymentIntentId = null;
if (tournament.RequiresPayment)
{
var paymentIntent = await stripeService.CreatePaymentIntent(
tournament.EntryFee,
tournament.Currency,
organizer.StripeAccountId); // Use organizer's Stripe Connect account
paymentIntentId = paymentIntent.Id;
}
var registration = new TournamentRegistration
{
TournamentId = tournamentId,
UserId = userId,
Status = RegistrationStatus.Pending,
EntryFee = tournament.EntryFee,
PaymentRequired = tournament.RequiresPayment,
StripePaymentIntentId = paymentIntentId,
AnglerName = $"{user.FirstName} {user.LastName}",
ContactEmail = user.Email,
RulesAcknowledged = true,
RulesAcknowledgedAt = DateTime.UtcNow
};
context.TournamentRegistrations.Add(registration);
tournament.CurrentParticipantCount++;
await context.SaveChangesAsync();
}
Payment Confirmation
// Webhook handler for Stripe payment confirmation
public async Task HandlePaymentSucceeded(string paymentIntentId)
{
var registration = await context.TournamentRegistrations
.FirstOrDefaultAsync(tr => tr.StripePaymentIntentId == paymentIntentId);
if (registration != null)
{
registration.PaymentReceived = true;
registration.PaymentReceivedAt = DateTime.UtcNow;
registration.Status = RegistrationStatus.Confirmed;
registration.ConfirmedAt = DateTime.UtcNow;
await context.SaveChangesAsync();
}
}
Result Submission
Submitting Tournament Result
public async Task SubmitTournamentResult(
int tournamentId,
Guid registrationId,
Guid fishingLogEntryId,
Guid catchDetailId)
{
var registration = await context.TournamentRegistrations
.Include(tr => tr.Tournament)
.Include(tr => tr.Category)
.FirstOrDefaultAsync(tr => tr.Id == registrationId);
var catchDetail = await context.CatchDetails
.Include(cd => cd.FishSpecies)
.FirstOrDefaultAsync(cd => cd.Id == catchDetailId);
// Validate catch meets tournament requirements
if (catchDetail.FishingLogEntry.CaughtAt < registration.Tournament.StartDate
|| catchDetail.FishingLogEntry.CaughtAt > registration.Tournament.EndDate)
throw new ValidationException("Catch must be during tournament dates");
if (registration.Tournament.SpeciesRestrictionId.HasValue
&& catchDetail.FishSpeciesId != registration.Tournament.SpeciesRestrictionId)
throw new ValidationException("Catch does not meet species requirement");
// Calculate score based on tournament scoring type
decimal score = registration.Tournament.ScoringType switch
{
TournamentScoringType.Weight => (decimal)(catchDetail.WeightKg * 2.20462), // Convert to lbs
TournamentScoringType.Length => (decimal)(catchDetail.LengthCm / 2.54), // Convert to inches
TournamentScoringType.BiggestFish => (decimal)(catchDetail.WeightKg * 2.20462),
_ => 0
};
var result = new TournamentResult
{
TournamentId = tournamentId,
RegistrationId = registrationId,
CategoryId = registration.CategoryId,
TeamId = registration.TeamId,
FishingLogEntryId = fishingLogEntryId,
CatchDetailId = catchDetailId,
Score = score,
WeightLbs = (decimal)(catchDetail.WeightKg * 2.20462),
LengthInches = (decimal)(catchDetail.LengthCm / 2.54),
CaughtAt = catchDetail.FishingLogEntry.CaughtAt,
IsVerified = registration.Tournament.RequiresCatchVerification
};
context.TournamentResults.Add(result);
await context.SaveChangesAsync();
// Update rankings
await UpdateTournamentRankings(tournamentId);
}
Sponsor Advertising
Displaying Sponsors on Tournament Page
var sponsors = await context.TournamentSponsors
.Where(ts => ts.TournamentId == tournamentId
&& ts.IsActive
&& ts.IsApproved)
.OrderBy(ts => ts.DisplayOrder)
.ToListAsync();
// Featured sponsors first
var featuredSponsors = sponsors.Where(s => s.IsFeatured);
var regularSponsors = sponsors.Where(s => !s.IsFeatured);
Benefits
- Verified Organizers: Only verified users can create paid tournaments
- Payment Processing: Stripe Connect integration for entry fees
- Flexible Formats: Individual, team, categories, multiple scoring types
- Sponsor Integration: Link brands/sponsors for advertising
- Result Tracking: Link catches to tournament scoring
- Verification: Optional catch verification for results
- Privacy Controls: Public, private, circle visibility
- Comprehensive: Rules, prizes, categories, teams
Future Enhancements
- Live Leaderboards: Real-time tournament standings
- Tournament Series: Multi-event series tracking
- Automated Payouts: Automatic prize distribution via Stripe
- Tournament Analytics: Statistics and insights
- Mobile Check-In: QR code check-in for tournaments
- Photo Contests: Photo-based tournament categories
- Social Features: Share tournament results, sponsor posts