+
Self-declared tags and evidence
+
Choose categories, specialties, skills, tools, and certifications that describe your work. Later passes can let buyers endorse or challenge these after completed work orders.
+
+ @foreach ($taxonomyTerms as $term)
+
+ id, $selectedTaxonomy, true)) class="rounded border-slate-300 text-sky-600 shadow-sm focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950">
+ {{ $term->name }} ({{ str_replace('_', ' ', $term->type) }})
+
+ @endforeach
+
+
+ @foreach ($profile->taxonomyTerms->take(8) as $term)
+ {{ $term->name }}
+ @endforeach
+
+ @endif
@empty
diff --git a/resources/views/providers/show.blade.php b/resources/views/providers/show.blade.php
index 56189f8..c0bcfc5 100644
--- a/resources/views/providers/show.blade.php
+++ b/resources/views/providers/show.blade.php
@@ -18,6 +18,12 @@
@if ($profile->insurance_status)
{{ $profile->insurance_status }}
@endif
+ {{ $profile->technicianLevel()['name'] }}
+
+
+
{{ $profile->technicianLevel()['short_name'] }}
+
{{ $profile->technicianLevel()['description'] }}
+
Level and tags are self-declared unless supported by buyer endorsements, completed TSE work, certification proof, or admin verification.
@if ($profile->visible('bio'))
@@ -25,6 +31,16 @@
@endif
+
+
Tags and evidence
+
+ @forelse ($profile->taxonomyTerms as $term)
+ {{ $term->name }}{{ $term->pivot?->evidence_source ? ' | '.str_replace('_', ' ', $term->pivot->evidence_source) : '' }}
+ @empty
+ No taxonomy tags yet.
+ @endforelse
+
+
@if ($profile->visible('services'))
Services
diff --git a/resources/views/work-orders/print.blade.php b/resources/views/work-orders/print.blade.php
index d7be547..e34fb4c 100644
--- a/resources/views/work-orders/print.blade.php
+++ b/resources/views/work-orders/print.blade.php
@@ -42,6 +42,7 @@
Scope Safeguards
Scope clarity: {{ str_replace('_', ' ', $workOrder->scope_clarity_status) }}
+ Technician level: {{ $workOrder->technicianLevel()['name'] }}
@if (is_array($workOrder->risk_flags) && $workOrder->risk_flags)
Risk flags: {{ collect($workOrder->risk_flags)->map(fn ($flag) => str_replace('_', ' ', $flag))->implode(', ') }}
@endif
diff --git a/resources/views/work-orders/show.blade.php b/resources/views/work-orders/show.blade.php
index d96cdd3..2cf3e4e 100644
--- a/resources/views/work-orders/show.blade.php
+++ b/resources/views/work-orders/show.blade.php
@@ -68,6 +68,7 @@
Scope {{ str_replace('_', ' ', $workOrder->scope_clarity_status) }}
+ {{ $workOrder->technicianLevel()['name'] }}
@if ($workOrder->contactSnapshotValue('contact_certified'))
Support certified
@else
diff --git a/tests/Feature/ExchangeWorkflowTest.php b/tests/Feature/ExchangeWorkflowTest.php
index 0a30e3c..71a7397 100644
--- a/tests/Feature/ExchangeWorkflowTest.php
+++ b/tests/Feature/ExchangeWorkflowTest.php
@@ -5,6 +5,7 @@ namespace Tests\Feature;
use App\Models\JobPost;
use App\Models\ProviderProfile;
use App\Models\SocialPost;
+use App\Models\TaxonomyTerm;
use App\Models\User;
use App\Models\WorkOrder;
use App\Notifications\ExchangeEventNotification;
@@ -26,6 +27,7 @@ class ExchangeWorkflowTest extends TestCase
$this->actingAs($provider)->put('/provider-profile', [
'business_name' => 'Provider LLC',
'service_area' => 'Oklahoma',
+ 'max_technician_level' => 2,
])->assertRedirect('/provider-profile');
$this->actingAs($buyer)->put('/buyer-profile', [
@@ -235,6 +237,8 @@ class ExchangeWorkflowTest extends TestCase
$this->actingAs($buyer)->post('/jobs', [
'title' => 'Replace POS lane router',
'service_category' => 'Point of Sale',
+ 'required_technician_level' => 3,
+ 'work_mode' => 'onsite',
'location' => 'Tulsa, OK',
'starts_at' => now()->addDays(3)->format('Y-m-d H:i:s'),
'time_window' => '9 AM - 12 PM',
@@ -399,6 +403,7 @@ class ExchangeWorkflowTest extends TestCase
$this->actingAs($provider)->put('/provider-profile', [
'business_name' => 'Structured Provider',
'bio' => 'Public bio.',
+ 'max_technician_level' => 3,
'services_text' => "Smart hands | entry\nNetwork troubleshooting | advanced",
'tool_inventory_text' => "Cable tester | network\nTone probe | cabling",
'certification_records_text' => "Low voltage license | Oklahoma\nA+ | CompTIA",
@@ -427,6 +432,48 @@ class ExchangeWorkflowTest extends TestCase
->assertDontSee('Internal only');
}
+ public function test_provider_taxonomy_tags_and_technician_level_can_filter_directory(): void
+ {
+ $provider = $this->userWithRole('provider');
+ $networkTag = TaxonomyTerm::create([
+ 'type' => 'skill',
+ 'name' => 'Field troubleshooter',
+ 'slug' => 'field-troubleshooter',
+ ]);
+
+ $this->actingAs($provider)->put('/provider-profile', [
+ 'business_name' => 'Tagged Provider',
+ 'service_area' => 'Tulsa',
+ 'max_technician_level' => 3,
+ 'skills' => 'Network and POS troubleshooting',
+ 'taxonomy_terms' => [$networkTag->id],
+ 'profile_visibility' => [
+ 'bio' => '1',
+ 'services' => '1',
+ 'tools' => '1',
+ 'certifications' => '1',
+ 'rate_card' => '1',
+ 'availability' => '1',
+ 'imports' => '1',
+ ],
+ ])->assertRedirect('/provider-profile');
+
+ $profile = $provider->providerProfile()->firstOrFail();
+
+ $this->assertSame(3, $profile->max_technician_level);
+ $this->assertDatabaseHas('provider_profile_taxonomy_term', [
+ 'provider_profile_id' => $profile->id,
+ 'taxonomy_term_id' => $networkTag->id,
+ 'evidence_source' => 'self_declared',
+ ]);
+
+ $this->get('/providers?technician_level=3&taxonomy_term_id='.$networkTag->id)
+ ->assertOk()
+ ->assertSee('Tagged Provider')
+ ->assertSee('Troubleshooter')
+ ->assertSee('Field troubleshooter');
+ }
+
public function test_buyer_directory_can_filter_and_sort_profiles(): void
{
$retailBuyer = $this->userWithRole('buyer');
@@ -497,6 +544,57 @@ class ExchangeWorkflowTest extends TestCase
->assertDontSee('Buyer internal note');
}
+ public function test_available_work_board_filters_by_category_level_and_safety_signals(): void
+ {
+ $buyer = $this->userWithRole('buyer');
+ $category = TaxonomyTerm::create([
+ 'type' => 'work_category',
+ 'name' => 'Point of Sale',
+ 'slug' => 'point-of-sale',
+ ]);
+
+ JobPost::create([
+ 'buyer_id' => $buyer->id,
+ 'title' => 'Clear POS troubleshooting',
+ 'status' => 'open',
+ 'service_category' => 'POS',
+ 'work_category_id' => $category->id,
+ 'required_technician_level' => 3,
+ 'work_mode' => 'onsite',
+ 'location' => 'Tulsa, OK',
+ 'scope' => 'Troubleshoot POS issue.',
+ 'primary_objective' => 'Find POS connectivity fault.',
+ 'included_work' => 'Test cabling and terminal connectivity.',
+ 'excluded_work' => 'No electrical work.',
+ 'deliverables' => 'Photos and notes',
+ 'closeout_conditions' => 'Buyer validates terminal online.',
+ 'contact_certified' => true,
+ 'scope_clarity_status' => 'clear',
+ 'risk_flags' => [],
+ 'visibility' => 'public',
+ ]);
+
+ JobPost::create([
+ 'buyer_id' => $buyer->id,
+ 'title' => 'Risky smart hands dump',
+ 'status' => 'open',
+ 'service_category' => 'Other',
+ 'required_technician_level' => 1,
+ 'work_mode' => 'onsite',
+ 'location' => 'OKC, OK',
+ 'scope' => 'Do everything onsite.',
+ 'scope_clarity_status' => 'needs_review',
+ 'risk_flags' => ['missing_scope_boundaries'],
+ 'visibility' => 'public',
+ ]);
+
+ $this->get('/jobs?work_category_id='.$category->id.'&technician_level=3&scope_clarity=clear&support_certified=1&hide_risky=1')
+ ->assertOk()
+ ->assertSee('Clear POS troubleshooting')
+ ->assertSee('Troubleshooter')
+ ->assertDontSee('Risky smart hands dump');
+ }
+
public function test_invalid_work_order_transition_is_rejected(): void
{
[, $provider, $workOrder] = $this->workOrderFixture();