Skip to main content

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:

  1. User submits organizer application
  2. Admin reviews identity documents, business licenses, tax IDs
  3. Stripe Connect account setup for payment processing
  4. 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);
}

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

  1. Verified Organizers: Only verified users can create paid tournaments
  2. Payment Processing: Stripe Connect integration for entry fees
  3. Flexible Formats: Individual, team, categories, multiple scoring types
  4. Sponsor Integration: Link brands/sponsors for advertising
  5. Result Tracking: Link catches to tournament scoring
  6. Verification: Optional catch verification for results
  7. Privacy Controls: Public, private, circle visibility
  8. Comprehensive: Rules, prizes, categories, teams

Future Enhancements

  1. Live Leaderboards: Real-time tournament standings
  2. Tournament Series: Multi-event series tracking
  3. Automated Payouts: Automatic prize distribution via Stripe
  4. Tournament Analytics: Statistics and insights
  5. Mobile Check-In: QR code check-in for tournaments
  6. Photo Contests: Photo-based tournament categories
  7. Social Features: Share tournament results, sponsor posts