Add provider tag verification

This commit is contained in:
Codex
2026-07-10 17:55:52 -05:00
parent 3b856b5e46
commit 71434c1ac4
14 changed files with 504 additions and 11 deletions
@@ -139,7 +139,15 @@ class ProviderProfileController extends Controller
public function show(ProviderProfile $provider): View
{
return view('providers.show', [
'profile' => $provider->load('user', 'externalImports.attachments', 'taxonomyTerms', 'attachments', 'ratings.user'),
'profile' => $provider->load(
'user',
'externalImports.attachments',
'taxonomyTerms',
'attachments',
'ratings.user',
'tagVerifications.workOrder.jobPost',
'tagVerifications.buyer',
),
]);
}
@@ -0,0 +1,86 @@
<?php
namespace App\Http\Controllers;
use App\Models\ProviderTagVerification;
use App\Models\TaxonomyTerm;
use App\Models\WorkOrder;
use App\Notifications\ExchangeEventNotification;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class ProviderTagVerificationController extends Controller
{
public function store(Request $request, WorkOrder $workOrder): RedirectResponse
{
abort_unless($request->user()->id === $workOrder->buyer_id || $request->user()->hasRole('admin'), 403);
abort_unless(in_array($workOrder->status, ['completed', 'buyer_approved', 'closed'], true), 422, 'Provider tag verification is available after completed work.');
$profile = $workOrder->provider?->providerProfile;
abort_unless($profile, 422, 'The assigned provider does not have a provider profile yet.');
$data = $request->validate([
'level_verdict' => ['required', 'string', 'in:'.implode(',', array_keys(ProviderTagVerification::LEVEL_VERDICTS))],
'confirmed_level' => ['nullable', 'integer', 'min:1', 'max:5'],
'confirmed_term_ids' => ['nullable', 'array'],
'confirmed_term_ids.*' => ['integer', 'exists:taxonomy_terms,id'],
'disputed_term_ids' => ['nullable', 'array'],
'disputed_term_ids.*' => ['integer', 'exists:taxonomy_terms,id'],
'suggested_tags_text' => ['nullable', 'string'],
'notes' => ['nullable', 'string', 'max:4000'],
]);
$confirmed = $this->knownProfileTermIds($profile->id, $data['confirmed_term_ids'] ?? []);
$disputed = $this->knownProfileTermIds($profile->id, $data['disputed_term_ids'] ?? []);
$suggestedTags = collect(preg_split('/\r\n|\r|\n/', (string) ($data['suggested_tags_text'] ?? '')))
->map(fn (string $tag): string => trim($tag))
->filter()
->unique()
->values()
->all();
$verification = $workOrder->providerTagVerification()->updateOrCreate(
['work_order_id' => $workOrder->id],
[
'provider_profile_id' => $profile->id,
'buyer_id' => $workOrder->buyer_id,
'provider_id' => $workOrder->provider_id,
'declared_level' => $profile->max_technician_level,
'confirmed_level' => $data['confirmed_level'] ?? null,
'level_verdict' => $data['level_verdict'],
'confirmed_term_ids' => $confirmed,
'disputed_term_ids' => $disputed,
'suggested_tags' => $suggestedTags,
'notes' => $data['notes'] ?? null,
]
);
if ($confirmed !== []) {
$profile->taxonomyTerms()->syncWithoutDetaching(
collect($confirmed)->mapWithKeys(fn (int $termId): array => [$termId => ['evidence_source' => 'buyer_endorsed']])->all()
);
}
$workOrder->provider->notify(new ExchangeEventNotification(
'Provider tags reviewed',
$request->user()->name.' added post-work competency evidence for '.$workOrder->jobPost->title.'.',
route('work-orders.show', $workOrder),
'provider_tag_verification'
));
return redirect()->route('work-orders.show', $workOrder)->with('status', 'Provider tag verification saved.');
}
private function knownProfileTermIds(int $profileId, array $termIds): array
{
if ($termIds === []) {
return [];
}
return TaxonomyTerm::whereIn('id', $termIds)
->whereHas('providerProfiles', fn ($query) => $query->whereKey($profileId))
->pluck('id')
->map(fn ($id): int => (int) $id)
->all();
}
}
+2 -1
View File
@@ -34,10 +34,11 @@ class WorkOrderController extends Controller
'workOrder' => $workOrder->load(
'jobPost',
'buyer.buyerProfile',
'provider.providerProfile',
'provider.providerProfile.taxonomyTerms',
'acceptedQuote',
'structuredChangeRequests.requester',
'contactEvents.user',
'providerTagVerification.buyer',
'reviews.reviewer',
'reviews.reviewee',
'disputes.openedBy',