Phase 2 directory quality

This commit is contained in:
Codex
2026-07-10 13:07:19 -05:00
parent be017d5ace
commit a72d2aa164
7 changed files with 367 additions and 36 deletions
+1
View File
@@ -12,6 +12,7 @@ This project uses Git as the primary rollback trail. The `.old` convention is re
- Added roadmap guardrails for open-source-friendly dependencies, public themes, and future license audits.
- Documented `pdo_sqlite` and `sqlite3` as required PHP extensions for SQLite-backed local tests and first-pass deployments.
- Documented the preferred local-to-GitHub-to-remote-update workflow and the ChristIT conversion from rsync test deploy to Git checkout.
- Added Phase 2 directory quality improvements with advanced provider/buyer filters, sort controls, profile badges, rating-aware cards, result counts, and empty states.
- Created the Laravel Provider Exchange prototype with Breeze authentication, seeded demo users, and role support for admins, providers, buyers, and hybrid accounts.
- Added provider and buyer profile management with service areas, profile details, and external profile snapshot records.
- Added social posts, buyer job posts, provider quotes, quote revisions, quote decline flow, and buyer quote acceptance.
@@ -11,7 +11,9 @@ class BuyerProfileController extends Controller
{
public function index(Request $request): View
{
$profiles = BuyerProfile::with('user', 'attachments', 'ratings.user');
$profiles = BuyerProfile::with('user', 'attachments', 'ratings.user')
->withAvg(['ratings as average_stars' => fn ($query) => $query->whereNotNull('stars')], 'stars')
->withCount('ratings');
if ($search = $request->string('q')->toString()) {
$profiles->where(fn ($query) => $query
@@ -22,8 +24,31 @@ class BuyerProfileController extends Controller
);
}
if ($category = $request->string('category')->toString()) {
$profiles->where('service_categories', 'like', "%{$category}%");
}
if ($region = $request->string('region')->toString()) {
$profiles->where('hiring_regions', 'like', "%{$region}%");
}
if ($payment = $request->string('payment')->toString()) {
$profiles->where('payment_terms', 'like', "%{$payment}%");
}
if ($request->boolean('public_contact')) {
$profiles->where('public_contact', true);
}
match ($request->string('sort')->toString()) {
'name' => $profiles->orderBy('company_name'),
'rating' => $profiles->orderByDesc('average_stars')->orderByDesc('ratings_count'),
default => $profiles->latest(),
};
return view('buyers.index', [
'profiles' => $profiles->latest()->paginate(20)->withQueryString(),
'profiles' => $profiles->paginate(20)->withQueryString(),
'filters' => $request->only(['q', 'category', 'region', 'payment', 'public_contact', 'sort']),
]);
}
@@ -11,7 +11,9 @@ class ProviderProfileController extends Controller
{
public function index(Request $request): View
{
$profiles = ProviderProfile::with('user', 'externalImports', 'attachments', 'ratings.user');
$profiles = ProviderProfile::with('user', 'externalImports', 'attachments', 'ratings.user')
->withAvg(['ratings as average_stars' => fn ($query) => $query->whereNotNull('stars')], 'stars')
->withCount('ratings');
if ($search = $request->string('q')->toString()) {
$profiles->where(fn ($query) => $query
@@ -22,8 +24,31 @@ class ProviderProfileController extends Controller
);
}
if ($serviceArea = $request->string('service_area')->toString()) {
$profiles->where('service_area', 'like', "%{$serviceArea}%");
}
if ($skill = $request->string('skill')->toString()) {
$profiles->where('skills', 'like', "%{$skill}%");
}
if ($insurance = $request->string('insurance')->toString()) {
$profiles->where('insurance_status', 'like', "%{$insurance}%");
}
if ($request->boolean('public_contact')) {
$profiles->where('public_contact', true);
}
match ($request->string('sort')->toString()) {
'name' => $profiles->orderBy('business_name'),
'rating' => $profiles->orderByDesc('average_stars')->orderByDesc('ratings_count'),
default => $profiles->latest(),
};
return view('providers.index', [
'profiles' => $profiles->latest()->paginate(20)->withQueryString(),
'profiles' => $profiles->paginate(20)->withQueryString(),
'filters' => $request->only(['q', 'service_area', 'skill', 'insurance', 'public_contact', 'sort']),
]);
}
+110 -11
View File
@@ -1,18 +1,117 @@
<x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold">Buyers</h2></x-slot>
<div class="mx-auto max-w-7xl space-y-4 p-6">
<form method="GET" action="{{ route('buyers.index') }}" class="flex gap-3 rounded border bg-white p-4">
<input name="q" value="{{ request('q') }}" placeholder="Search buyers..." class="w-full rounded-md border-gray-300">
<x-slot name="header">
<x-page-header
title="Buyers"
description="Find companies hiring technical providers by category, region, payment terms, contact visibility, and community reputation."
>
@auth
@role('buyer')
<a href="{{ route('buyers.edit') }}" class="rounded-md bg-sky-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-700">Edit profile</a>
@endrole
@endauth
</x-page-header>
</x-slot>
<div class="mx-auto max-w-7xl space-y-5 px-4 py-8 sm:px-6 lg:px-8">
<form method="GET" action="{{ route('buyers.index') }}" class="tse-panel p-5">
<div class="grid gap-4 lg:grid-cols-4">
<div class="lg:col-span-2">
<label for="q" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Search</label>
<input id="q" name="q" value="{{ $filters['q'] ?? '' }}" placeholder="Company, headline, category, or region" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
</div>
<div>
<label for="category" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Service category</label>
<input id="category" name="category" value="{{ $filters['category'] ?? '' }}" placeholder="Retail, IT, cabling" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
</div>
<div>
<label for="region" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Hiring region</label>
<input id="region" name="region" value="{{ $filters['region'] ?? '' }}" placeholder="Oklahoma, national, Midwest" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
</div>
<div>
<label for="payment" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Payment terms</label>
<input id="payment" name="payment" value="{{ $filters['payment'] ?? '' }}" placeholder="ACH, Net 15, direct" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
</div>
<div>
<label for="sort" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Sort</label>
<select id="sort" name="sort" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
<option value="">Newest</option>
<option value="name" @selected(($filters['sort'] ?? '') === 'name')>Name</option>
<option value="rating" @selected(($filters['sort'] ?? '') === 'rating')>Community rating</option>
</select>
</div>
<div class="flex items-end">
<label class="inline-flex items-center gap-2 rounded-md border border-slate-200 px-3 py-2 text-sm text-slate-700 dark:border-slate-800 dark:text-slate-300">
<input type="checkbox" name="public_contact" value="1" @checked(request()->boolean('public_contact')) class="rounded border-slate-300 text-sky-600 shadow-sm focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950">
Public contact
</label>
</div>
<div class="flex items-end gap-2">
<x-primary-button>Search</x-primary-button>
<a href="{{ route('buyers.index') }}" class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800">Reset</a>
</div>
</div>
</form>
@foreach ($profiles as $profile)
<a href="{{ route('buyers.show', $profile) }}" class="block rounded border bg-white p-4 shadow-sm">
<h3 class="font-semibold">{{ $profile->company_name }}</h3>
<p class="text-sm text-gray-600">{{ $profile->headline }}</p>
<p class="mt-2 text-sm">{{ $profile->hiring_regions }}</p>
<x-rating-summary :ratings="$profile->ratings" />
</a>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p class="text-sm text-slate-600 dark:text-slate-400">
Showing {{ $profiles->firstItem() ?? 0 }}-{{ $profiles->lastItem() ?? 0 }} of {{ $profiles->total() }} buyer profiles.
</p>
<div class="flex flex-wrap gap-2">
@foreach (['q' => 'Search', 'category' => 'Category', 'region' => 'Region', 'payment' => 'Payment'] as $key => $label)
@if (! empty($filters[$key]))
<x-badge tone="sky">{{ $label }}: {{ $filters[$key] }}</x-badge>
@endif
@endforeach
@if (request()->boolean('public_contact'))
<x-badge tone="emerald">Public contact</x-badge>
@endif
</div>
</div>
<div class="grid gap-4 lg:grid-cols-2">
@forelse ($profiles as $profile)
@php
$average = $profile->average_stars ? round($profile->average_stars, 1) : null;
@endphp
<a href="{{ route('buyers.show', $profile) }}" class="tse-panel block p-5 transition hover:border-sky-300 hover:shadow-md dark:hover:border-sky-700">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 class="text-lg font-semibold text-slate-950 dark:text-white">{{ $profile->company_name }}</h3>
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">{{ $profile->headline ?: 'No headline yet.' }}</p>
</div>
<div class="text-left sm:text-right">
<p class="text-sm font-semibold text-slate-950 dark:text-white">{{ $average ? $average.'/5' : 'No rating' }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">{{ $profile->ratings_count }} rating{{ $profile->ratings_count === 1 ? '' : 's' }}</p>
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
@if ($profile->hiring_regions)
<x-badge tone="slate">{{ $profile->hiring_regions }}</x-badge>
@endif
@if ($profile->public_contact)
<x-badge tone="emerald">Public contact</x-badge>
@endif
@if ($profile->payment_terms)
<x-badge tone="sky">{{ $profile->payment_terms }}</x-badge>
@endif
@if ($profile->vendor_onboarding)
<x-badge tone="amber">Onboarding notes</x-badge>
@endif
</div>
@if ($profile->service_categories)
<p class="mt-4 text-sm leading-6 text-slate-600 dark:text-slate-400">{{ \Illuminate\Support\Str::limit($profile->service_categories, 220) }}</p>
@endif
</a>
@empty
<div class="tse-panel p-8 text-center lg:col-span-2">
<h3 class="text-base font-semibold text-slate-950 dark:text-white">No buyers matched this search.</h3>
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Try removing one filter or broadening the hiring region.</p>
</div>
@endforelse
</div>
{{ $profiles->links() }}
</div>
</x-app-layout>
@@ -8,9 +8,9 @@
$down = $thumbRatings->where('thumbs_up', false)->count();
@endphp
<div class="mt-4 rounded border bg-gray-50 p-4 text-sm">
<p class="font-semibold">Community rating</p>
<p class="mt-1 text-gray-700">
<div class="mt-4 rounded-md border border-slate-200 bg-slate-50 p-4 text-sm dark:border-slate-800 dark:bg-slate-950">
<p class="font-semibold text-slate-950 dark:text-white">Community rating</p>
<p class="mt-1 text-slate-700 dark:text-slate-300">
Stars: {{ $average ? $average.'/5' : 'n/a' }} from {{ $starRatings->count() }} vote{{ $starRatings->count() === 1 ? '' : 's' }}
| Thumbs: {{ $up }} up / {{ $down }} down
</p>
@@ -20,6 +20,6 @@
? $rating->stars.'/5'
: ($rating->thumbs_up === null ? 'no vote' : ($rating->thumbs_up ? 'thumbs up' : 'thumbs down'));
@endphp
<p class="mt-2 text-gray-600">{{ $rating->user->name ?? 'User' }} | {{ $rating->category }} | {{ $value }} {{ $rating->body ? '- '.$rating->body : '' }}</p>
<p class="mt-2 text-slate-600 dark:text-slate-400">{{ $rating->user->name ?? 'User' }} | {{ $rating->category }} | {{ $value }} {{ $rating->body ? '- '.$rating->body : '' }}</p>
@endforeach
</div>
+113 -13
View File
@@ -1,21 +1,121 @@
<x-app-layout>
<x-slot name="header"><h2 class="text-xl font-semibold">Providers</h2></x-slot>
<div class="mx-auto max-w-7xl space-y-4 p-6">
<form method="GET" action="{{ route('providers.index') }}" class="flex gap-3 rounded border bg-white p-4">
<input name="q" value="{{ request('q') }}" placeholder="Search providers..." class="w-full rounded-md border-gray-300">
<x-slot name="header">
<x-page-header
title="Providers"
description="Find independent technical providers by location, skill, availability, public contact status, and community reputation."
>
@auth
@role('provider')
<a href="{{ route('providers.edit') }}" class="rounded-md bg-sky-600 px-4 py-2 text-sm font-semibold text-white shadow-sm hover:bg-sky-700">Edit profile</a>
@endrole
@endauth
</x-page-header>
</x-slot>
<div class="mx-auto max-w-7xl space-y-5 px-4 py-8 sm:px-6 lg:px-8">
<form method="GET" action="{{ route('providers.index') }}" class="tse-panel p-5">
<div class="grid gap-4 lg:grid-cols-4">
<div class="lg:col-span-2">
<label for="q" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Search</label>
<input id="q" name="q" value="{{ $filters['q'] ?? '' }}" placeholder="Business, headline, skill, or area" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
</div>
<div>
<label for="service_area" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Service area</label>
<input id="service_area" name="service_area" value="{{ $filters['service_area'] ?? '' }}" placeholder="Tulsa, Oklahoma, Midwest" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
</div>
<div>
<label for="skill" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Skill</label>
<input id="skill" name="skill" value="{{ $filters['skill'] ?? '' }}" placeholder="POS, network, cabling" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
</div>
<div>
<label for="insurance" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Insurance</label>
<input id="insurance" name="insurance" value="{{ $filters['insurance'] ?? '' }}" placeholder="Insured, COI available" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
</div>
<div>
<label for="sort" class="block text-sm font-medium text-slate-800 dark:text-slate-200">Sort</label>
<select id="sort" name="sort" class="mt-1 block w-full rounded-md border-slate-300 bg-white text-slate-950 shadow-sm focus:border-sky-500 focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950 dark:text-slate-100">
<option value="">Newest</option>
<option value="name" @selected(($filters['sort'] ?? '') === 'name')>Name</option>
<option value="rating" @selected(($filters['sort'] ?? '') === 'rating')>Community rating</option>
</select>
</div>
<div class="flex items-end">
<label class="inline-flex items-center gap-2 rounded-md border border-slate-200 px-3 py-2 text-sm text-slate-700 dark:border-slate-800 dark:text-slate-300">
<input type="checkbox" name="public_contact" value="1" @checked(request()->boolean('public_contact')) class="rounded border-slate-300 text-sky-600 shadow-sm focus:ring-sky-500 dark:border-slate-700 dark:bg-slate-950">
Public contact
</label>
</div>
<div class="flex items-end gap-2">
<x-primary-button>Search</x-primary-button>
<a href="{{ route('providers.index') }}" class="rounded-md border border-slate-300 bg-white px-4 py-2 text-sm font-semibold text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-200 dark:hover:bg-slate-800">Reset</a>
</div>
</div>
</form>
@foreach ($profiles as $profile)
<a href="{{ route('providers.show', $profile) }}" class="block rounded border bg-white p-4 shadow-sm">
<h3 class="font-semibold">{{ $profile->business_name }}</h3>
<p class="text-sm text-gray-600">{{ $profile->headline }}</p>
<p class="mt-2 text-sm">{{ $profile->service_area }}</p>
<x-rating-summary :ratings="$profile->ratings" />
@foreach ($profile->externalImports as $import)
<span class="mt-2 inline-block rounded bg-gray-100 px-2 py-1 text-xs">{{ $import->platform }} {{ $import->rating ? $import->rating.'/5' : '' }}</span>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<p class="text-sm text-slate-600 dark:text-slate-400">
Showing {{ $profiles->firstItem() ?? 0 }}-{{ $profiles->lastItem() ?? 0 }} of {{ $profiles->total() }} provider profiles.
</p>
<div class="flex flex-wrap gap-2">
@foreach (['q' => 'Search', 'service_area' => 'Area', 'skill' => 'Skill', 'insurance' => 'Insurance'] as $key => $label)
@if (! empty($filters[$key]))
<x-badge tone="sky">{{ $label }}: {{ $filters[$key] }}</x-badge>
@endif
@endforeach
@if (request()->boolean('public_contact'))
<x-badge tone="emerald">Public contact</x-badge>
@endif
</div>
</div>
<div class="grid gap-4 lg:grid-cols-2">
@forelse ($profiles as $profile)
@php
$average = $profile->average_stars ? round($profile->average_stars, 1) : null;
$imports = $profile->externalImports;
@endphp
<a href="{{ route('providers.show', $profile) }}" class="tse-panel block p-5 transition hover:border-sky-300 hover:shadow-md dark:hover:border-sky-700">
<div class="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
<div>
<h3 class="text-lg font-semibold text-slate-950 dark:text-white">{{ $profile->business_name }}</h3>
<p class="mt-1 text-sm text-slate-600 dark:text-slate-400">{{ $profile->headline ?: 'No headline yet.' }}</p>
</div>
<div class="text-left sm:text-right">
<p class="text-sm font-semibold text-slate-950 dark:text-white">{{ $average ? $average.'/5' : 'No rating' }}</p>
<p class="text-xs text-slate-500 dark:text-slate-400">{{ $profile->ratings_count }} rating{{ $profile->ratings_count === 1 ? '' : 's' }}</p>
</div>
</div>
<div class="mt-4 flex flex-wrap gap-2">
@if ($profile->service_area)
<x-badge tone="slate">{{ $profile->service_area }}</x-badge>
@endif
@if ($profile->public_contact)
<x-badge tone="emerald">Public contact</x-badge>
@endif
@if ($profile->insurance_status)
<x-badge tone="sky">{{ $profile->insurance_status }}</x-badge>
@endif
@if ($profile->availability_notes)
<x-badge tone="amber">Availability noted</x-badge>
@endif
@if ($imports->isNotEmpty())
<x-badge tone="slate">{{ $imports->count() }} imported profile{{ $imports->count() === 1 ? '' : 's' }}</x-badge>
@endif
</div>
@if ($profile->skills)
<p class="mt-4 text-sm leading-6 text-slate-600 dark:text-slate-400">{{ \Illuminate\Support\Str::limit($profile->skills, 220) }}</p>
@endif
</a>
@endforeach
@empty
<div class="tse-panel p-8 text-center lg:col-span-2">
<h3 class="text-base font-semibold text-slate-950 dark:text-white">No providers matched this search.</h3>
<p class="mt-2 text-sm text-slate-600 dark:text-slate-400">Try removing one filter or broadening the service area.</p>
</div>
@endforelse
</div>
{{ $profiles->links() }}
</div>
</x-app-layout>
+81
View File
@@ -115,6 +115,87 @@ class ExchangeWorkflowTest extends TestCase
]);
}
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&region=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();