421 lines
16 KiB
PHP
421 lines
16 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature\Api;
|
|
|
|
use App\Models\Announcement;
|
|
use App\Models\AttendanceRecord;
|
|
use App\Models\Branch;
|
|
use App\Models\Employee;
|
|
use App\Models\LeaveApplication;
|
|
use App\Models\OvertimeRequest;
|
|
use App\Models\Payslip;
|
|
use App\Models\Trip;
|
|
use App\Models\TripExpense;
|
|
use App\Models\User;
|
|
use App\Http\Controllers\TripController;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Artisan;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Tests\TestCase;
|
|
|
|
class CrossClientIntegrationTest extends TestCase
|
|
{
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
Artisan::call('migrate');
|
|
DB::statement('PRAGMA foreign_keys = OFF;');
|
|
DB::table('personal_access_tokens')->truncate();
|
|
DB::table('mobile_messages')->truncate();
|
|
DB::table('mobile_conversation_participants')->truncate();
|
|
DB::table('mobile_conversations')->truncate();
|
|
DB::table('announcement_reads')->truncate();
|
|
DB::table('announcements')->truncate();
|
|
DB::table('overtime_requests')->truncate();
|
|
DB::table('trip_expenses')->truncate();
|
|
DB::table('trips')->truncate();
|
|
DB::table('payslips')->truncate();
|
|
DB::table('payroll_entries')->truncate();
|
|
DB::table('payroll_runs')->truncate();
|
|
DB::table('leave_applications')->truncate();
|
|
DB::table('attendance_records')->truncate();
|
|
DB::table('employees')->truncate();
|
|
DB::table('branches')->truncate();
|
|
DB::table('users')->truncate();
|
|
DB::statement('PRAGMA foreign_keys = ON;');
|
|
}
|
|
|
|
private function createClientUser($email = 'crossclient@example.com', $type = 'company', $createdBy = null, $attachBranch = true)
|
|
{
|
|
$uniqueEmail = rand(1000, 9999) . '_' . $email;
|
|
$user = User::create([
|
|
'email' => $uniqueEmail,
|
|
'name' => 'Cross Client User',
|
|
'password' => bcrypt('password'),
|
|
'is_enable_login' => 1,
|
|
'status' => 'active',
|
|
'slug' => 'cross-client-user-' . rand(1000, 9999),
|
|
'type' => $type,
|
|
'created_by' => $createdBy ?? 1,
|
|
]);
|
|
|
|
$branchId = null;
|
|
if ($attachBranch) {
|
|
$branch = Branch::firstOrCreate(
|
|
['id' => 1],
|
|
[
|
|
'name' => 'HQ Branch',
|
|
'latitude' => 14.5995,
|
|
'longitude' => 120.9842,
|
|
'radius' => 500,
|
|
'enable_clock_in_out' => 1,
|
|
'created_by' => $user->created_by ?? 1,
|
|
]
|
|
);
|
|
$branchId = $branch->id;
|
|
}
|
|
|
|
$employee = Employee::firstOrCreate(
|
|
['user_id' => $user->id],
|
|
[
|
|
'name' => 'Cross Client User',
|
|
'email' => $uniqueEmail,
|
|
'employee_id' => 'EMP-CROSS-' . rand(1000, 9999),
|
|
'branch_id' => $branchId,
|
|
'created_by' => $user->created_by ?? 1,
|
|
]
|
|
);
|
|
|
|
return [$user, $employee];
|
|
}
|
|
|
|
public function test_clock_in_requires_assigned_branch()
|
|
{
|
|
[$userNoBranch, $empNoBranch] = $this->createClientUser('nobranch@example.com', 'employee', 1, false);
|
|
|
|
$res = $this->actingAs($userNoBranch, 'sanctum')
|
|
->postJson('/api/attendance/clock', [
|
|
'action' => 'clock_in',
|
|
'latitude' => 14.5995,
|
|
'longitude' => 120.9842,
|
|
]);
|
|
|
|
$res->assertStatus(422)
|
|
->assertJson(['success' => false, 'message' => 'No assigned branch found for employee. Please assign a branch prior to clocking in.']);
|
|
}
|
|
|
|
// Scenario 1: Web clock-in -> Mobile attendance history
|
|
public function test_web_clock_in_visible_in_mobile_history()
|
|
{
|
|
[$user, $employee] = $this->createClientUser('web_history_sc1@example.com');
|
|
|
|
AttendanceRecord::create([
|
|
'employee_id' => $user->id,
|
|
'branch_id' => $employee->branch_id,
|
|
'date' => date('Y-m-d'),
|
|
'clock_in' => '08:00:00',
|
|
'status' => 'present',
|
|
'notes' => 'Web Created Clockin',
|
|
'created_by' => $user->id,
|
|
]);
|
|
|
|
$res = $this->actingAs($user, 'sanctum')
|
|
->getJson('/api/attendance/history');
|
|
|
|
$res->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
|
|
$items = $res->json('data');
|
|
$this->assertNotEmpty($items);
|
|
$this->assertEquals('Web Created Clockin', $items[0]['notes']);
|
|
}
|
|
|
|
// Scenario 2: Mobile leave submission -> Web leave approval (with organizational scoping)
|
|
public function test_mobile_leave_submission_and_scoped_approval()
|
|
{
|
|
[$company, $cEmp] = $this->createClientUser('company_admin_sc2@example.com', 'company', 1);
|
|
[$otherCompany, $oCEmp] = $this->createClientUser('other_company_sc2@example.com', 'company', 1);
|
|
|
|
[$manager, $mEmp] = $this->createClientUser('manager_same_sc2@example.com', 'manager', $company->id);
|
|
[$employee, $eEmp] = $this->createClientUser('employee_same_sc2@example.com', 'employee', $company->id);
|
|
[$otherManager, $oEmp] = $this->createClientUser('other_org_mgr_sc2@example.com', 'manager', $otherCompany->id);
|
|
|
|
// Employee applies
|
|
$applyRes = $this->actingAs($employee, 'sanctum')
|
|
->postJson('/api/leaves/apply', [
|
|
'leave_type' => 'vacation',
|
|
'start_date' => date('Y-m-d', strtotime('+5 days')),
|
|
'end_date' => date('Y-m-d', strtotime('+7 days')),
|
|
'reason' => 'Family Holiday',
|
|
]);
|
|
|
|
$applyRes->assertStatus(201);
|
|
$leaveId = $applyRes->json('data.id');
|
|
|
|
// Other company manager attempt -> 403 Forbidden
|
|
$otherApproveRes = $this->actingAs($otherManager, 'sanctum')
|
|
->putJson("/api/leaves/applications/{$leaveId}/status", ['status' => 'approved']);
|
|
$otherApproveRes->assertStatus(403);
|
|
|
|
// Same company manager approval -> 200 OK
|
|
$sameApproveRes = $this->actingAs($manager, 'sanctum')
|
|
->putJson("/api/leaves/applications/{$leaveId}/status", ['status' => 'approved']);
|
|
$sameApproveRes->assertStatus(200)
|
|
->assertJson(['success' => true, 'data' => ['status' => 'approved']]);
|
|
}
|
|
|
|
// Scenario 3: Reimbursement back-and-forth flow (Mobile submission -> Web approval -> Mobile status verification)
|
|
public function test_reimbursement_back_and_forth_web_and_mobile()
|
|
{
|
|
[$user, $employee] = $this->createClientUser('reimbursement_sc3@example.com');
|
|
|
|
// 1. Mobile submits a reimbursement claim
|
|
$res = $this->actingAs($user, 'sanctum')
|
|
->postJson('/api/reimbursements', [
|
|
'item_name' => 'Taxi Ride',
|
|
'amount' => 350.00,
|
|
'category' => 'Transport',
|
|
'purchase_date' => date('Y-m-d'),
|
|
'notes' => 'Client Visit Transport',
|
|
]);
|
|
|
|
$res->assertStatus(201);
|
|
|
|
$trip = Trip::where('employee_id', $user->id)->first();
|
|
$this->assertNotNull($trip);
|
|
|
|
// 2. Mobile lists claims -> initial status is pending
|
|
$listRes = $this->actingAs($user, 'sanctum')
|
|
->getJson('/api/reimbursements');
|
|
|
|
$listRes->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
$this->assertEquals('Taxi Ride - Client Visit Transport', $listRes->json('data.0.item_name'));
|
|
$this->assertEquals('pending', $listRes->json('data.0.status'));
|
|
|
|
// 3. Web Manager/HR approves reimbursement through the actual web controller
|
|
$webRequest = Request::create(
|
|
"/hr/trips/{$trip->id}/update-reimbursement-status",
|
|
'PUT',
|
|
['reimbursement_status' => 'approved']
|
|
);
|
|
|
|
$this->actingAs($user, 'web');
|
|
$webResponse = app(TripController::class)
|
|
->updateReimbursementStatus($webRequest, $trip->fresh());
|
|
|
|
$this->assertSame(302, $webResponse->getStatusCode());
|
|
$this->assertSame('approved', $trip->fresh()->reimbursement_status);
|
|
$this->assertSame('approved', $trip->expenses()->first()->status);
|
|
|
|
// 4. Mobile fetches reimbursement list -> status is now approved
|
|
$updatedRes = $this->actingAs($user, 'sanctum')
|
|
->getJson('/api/reimbursements');
|
|
|
|
$updatedRes->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
$this->assertEquals('approved', $updatedRes->json('data.0.status'));
|
|
}
|
|
|
|
// Scenario 4: Web-approved leave -> Mobile leave status & balance update
|
|
public function test_web_approved_leave_updates_mobile_balance()
|
|
{
|
|
[$user, $employee] = $this->createClientUser('leave_balance_check_sc4@example.com');
|
|
|
|
LeaveApplication::create([
|
|
'employee_id' => $user->id,
|
|
'leave_policy_id' => 1,
|
|
'leave_type_id' => 1,
|
|
'start_date' => date('Y-m-d'),
|
|
'end_date' => date('Y-m-d', strtotime('+2 days')),
|
|
'total_days' => 3,
|
|
'reason' => 'Medical Rest',
|
|
'status' => 'approved',
|
|
'created_by' => $user->id,
|
|
]);
|
|
|
|
$res = $this->actingAs($user, 'sanctum')
|
|
->getJson('/api/leaves/balances');
|
|
|
|
$res->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
}
|
|
|
|
// Scenario 5: Payslip visibility and download URL
|
|
public function test_payslip_visibility_and_download_url()
|
|
{
|
|
[$user, $employee] = $this->createClientUser('payslip_e2e_sc5@example.com');
|
|
|
|
$runId = DB::table('payroll_runs')->insertGetId([
|
|
'title' => 'January 2026 Payroll',
|
|
'payroll_frequency' => 'monthly',
|
|
'pay_period_start' => date('Y-01-01'),
|
|
'pay_period_end' => date('Y-01-31'),
|
|
'pay_date' => date('Y-01-31'),
|
|
'status' => 'completed',
|
|
'created_by' => $user->id,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$entryId = DB::table('payroll_entries')->insertGetId([
|
|
'payroll_run_id' => $runId,
|
|
'employee_id' => $employee->id,
|
|
'basic_salary' => 40000.00,
|
|
'net_pay' => 45000.00,
|
|
'created_by' => $user->id,
|
|
'created_at' => now(),
|
|
'updated_at' => now(),
|
|
]);
|
|
|
|
$payslip = Payslip::create([
|
|
'employee_id' => $user->id,
|
|
'payroll_entry_id' => $entryId,
|
|
'payslip_number' => 'PS-E2E-99',
|
|
'pay_period_start' => date('Y-01-01'),
|
|
'pay_period_end' => date('Y-01-31'),
|
|
'pay_date' => date('Y-01-31'),
|
|
'net_payble' => 45000.00,
|
|
'basic_salary' => 40000.00,
|
|
'salary_month' => 'January 2026',
|
|
'created_by' => $user->id,
|
|
]);
|
|
|
|
$listRes = $this->actingAs($user, 'sanctum')
|
|
->getJson('/api/payslips');
|
|
$listRes->assertStatus(200);
|
|
|
|
$downloadRes = $this->actingAs($user, 'sanctum')
|
|
->getJson("/api/payslips/{$payslip->id}/download");
|
|
$downloadRes->assertStatus(200)
|
|
->assertJsonStructure(['success', 'data' => ['pdf_url', 'download_url']]);
|
|
}
|
|
|
|
// Scenario 6: Conversation creation and two-way real-time messaging back-and-forth between Web and Mobile
|
|
public function test_conversation_and_messaging_between_two_users()
|
|
{
|
|
[$user1, $emp1] = $this->createClientUser('chat_sender_sc6@example.com');
|
|
[$user2, $emp2] = $this->createClientUser('chat_receiver_sc6@example.com');
|
|
|
|
// 1. User 1 creates conversation with User 2
|
|
$convRes = $this->actingAs($user1, 'sanctum')
|
|
->postJson('/api/conversations', [
|
|
'recipient_id' => $user2->id,
|
|
'participant_ids' => [$user2->id],
|
|
]);
|
|
|
|
$convRes->assertStatus(201);
|
|
$convId = $convRes->json('data.id');
|
|
|
|
// 2. User 1 (Mobile) sends message to User 2
|
|
$msgRes1 = $this->actingAs($user1, 'sanctum')
|
|
->postJson("/api/conversations/{$convId}/messages", [
|
|
'content' => 'Hello from Mobile Client',
|
|
]);
|
|
$msgRes1->assertStatus(201);
|
|
|
|
// 3. User 2 (Web) fetches messages -> sees User 1's message
|
|
$getMsgRes2 = $this->actingAs($user2, 'sanctum')
|
|
->getJson("/api/conversations/{$convId}/messages");
|
|
|
|
$getMsgRes2->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
$this->assertEquals('Hello from Mobile Client', $getMsgRes2->json('data.0.content'));
|
|
|
|
// 4. User 2 (Web) replies back to User 1
|
|
$msgRes2 = $this->actingAs($user2, 'sanctum')
|
|
->postJson("/api/conversations/{$convId}/messages", [
|
|
'content' => 'Reply received from Web Client',
|
|
]);
|
|
$msgRes2->assertStatus(201);
|
|
|
|
// 5. User 1 (Mobile) fetches messages -> sees User 2's reply
|
|
$getMsgRes1 = $this->actingAs($user1, 'sanctum')
|
|
->getJson("/api/conversations/{$convId}/messages");
|
|
|
|
$getMsgRes1->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
$messages = $getMsgRes1->json('data');
|
|
$this->assertCount(2, $messages);
|
|
$this->assertEquals('Reply received from Web Client', end($messages)['content']);
|
|
}
|
|
|
|
// Scenario 7: Announcement listing and read persistence
|
|
public function test_announcement_read_persistence()
|
|
{
|
|
[$user, $employee] = $this->createClientUser('announcement_user_sc7@example.com');
|
|
|
|
$anc = Announcement::create([
|
|
'title' => 'Company Townhall',
|
|
'category' => 'General',
|
|
'content' => 'Quarterly sync meeting',
|
|
'description' => 'Quarterly sync meeting',
|
|
'start_date' => date('Y-m-d'),
|
|
'end_date' => date('Y-m-d', strtotime('+7 days')),
|
|
'branch_id' => 1,
|
|
'department_id' => 1,
|
|
'created_by' => $user->id,
|
|
]);
|
|
|
|
$listRes = $this->actingAs($user, 'sanctum')
|
|
->getJson('/api/announcements');
|
|
$listRes->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
$this->assertFalse($listRes->json('data.0.is_read'));
|
|
|
|
$readRes = $this->actingAs($user, 'sanctum')
|
|
->postJson("/api/announcements/{$anc->id}/read");
|
|
$readRes->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
|
|
$listRes2 = $this->actingAs($user, 'sanctum')
|
|
->getJson('/api/announcements');
|
|
$this->assertTrue($listRes2->json('data.0.is_read'));
|
|
}
|
|
|
|
// Scenario 8: Overtime submission and listing
|
|
public function test_overtime_submission_and_retrieval()
|
|
{
|
|
[$user, $employee] = $this->createClientUser('overtime_sc8@example.com');
|
|
|
|
$postRes = $this->actingAs($user, 'sanctum')
|
|
->postJson('/api/overtime', [
|
|
'date' => date('Y-m-d'),
|
|
'hours' => 3.5,
|
|
'reason' => 'Emergency Deployment',
|
|
]);
|
|
|
|
$postRes->assertStatus(201)
|
|
->assertJson(['success' => true]);
|
|
|
|
$listRes = $this->actingAs($user, 'sanctum')
|
|
->getJson('/api/overtime');
|
|
|
|
$listRes->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
$this->assertEquals(3.5, $listRes->json('data.0.hours'));
|
|
}
|
|
|
|
// Scenario 9: Profile update and cross-client reflection
|
|
public function test_profile_update_and_visibility()
|
|
{
|
|
[$user, $employee] = $this->createClientUser('profile_sc9@example.com');
|
|
|
|
$updateRes = $this->actingAs($user, 'sanctum')
|
|
->postJson('/api/profile', [
|
|
'name' => 'Updated Profile Name',
|
|
'phone' => '+639171234567',
|
|
]);
|
|
|
|
$updateRes->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
|
|
$getRes = $this->actingAs($user, 'sanctum')
|
|
->getJson('/api/profile');
|
|
|
|
$getRes->assertStatus(200)
|
|
->assertJson(['success' => true]);
|
|
$this->assertEquals('Updated Profile Name', $getRes->json('data.name'));
|
|
}
|
|
}
|