Hacktoberfest 2026: the issues maintainers tagged for October, open and beginner-friendly. Browse Hacktoberfest issues

[BE] RaffleController — browse draws, buy tickets with token deduction

Open
#471 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
30/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Active
Tech stack
laravel, php
Domain
backend

Research direction

Start with the RaffleController skeleton in app/Http/Controllers/RaffleController.php and the /raffle group in routes/web.php, then find the arcade game controller that already uses DB::transaction() with lockForUpdate() as the reference pattern. The models RaffleDraw and RaffleTicket come from #461 and the tables from #459, so confirm both are merged first. Done means every acceptance checkbox passes, including the 422 cases and the 5/min throttle:raffle limit.

Written by the indexing model from the issue text.

Description

backend gamification P1

Context & Objective

Create RaffleController — the backend for participants to browse active prize draws and buy raffle tickets using their tokens. Token deduction follows the same DB::transaction() + lockForUpdate() pattern as the arcade games.

Files to Create / Modify

  • Create: app/Http/Controllers/RaffleController.php
  • Modify: routes/web.php — add /raffle route group

Technical Requirements

Routes
Route::prefix('/raffle')->name('raffle.')->group(function () {
    Route::get('/', [RaffleController::class, 'index'])->name('index');
    Route::middleware(['auth:sanctum', ..., 'throttle:raffle'])
         ->post('/{draw}/buy', [RaffleController::class, 'buyTickets'])->name('buy');
});

Register a throttle:raffle limiter (5 requests/minute per user).

index(Request $request)
public function index(Request $request): Response
{
    $edition  = $request->input('edition');
    $user     = $request->user();
    $draws    = RaffleDraw::where('edition_id', $edition->id)
                          ->orderByDesc('is_active')
                          ->orderBy('created_at')
                          ->get();

    $myTickets = [];
    $points    = null;
    if ($user?->isParticipant()) {
        $enrollment = $user->usertype->enrollments()
                          ->where('edition_id', $edition->id)->first();
        if ($enrollment) {
            $points    = $enrollment->points;
            $myTickets = RaffleTicket::where('enrollment_id', $enrollment->id)
                             ->whereIn('raffle_draw_id', $draws->pluck('id'))
                             ->pluck('quantity', 'raffle_draw_id');
        }
    }

    // Append total_tickets count to each draw
    $draws->each(fn($d) => $d->total_tickets = $d->tickets()->sum('quantity'));

    return Inertia::render('Raffle', [
        'draws'     => $draws,
        'myTickets' => $myTickets,  // keyed by draw_id
        'points'    => $points,
        'isEnrolled'=> isset($enrollment) && $enrollment !== null,
    ]);
}
buyTickets(Request $request, RaffleDraw $draw)
$request->validate(['quantity' => 'required|integer|min:1|max:50']);

$edition    = $request->input('edition');
$user       = $request->user();
abort_unless($user?->isParticipant(), 403);

$enrollment = $user->usertype->enrollments()
                   ->where('edition_id', $edition->id)->first();
abort_if($enrollment === null, 422, 'Não inscrito nesta edição');
abort_if(!$draw->is_active, 422, 'Sorteio encerrado');
abort_if($draw->winner_enrollment_id !== null, 422, 'Sorteio já realizado');
abort_if($draw->draw_at !== null && $draw->draw_at->isPast(), 422, 'Prazo encerrado');

$cost = $draw->ticket_price * $request->integer('quantity');

DB::transaction(function () use ($enrollment, $draw, $request, $cost) {
    $enrollment = Enrollment::lockForUpdate()->find($enrollment->id);
    abort_if($enrollment->points < $cost, 422, 'Saldo insuficiente');
    $enrollment->decrement('points', $cost);

    // Upsert: increment quantity if ticket row already exists
    RaffleTicket::updateOrCreate(
        ['raffle_draw_id' => $draw->id, 'enrollment_id' => $enrollment->id],
        ['quantity' => DB::raw("quantity + {$request->integer('quantity')}")]
    );
});

return redirect()->back()->banner("Bilhetes comprados com sucesso!");

Acceptance Criteria

  • GET /raffle renders the Raffle page with all draws and the user's ticket counts
  • Buying tickets deducts the correct token amount (ticket_price × quantity)
  • Cannot buy tickets for an inactive draw (422)
  • Cannot buy tickets for a draw that already has a winner (422)
  • Cannot buy tickets if draw_at is in the past (422)
  • Insufficient balance returns 422
  • Repeat purchase increments quantity on the existing row (no duplicate rows)
  • Rate limit of 5/min is enforced

Dependencies

  • #459 (raffle tables)
  • #461 (RaffleDraw, RaffleTicket models)
  • #462 (transaction pattern reference)
Dominant language
Vue
Stars
12
Forks
1
PR merge metrics
No merged PRs in 30d

Getting set up

  • Ships a Dockerfile or Docker Compose file
  • No pull request template
  • No contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from semanadeinformatica/website

All issues in semanadeinformatica/website

Similar issues

More Backend & API Design issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.