[BE] RaffleController — browse draws, buy tickets with token deduction
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 30/100
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
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/raffleroute 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 /rafflerenders 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_atis 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
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from semanadeinformatica/website
-
backend gamification P0
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
semanadeinformatica/website#461 ·
-
backend gamification P0
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
semanadeinformatica/website#460 ·
-
backend gamification P0
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
semanadeinformatica/website#459 ·
-
gamification P1
Difficulty 4/5 3-5 days Newbie friendliness 25/100
semanadeinformatica/website#479 ·
-
frontend gamification P1
Difficulty 3/5 1-2 days Newbie friendliness 62/100
semanadeinformatica/website#478 ·
All issues in semanadeinformatica/website
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
bigcapitalhq/bigcapital#1545 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
conda-forge/conda-forge-bot#7025 ·
Maintainers usually reply within 1 day
-
area/backends bug session-analysis
Difficulty 2/5 1-3 hours Newbie friendliness 69/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
home-assistant/core#188424 ·
Maintainers usually reply within 1 day