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:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user