withoutVite(); Http::fake(); Mail::fake(); $this->owner = User::factory()->create(); $this->actingAs($this->owner); } private function input(array $changes = []): array { return ['client_key' => (string) Str::uuid(), 'title' => 'SYNTHETIC partnership draft', 'kind' => 'seeking', 'partnership_type' => 'joint_venture', 'offer' => 'SYNTHETIC existing resources', 'seeking' => 'SYNTHETIC collaboration', 'payload' => ['summary' => 'SYNTHETIC summary', 'requirements' => 'SYNTHETIC requirements', 'mandatory' => ['capacity'], 'consent' => true, 'confidentiality' => 'private', 'availability' => 'SYNTHETIC resources available from November'], 'origins' => [], 'locations' => [], ...$changes]; } private function payload(int $id): array { return json_decode(DB::table('portal_partnerships')->where('id', $id)->value('payload'), true, 512, JSON_THROW_ON_ERROR); } private function organization(string $name): string { $id = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $id, 'name' => 'SYNTHETIC '.$name, 'source_system' => 'unit-synthetic', 'organization_type' => 'private']); return $id; } private function links(): array { $place = (string) Str::uuid(); // Positive QA fixture for the existing workbook predicate, not an imported official place or geometry. DB::table('geo_locations')->insert(['id' => $place, 'code' => $place, 'name' => 'SYNTHETIC coverage reference', 'location_type' => 'country', 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:synthetic-'.substr($place, 0, 8)]); $project = Project::factory()->create(); InvestmentDossier::create(['project_id' => $project->id, 'title' => 'SYNTHETIC source dossier', 'source' => 'Synthetic fixture, no actual disclosure', 'content' => [], 'publication_status' => 'published', 'published_at' => now()->subMinute(), 'data_classification' => 'public', 'approval_basis' => 'document_owner', 'approval_evidence' => 'Synthetic QA fixture only']); return ['origins' => [['type' => 'project', 'target_id' => $project->id]], 'locations' => [['geo_location_id' => $place, 'role' => 'coverage', 'source_reference' => 'SYNTHETIC declared coverage']]]; } public function test_partial_edits_preserve_organization_and_scalar_fields_without_transferring_ownership(): void { $organization = $this->organization('retained organization'); $officer = User::factory()->create(); foreach ([$this->owner->id, $officer->id] as $actor) { DB::table('memberships')->insert(['user_id' => $actor, 'organization_id' => $organization, 'role' => 'officer']); } $sector = Sector::create(['name' => 'SYNTHETIC retained sector', 'slug' => 'synthetic-'.Str::uuid(), 'is_active' => true]); $input = $this->input(['organization_id' => $organization, 'sector_id' => $sector->id, 'deadline' => now()->addMonth()->toDateString(), ...$this->links()]); $record = app(PartnershipExchange::class)->save($this->owner, $input); $origins = DB::table('partnership_origins')->where('partnership_id', $record->id)->get()->toArray(); $locations = DB::table('partnership_locations')->where('partnership_id', $record->id)->get()->toArray(); $this->actingAs($officer)->putJson('/parcerias/'.$record->id, ['version' => $record->version, 'kind' => 'seeking', 'payload' => ['summary' => 'SYNTHETIC summary only']])->assertOk(); $saved = DB::table('portal_partnerships')->where('id', $record->id)->first(); foreach (['title', 'offer', 'seeking', 'organization_id', 'sector_id', 'partnership_type', 'deadline'] as $field) { $this->assertSame($input[$field], $saved->{$field}); } $this->assertSame($record->user_id, $saved->user_id); $this->assertEquals($origins, DB::table('partnership_origins')->where('partnership_id', $record->id)->get()->toArray()); $this->assertEquals($locations, DB::table('partnership_locations')->where('partnership_id', $record->id)->get()->toArray()); $this->get('/parcerias/'.$record->id.'/editar')->assertOk(); } public function test_explicit_empty_scalar_and_relation_values_still_remove_the_selected_information(): void { $organization = $this->organization('explicitly removed link'); DB::table('memberships')->insert(['user_id' => $this->owner->id, 'organization_id' => $organization, 'role' => 'officer']); $record = app(PartnershipExchange::class)->save($this->owner, $this->input(['organization_id' => $organization, ...$this->links()])); $this->putJson('/parcerias/'.$record->id, ['version' => $record->version, 'kind' => 'seeking', 'title' => null, 'offer' => null, 'seeking' => null, 'organization_id' => null, 'sector_id' => null, 'partnership_type' => null, 'deadline' => null, 'origins' => [], 'locations' => []])->assertOk(); $saved = DB::table('portal_partnerships')->where('id', $record->id)->first(); $this->assertSame('', $saved->title); $this->assertSame('', $saved->offer); $this->assertSame('', $saved->seeking); $this->assertNull($saved->organization_id); $this->assertNull($saved->sector_id); $this->assertNull($saved->deadline); $this->assertNull($saved->partnership_type); $this->assertSame(0, DB::table('partnership_origins')->where('partnership_id', $record->id)->count()); $this->assertSame(0, DB::table('partnership_locations')->where('partnership_id', $record->id)->count()); $this->assertSame('SYNTHETIC resources available from November', $this->payload($record->id)['availability']); $this->get('/parcerias/'.$record->id.'/editar')->assertOk(); } public function test_omitted_availability_is_preserved_when_an_older_editor_saves_other_fields(): void { $input = $this->input(); $record = app(PartnershipExchange::class)->save($this->owner, $input); unset($input['payload']['availability']); $this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version, 'title' => 'SYNTHETIC changed title'])->assertOk(); $this->assertSame('SYNTHETIC resources available from November', $this->payload($record->id)['availability'] ?? null); } public function test_explicit_clearing_is_preserved_and_previous_event_is_not_rewritten(): void { $input = $this->input(); $record = app(PartnershipExchange::class)->save($this->owner, $input); $before = DB::table('partnership_events')->where('partnership_id', $record->id)->orderBy('aggregate_version')->first(); $input['payload']['availability'] = null; $this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version])->assertOk(); $this->assertNull($this->payload($record->id)['availability']); $this->assertSame((array) $before, (array) DB::table('partnership_events')->where('id', $before->id)->first()); } public function test_omitted_payload_fields_are_retained_but_explicit_false_empty_text_and_lists_are_applied(): void { $input = $this->input(); $record = app(PartnershipExchange::class)->save($this->owner, $input); $original = $this->payload($record->id); $this->putJson('/parcerias/'.$record->id, ['version' => $record->version, 'kind' => 'seeking', 'payload' => ['summary' => 'SYNTHETIC only new summary']])->assertOk(); $saved = $this->payload($record->id); $this->assertSame($original['availability'], $saved['availability']); $this->assertSame($original['mandatory'], $saved['mandatory']); $this->assertTrue($saved['consent']); $version = DB::table('portal_partnerships')->where('id', $record->id)->value('version'); $this->putJson('/parcerias/'.$record->id, ['version' => $version, 'kind' => 'seeking', 'payload' => ['mandatory' => [], 'consent' => false, 'requirements' => '']])->assertOk(); $saved = $this->payload($record->id); $this->assertSame([], $saved['mandatory']); $this->assertFalse($saved['consent']); $this->assertNull($saved['requirements']); $this->assertSame($original['_creation_hash'], $saved['_creation_hash']); } public function test_creation_replay_requires_current_representation_after_the_record_moves_to_another_organization(): void { $a = $this->organization('original organization'); $b = $this->organization('current organization'); $representative = User::factory()->create(); foreach ([[$this->owner->id, $a], [$representative->id, $a], [$representative->id, $b]] as [$user, $organization]) { DB::table('memberships')->insert(['user_id' => $user, 'organization_id' => $organization, 'role' => 'officer']); } $input = $this->input(['organization_id' => $a]); $record = app(PartnershipExchange::class)->save($this->owner, $input); app(PartnershipExchange::class)->save($representative, [...$input, 'version' => $record->version, 'organization_id' => $b], $record->id); $before = (array) DB::table('portal_partnerships')->where('id', $record->id)->first(); $this->postJson('/parcerias/rascunhos', $input)->assertForbidden(); $this->assertSame($before, (array) DB::table('portal_partnerships')->where('id', $record->id)->first()); $this->get('/parcerias/'.$record->id.'/editar')->assertForbidden(); } public function test_identical_creation_replay_is_read_only_even_when_its_sector_is_no_longer_active(): void { $sector = Sector::create(['name' => 'SYNTHETIC old sector', 'slug' => 'synthetic-'.Str::uuid(), 'is_active' => true]); $input = $this->input(['sector_id' => $sector->id]); $record = app(PartnershipExchange::class)->save($this->owner, $input); $sector->update(['is_active' => false]); $before = (array) DB::table('portal_partnerships')->where('id', $record->id)->first(); $events = DB::table('partnership_events')->count(); $this->postJson('/parcerias/rascunhos', $input)->assertOk()->assertJsonPath('id', $record->id)->assertJsonPath('version', $record->version); $this->assertSame($before, (array) DB::table('portal_partnerships')->where('id', $record->id)->first()); $this->assertSame($events, DB::table('partnership_events')->count()); $this->postJson('/parcerias/rascunhos', [...$input, 'client_key' => (string) Str::uuid()])->assertUnprocessable()->assertJsonValidationErrors('sector_id')->assertJsonPath('errors.sector_id.0', 'Seleccione um sector activo ou deixe a classificação por definir.'); $this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version])->assertUnprocessable()->assertJsonValidationErrors('sector_id'); } public function test_stale_version_and_invalid_fields_cannot_overwrite_current_payload(): void { $input = $this->input(); $record = app(PartnershipExchange::class)->save($this->owner, $input); $this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version, 'title' => 'SYNTHETIC current revision'])->assertOk(); $current = (array) DB::table('portal_partnerships')->where('id', $record->id)->first(); $this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version, 'title' => 'SYNTHETIC stale revision'])->assertConflict(); $this->assertSame($current, (array) DB::table('portal_partnerships')->where('id', $record->id)->first()); $this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $current['version'], 'payload' => ['availability' => str_repeat('a', 1001)]])->assertUnprocessable()->assertJsonValidationErrors('payload.availability')->assertJsonPath('errors', fn ($errors) => ($errors['payload.availability'][0] ?? null) === 'Indique a disponibilidade em até 1000 caracteres.'); $this->assertSame($current, (array) DB::table('portal_partnerships')->where('id', $record->id)->first()); } public function test_editing_a_published_record_still_returns_to_draft_and_preserves_omitted_availability(): void { $manager = User::factory()->create(); config(['portal.managers' => [$manager->email]]); $input = $this->input(); $record = app(PartnershipExchange::class)->save($this->owner, $input); $exchange = app(PartnershipExchange::class); $record = $exchange->transition($this->owner, $record->id, $record->version, 'submit', 'SYNTHETIC submission'); $record = $exchange->transition($manager, $record->id, $record->version, 'publish', 'SYNTHETIC independent publication'); unset($input['payload']['availability']); $this->putJson('/parcerias/'.$record->id, [...$input, 'version' => $record->version, 'title' => 'SYNTHETIC revision for review'])->assertOk()->assertJsonPath('status', 'draft'); $this->assertSame('SYNTHETIC resources available from November', $this->payload($record->id)['availability']); } public function test_unverified_personal_drafts_and_editor_remain_available_without_organization_or_mfa(): void { $this->owner->update(['email_verified_at' => null]); $record = $this->postJson('/parcerias/rascunhos', $this->input(['title' => '']))->assertOk()->json('id'); $this->get('/parcerias/'.$record.'/editar')->assertOk(); Http::assertNothingSent(); Mail::assertNothingSent(); } }