create(['tenant_id' => $tenant->id, 'department_id' => $department->id, 'purchasing_role' => $role]); } function procurementPayload(int $center, string $price = '100.00', string $tax = '0'): array { return [ 'cost_center_id' => $center, 'priority' => 'normal', 'needed_by' => now()->addWeek()->format('Y-m-d'), 'justification' => 'Material necessário para as operações do departamento.', 'items' => [['description' => 'Material', 'category' => 'Consumíveis', 'purchase_type' => 'consumable', 'quantity' => '1', 'unit' => 'un', 'unit_price' => $price, 'tax_rate' => $tax]], ]; } beforeEach(function () { $this->tenant = Tenant::query()->create(['name' => 'Empresa A']); $this->department = Department::query()->create(['tenant_id' => $this->tenant->id, 'name' => 'Operações']); $this->center = CostCenter::query()->create(['tenant_id' => $this->tenant->id, 'department_id' => $this->department->id, 'code' => 'OPS', 'name' => 'Operações']); $this->requester = procurementUser($this->tenant, $this->department); }); test('an unassigned account can create an isolated company and becomes its administrator', function () { $user = User::factory()->create(); $this->actingAs($user)->post('/empresa/iniciar', ['name' => 'Nova empresa', 'department' => 'Geral'])->assertRedirect('/dashboard'); expect($user->refresh()->purchasing_role)->toBe(PurchasingRole::Admin); expect($user->tenant_id)->not->toBe($this->tenant->id); $this->actingAs($user)->post('/empresa/iniciar', ['name' => 'Outra empresa', 'department' => 'Geral'])->assertForbidden(); }); test('server calculates money and ignores forged totals identity status and currency', function () { $payload = procurementPayload($this->center->id, '0.05', '16'); $payload['items'][0]['quantity'] = '3'; $payload += ['tenant_id' => 999, 'requester_id' => 999, 'total_minor' => 1, 'status' => 'approved', 'currency' => 'USD']; $this->actingAs($this->requester)->post('/requisicoes', $payload)->assertSessionHasNoErrors()->assertRedirect(); $r = Requisition::query()->firstOrFail(); expect($r->subtotal_minor)->toBe(15)->and($r->tax_minor)->toBe(2)->and($r->total_minor)->toBe(17); expect($r->requester_id)->toBe($this->requester->id)->and($r->status)->toBe('draft')->and($r->currency)->toBe('MZN'); expect(AuditLog::query()->count())->toBe(1); }); test('approval boundaries are inclusive and snapshotted', function (string $price, int $steps) { $this->actingAs($this->requester)->post('/requisicoes', procurementPayload($this->center->id, $price))->assertRedirect(); $r = Requisition::query()->firstOrFail(); $this->post("/requisicoes/{$r->id}/submeter")->assertSessionHasNoErrors()->assertRedirect(); expect($r->refresh()->status)->toBe('pending')->and($r->steps()->count())->toBe($steps); expect($r->approval_snapshot['first_limit_minor'])->toBe(2500000); $this->post("/requisicoes/{$r->id}/submeter")->assertForbidden(); expect($r->steps()->count())->toBe($steps); })->with([['25000.00', 1], ['25000.01', 2], ['150000.00', 2], ['150000.01', 3]]); test('tax can move a request into a higher approval tier', function () { $this->actingAs($this->requester)->post('/requisicoes', procurementPayload($this->center->id, '25000.00', '16')); $r = Requisition::query()->firstOrFail(); $this->post("/requisicoes/{$r->id}/submeter")->assertRedirect(); expect($r->steps()->count())->toBe(2); }); test('approvals enforce order role department and distinct actors', function () { $this->actingAs($this->requester)->post('/requisicoes', procurementPayload($this->center->id, '150000.01')); $r = Requisition::query()->firstOrFail(); $this->post("/requisicoes/{$r->id}/submeter"); $head = procurementUser($this->tenant, $this->department, PurchasingRole::DepartmentHead); $procurement = procurementUser($this->tenant, $this->department, PurchasingRole::Procurement); $finance = procurementUser($this->tenant, $this->department, PurchasingRole::Finance); $admin = procurementUser($this->tenant, $this->department, PurchasingRole::Admin); $this->actingAs($finance)->post("/requisicoes/{$r->id}/decidir", ['decision' => 'approved'])->assertForbidden(); $this->actingAs($admin)->post("/requisicoes/{$r->id}/decidir", ['decision' => 'approved'])->assertForbidden(); $this->actingAs($head)->post("/requisicoes/{$r->id}/decidir", ['decision' => 'approved'])->assertRedirect(); $head->forceFill(['purchasing_role' => PurchasingRole::Procurement])->save(); $this->actingAs($head)->post("/requisicoes/{$r->id}/decidir", ['decision' => 'approved'])->assertForbidden(); $this->actingAs($procurement)->post("/requisicoes/{$r->id}/decidir", ['decision' => 'approved'])->assertRedirect(); $this->actingAs($finance)->post("/requisicoes/{$r->id}/decidir", ['decision' => 'approved'])->assertRedirect(); expect($r->refresh()->status)->toBe('approved'); $this->post("/requisicoes/{$r->id}/decidir", ['decision' => 'approved'])->assertForbidden(); expect(AuditLog::query()->where('requisition_id', $r->id)->count())->toBe(5); }); test('requesters cannot approve their own request even as department heads', function () { $this->requester->forceFill(['purchasing_role' => PurchasingRole::DepartmentHead])->save(); $this->actingAs($this->requester)->post('/requisicoes', procurementPayload($this->center->id)); $r = Requisition::query()->firstOrFail(); $this->post("/requisicoes/{$r->id}/submeter"); $this->post("/requisicoes/{$r->id}/decidir", ['decision' => 'approved'])->assertForbidden(); }); test('a head from another department cannot approve or read the request', function () { $this->actingAs($this->requester)->post('/requisicoes', procurementPayload($this->center->id)); $r = Requisition::query()->firstOrFail(); $this->post("/requisicoes/{$r->id}/submeter"); $other = Department::query()->create(['tenant_id' => $this->tenant->id, 'name' => 'Outro']); $head = procurementUser($this->tenant, $other, PurchasingRole::DepartmentHead); $this->actingAs($head)->get("/requisicoes/{$r->id}")->assertNotFound(); $this->post("/requisicoes/{$r->id}/decidir", ['decision' => 'approved'])->assertForbidden(); }); test('rejection requires a reason and ends the approval flow', function () { $this->actingAs($this->requester)->post('/requisicoes', procurementPayload($this->center->id)); $r = Requisition::query()->firstOrFail(); $this->post("/requisicoes/{$r->id}/submeter"); $this->actingAs(procurementUser($this->tenant, $this->department, PurchasingRole::DepartmentHead)); $this->post("/requisicoes/{$r->id}/decidir", ['decision' => 'rejected'])->assertSessionHasErrors('comment'); $this->post("/requisicoes/{$r->id}/decidir", ['decision' => 'rejected', 'comment' => 'Necessidade não demonstrada.'])->assertRedirect(); expect($r->refresh()->status)->toBe('rejected'); }); test('submitted requests cannot be edited', function () { $payload = procurementPayload($this->center->id); $this->actingAs($this->requester)->post('/requisicoes', $payload); $r = Requisition::query()->firstOrFail(); $this->put("/requisicoes/{$r->id}", $payload)->assertRedirect(); $this->post("/requisicoes/{$r->id}/submeter"); $this->put("/requisicoes/{$r->id}", $payload)->assertForbidden(); }); test('another company cannot list read edit submit or approve a request', function () { $this->actingAs($this->requester)->post('/requisicoes', procurementPayload($this->center->id)); $r = Requisition::query()->firstOrFail(); $otherTenant = Tenant::query()->create(['name' => 'Empresa B']); $otherDepartment = Department::query()->create(['tenant_id' => $otherTenant->id, 'name' => 'Geral']); $otherUser = procurementUser($otherTenant, $otherDepartment, PurchasingRole::Admin); $this->actingAs($otherUser)->get('/requisicoes')->assertInertia(fn (Assert $page) => $page->has('requisitions.data', 0)); $this->get("/requisicoes/{$r->id}")->assertNotFound(); $this->get("/requisicoes/{$r->id}/editar")->assertNotFound(); $this->post("/requisicoes/{$r->id}/submeter")->assertNotFound(); $this->post("/requisicoes/{$r->id}/decidir", ['decision' => 'approved'])->assertNotFound(); $this->post('/requisicoes', procurementPayload($this->center->id))->assertSessionHasErrors('cost_center_id'); }); test('ordinary users cannot administer the company or assign roles', function () { $this->actingAs($this->requester)->get('/empresa')->assertForbidden(); $this->post('/empresa/departamentos', ['name' => 'Novo'])->assertForbidden(); $this->post('/empresa/utilizadores', [])->assertForbidden(); }); test('invalid monetary inputs are rejected', function (string $quantity, string $price) { $payload = procurementPayload($this->center->id, $price); $payload['items'][0]['quantity'] = $quantity; $this->actingAs($this->requester)->post('/requisicoes', $payload)->assertSessionHasErrors(); expect(Requisition::query()->count())->toBe(0); })->with([['0', '10'], ['1', '-10'], ['1', '0.001'], ['0.0001', '10'], ['1e3', '10']]); test('application audit entries cannot be updated or deleted', function () { $this->actingAs($this->requester)->post('/requisicoes', procurementPayload($this->center->id)); $audit = AuditLog::query()->firstOrFail(); expect(fn () => $audit->update(['action' => 'changed']))->toThrow(LogicException::class); expect(fn () => $audit->delete())->toThrow(LogicException::class); }); test('tenant models fail closed without a signed in user', function () { expect(Department::query()->count())->toBe(0)->and(CostCenter::query()->count())->toBe(0)->and(ApprovalStep::query()->count())->toBe(0); }); test('company accounts cannot be deleted through personal settings', function () { $this->actingAs($this->requester)->delete(route('profile.destroy'), ['password' => 'password'])->assertSessionHasErrors('password'); $this->assertAuthenticatedAs($this->requester); $this->assertDatabaseHas('users', ['id' => $this->requester->id]); }); test('administrators can provision team accounts scoped to their company', function () { $admin = procurementUser($this->tenant, $this->department, PurchasingRole::Admin); $this->actingAs($admin)->post('/empresa/utilizadores', [ 'name' => 'Chefe de teste', 'email' => 'chefe@example.test', 'password' => 'Test-password-1234', 'password_confirmation' => 'Test-password-1234', 'department_id' => $this->department->id, 'purchasing_role' => 'department_head', 'tenant_id' => 999, ])->assertSessionHasNoErrors()->assertRedirect(); $member = User::query()->where('email', 'chefe@example.test')->firstOrFail(); expect($member->tenant_id)->toBe($this->tenant->id)->and($member->purchasing_role)->toBe(PurchasingRole::DepartmentHead); $this->get('/empresa')->assertInertia(fn (Assert $page) => $page->component('company/index')->has('users', 3)); }); test('requisition pages expose the stored lines and actual approval capabilities', function () { $this->actingAs($this->requester)->get('/requisicoes/criar')->assertInertia(fn (Assert $page) => $page->component('requisitions/form')->has('costCenters', 1)); $this->post('/requisicoes', procurementPayload($this->center->id)); $r = Requisition::query()->firstOrFail(); $this->get("/requisicoes/{$r->id}")->assertInertia(fn (Assert $page) => $page->component('requisitions/show')->has('requisition.items', 1)->where('canEdit', true)->where('canDecide', false)); $this->post("/requisicoes/{$r->id}/submeter"); $head = procurementUser($this->tenant, $this->department, PurchasingRole::DepartmentHead); $this->actingAs($head)->get("/requisicoes/{$r->id}")->assertInertia(fn (Assert $page) => $page->where('canEdit', false)->where('canDecide', true)->has('requisition.steps', 1)); $this->get('/dashboard')->assertInertia(fn (Assert $page) => $page->where('stats.awaitingMe', 1)->has('approvals', 1)); });