diff --git a/app/Actions/Tabulation/EnterBonusScore.php b/app/Actions/Tabulation/EnterBonusScore.php new file mode 100644 index 0000000..042abe5 --- /dev/null +++ b/app/Actions/Tabulation/EnterBonusScore.php @@ -0,0 +1,75 @@ +basicValidations($judge, $entry); + $this->validateJudgeValidity($judge, $entry, $score); + $entries = $this->getRelatedEntries($entry); + + // Create the score for each related entry + foreach ($entries as $relatedEntry) { + BonusScore::create([ + 'entry_id' => $relatedEntry->id, + 'user_id' => $judge->id, + 'originally_scored_entry' => $entry->id, + 'score' => $score, + ]); + } + } + + protected function getRelatedEntries(Entry $entry): Collection + { + $bonusScore = $entry->audition->bonusScore->first(); + $relatedAuditions = $bonusScore->auditions; + + // Get all entries that have a student_id equal to that of entry and an audition_id in the related auditions + return Entry::where('student_id', $entry->student_id) + ->whereIn('audition_id', $relatedAuditions->pluck('id')) + ->get(); + } + + protected function basicValidations(User $judge, Entry $entry): void + { + if (! $judge->exists) { + throw new ScoreEntryException('Invalid judge provided'); + } + if (! $entry->exists) { + throw new ScoreEntryException('Invalid entry provided'); + } + if ($entry->audition->bonusScore->count() === 0) { + throw new ScoreEntryException('Entry does not have a bonus score'); + } + + } + + protected function validateJudgeValidity(User $judge, Entry $entry, $score): void + { + if (BonusScore::where('entry_id', $entry->id)->where('user_id', $judge->id)->exists()) { + throw new ScoreEntryException('That judge has already scored that entry'); + } + + $bonusScore = $entry->audition->bonusScore->first(); + if (! $bonusScore->judges->contains($judge)) { + throw new ScoreEntryException('That judge is not assigned to judge that bonus score'); + } + if ($score > $bonusScore->max_score) { + throw new ScoreEntryException('That score exceeds the maximum'); + } + } +} diff --git a/app/Models/BonusScore.php b/app/Models/BonusScore.php index 6629a7f..f75fba5 100644 --- a/app/Models/BonusScore.php +++ b/app/Models/BonusScore.php @@ -2,10 +2,9 @@ namespace App\Models; -use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class BonusScore extends Model { - use HasFactory; + protected $guarded = []; } diff --git a/app/Models/User.php b/app/Models/User.php index eeb74cb..4c0a9ab 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -11,7 +11,6 @@ use Illuminate\Database\Eloquent\Relations\HasManyThrough; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Collection; -use App\Models\ScoreSheet; class User extends Authenticatable implements MustVerifyEmail { @@ -118,6 +117,11 @@ class User extends Authenticatable implements MustVerifyEmail return $this->rooms(); } + public function bonusJudgingAssignments(): BelongsToMany + { + return $this->belongsToMany(BonusScoreDefinition::class, 'bonus_score_judge_assignment'); + } + public function advancementVotes(): HasMany { return $this->hasMany(JudgeAdvancementVote::class); diff --git a/tests/Feature/Actions/EnterBonusScoreTest.php b/tests/Feature/Actions/EnterBonusScoreTest.php new file mode 100644 index 0000000..f29af82 --- /dev/null +++ b/tests/Feature/Actions/EnterBonusScoreTest.php @@ -0,0 +1,134 @@ +enterBonusScore = App::make(EnterBonusScore::class); +}); + +it('rejects a non existent entry', function () { + $judge = User::factory()->create(); + $entry = Entry::factory()->make(); + $this->enterBonusScore->__invoke($judge, $entry, 42); +})->throws(ScoreEntryException::class, 'Invalid entry provided'); +it('rejects a non existent judge', function () { + $judge = User::factory()->make(); + $entry = Entry::factory()->create(); + $this->enterBonusScore->__invoke($judge, $entry, 42); +})->throws(ScoreEntryException::class, 'Invalid judge provided'); +it('rejects a submission if the entries audition does not have a bonus score', function () { + $judge = User::factory()->create(); + $entry = Entry::factory()->create(); + $this->enterBonusScore->__invoke($judge, $entry, 42); +})->throws(ScoreEntryException::class, 'Entry does not have a bonus score'); +it('rejects a submission if the entry already has a score from the given judge', function () { + // Arrange + $judge = User::factory()->create(); + $bonusScore = BonusScoreDefinition::factory()->create(); + $entry = Entry::factory()->create(); + $entry->audition->bonusScore()->attach($bonusScore->id); + $score = BonusScore::create([ + 'entry_id' => $entry->id, + 'user_id' => $judge->id, + 'originally_scored_entry' => $entry->id, + 'score' => 42, + ]); + // Act & Assert + $this->enterBonusScore->__invoke($judge, $entry, 43); +})->throws(ScoreEntryException::class, 'That judge has already scored that entry'); +it('rejects a submission for a judge not assigned to judge that bonus score', function () { + // Arrange + $judge = User::factory()->create(); + $bonusScore = BonusScoreDefinition::factory()->create(); + $entry = Entry::factory()->create(); + $entry->audition->bonusScore()->attach($bonusScore->id); + // Act & Assert + $this->enterBonusScore->__invoke($judge, $entry, 43); +})->throws(ScoreEntryException::class, 'That judge is not assigned to judge that bonus score'); +it('rejects a submission for a score that exceeds the maximum', function () { + // Arrange + $judge = User::factory()->create(); + $bonusScore = BonusScoreDefinition::factory()->create(['max_score' => 50]); + $bonusScore->judges()->attach($judge); + $entry = Entry::factory()->create(); + $entry->audition->bonusScore()->attach($bonusScore->id); + // Act & Assert + $this->enterBonusScore->__invoke($judge, $entry, 51); +})->throws(ScoreEntryException::class, 'That score exceeds the maximum'); + +it('records a valid bonus score submission on the submitted entry', function () { + // Arrange + $judge = User::factory()->create(); + $bonusScore = BonusScoreDefinition::factory()->create(['max_score' => 100]); + $entry = Entry::factory()->create(); + $entry->audition->bonusScore()->attach($bonusScore->id); + $bonusScore->judges()->attach($judge); + // Act & Assert + $this->enterBonusScore->__invoke($judge, $entry, 42); + expect( + BonusScore::where('entry_id', $entry->id) + ->where('user_id', $judge->id) + ->where('score', 42)->exists()) + ->toBeTrue(); +}); +it('records a valid bonus score on all related entries', function () { + // Arrange + $judge = User::factory()->create(); + $bonusScore = BonusScoreDefinition::factory()->create(['name' => 'Saxophone Improvisation', 'max_score' => 100]); + $bonusScore->judges()->attach($judge); + $jazzAltoAudition = Audition::factory()->create(['name' => 'Jazz Alto Saxophone']); + $jazzTenorAudition = Audition::factory()->create(['name' => 'Jazz Tenor Saxophone']); + $jazzBariAudition = Audition::factory()->create(['name' => 'Jazz Bari Saxophone']); + $bonusScore->auditions()->attach($jazzAltoAudition->id); + $bonusScore->auditions()->attach($jazzTenorAudition->id); + $bonusScore->auditions()->attach($jazzBariAudition->id); + $saxStudent = Student::factory()->create(); + $jazzAltoEntry = Entry::factory()->create([ + 'student_id' => $saxStudent->id, 'audition_id' => $jazzAltoAudition->id, + ]); + $jazzTenorEntry = Entry::factory()->create(['student_id' => $saxStudent->id, + 'audition_id' => $jazzTenorAudition->id, + ]); + $jazzBariEntry = Entry::factory()->create(['student_id' => $saxStudent->id, 'audition_id' => $jazzBariAudition->id, + ]); + Entry::factory()->count(4)->create(['audition_id' => $jazzAltoAudition->id]); + Entry::factory()->count(4)->create(['audition_id' => $jazzTenorAudition->id]); + Entry::factory()->count(4)->create(['audition_id' => $jazzBariAudition->id]); + // Act + $this->enterBonusScore->__invoke($judge, $jazzAltoEntry, 42); + // Assert + expect( + BonusScore::where('entry_id', $jazzAltoEntry->id) + ->where('user_id', $judge->id) + ->where('originally_scored_entry', $jazzAltoEntry->id) + ->where('score', 42)->exists()) + ->toBeTrue() + + ->and(BonusScore::count())->toBe(3) + + ->and( + BonusScore::where('entry_id', $jazzTenorEntry->id) + ->where('user_id', $judge->id) + ->where('originally_scored_entry', $jazzAltoEntry->id) + ->where('score', 42)->exists()) + ->toBeTrue() + + ->and( + BonusScore::where('entry_id', $jazzBariEntry->id) + ->where('user_id', $judge->id) + ->where('originally_scored_entry', $jazzAltoEntry->id) + ->where('score', 42)->exists()) + ->toBeTrue(); + +});