101 lines
6.2 KiB
PHP
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)]);
|
|
}
|
|
}
|