'2020-02-15', 'reference' => 'Membership payment', 'person_or_company' => 'Test company', 'note' => 'Test note', 'amount' => '125.50', 'account_type_id' => $this->incomeAccount->id, 'project_id' => $this->project->id, 'person_target_id' => null, 'personTarget' => ['person_id' => null, 'date' => null], ], $attributes); } public function test_user_can_create_a_booking_with_its_account_and_project() { $this->actingAs($this->user)->post('/bookings', $this->payload()) ->assertRedirect('/bookings/year-2020')->assertSessionHasNoErrors(); $booking = Booking::where('reference', 'Membership payment')->firstOrFail(); $this->assertSame('2020-02-15', $booking->date); $this->assertSame('Test company', $booking->person_or_company); $this->assertSame('Test note', $booking->note); $this->assertCount(1, $booking->subBookings); $this->assertDatabaseHas('sub_bookings', [ 'booking_id' => $booking->id, 'amount' => 125.50, 'reference' => 'Membership payment', 'account_type_id' => $this->incomeAccount->id, 'project_id' => $this->project->id, ]); } public function test_booking_forms_render_with_account_project_and_target_choices() { $target = $this->target(); $booking = $this->booking('2020-01-15', 100, null, ['person_target_id' => $target->id]); $this->actingAs($this->user)->get('/bookings/create')->assertStatus(200) ->assertSee($this->project->name)->assertSee($this->incomeAccount->name); // A fully paid target must still be selectable when editing its booking. $this->get('/bookings/' . $booking->id . '/edit')->assertStatus(200) ->assertSee($target->reference)->assertSee($booking->reference); $this->get('/bookings/' . $booking->id . '/delete')->assertStatus(200); } /** @dataProvider invalidBookings */ public function test_invalid_bookings_do_not_write_any_records($field, $value) { $this->actingAs($this->user)->from('/bookings/create') ->post('/bookings', $this->payload([$field => $value])) ->assertRedirect('/bookings/create')->assertSessionHasErrors($field); $this->assertSame(0, Booking::count()); $this->assertSame(0, SubBooking::count()); } public function invalidBookings() { return [ ['date', ''], ['reference', ''], ['person_or_company', ''], ['amount', ''], ['amount', 'not money'], ['amount', '-10'], ['account_type_id', ''], ]; } public function test_booking_can_pay_an_existing_target() { $target = $this->target(125.50); $this->actingAs($this->user)->post('/bookings', $this->payload(['person_target_id' => $target->id])) ->assertRedirect('/bookings/year-2020')->assertSessionHasNoErrors(); $this->assertDatabaseHas('sub_bookings', ['person_target_id' => $target->id, 'amount' => 125.50]); $this->assertTrue($target->fresh()->isPaid()); $this->assertSame(1, PersonTarget::count()); } public function test_booking_can_create_a_target_using_the_booking_date_by_default() { $person = $this->target()->person; $this->actingAs($this->user)->post('/bookings', $this->payload([ 'personTarget' => ['person_id' => $person->id, 'date' => null], ]))->assertRedirect('/bookings/year-2020')->assertSessionHasNoErrors(); $target = SubBooking::firstOrFail()->personTarget; $this->assertSame($person->id, $target->person_id); $this->assertSame('2020-02-15', $target->date); $this->assertEquals(125.50, $target->amount); $this->assertTrue($target->isPaid()); } public function test_user_cannot_choose_an_existing_target_and_create_one_together() { $target = $this->target(); $this->actingAs($this->user)->post('/bookings', $this->payload([ 'person_target_id' => $target->id, 'personTarget' => ['person_id' => $target->person_id, 'date' => '2020-01-01'], ]))->assertSessionHasErrors('person_target_id'); $this->assertSame(0, Booking::count()); $this->assertSame(1, PersonTarget::count()); } public function test_user_can_update_booking_and_split_it_into_income_and_expense_lines() { $booking = $this->booking('2019-12-31', 100); $line = $booking->subBookings->first(); $this->actingAs($this->user)->patch('/bookings/' . $booking->id, [ 'booking' => ['date' => '2020-01-15', 'reference' => 'Corrected', 'person_or_company' => 'Updated company'], 'sub_booking' => [ $line->id => ['reference' => 'Income', 'amount' => '120.50', 'account_type_id' => $this->incomeAccount->id], 0 => ['reference' => 'Fee', 'amount' => '20.25', 'account_type_id' => $this->expenseAccount->id], ], ])->assertRedirect('/bookings/year-2020')->assertSessionHasNoErrors(); $booking = $booking->fresh(); $this->assertSame('Corrected', $booking->reference); $this->assertSame('2020-01-15', $booking->date); $this->assertSame('Updated company', $booking->person_or_company); $this->assertCount(2, $booking->subBookings); $this->assertEquals(100.25, $booking->signedAmount()); $this->assertDatabaseHas('sub_bookings', ['id' => $line->id, 'amount' => 120.50]); $this->assertDatabaseHas('sub_bookings', ['booking_id' => $booking->id, 'reference' => 'Fee', 'amount' => 20.25]); } public function test_invalid_update_leaves_booking_and_lines_unchanged() { $booking = $this->booking('2020-01-15', 100); $line = $booking->subBookings->first(); $this->actingAs($this->user)->patch('/bookings/' . $booking->id, [ 'booking' => ['date' => '2020-02-01', 'reference' => 'Changed', 'person_or_company' => 'Company'], 'sub_booking' => [$line->id => ['reference' => 'Invalid', 'amount' => -1, 'account_type_id' => $this->incomeAccount->id]], ])->assertSessionHasErrors('sub_booking.' . $line->id . '.amount'); $this->assertSame('2020-01-15', $booking->fresh()->date); $this->assertEquals(100, $line->fresh()->amount); } public function test_empty_line_is_removed_while_other_lines_are_kept() { $booking = $this->booking('2020-01-15', 100); $line = $booking->subBookings->first(); $this->actingAs($this->user)->patch('/bookings/' . $booking->id, [ 'booking' => ['date' => $booking->date, 'reference' => $booking->reference, 'person_or_company' => $booking->person_or_company], 'sub_booking' => [ $line->id => ['reference' => null, 'amount' => null, 'account_type_id' => null], 0 => ['reference' => 'Replacement', 'amount' => 75, 'account_type_id' => $this->incomeAccount->id], ], ])->assertRedirect('/bookings/year-2020')->assertSessionHasNoErrors(); $this->assertDatabaseMissing('sub_bookings', ['id' => $line->id]); $this->assertCount(1, $booking->fresh()->subBookings); $this->assertEquals(75, $booking->fresh()->signedAmount()); } public function test_deleting_booking_cascades_to_its_lines_but_keeps_target_and_other_bookings() { $target = $this->target(); $booking = $this->booking('2020-01-15', 100, null, ['person_target_id' => $target->id]); $other = $this->booking('2020-02-15', 50); $this->actingAs($this->user)->delete('/bookings/' . $booking->id, ['date' => $booking->date]) ->assertRedirect('/bookings/year-2020'); $this->assertDatabaseMissing('bookings', ['id' => $booking->id]); $this->assertDatabaseMissing('sub_bookings', ['booking_id' => $booking->id]); $this->assertDatabaseHas('bookings', ['id' => $other->id]); $this->assertDatabaseHas('person_targets', ['id' => $target->id]); $this->assertEquals(100, $target->fresh()->difference()); } public function test_clearing_all_lines_redirects_to_delete_confirmation_without_deleting() { $booking = $this->booking('2020-01-15', 100); $line = $booking->subBookings->first(); $this->actingAs($this->user)->patch('/bookings/' . $booking->id, [ 'booking' => ['date' => $booking->date, 'reference' => $booking->reference, 'person_or_company' => $booking->person_or_company], 'sub_booking' => [$line->id => ['reference' => null, 'amount' => null, 'account_type_id' => null]], ])->assertRedirect('/bookings/' . $booking->id . '/delete')->assertSessionHasNoErrors(); $this->assertDatabaseHas('bookings', ['id' => $booking->id]); $this->assertDatabaseHas('sub_bookings', ['id' => $line->id, 'amount' => 100]); } public function test_unknown_booking_returns_not_found() { $this->actingAs($this->user)->get('/bookings/999999/edit')->assertStatus(404); } }