204 lines
14 KiB
PHP
204 lines
14 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\InterestRequest;
|
|
use App\Models\User;
|
|
use App\Services\CountryCatalogue;
|
|
use App\Services\DeclaredOriginStatistics;
|
|
use Database\Seeders\InvestmentReferenceSeeder;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Inertia\Testing\AssertableInertia;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
use Tests\TestCase;
|
|
|
|
class InvestmentWorkspaceTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private function draft(User $user): string
|
|
{
|
|
return $this->actingAs($user)->postJson('/api/v1/investment/submissions')->assertCreated()->json('id');
|
|
}
|
|
|
|
public function test_country_meanings_are_persisted_separately_without_inference_and_recovered_by_account(): void
|
|
{
|
|
$user = User::factory()->create();
|
|
$this->actingAs($user)->putJson('/api/v1/investment/profile', ['version' => 0, 'payload' => ['country' => 'Valor histórico', 'residence_country_code' => 'PT']])->assertOk();
|
|
$company = $this->postJson('/api/v1/investment/companies', ['version' => 0, 'payload' => ['name' => 'Entidade QA', 'role' => 'Representante', 'incorporation_country_code' => 'MZ']])->assertOk()->json('id');
|
|
$draft = $this->postJson('/api/v1/investment/submissions', ['company_id' => $company])->assertCreated()->json('id');
|
|
$this->getJson('/api/v1/investment/submissions/'.$draft)->assertJsonPath('submission.payload.residence_country_code', 'PT')->assertJsonPath('submission.payload.incorporation_country_code', 'MZ')->assertJsonPath('submission.payload.country', 'Valor histórico')->assertJsonMissingPath('submission.payload.declared_origin_country_code');
|
|
$this->putJson('/api/v1/investment/submissions/'.$draft, ['version' => 1, 'payload' => ['residence_country_code' => 'PT', 'incorporation_country_code' => 'MZ', 'declared_origin_country_code' => 'ZA']])->assertOk();
|
|
// Discard the original authenticated context: recovery uses server data, not local storage.
|
|
$this->actingAs(User::factory()->create())->getJson('/api/v1/investment/submissions/'.$draft)->assertNotFound();
|
|
$this->actingAs(User::findOrFail($user->id))->getJson('/api/v1/investment/submissions/'.$draft)->assertJsonPath('submission.payload.declared_origin_country_code', 'ZA');
|
|
$this->putJson('/api/v1/investment/submissions/'.$draft, ['version' => 2, 'payload' => ['declared_origin_country_code' => 'ZZ']])->assertUnprocessable()->assertJsonValidationErrors('payload.declared_origin_country_code');
|
|
$this->getJson('/api/v1/investment/submissions/'.$draft)->assertJsonPath('submission.version', 2)->assertJsonPath('submission.payload.declared_origin_country_code', 'ZA');
|
|
$countries = app(CountryCatalogue::class)->all();
|
|
$this->assertCount(249, $countries);
|
|
$this->assertCount(249, array_unique(array_column($countries, 'code')));
|
|
}
|
|
|
|
public function test_declared_origin_is_snapshotted_on_submission_and_statistics_do_not_infer_old_country(): void
|
|
{
|
|
$this->seed(InvestmentReferenceSeeder::class);
|
|
$user = User::factory()->create();
|
|
$id = $this->draft($user);
|
|
$data = $this->payload();
|
|
$data['payload']['declared_origin_country_code'] = 'ZA';
|
|
$this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $data)->assertOk();
|
|
$interest = InterestRequest::firstOrFail();
|
|
$this->assertSame('ZA', $interest->metadata['declared_origin_country_code']);
|
|
$this->assertSame('Moçambique', $interest->country);
|
|
$this->expectException(HttpException::class);
|
|
app(DeclaredOriginStatistics::class)->forManager($user);
|
|
}
|
|
|
|
public function test_authorized_origin_statistics_keep_historical_missing_as_not_informed(): void
|
|
{
|
|
$this->seed(InvestmentReferenceSeeder::class);
|
|
$user = User::factory()->create();
|
|
$id = $this->draft($user);
|
|
$this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $this->payload())->assertOk();
|
|
config(['portal.managers' => [$user->email]]);
|
|
$stats = app(DeclaredOriginStatistics::class)->forManager($user);
|
|
$this->assertSame([['code' => null, 'name' => 'Não informado', 'count' => 1]], $stats['interests']);
|
|
$this->assertSame([], $stats['desk']);
|
|
}
|
|
|
|
public function test_crm_origin_filter_separates_explicit_missing_and_empty_results_without_writing(): void
|
|
{
|
|
$this->withoutVite();
|
|
$this->seed(InvestmentReferenceSeeder::class);
|
|
$user = User::factory()->create();
|
|
foreach ([null, 'PT', 'ZA'] as $origin) {
|
|
$id = $this->draft($user);
|
|
$data = $this->payload();
|
|
if ($origin !== null) {
|
|
$data['payload']['declared_origin_country_code'] = $origin;
|
|
}
|
|
$this->postJson('/api/v1/investment/submissions/'.$id.'/submit', $data)->assertOk();
|
|
}
|
|
$before = InterestRequest::orderBy('id')->get()->toArray();
|
|
config(['portal.managers' => [$user->email]]);
|
|
$this->get('/management/crm/leads?origin=PT')->assertOk()->assertInertia(fn (AssertableInertia $page) => $page
|
|
->where('filters.origin', 'PT')->has('originStatistics.interests', 1)
|
|
->where('originStatistics.interests.0.code', 'PT')->where('originStatistics.interests.0.count', 1));
|
|
$this->get('/management/crm/leads?origin=unknown')->assertOk()->assertInertia(fn (AssertableInertia $page) => $page
|
|
->has('originStatistics.interests', 1)->where('originStatistics.interests.0.code', null)
|
|
->where('originStatistics.interests.0.count', 1));
|
|
$this->get('/management/crm/leads?origin=MZ')->assertOk()->assertInertia(fn (AssertableInertia $page) => $page
|
|
->has('originStatistics.interests', 0)->has('originStatistics.desk', 0));
|
|
$this->get('/management/crm/leads')->assertOk()->assertInertia(fn (AssertableInertia $page) => $page
|
|
->has('originStatistics.interests', 3));
|
|
$this->getJson('/management/crm/leads?origin=ZZ')->assertUnprocessable()->assertJsonValidationErrors('origin');
|
|
$this->assertSame($before, InterestRequest::orderBy('id')->get()->toArray());
|
|
$this->actingAs(User::factory()->create())->get('/management/crm/leads?origin=PT')->assertForbidden();
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|