Add provider tag verification
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -56,6 +56,11 @@ class ProviderProfile extends Model
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
public function tagVerifications(): HasMany
|
||||
{
|
||||
return $this->hasMany(ProviderTagVerification::class);
|
||||
}
|
||||
|
||||
public function technicianLevel(): array
|
||||
{
|
||||
return config('technician-levels')[(int) $this->max_technician_level] ?? config('technician-levels')[1];
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
#[Fillable([
|
||||
'work_order_id', 'provider_profile_id', 'buyer_id', 'provider_id',
|
||||
'declared_level', 'confirmed_level', 'level_verdict',
|
||||
'confirmed_term_ids', 'disputed_term_ids', 'suggested_tags', 'notes',
|
||||
])]
|
||||
class ProviderTagVerification extends Model
|
||||
{
|
||||
public const LEVEL_VERDICTS = [
|
||||
'confirmed' => 'Level matched the work',
|
||||
'overstated' => 'Provider level seemed overstated',
|
||||
'understated' => 'Provider could handle more advanced work',
|
||||
'not_observed' => 'Not enough evidence from this job',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'declared_level' => 'integer',
|
||||
'confirmed_level' => 'integer',
|
||||
'confirmed_term_ids' => 'array',
|
||||
'disputed_term_ids' => 'array',
|
||||
'suggested_tags' => 'array',
|
||||
];
|
||||
}
|
||||
|
||||
public function workOrder(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(WorkOrder::class);
|
||||
}
|
||||
|
||||
public function providerProfile(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(ProviderProfile::class);
|
||||
}
|
||||
|
||||
public function buyer(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'buyer_id');
|
||||
}
|
||||
|
||||
public function provider(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class, 'provider_id');
|
||||
}
|
||||
|
||||
public function confirmedTerms()
|
||||
{
|
||||
return TaxonomyTerm::whereIn('id', $this->confirmed_term_ids ?? [])->get();
|
||||
}
|
||||
|
||||
public function disputedTerms()
|
||||
{
|
||||
return TaxonomyTerm::whereIn('id', $this->disputed_term_ids ?? [])->get();
|
||||
}
|
||||
|
||||
public function confirmedLevelDefinition(): ?array
|
||||
{
|
||||
if (! $this->confirmed_level) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return config('technician-levels')[$this->confirmed_level] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ namespace App\Models;
|
||||
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasOne;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphMany;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
@@ -111,6 +112,11 @@ class WorkOrder extends Model
|
||||
return $this->hasMany(WorkOrderContactEvent::class);
|
||||
}
|
||||
|
||||
public function providerTagVerification(): HasOne
|
||||
{
|
||||
return $this->hasOne(ProviderTagVerification::class);
|
||||
}
|
||||
|
||||
public function canTransitionTo(string $status): bool
|
||||
{
|
||||
return in_array($status, self::ALLOWED_TRANSITIONS[$this->status] ?? [], true);
|
||||
|
||||
Reference in New Issue
Block a user