Add external import sources, work history, screening fields and Phase 1 docs

- New ExternalImportSource and ExternalWorkHistoryEntry models, controller and migrations
- Screening fields on external profile imports
- Admin and provider profile views, routes and workflow tests updated
- Phase 1 validation, route inventory, gap report template and roadmap docs
- Add the Google Sheets work-order parser tool (tools/google-sheets-wo-parser)
This commit is contained in:
Codex
2026-10-02 15:50:18 -05:00
parent 0670f48878
commit 62220186af
37 changed files with 3284 additions and 4 deletions
+263
View File
@@ -4,6 +4,8 @@ namespace Tests\Feature;
use App\Models\JobPost;
use App\Models\AuditLog;
use App\Models\ExternalImportSource;
use App\Models\ExternalWorkHistoryEntry;
use App\Models\ModerationReport;
use App\Models\NotificationPreference;
use App\Models\ProviderProfile;
@@ -409,6 +411,267 @@ class ExchangeWorkflowTest extends TestCase
]);
}
public function test_provider_can_upload_private_external_import_source(): void
{
Storage::fake('local');
config()->set('provider-exchange.import_sources.disk', 'local');
config()->set('provider-exchange.import_sources.root', 'test-import-sources');
$provider = $this->userWithRole('provider');
$admin = $this->userWithRole('admin');
$profile = $provider->providerProfile()->create(['business_name' => 'Import Source Provider']);
$import = $profile->externalImports()->create([
'platform' => 'Field Nation',
'visibility' => 'private',
'verification_status' => 'provider_attested',
'status' => 'manual',
]);
$this->actingAs($provider)->post('/provider-profile/import-sources', [
'platform' => 'Field Nation',
'source_type' => 'profile_pdf',
'external_profile_import_id' => $import->id,
'provider_notes' => 'Contains private customer details. Redact before publishing.',
'source_file' => UploadedFile::fake()->create('field-nation-profile.pdf', 24, 'application/pdf'),
])->assertRedirect('/provider-profile');
$source = ExternalImportSource::firstOrFail();
$this->assertSame($profile->id, $source->provider_profile_id);
$this->assertSame($import->id, $source->external_profile_import_id);
$this->assertSame('pending', $source->parse_status);
$this->assertSame('needs_review', $source->review_status);
$this->assertStringStartsWith('test-import-sources/field-nation/', $source->path);
Storage::disk('local')->assertExists($source->path);
$this->assertDatabaseHas('audit_logs', [
'action' => 'external_import_source.uploaded',
'auditable_type' => ExternalImportSource::class,
'auditable_id' => $source->id,
]);
$this->actingAs($provider)->get('/provider-profile')
->assertOk()
->assertSee('Private import source vault')
->assertSee('field-nation-profile.pdf')
->assertSee('Contains private customer details');
$this->actingAs($admin)->get('/admin')
->assertOk()
->assertSee('Import source batches')
->assertSee('Import Source Provider')
->assertSee('field-nation-profile.pdf');
}
public function test_provider_cannot_attach_import_source_to_another_provider_import(): void
{
Storage::fake('local');
$provider = $this->userWithRole('provider');
$otherProvider = $this->userWithRole('provider');
$provider->providerProfile()->create(['business_name' => 'Owner Provider']);
$otherProfile = $otherProvider->providerProfile()->create(['business_name' => 'Other Provider']);
$otherImport = $otherProfile->externalImports()->create([
'platform' => 'Field Nation',
'visibility' => 'private',
'verification_status' => 'provider_attested',
'status' => 'manual',
]);
$this->actingAs($provider)->post('/provider-profile/import-sources', [
'platform' => 'Field Nation',
'source_type' => 'profile_pdf',
'external_profile_import_id' => $otherImport->id,
'source_file' => UploadedFile::fake()->create('other-provider.pdf', 12, 'application/pdf'),
])->assertNotFound();
$this->assertDatabaseCount('external_import_sources', 0);
}
public function test_provider_can_parse_and_approve_field_nation_text_source(): void
{
Storage::fake('local');
config()->set('provider-exchange.import_sources.disk', 'local');
config()->set('provider-exchange.import_sources.root', 'test-import-sources');
$provider = $this->userWithRole('provider');
$profile = $provider->providerProfile()->create(['business_name' => 'Parsed Provider']);
$sourceText = <<<TEXT
Field Nation Profile
Provider ID: 172-630
https://app.fieldnation.com/providers/172-630
Rating: 4.92
Reviews: 384
Completed Jobs: 1225
Clients: 74
On-time: 98%
Backout: 1%
Categories: POS, Networking, Cabling, Printer
Reliable and professional provider with excellent communication.
Prepared, responsive, and great quality work on point of sale installs.
TEXT;
$this->actingAs($provider)->post('/provider-profile/import-sources', [
'platform' => 'Field Nation',
'source_type' => 'plain_text',
'provider_notes' => 'Copied from my Field Nation profile for parser testing.',
'source_file' => UploadedFile::fake()->createWithContent('field-nation-profile.txt', $sourceText),
])->assertRedirect('/provider-profile');
$source = ExternalImportSource::firstOrFail();
$this->actingAs($provider)->post("/provider-profile/import-sources/{$source->id}/parse")
->assertRedirect('/provider-profile');
$source->refresh();
$this->assertSame('parsed', $source->parse_status);
$this->assertSame(4.92, $source->parsed_payload['rating']);
$this->assertSame(384, $source->parsed_payload['review_count']);
$this->assertSame(1225, $source->parsed_payload['completed_jobs']);
$this->assertContains('POS', $source->parsed_payload['work_categories']);
$this->actingAs($provider)->post("/provider-profile/import-sources/{$source->id}/approve", [
'visibility' => 'private',
'notes' => 'Approved into private imported history after review.',
])->assertRedirect('/provider-profile');
$this->assertDatabaseHas('external_profile_imports', [
'provider_profile_id' => $profile->id,
'platform' => 'Field Nation',
'external_id' => '172-630',
'status' => 'parsed_source',
'visibility' => 'private',
'rating' => 4.92,
'review_count' => 384,
'completed_jobs' => 1225,
'notes' => 'Approved into private imported history after review.',
]);
$source->refresh();
$this->assertSame('provider_reviewed', $source->review_status);
$this->assertNotNull($source->external_profile_import_id);
}
public function test_provider_can_parse_live_field_nation_profile_text_shape(): void
{
Storage::fake('local');
config()->set('provider-exchange.import_sources.disk', 'local');
config()->set('provider-exchange.import_sources.root', 'test-import-sources');
$provider = $this->userWithRole('provider');
$provider->providerProfile()->create(['business_name' => 'Live Shape Provider']);
$sourceText = <<<TEXT
Jason E. GarrisonID 172630 Oak Grove, KY, US
Job history 961 jobs completed 201 clients
Provider Success 85 good Provider Success Score
Timeliness Score 100% Excellent Based on your last 30 check-ins
Backout Score 0% Excellent Based on your last 30 assignments
Work summary Cameras 131 Jobs Point of Sale 91 Jobs Office Equipment 217 Jobs Telecom 222 Jobs Low Voltage Cabling 558 Jobs Server & Networking 496 Jobs
Rating All time 4.9 Marketplace 421 ratings See all 421 ratings
Endorsements Communication 76 Professionalism 78 Problem solving 72 Preparedness 72
Verified Screenings Background Check Passed on Jan 2020 Drug Test Passed on Feb 2026
TEXT;
$this->actingAs($provider)->post('/provider-profile/import-sources', [
'platform' => 'Field Nation',
'source_type' => 'plain_text',
'source_file' => UploadedFile::fake()->createWithContent('field-nation-live-profile.txt', $sourceText),
])->assertRedirect('/provider-profile');
$source = ExternalImportSource::firstOrFail();
$this->actingAs($provider)->post("/provider-profile/import-sources/{$source->id}/parse")
->assertRedirect('/provider-profile');
$source->refresh();
$this->assertSame('parsed', $source->parse_status);
$this->assertSame('172630', $source->parsed_payload['external_id']);
$this->assertSame(4.9, $source->parsed_payload['rating']);
$this->assertSame(421, $source->parsed_payload['review_count']);
$this->assertSame(961, $source->parsed_payload['completed_jobs']);
$this->assertSame(201, $source->parsed_payload['client_count']);
$this->assertEquals(100.0, $source->parsed_payload['on_time_rate']);
$this->assertEquals(0.0, $source->parsed_payload['backout_rate']);
$this->assertSame('passed', $source->parsed_payload['background_check_status']);
$this->assertSame('Jan 2020', $source->parsed_payload['background_check_date']);
$this->assertSame('passed', $source->parsed_payload['drug_test_status']);
$this->assertSame('Feb 2026', $source->parsed_payload['drug_test_date']);
$this->assertContains('Point of Sale', $source->parsed_payload['work_categories']);
$this->actingAs($provider)->post("/provider-profile/import-sources/{$source->id}/approve", [
'visibility' => 'summary',
])->assertRedirect('/provider-profile');
$this->get("/providers/{$provider->providerProfile->id}")
->assertOk()
->assertSee('Background check passed Jan 2020')
->assertSee('Drug test passed Feb 2026');
}
public function test_provider_can_parse_and_display_field_nation_work_history_csv(): void
{
Storage::fake('local');
config()->set('provider-exchange.import_sources.disk', 'local');
config()->set('provider-exchange.import_sources.root', 'test-import-sources');
$provider = $this->userWithRole('provider');
$profile = $provider->providerProfile()->create(['business_name' => 'CSV History Provider']);
$import = $profile->externalImports()->create([
'platform' => 'Field Nation',
'external_id' => '172630',
'visibility' => 'summary',
'verification_status' => 'provider_attested',
'status' => 'parsed_source',
]);
$csv = <<<CSV
"Service Title","Date","Customer","Type of Work","Location","Rating","Status","Work Order URL"
"Connect New Circuits to Customer-Provided Router none","Jul 16th, 2026","CommandLink","Networking","HOPKINSVILLE, KY","","Paid","https://app.fieldnation.com/wo/19490972"
"POS Remodel Installation none","Sep 2nd, 2015","Service Solutions INC","Point of Sale (POS)","North Vernon, IN","★ ★ ★ ★ ★ 5","Paid","https://app.fieldnation.com/wo/2084733"
CSV;
$this->actingAs($provider)->post('/provider-profile/import-sources', [
'platform' => 'Field Nation',
'source_type' => 'csv_export',
'external_profile_import_id' => $import->id,
'source_file' => UploadedFile::fake()->createWithContent('field-nation-history.csv', $csv),
])->assertRedirect('/provider-profile');
$source = ExternalImportSource::firstOrFail();
$this->actingAs($provider)->post("/provider-profile/import-sources/{$source->id}/parse")
->assertRedirect('/provider-profile');
$source->refresh();
$this->assertSame('parsed', $source->parse_status);
$this->assertSame('work_history_csv', $source->parsed_payload['import_kind']);
$this->assertSame(2, $source->parsed_payload['row_count']);
$this->actingAs($provider)->post("/provider-profile/import-sources/{$source->id}/approve", [
'visibility' => 'summary',
])->assertRedirect('/provider-profile');
$this->assertDatabaseHas('external_work_history_entries', [
'external_profile_import_id' => $import->id,
'external_work_order_id' => '19490972',
'service_title' => 'Connect New Circuits to Customer-Provided Router',
'customer_name' => 'CommandLink',
'work_type' => 'Networking',
'status' => 'Paid',
]);
$this->assertSame(2, ExternalWorkHistoryEntry::count());
$this->actingAs($provider)->get('/provider-profile')
->assertOk()
->assertSee('External work history')
->assertSee('CommandLink');
$this->get("/providers/{$profile->id}")
->assertOk()
->assertSee('Recent imported work')
->assertSee('Connect New Circuits to Customer-Provided Router')
->assertDontSee('CommandLink');
}
public function test_provider_directory_can_filter_and_sort_profiles(): void
{
$networkProvider = $this->userWithRole('provider');