save($owner, array_replace_recursive(['client_key' => (string) Str::uuid(), 'title' => 'QA partnership', 'kind' => 'seeking', 'partnership_type' => 'joint_venture', 'offer' => 'QA documented capacity', 'seeking' => 'QA technical contribution', 'payload' => ['summary' => 'QA summary', 'consent' => true, 'mandatory' => ['capacity']], 'origins' => [], 'locations' => []], $extra)); } private function publish(User $owner): array { $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $p = $this->draft($owner); $p = app(PartnershipExchange::class)->transition($owner, $p->id, $p->version, 'submit', 'QA submission'); $p = app(PartnershipExchange::class)->transition($manager, $p->id, $p->version, 'publish', 'QA reviewed disclosure'); return [$p, $manager]; } private function candidature(User $candidate, object $p): object { return app(PartnershipExchange::class)->interest($candidate, $p->id, ['proposal' => 'QA proposal', 'payload' => ['consent' => true, 'capacity' => 'Declared capacity, not certified']]); } public function test_complete_local_flow_from_personal_draft_to_agreement_and_completed_milestone(): void { $owner = User::factory()->create(['email_verified_at' => null]); $candidate = User::factory()->create(['email_verified_at' => null]); [$p, $manager] = $this->publish($owner); $this->getJson('/parcerias/'.$p->id)->assertOk()->assertJsonPath('record.status', 'published'); $i = $this->candidature($candidate, $p); $other = $this->candidature(User::factory()->create(), $p); $i = app(PartnershipExchange::class)->decideInterest($owner, $i->id, $i->version, 'shortlist', 'Potential match; unknown data to clarify'); $i = app(PartnershipExchange::class)->decideInterest($owner, $i->id, $i->version, 'connect', 'Private negotiation authorized'); $room = DB::table('partnership_rooms')->where('interest_id', $i->id)->first(); $this->actingAs($owner)->postJson('/parcerias/salas/'.$room->id.'/acordo', ['version' => 1, 'action' => 'record', 'title' => 'QA work plan', 'terms' => 'Parties, objectives, contributions, risks and exit conditions', 'reason' => 'QA instrument recorded'])->assertOk(); $this->postJson('/parcerias/salas/'.$room->id.'/acordo', ['version' => 2, 'action' => 'confirm', 'reason' => 'Proponent confirmation'])->assertOk(); $this->actingAs($candidate)->postJson('/parcerias/salas/'.$room->id.'/acordo', ['version' => 3, 'action' => 'confirm', 'reason' => 'Candidate confirmation'])->assertOk(); $this->assertDatabaseHas('partnership_rooms', ['id' => $room->id, 'status' => 'agreed']); $this->postJson('/parcerias/salas/'.$room->id.'/marcos', ['title' => 'Pilot deliverable', 'responsible_id' => $candidate->id, 'status' => 'pending'])->assertOk(); $this->postJson('/parcerias/salas/'.$room->id.'/acordo', ['version' => 4, 'action' => 'start', 'reason' => 'Start pilot'])->assertOk(); $this->postJson('/parcerias/salas/'.$room->id.'/acordo', ['version' => 5, 'action' => 'complete', 'reason' => 'Premature completion'])->assertConflict(); $milestone = DB::table('partnership_milestones')->where('room_id', $room->id)->first(); $this->putJson('/parcerias/salas/'.$room->id.'/marcos/'.$milestone->id, ['version' => 1, 'title' => 'Pilot deliverable', 'responsible_id' => $candidate->id, 'status' => 'completed', 'evidence' => 'QA evidence reference'])->assertOk(); $this->postJson('/parcerias/salas/'.$room->id.'/acordo', ['version' => 5, 'action' => 'complete', 'reason' => 'Deliverables complete'])->assertOk(); $this->assertDatabaseHas('partnership_rooms', ['id' => $room->id, 'status' => 'closed']); $this->assertDatabaseHas('partnership_interests', ['id' => $other->id, 'status' => 'submitted']); $this->assertDatabaseHas('portal_partnerships', ['id' => $p->id, 'status' => 'published']); $this->assertNotEmpty(DB::table('audit_events')->where('source_system', 'partnership-exchange')->get()); $this->assertSame('not_verified', json_decode(DB::table('partnership_rooms')->where('id', $room->id)->value('agreement'), true)['legal_signature_status']); } public function test_preserves_legacy_proposal_and_rejects_self_moderation_and_concurrent_updates(): void { $owner = User::factory()->create(); $manager = User::factory()->create(); config(['portal.managers' => [$owner->email, $manager->email]]); $id = DB::table('portal_partnerships')->insertGetId(['user_id' => $owner->id, 'title' => 'Legacy', 'sector' => 'Original sector', 'province' => 'Original province', 'offer' => 'Original offer', 'seeking' => 'Original request', 'status' => 'pending']); $this->actingAs($owner)->postJson('/parcerias/'.$id.'/estado', ['version' => 1, 'action' => 'publish', 'reason' => 'Self moderation'])->assertForbidden(); $this->actingAs($manager)->postJson('/parcerias/'.$id.'/estado', ['version' => 1, 'action' => 'publish', 'reason' => 'Legacy reviewed'])->assertOk(); $this->assertDatabaseHas('portal_partnerships', ['id' => $id, 'user_id' => $owner->id, 'sector' => 'Original sector', 'province' => 'Original province', 'offer' => 'Original offer']); $this->postJson('/parcerias/'.$id.'/estado', ['version' => 1, 'action' => 'return', 'reason' => 'Stale'])->assertConflict(); } public function test_draft_recovery_and_idempotency_do_not_require_organization_or_documents(): void { $owner = User::factory()->create(['email_verified_at' => null]); $input = ['client_key' => (string) Str::uuid(), 'title' => '', 'kind' => 'seeking', 'payload' => [], 'origins' => [], 'locations' => []]; $this->actingAs($owner)->postJson('/parcerias/rascunhos', $input)->assertOk(); $this->postJson('/parcerias/rascunhos', $input)->assertOk(); $this->postJson('/parcerias/rascunhos', [...$input, 'title' => 'Different bytes'])->assertConflict(); $this->assertDatabaseCount('portal_partnerships', 1); $p = DB::table('portal_partnerships')->first(); $this->get('/parcerias/'.$p->id.'/editar')->assertOk(); $this->postJson('/parcerias/'.$p->id.'/estado', ['version' => $p->version, 'action' => 'submit', 'reason' => 'Still incomplete'])->assertUnprocessable(); $this->actingAs(User::factory()->create())->get('/parcerias/'.$p->id.'/editar')->assertForbidden(); } public function test_competing_candidates_and_moderator_cannot_enter_private_room_or_read_other_interests(): void { $owner = User::factory()->create(); $candidate = User::factory()->create(); [$p, $moderator] = $this->publish($owner); $i = $this->candidature($candidate, $p); $outsider = User::factory()->create(); $this->candidature($outsider, $p); $this->actingAs($outsider)->getJson('/parcerias/'.$p->id)->assertOk()->assertJsonCount(1, 'interests'); $i = app(PartnershipExchange::class)->decideInterest($owner, $i->id, $i->version, 'shortlist', 'QA shortlist'); app(PartnershipExchange::class)->decideInterest($owner, $i->id, $i->version, 'connect', 'QA connect'); $room = DB::table('partnership_rooms')->first(); $this->getJson('/parcerias/salas/'.$room->id)->assertForbidden(); $this->actingAs($moderator)->getJson('/parcerias/salas/'.$room->id)->assertForbidden(); $this->getJson('/parcerias/'.$p->id)->assertJsonCount(0, 'interests'); $this->actingAs($candidate)->postJson('/parcerias/candidaturas/'.$i->id, ['version' => $i->version + 1, 'action' => 'assess', 'reason' => 'Self assessment', 'assessment' => ['capacity' => ['state' => 'met', 'evidence' => 'Claim']]])->assertForbidden(); $key = (string) Str::uuid(); $this->postJson('/parcerias/salas/'.$room->id.'/mensagens', ['client_key' => $key, 'body' => 'Private QA message'])->assertOk(); $this->postJson('/parcerias/salas/'.$room->id.'/mensagens', ['client_key' => $key, 'body' => 'Private QA message'])->assertOk(); $this->postJson('/parcerias/salas/'.$room->id.'/mensagens', ['client_key' => $key, 'body' => 'Changed'])->assertConflict(); $this->assertDatabaseCount('partnership_messages', 1); } public function test_matching_keeps_unknown_information_separate_from_proven_failure_and_desirable_criteria(): void { $owner = User::factory()->create(); [$p] = $this->publish($owner); $i = $this->candidature(User::factory()->create(), $p); $engine = app(PartnershipMatching::class); $unknown = $engine->explain($p, $i); $this->assertSame('unknown', $unknown['eligibility']); $this->assertSame(100.0, $unknown['score_max']); $failed = $engine->explain($p, $i, ['capacity' => ['state' => 'unmet', 'evidence' => 'QA documented inability']]); $this->assertSame('unmet', $failed['eligibility']); $desirable = $engine->explain($p, $i, ['impact' => ['state' => 'unmet', 'evidence' => 'QA reference']]); $this->assertSame('unknown', $desirable['eligibility']); $this->assertFalse($desirable['recommendation_is_approval']); $all = []; foreach (array_keys(config('partnerships.criteria')) as $code) { $all[$code] = ['state' => 'met', 'evidence' => 'Synthetic QA source for '.$code]; } $this->assertSame(100.0, $engine->explain($p, $i, $all)['score_min']); } public function test_document_sharing_reuses_private_repository_and_scan_controls(): void { Queue::fake(); Storage::fake('local'); $owner = User::factory()->create(); $candidate = User::factory()->create(); [$p] = $this->publish($owner); $i = $this->candidature($candidate, $p); $i = app(PartnershipExchange::class)->decideInterest($owner, $i->id, $i->version, 'shortlist', 'QA shortlist'); app(PartnershipExchange::class)->decideInterest($owner, $i->id, $i->version, 'connect', 'QA room'); $room = DB::table('partnership_rooms')->first(); $this->actingAs($owner)->postJson('/parcerias/salas/'.$room->id.'/documentos', ['file' => UploadedFile::fake()->create('qa.pdf', 1, 'application/pdf'), 'shared' => false])->assertOk(); $link = DB::table('partnership_documents')->first(); $this->actingAs($candidate)->getJson('/parcerias/salas/'.$room->id)->assertJsonCount(0, 'documents'); $this->get('/parcerias/salas/'.$room->id.'/documentos/'.$link->id)->assertNotFound(); $this->postJson('/parcerias/salas/'.$room->id.'/documentos/'.$link->id.'/partilha', ['shared' => true])->assertForbidden(); $this->actingAs($owner)->postJson('/parcerias/salas/'.$room->id.'/documentos/'.$link->id.'/partilha', ['shared' => true])->assertOk(); $this->actingAs($candidate); $this->get('/parcerias/salas/'.$room->id.'/documentos/'.$link->id)->assertNotFound(); $document = DB::table('documents')->where('id', $link->document_id)->first(); DB::table('documents')->where('id', $document->id)->update(['status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => $document->sha256]); $this->get('/parcerias/salas/'.$room->id.'/documentos/'.$link->id)->assertOk(); Storage::disk('local')->put($document->storage_key, 'Changed bytes'); $this->get('/parcerias/salas/'.$room->id.'/documentos/'.$link->id)->assertNotFound(); } public function test_organization_representation_blocks_viewer_writes_and_revoked_authors(): void { $owner = User::factory()->create(); $representative = User::factory()->create(); $viewer = User::factory()->create(); $organization = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $organization, 'name' => 'Synthetic QA organization', 'organization_type' => 'private']); foreach ([[$owner, 'manager'], [$representative, 'officer'], [$viewer, 'viewer']] as [$user, $role]) { DB::table('memberships')->insert(['user_id' => $user->id, 'organization_id' => $organization, 'role' => $role]); } $p = $this->draft($owner, ['organization_id' => $organization]); $this->actingAs($viewer)->getJson('/parcerias/'.$p->id)->assertOk()->assertJsonPath('canEdit', false); $this->postJson('/parcerias/'.$p->id.'/estado', ['version' => $p->version, 'action' => 'submit', 'reason' => 'Viewer attempt'])->assertForbidden(); $this->actingAs($representative)->get('/parcerias/'.$p->id.'/editar')->assertOk(); DB::table('memberships')->where('user_id', $owner->id)->update(['deleted_at' => now()]); $this->actingAs($owner)->get('/parcerias/'.$p->id.'/editar')->assertForbidden(); $this->getJson('/parcerias/'.$p->id)->assertNotFound(); } public function test_hierarchical_filters_and_source_links_use_only_explicit_documented_relations(): void { $parent = null; $ids = []; foreach (['country', 'province', 'district', 'admin_post', 'locality'] as $type) { $id = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $id, 'code' => $id, 'name' => 'QA '.$type, 'location_type' => $type, 'parent_id' => $parent, 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$id]); $ids[$type] = $id; $parent = $id; } $owner = User::factory()->create(); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $project = Project::factory()->create(); $dossier = InvestmentDossier::create(['project_id' => $project->id, 'title' => 'QA documented anchor', 'source' => 'Synthetic QA source', 'content' => ['location' => 'QA locality'], 'publication_status' => 'published', 'published_at' => now()->subMinute(), 'approval_basis' => 'document_owner', 'approval_evidence' => 'Synthetic QA fixture disclosure only']); DB::table('project_locations')->insert(['project_id' => $project->id, 'geo_location_id' => $ids['locality'], 'role' => 'primary', 'data_classification' => 'public']); $p = $this->draft($owner, ['origins' => [['type' => 'project', 'target_id' => $project->id]]]); $p = app(PartnershipExchange::class)->transition($owner, $p->id, $p->version, 'submit', 'QA submission'); $p = app(PartnershipExchange::class)->transition($manager, $p->id, $p->version, 'publish', 'QA source checked'); foreach (['province', 'district', 'admin_post', 'locality'] as $level) { $this->getJson('/api/v1/partnerships?territory_id='.$ids[$level])->assertOk()->assertJsonPath('data.0.id', $p->id)->assertJsonPath('total', 1); } $this->getJson('/api/v1/partnerships/source/project/'.$project->id)->assertOk()->assertJsonPath('items.0.id', $p->id); $this->getJson('/parcerias/'.$p->id)->assertOk()->assertJsonPath('origins.0.href', '/carteira/'.$dossier->id); $this->assertDatabaseCount('partnership_locations', 0); $this->assertDatabaseCount('opportunity_projects', 0); } public function test_authenticated_installation_preserves_legacy_rows_and_is_repeatable_in_qa(): void { $migration = require database_path('migrations/2026_10_07_160000_extend_internal_partnership_exchange.php'); $migration->down(); $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $id = DB::table('portal_partnerships')->insertGetId(['user_id' => $manager->id, 'title' => 'Legacy before migration', 'sector' => 'Original', 'province' => 'Original', 'offer' => 'Retained', 'seeking' => 'Retained', 'status' => 'pending']); $dir = storage_path('framework/testing/partnership-install-'.Str::uuid()); mkdir($dir, 0700, true); config(['partnerships.proof_directory' => $dir]); $dump = $dir.'/synthetic-unit-fixture.dump'; file_put_contents($dump, 'Isolated synthetic unit proof, not active backup'); file_put_contents($dir.'/backup-proof.json', json_encode(['restore_verified' => true, 'target' => ['database' => DB::connection()->getDatabaseName(), 'port' => 55441], 'dump_path' => $dump, 'sha256' => hash_file('sha256', $dump)])); $installer = app(PartnershipInstallation::class); file_put_contents($dir.'/qa-evidence.json', json_encode(['passed' => true, 'code_hash' => $installer->fingerprint(), 'database' => DB::connection()->getDatabaseName(), 'port' => 55441])); $hash = $installer->preview()['hash']; $this->actingAs($manager)->post('/management/partnerships-install', ['hash' => str_repeat('0', 64)])->assertConflict(); $this->post('/management/partnerships-install', ['hash' => $hash])->assertRedirect(); $this->assertDatabaseHas('portal_partnerships', ['id' => $id, 'user_id' => $manager->id, 'title' => 'Legacy before migration', 'status' => 'pending', 'offer' => 'Retained']); $this->post('/management/partnerships-install', ['hash' => $hash])->assertRedirect(); $this->assertSame(1, DB::table('audit_events')->where('action', 'partnership_module_installed')->count()); } }