Files
Investidor-V2/app/Http/Controllers/InvestmentContextController.php
nala 250ac35fd1
tests / ci (push) Canceled after 0s
first commit
2026-09-28 10:01:09 +02:00

101 lines
6.2 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\Opportunity;
use App\Services\InvestmentCatalogue;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\Rule;
use Inertia\Inertia;
use Inertia\Response;
class InvestmentContextController extends Controller
{
public function edit(int $id, InvestmentCatalogue $catalogue): Response
{
return Inertia::render('investment/context', ['record' => Opportunity::findOrFail($id), 'context' => $catalogue->context($id, true), 'references' => $catalogue->references(true)]);
}
public function save(Request $request, int $id): RedirectResponse|JsonResponse
{
$rules = ['version' => ['required', 'integer', 'min:0'], 'is_education' => ['required', 'boolean'],
'business_model' => ['nullable', 'string', 'max:5000'], 'requirements' => ['nullable', 'string', 'max:10000'],
'risks' => ['nullable', 'string', 'max:5000'], 'target_date' => ['nullable', 'date_format:Y-m-d'],
'source_reference' => ['required', 'string', 'min:10', 'max:2000'], 'facets' => ['array', 'max:100'],
'facets.*' => ['integer', 'distinct', Rule::exists('investment_reference_options', 'id')->where('active', true)]];
foreach (InvestmentCatalogue::RELATIONS as $relation => [$foreign, $table]) {
$rules[$relation] = ['array', 'max:100'];
$rules[$relation.'.*'] = ['uuid', 'distinct', Rule::exists($table, 'id')->whereNull('deleted_at')];
}
$data = $request->validate($rules);
$version = DB::transaction(function () use ($request, $id, $data) {
$opportunity = Opportunity::whereKey($id)->lockForUpdate()->firstOrFail();
$profile = DB::table('opportunity_profiles')->where('opportunity_id', $id)->first();
abort_unless((int) ($profile->version ?? 0) === (int) $data['version'], 409, 'Contexto alterado. Recarregue.');
$version = $data['version'] + 1;
DB::table('opportunity_profiles')->updateOrInsert(['opportunity_id' => $id], [
...array_intersect_key($data, array_flip(['is_education', 'business_model', 'requirements', 'risks', 'target_date', 'source_reference'])),
'version' => $version, 'created_at' => $profile->created_at ?? now(), 'updated_at' => now()]);
foreach (InvestmentCatalogue::RELATIONS as $relation => [$foreign, $table]) {
DB::table('opportunity_'.$relation)->where('opportunity_id', $id)->delete();
foreach ($data[$relation] ?? [] as $value) {
DB::table('opportunity_'.$relation)->insert(['opportunity_id' => $id, $foreign => $value]);
}
}
DB::table('opportunity_facets')->where('opportunity_id', $id)->delete();
foreach ($data['facets'] ?? [] as $value) {
DB::table('opportunity_facets')->insert(['opportunity_id' => $id, 'option_id' => $value]);
}
// Editing public context produces a draft for explicit approval, never silent publication.
$opportunity->update(['status' => 'draft', 'published_at' => null]);
$this->audit($request, 'opportunity_context', $id, 'context_saved', ['version' => $version]);
return $version;
});
return $request->expectsJson() ? response()->json(['version' => $version]) : back()->with('success', 'Contexto guardado. A ficha está em rascunho para aprovação.');
}
public function references(): Response
{
return Inertia::render('investment/references', ['options' => DB::table('investment_reference_options')->orderBy('group')->orderBy('label')->get()]);
}
public function saveReference(Request $request, ?int $id = null): RedirectResponse|JsonResponse
{
$data = $request->validate(['group' => ['required', 'string', Rule::exists('investment_reference_options', 'group')],
'code' => ['required', 'alpha_dash:ascii', 'max:80'], 'label' => ['required', 'string', 'max:200'],
'active' => ['required', 'boolean'], 'requires_description' => ['required', 'boolean'], 'sort_order' => ['required', 'integer', 'between:0,10000']]);
$savedId = DB::transaction(function () use ($request, $id, $data) {
DB::select('select pg_advisory_xact_lock(270927, hashtext(?))', [$data['group']]);
if ($id) {
$current = DB::table('investment_reference_options')->where('id', $id)->lockForUpdate()->first();
abort_unless($current !== null, 404);
abort_unless($current->group === $data['group'] && $current->code === $data['code'], 422, 'Grupo e código são estáveis para preservar referências.');
abort_if($current->code === 'other' && (! $data['active'] || ! $data['requires_description']), 422, 'Preserve a opção Outro — especificar.');
DB::table('investment_reference_options')->where('id', $id)->update([...$data, 'updated_at' => now()]);
$savedId = $id;
} else {
abort_if(DB::table('investment_reference_options')->where('group', $data['group'])->where('code', $data['code'])->exists(), 422, 'Código já existente neste grupo.');
$savedId = DB::table('investment_reference_options')->insertGetId([...$data, 'created_at' => now(), 'updated_at' => now()]);
}
$this->audit($request, 'investment_reference_options', $savedId, 'reference_saved', ['active' => $data['active']]);
return $savedId;
});
return $request->expectsJson() ? response()->json(['id' => $savedId]) : back()->with('success', 'Opção guardada. Os registos anteriores foram preservados.');
}
/** @param array<string,mixed> $values */
private function audit(Request $request, string $entity, int $id, string $action, array $values): void
{
DB::table('audit_events')->insert(['source_system' => 'investment-context', 'entity_type' => $entity,
'actor_user_id' => $request->user()->id, 'action' => $action,
'after_data' => json_encode(['record_id' => $id, ...$values], JSON_THROW_ON_ERROR)]);
}
}