From e01caf678be0d8084093b0253362c1997f6e1361 Mon Sep 17 00:00:00 2001 From: Tim Basten Date: Fri, 6 Mar 2026 04:23:55 +0000 Subject: [PATCH] Fix form submission staying in draft status When no active approval workflow exists, ApprovalService::submit() was silently returning, leaving the request in Draft while showing a false success message. Now throws a RuntimeException as a safety net, and the Livewire component guards before creating any records and shows a clear error to the user. --- app/Services/ApprovalService.php | 2 +- .../livewire/travel-request/create.blade.php | 13 +- tests/Feature/TravelRequestSubmissionTest.php | 124 ++++++++++++++++++ 3 files changed, 137 insertions(+), 2 deletions(-) create mode 100644 tests/Feature/TravelRequestSubmissionTest.php diff --git a/app/Services/ApprovalService.php b/app/Services/ApprovalService.php index 474a8dd..744e3a0 100644 --- a/app/Services/ApprovalService.php +++ b/app/Services/ApprovalService.php @@ -17,7 +17,7 @@ class ApprovalService $workflow = $travelRequest->workflow; if (! $workflow) { - return; + throw new \RuntimeException('No active approval workflow is configured.'); } $steps = $workflow->steps()->orderBy('order')->get(); diff --git a/resources/views/livewire/travel-request/create.blade.php b/resources/views/livewire/travel-request/create.blade.php index 95353f5..db1de19 100644 --- a/resources/views/livewire/travel-request/create.blade.php +++ b/resources/views/livewire/travel-request/create.blade.php @@ -92,7 +92,6 @@ new #[Layout('components.layouts.app')] class extends Component { public function saveDraft(): void { $this->saveRequest(submit: false); - session()->flash('success', 'Draft saved successfully.'); } public function submit(): void @@ -102,6 +101,12 @@ new #[Layout('components.layouts.app')] class extends Component { private function saveRequest(bool $submit): void { + if ($submit && ! ApprovalWorkflow::where('is_active', true)->exists()) { + $this->addError('workflow', 'No active approval workflow is configured. Please contact an administrator.'); + + return; + } + $this->validate([ 'emergencyFullName' => ['required', 'string', 'max:255'], 'emergencyPhone' => ['required', 'string', 'max:50'], @@ -182,6 +187,8 @@ new #[Layout('components.layouts.app')] class extends Component { if ($submit) { app(ApprovalService::class)->submit($travelRequest); session()->flash('success', 'Travel request submitted for approval.'); + } else { + session()->flash('success', 'Draft saved successfully.'); } $this->redirect(route('travel-requests.show', $travelRequest), navigate: true); @@ -209,6 +216,10 @@ new #[Layout('components.layouts.app')] class extends Component { @endif + @error('workflow') +
{{ $message }}
+ @enderror +
{{-- Applicant Details --}} diff --git a/tests/Feature/TravelRequestSubmissionTest.php b/tests/Feature/TravelRequestSubmissionTest.php new file mode 100644 index 0000000..ebfb97d --- /dev/null +++ b/tests/Feature/TravelRequestSubmissionTest.php @@ -0,0 +1,124 @@ + */ + private array $validFormData; + + protected function setUp(): void + { + parent::setUp(); + + Role::firstOrCreate(['name' => 'staff']); + Role::firstOrCreate(['name' => 'travel_approver']); + Role::firstOrCreate(['name' => 'administrator']); + + $this->staff = User::factory()->create(); + $this->staff->assignRole('staff'); + + $this->validFormData = [ + 'emergencyFullName' => 'Jane Doe', + 'emergencyPhone' => '0400000000', + 'emergencyRelationship' => 'Spouse', + 'reasonSummary' => 'Attending a medical conference in Sydney', + 'journeys' => [ + [ + 'origin' => 'Perth', + 'destination' => 'Sydney', + 'date' => '2026-04-01', + 'time' => '09:00', + 'method' => JourneyMethod::Air->value, + ], + ], + 'costCodes' => [], + ]; + } + + private function makeWorkflow(): ApprovalWorkflow + { + $workflow = ApprovalWorkflow::factory()->create(['is_active' => true]); + + ApprovalStep::factory()->create([ + 'workflow_id' => $workflow->id, + 'order' => 1, + 'name' => 'Travel Approver Review', + 'role' => 'travel_approver', + ]); + + return $workflow; + } + + public function test_submitting_form_with_active_workflow_sets_status_to_pending(): void + { + $this->makeWorkflow(); + + Livewire::actingAs($this->staff) + ->test('travel-request.create') + ->set($this->validFormData) + ->call('submit'); + + $request = TravelRequest::first(); + $this->assertNotNull($request); + $this->assertSame(TravelStatus::Pending, $request->status); + $this->assertNotNull($request->submitted_at); + } + + public function test_submitting_form_with_active_workflow_creates_pending_approval(): void + { + $this->makeWorkflow(); + + Livewire::actingAs($this->staff) + ->test('travel-request.create') + ->set($this->validFormData) + ->call('submit'); + + $request = TravelRequest::first(); + $this->assertCount(1, $request->approvals); + $this->assertSame(ApprovalStatus::Pending->value, $request->approvals->first()->status->value); + } + + public function test_submitting_form_with_no_active_workflow_shows_error(): void + { + Livewire::actingAs($this->staff) + ->test('travel-request.create') + ->set($this->validFormData) + ->call('submit') + ->assertHasErrors(['workflow']); + + $this->assertDatabaseCount('travel_requests', 0); + } + + public function test_saving_draft_does_not_submit_for_approval(): void + { + $this->makeWorkflow(); + + Livewire::actingAs($this->staff) + ->test('travel-request.create') + ->set($this->validFormData) + ->call('saveDraft'); + + $request = TravelRequest::first(); + $this->assertNotNull($request); + $this->assertSame(TravelStatus::Draft, $request->status); + $this->assertNull($request->submitted_at); + $this->assertCount(0, $request->approvals); + } +}