userWithRole('provider'); $buyer = $this->userWithRole('buyer'); $this->actingAs($provider)->put('/provider-profile', [ 'business_name' => 'Provider LLC', 'service_area' => 'Oklahoma', ])->assertRedirect('/provider-profile'); $this->actingAs($buyer)->put('/buyer-profile', [ 'company_name' => 'Buyer Inc', 'payment_terms' => 'Direct ACH', ])->assertRedirect('/buyer-profile'); $this->assertDatabaseHas('provider_profiles', ['business_name' => 'Provider LLC']); $this->assertDatabaseHas('buyer_profiles', ['company_name' => 'Buyer Inc']); } public function test_provider_can_submit_quote_and_buyer_can_create_work_order(): void { $provider = $this->userWithRole('provider'); $buyer = $this->userWithRole('buyer'); $job = JobPost::create([ 'buyer_id' => $buyer->id, 'title' => 'Install router', 'location' => 'Tulsa, OK', 'scope' => 'Install and test router.', 'visibility' => 'public', ]); $this->actingAs($provider)->post("/jobs/{$job->id}/quotes", [ 'requested_amount' => 300, 'rate_summary' => 'Two-hour minimum', ])->assertRedirect("/jobs/{$job->id}"); $quote = $job->quotes()->firstOrFail(); $this->actingAs($buyer)->post("/quotes/{$quote->id}/accept") ->assertRedirect(); $this->assertDatabaseHas('work_orders', [ 'job_post_id' => $job->id, 'provider_id' => $provider->id, 'buyer_id' => $buyer->id, 'status' => 'assigned', ]); } public function test_work_order_can_transition_review_and_open_dispute(): void { [$buyer, $provider, $workOrder] = $this->workOrderFixture(); foreach (['en_route', 'on_site', 'in_progress', 'completed'] as $status) { $this->actingAs($provider)->patch("/work-orders/{$workOrder->id}/transition", [ 'status' => $status, 'completion_notes' => $status === 'completed' ? 'Completed with photos.' : null, ])->assertRedirect("/work-orders/{$workOrder->id}"); $workOrder->refresh(); } $this->actingAs($buyer)->post("/work-orders/{$workOrder->id}/reviews", [ 'rating' => 5, 'body' => 'Clean work.', ])->assertRedirect("/work-orders/{$workOrder->id}"); $this->actingAs($provider)->post("/work-orders/{$workOrder->id}/disputes", [ 'summary' => 'Scope changed', 'claim' => 'Additional work was requested onsite.', ])->assertRedirect(); $this->assertDatabaseHas('work_orders', ['id' => $workOrder->id, 'status' => 'disputed']); $this->assertDatabaseHas('reviews', ['work_order_id' => $workOrder->id, 'rating' => 5]); $this->assertDatabaseHas('disputes', ['work_order_id' => $workOrder->id, 'summary' => 'Scope changed']); } public function test_external_profile_snapshot_can_be_saved(): void { $provider = $this->userWithRole('provider'); $provider->providerProfile()->create(['business_name' => 'Provider LLC']); $this->actingAs($provider)->post('/provider-profile/imports', [ 'platform' => 'Field Nation', 'external_id' => '172-630', 'rating' => 4.9, 'review_count' => 500, 'completed_jobs' => 1200, ])->assertRedirect('/provider-profile'); $this->assertDatabaseHas('external_profile_imports', [ 'platform' => 'Field Nation', 'external_id' => '172-630', ]); } public function test_provider_directory_can_filter_and_sort_profiles(): void { $networkProvider = $this->userWithRole('provider'); $fiberProvider = $this->userWithRole('provider'); $networkProfile = ProviderProfile::create([ 'user_id' => $networkProvider->id, 'business_name' => 'Alpha Network Services', 'headline' => 'Retail network support', 'service_area' => 'Tulsa and Oklahoma City', 'skills' => 'POS, network, router installs', 'insurance_status' => 'COI available', 'public_contact' => true, ]); ProviderProfile::create([ 'user_id' => $fiberProvider->id, 'business_name' => 'Beta Fiber Works', 'headline' => 'Fiber contractor', 'service_area' => 'Kansas City', 'skills' => 'Fiber splicing', 'insurance_status' => 'Insured', 'public_contact' => false, ]); $this->actingAs($fiberProvider)->post('/ratings', [ 'rateable_type' => 'provider_profile', 'rateable_id' => $networkProfile->id, 'category' => 'provider_overall', 'stars' => 5, 'body' => 'Strong network work.', ])->assertRedirect(); $this->get('/providers?service_area=Tulsa&skill=network&insurance=COI&public_contact=1&sort=rating') ->assertOk() ->assertSee('Alpha Network Services') ->assertSee('Public contact') ->assertSee('5/5') ->assertDontSee('Beta Fiber Works'); } public function test_buyer_directory_can_filter_and_sort_profiles(): void { $retailBuyer = $this->userWithRole('buyer'); $warehouseBuyer = $this->userWithRole('buyer'); $retailProfile = $retailBuyer->buyerProfile()->create([ 'company_name' => 'Alpha Retail Dispatch', 'headline' => 'Nationwide retail IT', 'service_categories' => 'Retail IT, POS, networking', 'hiring_regions' => 'Oklahoma and Texas', 'payment_terms' => 'Direct ACH Net 15', 'vendor_onboarding' => 'W9 and COI required', 'public_contact' => true, ]); $warehouseBuyer->buyerProfile()->create([ 'company_name' => 'Beta Warehouse Group', 'headline' => 'Warehouse projects', 'service_categories' => 'Low voltage', 'hiring_regions' => 'Michigan', 'payment_terms' => 'Paper check', 'public_contact' => false, ]); $this->actingAs($warehouseBuyer)->post('/ratings', [ 'rateable_type' => 'buyer_profile', 'rateable_id' => $retailProfile->id, 'category' => 'buyer_overall', 'stars' => 4, 'body' => 'Clear terms.', ])->assertRedirect(); $this->get('/buyers?category=POS®ion=Oklahoma&payment=ACH&public_contact=1&sort=rating') ->assertOk() ->assertSee('Alpha Retail Dispatch') ->assertSee('Public contact') ->assertSee('4/5') ->assertDontSee('Beta Warehouse Group'); } public function test_invalid_work_order_transition_is_rejected(): void { [, $provider, $workOrder] = $this->workOrderFixture(); $this->actingAs($provider)->patch("/work-orders/{$workOrder->id}/transition", [ 'status' => 'completed', ])->assertStatus(422); } public function test_comments_messages_dispute_votes_and_uploads_work(): void { Storage::fake('public'); [$buyer, $provider, $workOrder] = $this->workOrderFixture(); $post = SocialPost::create([ 'user_id' => $provider->id, 'body' => 'Available for work.', 'visibility' => 'public', ]); $this->actingAs($buyer)->post('/comments', [ 'commentable_type' => 'social_post', 'commentable_id' => $post->id, 'body' => 'Good to know.', ])->assertRedirect(); $this->actingAs($buyer)->post("/work-orders/{$workOrder->id}/messages", [ 'body' => 'Please upload completion photos.', ])->assertRedirect("/work-orders/{$workOrder->id}"); $this->actingAs($provider)->post('/attachments', [ 'attachable_type' => 'work_order', 'attachable_id' => $workOrder->id, 'caption' => 'Completion photo', 'file' => UploadedFile::fake()->image('complete.jpg'), ])->assertRedirect(); $response = $this->actingAs($provider)->post("/work-orders/{$workOrder->id}/disputes", [ 'summary' => 'Scope changed', 'claim' => 'Additional scope was requested.', ])->assertRedirect(); $dispute = $workOrder->disputes()->firstOrFail(); $this->actingAs($buyer)->post("/disputes/{$dispute->id}/votes", [ 'recommendation' => 'split', 'reason' => 'Both sides have partial documentation.', ])->assertRedirect("/disputes/{$dispute->id}"); $this->assertDatabaseHas('comments', ['body' => 'Good to know.']); $this->assertDatabaseHas('work_order_messages', ['body' => 'Please upload completion photos.']); $this->assertDatabaseHas('attachments', ['caption' => 'Completion photo']); $this->assertDatabaseHas('dispute_votes', ['recommendation' => 'split']); } public function test_everything_rating_layer_accepts_profile_and_work_order_ratings(): void { [$buyer, $provider, $workOrder] = $this->workOrderFixture(); $profile = ProviderProfile::create([ 'user_id' => $provider->id, 'business_name' => 'Rated Provider', ]); $this->actingAs($buyer)->post('/ratings', [ 'rateable_type' => 'provider_profile', 'rateable_id' => $profile->id, 'category' => 'provider_overall', 'stars' => 5, 'body' => 'Reliable provider.', ])->assertRedirect(); $this->actingAs($provider)->post('/ratings', [ 'rateable_type' => 'work_order', 'rateable_id' => $workOrder->id, 'category' => 'work_order_outcome', 'thumbs_up' => 1, 'body' => 'Good job outcome.', ])->assertRedirect(); $this->assertDatabaseHas('ratings', ['category' => 'provider_overall', 'stars' => 5]); $this->assertDatabaseHas('ratings', ['category' => 'work_order_outcome', 'thumbs_up' => true]); } public function test_notification_inbox_lists_and_marks_notifications_read(): void { $user = $this->userWithRole('provider'); $user->notify(new ExchangeEventNotification('Test notice', 'Body text', '/jobs', 'test')); $notification = $user->notifications()->firstOrFail(); $this->actingAs($user)->get('/notifications') ->assertOk() ->assertSee('Test notice'); $this->actingAs($user)->post("/notifications/{$notification->id}/read") ->assertRedirect(); $this->assertNotNull($notification->fresh()->read_at); } private function userWithRole(string $role): User { Role::findOrCreate($role); $user = User::factory()->create(); $user->assignRole($role); return $user; } private function workOrderFixture(): array { $provider = $this->userWithRole('provider'); $buyer = $this->userWithRole('buyer'); $job = JobPost::create([ 'buyer_id' => $buyer->id, 'title' => 'Repair printer', 'location' => 'OKC, OK', 'scope' => 'Repair printer.', 'visibility' => 'public', ]); $workOrder = WorkOrder::create([ 'job_post_id' => $job->id, 'buyer_id' => $buyer->id, 'provider_id' => $provider->id, 'status' => 'assigned', ]); return [$buyer, $provider, $workOrder]; } }