1139 lines
45 KiB
PHP
1139 lines
45 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\User;
|
|
use App\Models\Worker;
|
|
use App\Models\JobPost;
|
|
use App\Models\JobApplication;
|
|
use App\Models\Plan;
|
|
use App\Models\Subscription;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class WorkerJobApiTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
protected $employer;
|
|
protected $worker;
|
|
protected $plan;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
|
|
// Get standard plan
|
|
$this->plan = Plan::find('premium');
|
|
|
|
// Create employer with active subscription
|
|
$this->employer = User::create([
|
|
'name' => 'Employer User',
|
|
'email' => 'employer@example.com',
|
|
'password' => bcrypt('password'),
|
|
'role' => 'employer',
|
|
'subscription_status' => 'active',
|
|
'subscription_expires_at' => now()->addDays(30),
|
|
'api_token' => 'employer_api_token',
|
|
]);
|
|
|
|
Subscription::create([
|
|
'user_id' => $this->employer->id,
|
|
'plan_id' => 'premium',
|
|
'amount_aed' => 100.00,
|
|
'starts_at' => now(),
|
|
'expires_at' => now()->addDays(30),
|
|
'status' => 'active',
|
|
]);
|
|
|
|
// Create worker
|
|
$this->worker = Worker::create([
|
|
'name' => 'Worker User',
|
|
'email' => 'worker@example.com',
|
|
'password' => bcrypt('password'),
|
|
'phone' => '971501234567',
|
|
'nationality' => 'Indian',
|
|
'status' => 'active',
|
|
'api_token' => 'worker_api_token',
|
|
'age' => 25,
|
|
'salary' => 2000,
|
|
'availability' => 'Immediate',
|
|
'experience' => '2 Years',
|
|
'religion' => 'Christian',
|
|
'bio' => 'Helper info',
|
|
'passport_status' => 'valid',
|
|
'main_profession' => 'Plumber',
|
|
]);
|
|
|
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
|
$this->worker->skills()->sync([$skill->id]);
|
|
}
|
|
|
|
public function test_employer_job_crud_web()
|
|
{
|
|
// 1. Create Job (POST /employer/jobs/create)
|
|
$response = $this->actingAs($this->employer)
|
|
->withSession(['user' => $this->employer])
|
|
->post('/employer/jobs/create', [
|
|
'title' => 'Test Mason Job',
|
|
'main_profession' => 'Plumber',
|
|
'skills' => 'Piping',
|
|
'workers_needed' => 3,
|
|
'location' => 'Dubai',
|
|
'salary' => 2000,
|
|
'job_type' => 'Full Time',
|
|
'start_date' => now()->addDays(5)->format('Y-m-d'),
|
|
'description' => 'Test mason job description',
|
|
'requirements' => 'Must have experience',
|
|
]);
|
|
|
|
$response->assertRedirect('/employer/jobs');
|
|
|
|
$this->assertDatabaseHas('job_posts', [
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'Test Mason Job',
|
|
'location' => 'Dubai',
|
|
'status' => 'active',
|
|
]);
|
|
|
|
$job = JobPost::first();
|
|
|
|
// 2. View Job Details (GET /employer/jobs/{id})
|
|
$response = $this->actingAs($this->employer)
|
|
->withSession(['user' => $this->employer])
|
|
->get("/employer/jobs/{$job->id}");
|
|
$response->assertStatus(200);
|
|
|
|
// 3. Edit Job (POST /employer/jobs/{id}/edit)
|
|
$response = $this->actingAs($this->employer)
|
|
->withSession(['user' => $this->employer])
|
|
->post("/employer/jobs/{$job->id}/edit", [
|
|
'title' => 'Updated Mason Job',
|
|
'main_profession' => 'Plumber',
|
|
'skills' => 'Piping',
|
|
'workers_needed' => 2,
|
|
'location' => 'Abu Dhabi',
|
|
'salary' => 2500,
|
|
'job_type' => 'Part Time',
|
|
'start_date' => now()->addDays(10)->format('Y-m-d'),
|
|
'description' => 'Updated description',
|
|
'requirements' => 'Must speak English',
|
|
'status' => 'closed',
|
|
]);
|
|
|
|
$response->assertRedirect('/employer/jobs');
|
|
$this->assertDatabaseHas('job_posts', [
|
|
'id' => $job->id,
|
|
'title' => 'Updated Mason Job',
|
|
'status' => 'closed',
|
|
]);
|
|
|
|
// 4. Delete Job (DELETE /employer/jobs/{id})
|
|
$response = $this->actingAs($this->employer)
|
|
->withSession(['user' => $this->employer])
|
|
->delete("/employer/jobs/{$job->id}");
|
|
|
|
$response->assertRedirect('/employer/jobs');
|
|
$this->assertSoftDeleted('job_posts', [
|
|
'id' => $job->id,
|
|
]);
|
|
}
|
|
|
|
public function test_worker_job_apis()
|
|
{
|
|
// Setup an active job post
|
|
$job = JobPost::create([
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'Available Plumber Job',
|
|
'main_profession' => 'Plumber',
|
|
'location' => 'Dubai Marina',
|
|
'salary' => 3000,
|
|
'workers_needed' => 1,
|
|
'job_type' => 'Contract',
|
|
'start_date' => now()->addDays(2),
|
|
'description' => 'Plumber description',
|
|
'status' => 'active',
|
|
]);
|
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
|
$job->skills()->sync([$skill->id]);
|
|
|
|
// 1. List available jobs
|
|
$response = $this->getJson('/api/workers/jobs', [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
$response->assertJsonCount(1, 'data.jobs');
|
|
$response->assertJsonPath('data.jobs.0.title', 'Available Plumber Job');
|
|
$response->assertJsonPath('data.jobs.0.posted_by', 'Employer User');
|
|
$response->assertJsonPath('data.jobs.0.status', 'new');
|
|
|
|
// 2. Retrieve job details
|
|
$response = $this->getJson("/api/workers/jobs/{$job->id}", [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
$response->assertJsonPath('data.job.title', 'Available Plumber Job');
|
|
$response->assertJsonPath('data.job.posted_by', 'Employer User');
|
|
$response->assertJsonPath('data.job.status', 'new');
|
|
|
|
// 3. Apply for job
|
|
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(201);
|
|
$response->assertJsonPath('success', true);
|
|
$this->assertDatabaseHas('job_applications', [
|
|
'worker_id' => $this->worker->id,
|
|
'job_id' => $job->id,
|
|
'status' => 'applied',
|
|
]);
|
|
|
|
// Verify status becomes 'applied' in list available jobs
|
|
$response = $this->getJson('/api/workers/jobs', [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('data.jobs.0.status', 'applied');
|
|
|
|
// 4. Prevent duplicate job applications
|
|
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(409);
|
|
$response->assertJsonPath('success', false);
|
|
$response->assertJsonPath('message', 'You have already applied for this job.');
|
|
|
|
// 5. View applied jobs
|
|
$response = $this->getJson('/api/workers/applied-jobs', [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
$response->assertJsonCount(1, 'data.applications');
|
|
$response->assertJsonPath('data.applications.0.job.title', 'Available Plumber Job');
|
|
$response->assertJsonPath('data.applications.0.job.posted_by', 'Employer User');
|
|
|
|
$application = JobApplication::first();
|
|
|
|
// 6. Employer view applicants for job
|
|
$response = $this->getJson("/api/employers/jobs/{$job->id}/applicants", [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
$response->assertJsonCount(1, 'data.applicants');
|
|
$response->assertJsonPath('data.applicants.0.worker.name', 'Worker User');
|
|
|
|
// 7. Employer update application status
|
|
$response = $this->postJson("/api/employers/applications/{$application->id}/status", [
|
|
'status' => 'shortlisted'
|
|
], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
$this->assertDatabaseHas('job_applications', [
|
|
'id' => $application->id,
|
|
'status' => 'shortlisted',
|
|
]);
|
|
}
|
|
|
|
public function test_employer_view_applicants_filtering()
|
|
{
|
|
// 1. Create a job post
|
|
$job = JobPost::create([
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'Filter Test Job',
|
|
'main_profession' => 'Plumber',
|
|
'location' => 'Dubai',
|
|
'salary' => 2000,
|
|
'workers_needed' => 5,
|
|
'job_type' => 'Full Time',
|
|
'start_date' => now()->addDays(5)->format('Y-m-d'),
|
|
'description' => 'Filter test job description',
|
|
'status' => 'active',
|
|
]);
|
|
|
|
// 2. Create worker A: Male, Indian, Salary 2000, Age 25, Experience 2 Years
|
|
$workerA = Worker::create([
|
|
'name' => 'Worker A',
|
|
'email' => 'workera@example.com',
|
|
'password' => bcrypt('password'),
|
|
'phone' => '971500000010',
|
|
'nationality' => 'Indian',
|
|
'gender' => 'male',
|
|
'age' => 25,
|
|
'salary' => 2000,
|
|
'experience' => '2 Years',
|
|
'status' => 'active',
|
|
'api_token' => 'worker_a_token',
|
|
'availability' => 'Immediate',
|
|
'religion' => 'Christian',
|
|
'bio' => 'Bio info',
|
|
'passport_status' => 'valid',
|
|
'main_profession' => 'Plumber',
|
|
]);
|
|
|
|
// 3. Create worker B: Female, Filipino, Salary 3000, Age 30, Experience 5 Years
|
|
$workerB = Worker::create([
|
|
'name' => 'Worker B',
|
|
'email' => 'workerb@example.com',
|
|
'password' => bcrypt('password'),
|
|
'phone' => '971500000020',
|
|
'nationality' => 'Filipino',
|
|
'gender' => 'female',
|
|
'age' => 30,
|
|
'salary' => 3000,
|
|
'experience' => '5 Years',
|
|
'status' => 'active',
|
|
'api_token' => 'worker_b_token',
|
|
'availability' => 'Immediate',
|
|
'religion' => 'Christian',
|
|
'bio' => 'Bio info',
|
|
'passport_status' => 'valid',
|
|
'main_profession' => 'Plumber',
|
|
]);
|
|
|
|
// 4. Submit applications
|
|
JobApplication::create([
|
|
'job_id' => $job->id,
|
|
'worker_id' => $workerA->id,
|
|
'status' => 'applied'
|
|
]);
|
|
|
|
JobApplication::create([
|
|
'job_id' => $job->id,
|
|
'worker_id' => $workerB->id,
|
|
'status' => 'applied'
|
|
]);
|
|
|
|
// Test filter by gender=female (should return only Worker B)
|
|
$response = $this->getJson("/api/employers/jobs/{$job->id}/applicants?gender=female", [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonCount(1, 'data.applicants');
|
|
$response->assertJsonPath('data.applicants.0.worker.name', 'Worker B');
|
|
|
|
// Test filter by nationality=Indian (should return only Worker A)
|
|
$response = $this->getJson("/api/employers/jobs/{$job->id}/applicants?nationality=Indian", [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonCount(1, 'data.applicants');
|
|
$response->assertJsonPath('data.applicants.0.worker.name', 'Worker A');
|
|
|
|
// Test filter by salary_max=2500 (should return only Worker A)
|
|
$response = $this->getJson("/api/employers/jobs/{$job->id}/applicants?salary_max=2500", [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonCount(1, 'data.applicants');
|
|
$response->assertJsonPath('data.applicants.0.worker.name', 'Worker A');
|
|
|
|
// Test filter by age range "28-35" (should return only Worker B)
|
|
$response = $this->getJson("/api/employers/jobs/{$job->id}/applicants?age=28-35", [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonCount(1, 'data.applicants');
|
|
$response->assertJsonPath('data.applicants.0.worker.name', 'Worker B');
|
|
|
|
// Test filter by experience="5 Years" (should return only Worker B)
|
|
$response = $this->getJson("/api/employers/jobs/{$job->id}/applicants?experience=5%20Years", [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonCount(1, 'data.applicants');
|
|
$response->assertJsonPath('data.applicants.0.worker.name', 'Worker B');
|
|
}
|
|
|
|
public function test_employer_job_apis_and_access_restrictions()
|
|
{
|
|
// 1. List jobs via API (premium plan by default)
|
|
$response = $this->getJson('/api/employers/jobs', [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
$response->assertJsonCount(0, 'data.jobs');
|
|
|
|
// 2. Create job via API
|
|
$response = $this->postJson('/api/employers/jobs', [
|
|
'title' => 'API Clean Job',
|
|
'main_profession' => 'Plumber',
|
|
'skills' => 'Piping',
|
|
'workers_needed' => 3,
|
|
'location' => 'Dubai JLT',
|
|
'salary' => 2200,
|
|
'job_type' => 'Full Time',
|
|
'start_date' => now()->addDays(3)->format('Y-m-d'),
|
|
'description' => 'Clean job description',
|
|
'requirements' => 'Requirements content'
|
|
], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(201);
|
|
$response->assertJsonPath('success', true);
|
|
$jobId = $response->json('data.job.id');
|
|
|
|
$this->assertDatabaseHas('job_posts', [
|
|
'id' => $jobId,
|
|
'title' => 'API Clean Job',
|
|
'location' => 'Dubai JLT',
|
|
]);
|
|
|
|
// 3. Get job detail via API
|
|
$response = $this->getJson("/api/employers/jobs/{$jobId}", [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('data.job.title', 'API Clean Job');
|
|
|
|
// 4. Update job via API
|
|
$response = $this->postJson("/api/employers/jobs/{$jobId}/update", [
|
|
'title' => 'Updated API Clean Job',
|
|
'main_profession' => 'Plumber',
|
|
'skills' => 'Piping',
|
|
'workers_needed' => 4,
|
|
'location' => 'Dubai Marina',
|
|
'salary' => 2500,
|
|
'job_type' => 'Contract',
|
|
'start_date' => now()->addDays(4)->format('Y-m-d'),
|
|
'description' => 'Updated clean job description',
|
|
'requirements' => 'Updated requirements',
|
|
'status' => 'draft'
|
|
], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('data.job.title', 'Updated API Clean Job');
|
|
$response->assertJsonPath('data.job.status', 'draft');
|
|
|
|
// 5. Delete job via API
|
|
$response = $this->deleteJson("/api/employers/jobs/{$jobId}", [], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
$this->assertSoftDeleted('job_posts', [
|
|
'id' => $jobId
|
|
]);
|
|
|
|
// 6. Test basic plan restrictions (no job access)
|
|
Subscription::where('user_id', $this->employer->id)->update(['plan_id' => 'basic']);
|
|
|
|
// Check List Jobs block
|
|
$response = $this->getJson('/api/employers/jobs', [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(403);
|
|
$response->assertJsonPath('success', false);
|
|
$response->assertJsonPath('message', 'Your current plan does not support the Job Apply module. Please upgrade to a Premium plan to access this feature.');
|
|
|
|
// Check Create Job block
|
|
$response = $this->postJson('/api/employers/jobs', [
|
|
'title' => 'Blocked Job'
|
|
], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(403);
|
|
|
|
// Check Get Detail block
|
|
$response = $this->getJson("/api/employers/jobs/{$jobId}", [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(403);
|
|
|
|
// Check Update block
|
|
$response = $this->postJson("/api/employers/jobs/{$jobId}/update", [
|
|
'title' => 'Blocked Job'
|
|
], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(403);
|
|
|
|
// Check Delete block
|
|
$response = $this->deleteJson("/api/employers/jobs/{$jobId}", [], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(403);
|
|
|
|
// Check Applicants block
|
|
$response = $this->getJson("/api/employers/jobs/{$jobId}/applicants", [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(403);
|
|
|
|
// Check Update status block
|
|
$response = $this->postJson("/api/employers/applications/1/status", [
|
|
'status' => 'shortlisted'
|
|
], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(403);
|
|
|
|
// 7. Test contact limits enforcement (both Web and API)
|
|
// Set plan limits to max 1 contacted worker
|
|
Subscription::where('user_id', $this->employer->id)->update(['plan_id' => 'premium']);
|
|
$premiumPlan = Plan::find('premium');
|
|
$premiumPlan->update([
|
|
'unlimited_contacts' => false,
|
|
'max_contact_people' => 1
|
|
]);
|
|
|
|
// Create 2 workers
|
|
$workerA = Worker::create([
|
|
'name' => 'Worker A',
|
|
'email' => 'workera@example.com',
|
|
'phone' => '971501234568',
|
|
'nationality' => 'Indian',
|
|
'status' => 'active',
|
|
'age' => 25,
|
|
'salary' => 2000,
|
|
'availability' => 'Immediate',
|
|
'experience' => '2 Years',
|
|
'religion' => 'Christian',
|
|
'bio' => 'Helper A',
|
|
]);
|
|
|
|
$workerB = Worker::create([
|
|
'name' => 'Worker B',
|
|
'email' => 'workerb@example.com',
|
|
'phone' => '971501234569',
|
|
'nationality' => 'Indian',
|
|
'status' => 'active',
|
|
'age' => 25,
|
|
'salary' => 2000,
|
|
'availability' => 'Immediate',
|
|
'experience' => '2 Years',
|
|
'religion' => 'Christian',
|
|
'bio' => 'Helper B',
|
|
]);
|
|
|
|
// Create an active job for testing applications status updates
|
|
$activeJob = JobPost::create([
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'Active Job Limit Test',
|
|
'location' => 'Dubai',
|
|
'salary' => 2000,
|
|
'workers_needed' => 5,
|
|
'job_type' => 'Full Time',
|
|
'start_date' => now()->addDays(5),
|
|
'description' => 'Test description',
|
|
'status' => 'active',
|
|
]);
|
|
|
|
$appA = JobApplication::create([
|
|
'job_id' => $activeJob->id,
|
|
'worker_id' => $workerA->id,
|
|
'status' => 'applied'
|
|
]);
|
|
|
|
$appB = JobApplication::create([
|
|
'job_id' => $activeJob->id,
|
|
'worker_id' => $workerB->id,
|
|
'status' => 'applied'
|
|
]);
|
|
|
|
// First contact (Worker A) -> Should succeed
|
|
$response = $this->postJson("/api/employers/applications/{$appA->id}/status", [
|
|
'status' => 'shortlisted'
|
|
], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$this->assertEquals(1, User::contactedWorkersCount($this->employer->id));
|
|
|
|
// Second contact (Worker B) -> Should fail with 403 (contact limit reached)
|
|
$response = $this->postJson("/api/employers/applications/{$appB->id}/status", [
|
|
'status' => 'shortlisted'
|
|
], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(403);
|
|
$response->assertJsonPath('success', false);
|
|
$response->assertJsonFragment([
|
|
'message' => 'You have reached your contacted workers limit of 1 under your active plan. Please upgrade or renew your subscription to contact more workers.'
|
|
]);
|
|
|
|
// Web status update: first check web login and update status
|
|
// Web: contact limit block redirect
|
|
$response = $this->actingAs($this->employer)
|
|
->withSession(['user' => $this->employer])
|
|
->post("/employer/candidates/{$appB->id}/status", [
|
|
'status' => 'Offer Sent'
|
|
]);
|
|
|
|
$response->assertRedirect('/employer/subscription');
|
|
$response->assertSessionHas('error', 'You have reached your contacted workers limit of 1 under your active plan. Please upgrade or renew your subscription to contact more workers.');
|
|
}
|
|
|
|
public function test_job_hiring_workflow_enhancements()
|
|
{
|
|
// 1. Create a job
|
|
$job = JobPost::create([
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'Workflow Developer Job',
|
|
'location' => 'Dubai Marina',
|
|
'salary' => 5000,
|
|
'workers_needed' => 1,
|
|
'job_type' => 'Full Time',
|
|
'start_date' => now()->addDays(2),
|
|
'description' => 'Developer description',
|
|
'status' => 'active',
|
|
]);
|
|
|
|
// 2. Worker applies
|
|
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(201);
|
|
$app = JobApplication::first();
|
|
|
|
// 3. Worker views details of application
|
|
$response = $this->getJson("/api/workers/applied-jobs/{$app->id}", [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
$response->assertJsonPath('data.application.status', 'applied');
|
|
$response->assertJsonPath('data.application.job.posted_by', 'Employer User');
|
|
$response->assertJsonCount(0, 'data.application.status_history');
|
|
|
|
// 4. Employer updates status to shortlisted with notes
|
|
$response = $this->postJson("/api/employers/applications/{$app->id}/status", [
|
|
'status' => 'shortlisted',
|
|
'notes' => 'Very qualified candidate.'
|
|
], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
$response->assertJsonPath('data.application.status', 'shortlisted');
|
|
$response->assertJsonPath('data.application.notes', 'Very qualified candidate.');
|
|
$response->assertJsonCount(1, 'data.application.status_history');
|
|
$response->assertJsonPath('data.application.status_history.0.status', 'shortlisted');
|
|
$response->assertJsonPath('data.application.status_history.0.notes', 'Very qualified candidate.');
|
|
|
|
// 5. Verify saved workers sync: worker is now saved (shortlisted)
|
|
$this->assertDatabaseHas('shortlists', [
|
|
'employer_id' => $this->employer->id,
|
|
'worker_id' => $this->worker->id,
|
|
]);
|
|
|
|
// 6. Worker withdraws application -> should fail because status is shortlisted (not applied)
|
|
$response = $this->postJson("/api/workers/applied-jobs/{$app->id}/withdraw", [], [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(400);
|
|
|
|
// 7. Reset application status to applied to test withdraw
|
|
$app->refresh();
|
|
$app->update(['status' => 'applied']);
|
|
$response = $this->postJson("/api/workers/applied-jobs/{$app->id}/withdraw", [], [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$this->assertDatabaseMissing('job_applications', [
|
|
'id' => $app->id
|
|
]);
|
|
}
|
|
|
|
public function test_hired_worker_visibility_and_profile_access()
|
|
{
|
|
// Create EmployerProfile with a phone number
|
|
\App\Models\EmployerProfile::create([
|
|
'user_id' => $this->employer->id,
|
|
'company_name' => 'Premium Sponsoring Group',
|
|
'phone' => '971501234567',
|
|
'city' => 'Dubai',
|
|
'nationality' => 'UAE',
|
|
]);
|
|
|
|
// 1. Create a job post
|
|
$job = JobPost::create([
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'Special Hired Job',
|
|
'main_profession' => 'Plumber',
|
|
'location' => 'Dubai Jumeirah',
|
|
'salary' => 4500,
|
|
'workers_needed' => 1,
|
|
'job_type' => 'Full Time',
|
|
'start_date' => now()->addDays(5)->format('Y-m-d'),
|
|
'description' => 'A special job details description',
|
|
'status' => 'active',
|
|
]);
|
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
|
$job->skills()->sync([$skill->id]);
|
|
|
|
// 2. Worker applies for the job
|
|
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(201);
|
|
$app = JobApplication::latest()->first();
|
|
|
|
// 3. Employer hires the worker by setting status to 'hired' (lowercase, as in the database)
|
|
$response = $this->postJson("/api/employers/applications/{$app->id}/status", [
|
|
'status' => 'hired',
|
|
'notes' => 'Hired through job flow'
|
|
], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
|
|
// Under two-way confirmation, it should be in hire_requested status
|
|
$this->assertDatabaseHas('job_applications', [
|
|
'id' => $app->id,
|
|
'status' => 'hire_requested',
|
|
]);
|
|
|
|
// Worker confirms the hiring
|
|
$response = $this->postJson("/api/workers/applied-jobs/{$app->id}/confirm-hire", [], [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
|
|
// Verify status updates
|
|
$this->assertDatabaseHas('job_applications', [
|
|
'id' => $app->id,
|
|
'status' => 'hired',
|
|
]);
|
|
$this->assertDatabaseHas('workers', [
|
|
'id' => $this->worker->id,
|
|
'status' => 'Hired',
|
|
]);
|
|
|
|
// 4. Verify candidate is visible in Hired list (web: CandidateController::index)
|
|
$response = $this->actingAs($this->employer)
|
|
->withSession(['user' => $this->employer])
|
|
->get('/employer/candidates');
|
|
$response->assertStatus(200);
|
|
// Extract the Inertia prop 'selectedWorkers' to check
|
|
$inertiaData = $response->original->getData();
|
|
$selectedWorkers = $inertiaData['page']['props']['selectedWorkers'] ?? [];
|
|
$hiredList = array_values(array_filter($selectedWorkers, function ($w) {
|
|
return $w['worker_id'] === $this->worker->id;
|
|
}));
|
|
$this->assertCount(1, $hiredList);
|
|
$this->assertEquals('Hired', $hiredList[0]['status']);
|
|
|
|
// 5. Verify candidate is visible in Hired list (API: EmployerWorkerController::getCandidates)
|
|
$response = $this->getJson('/api/employers/candidates', [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
// Find the worker in candidates array
|
|
$candidates = $response->json('data.candidates');
|
|
$hiredApiList = array_values(array_filter($candidates, function ($c) {
|
|
return $c['worker_id'] === $this->worker->id;
|
|
}));
|
|
$this->assertCount(1, $hiredApiList);
|
|
$this->assertEquals('Hired', $hiredApiList[0]['status']);
|
|
|
|
// 6. Verify hired worker can view employer profile details bypassing subscription limits (WorkerProfileController::getEmployerProfile)
|
|
$response = $this->getJson("/api/workers/employers/{$this->employer->id}", [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
$response->assertJsonPath('data.employer.email', 'employer@example.com');
|
|
$response->assertJsonPath('data.employer.phone', '971501234567'); // should be exposed/visible because worker is hired
|
|
|
|
// 7. Verify hired worker dashboard shows correct currently working sponsor
|
|
$response = $this->getJson('/api/workers/dashboard', [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
$response->assertJsonPath('data.current_employer.id', $this->employer->id);
|
|
$response->assertJsonPath('data.currently_working_sponsor.id', $this->employer->id);
|
|
}
|
|
|
|
public function test_worker_can_decline_hire()
|
|
{
|
|
// 1. Create a job posting matching the worker's profession/skills
|
|
$job = JobPost::create([
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'Housekeeper/Nanny',
|
|
'main_profession' => 'Housekeeper',
|
|
'location' => 'Dubai Marina',
|
|
'salary' => 2500,
|
|
'workers_needed' => 1,
|
|
'job_type' => 'Full Time',
|
|
'start_date' => now()->addDays(5)->format('Y-m-d'),
|
|
'description' => 'Need clean house and nanny care.',
|
|
'status' => 'active',
|
|
]);
|
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
|
$job->skills()->sync([$skill->id]);
|
|
|
|
// 2. Worker applies for the job
|
|
$response = $this->postJson("/api/workers/jobs/{$job->id}/apply", [], [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(201);
|
|
$app = JobApplication::latest()->first();
|
|
|
|
// 3. Employer hires the worker by setting status to 'hired'
|
|
$response = $this->postJson("/api/employers/applications/{$app->id}/status", [
|
|
'status' => 'hired',
|
|
'notes' => 'Declining test offer'
|
|
], [
|
|
'Authorization' => 'Bearer employer_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
|
|
// Under two-way confirmation, it should be in hire_requested status
|
|
$this->assertDatabaseHas('job_applications', [
|
|
'id' => $app->id,
|
|
'status' => 'hire_requested',
|
|
]);
|
|
|
|
// Worker declines the hiring
|
|
$response = $this->postJson("/api/workers/applied-jobs/{$app->id}/decline-hire", [], [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
|
|
// Verify status updates
|
|
$this->assertDatabaseHas('job_applications', [
|
|
'id' => $app->id,
|
|
'status' => 'declined',
|
|
]);
|
|
$this->assertDatabaseHas('workers', [
|
|
'id' => $this->worker->id,
|
|
'status' => 'active', // Should still be active
|
|
]);
|
|
}
|
|
|
|
public function test_worker_can_view_job_matching_only_skills()
|
|
{
|
|
// 1. Create a job where main_profession is different from worker's (Plumber)
|
|
// Let's set it to 'Electrician'
|
|
$job = JobPost::create([
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'Electrician Needed',
|
|
'main_profession' => 'Electrician',
|
|
'location' => 'Dubai Marina',
|
|
'salary' => 3000,
|
|
'workers_needed' => 1,
|
|
'job_type' => 'Contract',
|
|
'start_date' => now()->addDays(2),
|
|
'description' => 'Electrician description',
|
|
'status' => 'active',
|
|
]);
|
|
|
|
// Sync the same skill 'Piping' (which the worker has)
|
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
|
$job->skills()->sync([$skill->id]);
|
|
|
|
// 2. Request list jobs
|
|
$response = $this->getJson('/api/workers/jobs', [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
|
|
$response->assertStatus(200);
|
|
$response->assertJsonPath('success', true);
|
|
|
|
// The worker's main profession is 'Plumber', but the job's main profession is 'Electrician'.
|
|
// However, the worker has the 'Piping' skill, which matches the job.
|
|
// Therefore, the job should show up.
|
|
$response->assertJsonFragment([
|
|
'title' => 'Electrician Needed'
|
|
]);
|
|
}
|
|
|
|
public function test_worker_can_filter_and_sort_jobs()
|
|
{
|
|
// 1. Create jobs with different contractTypes and salaries
|
|
$job1 = JobPost::create([
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'Contract Plumber Job',
|
|
'main_profession' => 'Plumber',
|
|
'location' => 'Dubai Marina',
|
|
'salary' => 1000,
|
|
'workers_needed' => 1,
|
|
'job_type' => 'Contract',
|
|
'start_date' => now()->addDays(2),
|
|
'description' => 'Fix pipes description',
|
|
'status' => 'active',
|
|
]);
|
|
$skill = \App\Models\Skill::firstOrCreate(['name' => 'Piping']);
|
|
$job1->skills()->sync([$skill->id]);
|
|
|
|
$job2 = JobPost::create([
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'Full Time Plumber Job',
|
|
'main_profession' => 'Plumber',
|
|
'location' => 'Dubai Marina',
|
|
'salary' => 5000,
|
|
'workers_needed' => 1,
|
|
'job_type' => 'Full Time',
|
|
'start_date' => now()->addDays(2),
|
|
'description' => 'Manage plumbing description',
|
|
'status' => 'active',
|
|
]);
|
|
$job2->skills()->sync([$skill->id]);
|
|
|
|
// 2. Filter by job_type = Contract
|
|
$response = $this->getJson('/api/workers/jobs?job_type=Contract', [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonFragment(['title' => 'Contract Plumber Job']);
|
|
$response->assertJsonMissingExact(['title' => 'Full Time Plumber Job']);
|
|
|
|
// 3. Search for "Manage"
|
|
$response = $this->getJson('/api/workers/jobs?search=Manage', [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonFragment(['title' => 'Full Time Plumber Job']);
|
|
$response->assertJsonMissingExact(['title' => 'Contract Plumber Job']);
|
|
|
|
// 4. Sort by PRICE: HIGH TO LOW
|
|
$response = $this->getJson('/api/workers/jobs?sortBy=PRICE:%20HIGH%20TO%20LOW', [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
// The first job in list should be the one with salary 5000 (Job 2)
|
|
$this->assertEquals('Full Time Plumber Job', $response->json('data.jobs.0.title'));
|
|
|
|
// 5. Filter by job_type directly
|
|
$response = $this->getJson('/api/workers/jobs?job_type=Full%20Time', [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonFragment(['title' => 'Full Time Plumber Job']);
|
|
$response->assertJsonMissingExact(['title' => 'Contract Plumber Job']);
|
|
|
|
// 6. Filter by skills
|
|
$cookingSkill = \App\Models\Skill::firstOrCreate(['name' => 'Cooking']);
|
|
$job2->skills()->sync([$skill->id, $cookingSkill->id]);
|
|
|
|
$response = $this->getJson('/api/workers/jobs?skills=Cooking', [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonFragment(['title' => 'Full Time Plumber Job']);
|
|
$response->assertJsonMissingExact(['title' => 'Contract Plumber Job']);
|
|
|
|
// 7. Filter by salary range
|
|
$response = $this->getJson('/api/workers/jobs?salary_min=2000&salary_max=6000', [
|
|
'Authorization' => 'Bearer worker_api_token'
|
|
]);
|
|
$response->assertStatus(200);
|
|
$response->assertJsonFragment(['title' => 'Full Time Plumber Job']);
|
|
$response->assertJsonMissingExact(['title' => 'Contract Plumber Job']);
|
|
}
|
|
|
|
public function test_candidate_status_update_sends_correct_notifications_to_worker()
|
|
{
|
|
// Fake Google OAuth and FCM endpoints
|
|
\Illuminate\Support\Facades\Http::fake([
|
|
'https://oauth2.googleapis.com/token' => \Illuminate\Support\Facades\Http::response(['access_token' => 'fake_token'], 200),
|
|
'https://fcm.googleapis.com/v1/projects/*' => \Illuminate\Support\Facades\Http::response(['name' => 'projects/migrant-fe5a7/messages/1234'], 200),
|
|
]);
|
|
|
|
// Create standard credentials file dummy for FCMService if not exists
|
|
$credentialsPath = base_path('migrant-fe5a7-firebase-adminsdk-fbsvc-f8b365ecd0.json');
|
|
$createdFile = false;
|
|
if (!file_exists($credentialsPath)) {
|
|
file_put_contents($credentialsPath, json_encode([
|
|
'private_key' => "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3\n-----END PRIVATE KEY-----",
|
|
'client_email' => 'fake-fcm@migrant.iam.gserviceaccount.com',
|
|
'project_id' => 'migrant-fe5a7'
|
|
]));
|
|
$createdFile = true;
|
|
}
|
|
|
|
// Give the worker an FCM token so FcmChannel is used
|
|
$this->worker->update(['fcm_token' => 'worker_test_fcm_token']);
|
|
|
|
// Create a job post
|
|
$job = JobPost::create([
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'Test Notification Job',
|
|
'location' => 'Dubai Marina',
|
|
'salary' => 3000,
|
|
'workers_needed' => 1,
|
|
'job_type' => 'Full Time',
|
|
'start_date' => now()->addDays(2),
|
|
'description' => 'Description here',
|
|
'status' => 'active',
|
|
]);
|
|
|
|
// Apply for the job
|
|
$application = JobApplication::create([
|
|
'worker_id' => $this->worker->id,
|
|
'job_id' => $job->id,
|
|
'status' => 'applied'
|
|
]);
|
|
|
|
// 1. Update status to 'shortlisted' via Web endpoint
|
|
$response = $this->actingAs($this->employer)
|
|
->withSession(['user' => $this->employer])
|
|
->post("/employer/candidates/{$application->id}/status", [
|
|
'status' => 'Shortlisted',
|
|
'notes' => 'Shortlist note'
|
|
]);
|
|
|
|
$response->assertSessionHasNoErrors();
|
|
|
|
// Assert that the database notification was created for worker
|
|
$this->assertDatabaseHas('notifications', [
|
|
'notifiable_id' => $this->worker->id,
|
|
'notifiable_type' => get_class($this->worker),
|
|
]);
|
|
|
|
// Verify FCM request was triggered
|
|
\Illuminate\Support\Facades\Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), 'messages:send') &&
|
|
str_contains($request['message']['token'], 'worker_test_fcm_token') &&
|
|
str_contains($request['message']['notification']['title'], 'Job Application Update') &&
|
|
str_contains($request['message']['data']['type'], 'shortlisted');
|
|
});
|
|
|
|
// 2. Update status to 'applied' (Review) and assert notification type is 'review'
|
|
$response = $this->actingAs($this->employer)
|
|
->withSession(['user' => $this->employer])
|
|
->post("/employer/candidates/{$application->id}/status", [
|
|
'status' => 'Applied',
|
|
'notes' => 'Reviewing again'
|
|
]);
|
|
|
|
$response->assertSessionHasNoErrors();
|
|
|
|
// Verify FCM request for 'review' was triggered
|
|
\Illuminate\Support\Facades\Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), 'messages:send') &&
|
|
str_contains($request['message']['token'], 'worker_test_fcm_token') &&
|
|
str_contains($request['message']['data']['type'], 'review');
|
|
});
|
|
|
|
// Clean up temporary credentials file if we created it
|
|
if ($createdFile && file_exists($credentialsPath)) {
|
|
unlink($credentialsPath);
|
|
}
|
|
}
|
|
|
|
public function test_direct_hiring_sends_correct_notifications_to_worker()
|
|
{
|
|
// Fake Google OAuth and FCM endpoints
|
|
\Illuminate\Support\Facades\Http::fake([
|
|
'https://oauth2.googleapis.com/token' => \Illuminate\Support\Facades\Http::response(['access_token' => 'fake_token'], 200),
|
|
'https://fcm.googleapis.com/v1/projects/*' => \Illuminate\Support\Facades\Http::response(['name' => 'projects/migrant-fe5a7/messages/1234'], 200),
|
|
]);
|
|
|
|
$credentialsPath = base_path('migrant-fe5a7-firebase-adminsdk-fbsvc-f8b365ecd0.json');
|
|
$createdFile = false;
|
|
if (!file_exists($credentialsPath)) {
|
|
file_put_contents($credentialsPath, json_encode([
|
|
'private_key' => "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC3\n-----END PRIVATE KEY-----",
|
|
'client_email' => 'fake-fcm@migrant.iam.gserviceaccount.com',
|
|
'project_id' => 'migrant-fe5a7'
|
|
]));
|
|
$createdFile = true;
|
|
}
|
|
|
|
$this->worker->update(['fcm_token' => 'worker_test_fcm_token']);
|
|
|
|
// 1. Create a direct Job Offer for the worker
|
|
$offer = \App\Models\JobOffer::create([
|
|
'employer_id' => $this->employer->id,
|
|
'worker_id' => $this->worker->id,
|
|
'work_date' => now()->format('Y-m-d'),
|
|
'location' => 'Dubai',
|
|
'salary' => 3000,
|
|
'notes' => 'Direct sponsoring',
|
|
'status' => 'pending',
|
|
]);
|
|
|
|
// Send a status update to 'Hired' via CandidateController (maps to pending for direct offers)
|
|
$response = $this->actingAs($this->employer)
|
|
->withSession(['user' => $this->employer])
|
|
->post("/employer/candidates/offer_{$offer->id}/status", [
|
|
'status' => 'Hired',
|
|
'notes' => 'Re-initiating direct hire offer'
|
|
]);
|
|
|
|
$response->assertSessionHasNoErrors();
|
|
|
|
// Verify FCM request for 'direct_hire_request' was triggered with correct text
|
|
\Illuminate\Support\Facades\Http::assertSent(function ($request) {
|
|
return str_contains($request->url(), 'messages:send') &&
|
|
str_contains($request['message']['token'], 'worker_test_fcm_token') &&
|
|
str_contains($request['message']['notification']['title'], 'Direct Hiring Offer') &&
|
|
str_contains($request['message']['notification']['body'], 'wants to hire you. Do you want to accept this job offer?') &&
|
|
str_contains($request['message']['data']['type'], 'direct_hire_request');
|
|
});
|
|
|
|
if ($createdFile && file_exists($credentialsPath)) {
|
|
unlink($credentialsPath);
|
|
}
|
|
}
|
|
|
|
public function test_basic_plan_employer_can_directly_hire_without_subscription_redirect()
|
|
{
|
|
// 1. Change employer subscription to 'basic'
|
|
$this->employer->subscription()->delete();
|
|
Subscription::create([
|
|
'user_id' => $this->employer->id,
|
|
'plan_id' => 'basic',
|
|
'amount_aed' => 0.00,
|
|
'starts_at' => now(),
|
|
'expires_at' => now()->addDays(30),
|
|
'status' => 'active',
|
|
]);
|
|
|
|
// Create a job post (representing a placeholder or existing one)
|
|
$job = JobPost::create([
|
|
'employer_id' => $this->employer->id,
|
|
'title' => 'General Helper Placeholder',
|
|
'location' => 'Dubai',
|
|
'salary' => 2000,
|
|
'job_type' => 'Full Time',
|
|
'start_date' => now()->addDays(5)->format('Y-m-d'),
|
|
'description' => 'Placeholder',
|
|
'status' => 'active',
|
|
]);
|
|
|
|
// Create a job application
|
|
$application = JobApplication::create([
|
|
'worker_id' => $this->worker->id,
|
|
'job_id' => $job->id,
|
|
'status' => 'applied'
|
|
]);
|
|
|
|
// 2. Try to update status to 'Shortlisted' - should redirect to subscription
|
|
$responseShortlist = $this->actingAs($this->employer)
|
|
->withSession(['user' => $this->employer])
|
|
->post("/employer/candidates/{$application->id}/status", [
|
|
'status' => 'Shortlisted',
|
|
'notes' => 'Try to shortlist'
|
|
]);
|
|
$responseShortlist->assertRedirect(route('employer.subscription'));
|
|
|
|
// 3. Try to update status to 'Hired' - should bypass redirect and succeed
|
|
$responseHire = $this->actingAs($this->employer)
|
|
->withSession(['user' => $this->employer])
|
|
->post("/employer/candidates/{$application->id}/status", [
|
|
'status' => 'Hired',
|
|
'notes' => 'Direct hiring worker'
|
|
]);
|
|
$responseHire->assertSessionHasNoErrors();
|
|
|
|
$this->assertDatabaseHas('job_applications', [
|
|
'id' => $application->id,
|
|
'status' => 'hire_requested',
|
|
]);
|
|
}
|
|
}
|
|
|
|
|