'Pessoa de teste', 'email' => 'portal-integration-test@gmail.com', 'investorType' => 'empresa', 'country' => 'Moçambique', 'sectorInterest' => 'Energia', 'message' => 'Pedido de teste na base isolada.', 'privacyConsent' => true]; } public function test_replay_reuses_original_confirmation_event_and_notification(): void { config(['portal.notifications_enabled' => true, 'portal.notification_recipients' => ['team@example.invalid']]); $headers = ['Idempotency-Key' => (string) Str::uuid()]; $first = $this->postJson('/api/v1/interest-requests', $this->payload(), $headers)->assertCreated(); InterestRequest::firstOrFail()->update(['status' => 'closed']); $second = $this->postJson('/api/v1/interest-requests', $this->payload(), $headers)->assertCreated(); $this->assertSame($first->json(), $second->json()); $this->assertDatabaseCount('interest_requests', 1); $this->assertDatabaseCount('interest_submission_receipts', 1); $this->assertDatabaseCount('outbox_events', 1); $this->assertDatabaseCount('jobs', 1); $event = DB::table('outbox_events')->first(); $this->assertSame('pending', $event->status); $this->assertSame(InterestRequest::firstOrFail()->integration_id, $event->aggregate_id); $this->assertEquals(['schema_version' => 1, 'interest_id' => $event->aggregate_id], json_decode($event->payload, true)); $this->assertStringNotContainsString('gmail', $event->payload); } public function test_key_reuse_with_changed_payload_is_conflict_and_invalid_keys_are_rejected(): void { $headers = ['Idempotency-Key' => (string) Str::uuid()]; $this->postJson('/api/v1/interest-requests', $this->payload(), $headers)->assertCreated(); $this->postJson('/api/v1/interest-requests', [...$this->payload(), 'message' => 'Uma mensagem diferente com outros dados.'], $headers)->assertStatus(409); $this->postJson('/api/v1/interest-requests', $this->payload(), ['Idempotency-Key' => 'unsafe-key'])->assertUnprocessable(); $this->postJson('/api/v1/interest-requests', $this->payload(), ['Idempotency-Key' => ''])->assertUnprocessable(); $this->assertDatabaseCount('interest_requests', 1); $this->assertDatabaseCount('outbox_events', 1); } public function test_old_clients_still_work_and_distinct_keys_create_distinct_submissions(): void { $this->postJson('/api/v1/interest-requests', $this->payload())->assertCreated(); foreach ([Str::uuid(), Str::uuid()] as $key) { $this->postJson('/api/v1/interest-requests', $this->payload(), ['Idempotency-Key' => (string) $key])->assertCreated(); } $this->assertDatabaseCount('interest_requests', 3); $this->assertDatabaseCount('outbox_events', 3); $this->assertDatabaseCount('interest_submission_receipts', 2); } public function test_event_insert_failure_rolls_back_request_receipt_and_notification(): void { DB::statement('CREATE TRIGGER test_reject_outbox BEFORE INSERT ON outbox_events FOR EACH ROW EXECUTE FUNCTION investmoz_reject_audit_change()'); config(['portal.notifications_enabled' => true, 'portal.notification_recipients' => ['team@example.invalid']]); $headers = ['Idempotency-Key' => (string) Str::uuid()]; $this->postJson('/api/v1/interest-requests', $this->payload(), $headers)->assertStatus(500); $this->assertDatabaseCount('interest_requests', 0); $this->assertDatabaseCount('interest_submission_receipts', 0); $this->assertDatabaseCount('jobs', 0); DB::statement('DROP TRIGGER test_reject_outbox ON outbox_events'); $this->postJson('/api/v1/interest-requests', $this->payload(), $headers)->assertCreated(); $this->assertDatabaseCount('outbox_events', 1); } public function test_schema_rollback_refuses_to_destroy_captured_identities(): void { $this->postJson('/api/v1/interest-requests', $this->payload(), ['Idempotency-Key' => (string) Str::uuid()])->assertCreated(); $migration = require database_path('migrations/2026_09_25_120000_add_interest_submission_receipts.php'); $this->expectException(\RuntimeException::class); $this->expectExceptionMessage('Preserve submission identities'); $migration->down(); } public function test_accepted_request_can_be_replayed_after_opportunity_is_archived(): void { $sector = Sector::create(['name' => 'Energia', 'slug' => 'energia']); $opportunity = Opportunity::create(['sector_id' => $sector->id, 'reference' => 'INV-REPLAY', 'title' => 'Teste', 'slug' => 'teste-replay', 'status' => 'published', 'published_at' => now()->subDay()]); $payload = [...$this->payload(), 'opportunityReference' => $opportunity->reference]; $headers = ['Idempotency-Key' => (string) Str::uuid()]; $first = $this->postJson('/api/v1/interest-requests', $payload, $headers)->assertCreated(); $opportunity->update(['status' => 'archived']); $this->postJson('/api/v1/interest-requests', $payload, $headers)->assertCreated()->assertJsonPath('data.reference', $first->json('data.reference')); $this->postJson('/api/v1/interest-requests', $payload, ['Idempotency-Key' => (string) Str::uuid()])->assertUnprocessable(); $this->assertDatabaseCount('interest_requests', 1); } }