actingAs($user)->postJson('/api/v1/investment/submissions')->assertCreated()->json('id'); } private function payload(int $version = 1): array { return ['version' => $version, 'payload' => ['full_name' => 'Investidor sintético', 'email' => 'qa@example.test', 'country' => 'Moçambique', 'profile_id' => DB::table('investment_reference_options')->where('group', 'investor_profile')->where('code', 'individual')->value('id'), 'participation_id' => DB::table('investment_reference_options')->where('group', 'participation')->where('code', 'invest')->value('id'), 'sector' => 'Educação', 'objective' => 'Objectivo exclusivamente sintético para teste.', 'horizon' => '12 meses', 'contact_consent' => true], 'needs' => []]; } public function test_profile_and_company_are_private_versioned_and_do_not_grant_organization_access(): void { $user = User::factory()->create(); $this->actingAs($user)->putJson('/api/v1/investment/profile', ['version' => 0, 'payload' => ['country' => 'Moçambique', 'phone' => '123456', 'sector' => 'Educação']])->assertOk()->assertJsonPath('version', 1); $this->putJson('/api/v1/investment/profile', ['version' => 0, 'payload' => ['country' => 'Outro']])->assertConflict(); $data = ['version' => 0, 'payload' => ['name' => 'Empresa sintética', 'country' => 'Moçambique', 'role' => 'Representante declarado']]; $company = $this->postJson('/api/v1/investment/companies', $data)->assertOk()->json('id'); $id = $this->postJson('/api/v1/investment/submissions', ['company_id' => $company])->assertCreated()->json('id'); $this->getJson('/api/v1/investment/submissions/'.$id)->assertJsonPath('submission.payload.organisation', 'Empresa sintética')->assertJsonPath('submission.payload.phone', '123456'); $this->assertStringNotContainsString('Empresa sintética', DB::table('investment_company_declarations')->value('payload')); $this->assertStringNotContainsString('123456', DB::table('investment_accounts')->value('payload')); $this->assertDatabaseCount('memberships', 0); $this->assertDatabaseCount('organizations', 0); config(['portal.managers' => [$user->email]]); $this->get('/management/history')->assertInertia(fn (AssertableInertia $page) => $page ->has('events.data', 3)->where('events.data.0.after_data.values.version', 1)); $this->actingAs(User::factory()->create())->getJson('/api/v1/investment/profile')->assertJsonCount(0, 'companies'); $this->postJson('/api/v1/investment/companies/'.$company, [...$data, 'version' => 1])->assertNotFound(); $this->postJson('/api/v1/investment/submissions', ['company_id' => $company])->assertNotFound(); $this->putJson('/api/v1/investment/profile', ['version' => 0, 'payload' => ['is_admin' => true]])->assertUnprocessable(); } public function test_conversation_is_encrypted_authorized_and_idempotent_with_private_notifications(): void { $this->seed(InvestmentReferenceSeeder::class); $owner = User::factory()->create(); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $id = $this->draft($owner); $message = ['client_key' => (string) Str::uuid(), 'body' => 'Mensagem privada sintética do investidor.']; $url = '/api/v1/investment/submissions/'.$id.'/messages'; $this->postJson($url, $message)->assertConflict(); $this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload())->assertOk(); $first = $this->postJson($url, $message)->assertOk()->json('id'); $this->postJson($url, $message)->assertJsonPath('id', $first); $this->postJson($url, [...$message, 'body' => 'Outro conteúdo com a mesma chave.'])->assertConflict(); $this->assertDatabaseCount('investment_messages', 1); $this->assertStringNotContainsString('Mensagem privada', DB::table('investment_messages')->value('body')); $this->actingAs($manager)->getJson('/api/v1/investment/notifications')->assertJsonPath('unread', 1); $this->postJson($url, ['client_key' => (string) Str::uuid(), 'body' => 'Resposta sintética da equipa.'])->assertOk(); $this->actingAs($owner)->getJson('/api/v1/investment/notifications')->assertJsonPath('unread', 2); $this->getJson('/api/v1/investment/submissions/'.$id)->assertJsonCount(2, 'messages')->assertJsonPath('messages.1.sender_role', 'support'); $notification = DB::table('investment_notifications')->where('user_id', $owner->id)->first(); $this->postJson('/api/v1/investment/notifications/'.$notification->id.'/read')->assertOk(); $this->getJson('/api/v1/investment/notifications')->assertJsonPath('unread', 1); $this->actingAs(User::factory()->create())->postJson($url, $message)->assertNotFound(); $this->postJson('/api/v1/investment/notifications/'.$notification->id.'/read')->assertNotFound(); config(['portal.managers' => []]); $this->actingAs($manager)->getJson('/api/v1/investment/notifications')->assertJsonCount(0, 'notifications.data'); $this->getJson('/api/v1/investment/submissions/'.$id)->assertNotFound(); } public function test_revisions_preserve_encrypted_prior_values_without_exposing_them_to_other_users(): void { $this->seed(InvestmentReferenceSeeder::class); $owner = User::factory()->create(); $id = $this->draft($owner); $this->putJson('/api/v1/investment/submissions/'.$id, $this->payload())->assertOk(); $next = $this->payload(2); $next['payload']['objective'] = 'Objectivo posterior exclusivamente sintético.'; $this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $next)->assertOk(); $this->assertDatabaseCount('investment_submission_revisions', 3); $url = '/api/v1/investment/submissions/'.$id.'/revisions/2'; $this->getJson($url)->assertOk()->assertJsonPath('snapshot.payload.objective', $this->payload()['payload']['objective'])->assertHeader('Cache-Control', 'no-store, private'); $this->assertStringNotContainsString('Objectivo', DB::table('investment_submission_revisions')->where('version', 2)->value('snapshot')); $this->actingAs(User::factory()->create())->getJson($url)->assertNotFound(); } public function test_status_review_notifies_owner_once_per_effective_change(): void { $this->seed(InvestmentReferenceSeeder::class); $owner = User::factory()->create(); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $id = $this->draft($owner); $this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload())->assertOk(); $interest = InterestRequest::firstOrFail(); $this->actingAs($manager)->patch('/management/interests/'.$interest->id, ['status' => 'in_review'])->assertRedirect(); $this->patch('/management/interests/'.$interest->id, ['status' => 'in_review'])->assertRedirect(); $this->patchJson('/api/v1/investment/admin/interests/'.$interest->id.'/status', ['status' => 'in_review'])->assertOk()->assertJsonPath('status', 'in_review'); $this->assertDatabaseCount('investment_notifications', 2); $this->actingAs($owner)->getJson('/api/v1/investment/notifications')->assertJsonPath('unread', 2); } public function test_incomplete_text_can_be_saved_as_draft_but_cannot_be_submitted(): void { $owner = User::factory()->create(); $id = $this->draft($owner); $partial = ['version' => 1, 'payload' => ['full_name' => 'A', 'email' => 'parcial@', 'objective' => 'Ideia']]; $this->putJson('/api/v1/investment/submissions/'.$id, $partial)->assertOk()->assertJsonPath('version', 2); $this->getJson('/api/v1/investment/submissions/'.$id)->assertJsonPath('submission.payload.email', 'parcial@'); $this->postJson('/api/v1/investment/submissions/'.$id.'/submit', [...$partial, 'version' => 2])->assertUnprocessable(); $this->assertDatabaseCount('interest_requests', 0); } }