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

95 lines
5.7 KiB
PHP

<?php
namespace App\Services;
use App\Models\InvestmentZone;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
class InvestmentZones
{
public function links(bool $territorial = false): \Illuminate\Database\Query\Builder
{
return DB::table(($territorial ? 'investment_zone_relations' : 'investment_zone_links').' as zl')->where('zl.validation_status', 'validated')
->whereIn('zl.zone_id', $this->visible()->select('investment_zones.id'))
->whereDate('zl.valid_from', '<=', today())->where(fn ($q) => $q->whereNull('zl.valid_until')->orWhereDate('zl.valid_until', '>=', today()))
->where(function ($q) {
$q->whereNull('zl.legal_source_id')->orWhereExists(fn ($s) => $s->selectRaw('1')->from('investment_legal_sources as ls')
->whereColumn('ls.id', 'zl.legal_source_id')->where('ls.validation_status', 'validated')->whereDate('ls.valid_from', '<=', today())
->where(fn ($s) => $s->whereNull('ls.valid_until')->orWhereDate('ls.valid_until', '>=', today())));
});
}
/** @return Builder<InvestmentZone> */
public function visible(): Builder
{
return InvestmentZone::query()->where('publication_status', 'published')->where('validation_status', 'validated')
->whereExists(fn ($q) => $q->selectRaw('1')->from('investment_legal_sources as source')
->whereColumn('source.id', 'investment_zones.legal_source_id')->where('source.validation_status', 'validated')
->whereDate('source.valid_from', '<=', today())
->where(fn ($q) => $q->whereNull('source.valid_until')->orWhereDate('source.valid_until', '>=', today())))
->whereExists(fn ($q) => $q->selectRaw('1')->from('organizations')->whereColumn('organizations.id', 'investment_zones.operator_id')->whereNull('organizations.deleted_at'));
}
/** @param array<string,mixed> $data */
public function save(?string $id, array $data, int $actor): InvestmentZone
{
return DB::transaction(function () use ($id, $data, $actor) {
$zone = $id ? InvestmentZone::whereKey($id)->lockForUpdate()->firstOrFail() : new InvestmentZone;
abort_if($id && (int) $zone->version_no !== (int) $data['version_no'], 409, 'Versão alterada. Recarregue antes de guardar.');
$version = $id ? (int) $zone->version_no + 1 : 1;
unset($data['version_no']);
$zone->fill([...$data, 'editor_id' => $actor, 'reviewer_id' => null, 'published_at' => null,
'version_no' => $version, 'publication_status' => 'draft', 'validation_status' => 'pending'])->save();
$this->snapshot($zone, $actor, 'saved');
return $zone;
});
}
public function publish(string $id, int $version, int $actor): InvestmentZone
{
return DB::transaction(function () use ($id, $version, $actor) {
$zone = InvestmentZone::whereKey($id)->lockForUpdate()->firstOrFail();
abort_if((int) $zone->version_no !== $version, 409, 'Versão alterada. Recarregue antes de aprovar.');
abort_if((int) $zone->editor_id === $actor, 403, 'A revisão exige outro gestor autorizado.');
$source = DB::table('investment_legal_sources')->where('id', $zone->legal_source_id)->lockForUpdate()->first();
abort_unless($source && $source->validation_status === 'validated' && $source->valid_from <= today()->toDateString()
&& (! $source->valid_until || $source->valid_until >= today()->toDateString()), 422, 'Fonte jurídica válida e revista em falta.');
abort_unless($zone->category !== 'pending' && $zone->operator_id && trim((string) $zone->name_en) !== ''
&& trim((string) $zone->description) !== '' && trim((string) $zone->description_en) !== '', 422, 'Categoria, operador e conteúdo PT/EN são obrigatórios.');
abort_unless(DB::table('organizations')->where('id', $zone->operator_id)->whereNull('deleted_at')->exists(), 422, 'Operador indisponível.');
$zone->fill(['version_no' => $version + 1, 'reviewer_id' => $actor, 'publication_status' => 'published',
'validation_status' => 'validated', 'published_at' => now()])->save();
$this->snapshot($zone, $actor, 'published');
return $zone;
});
}
public function suspend(string $id, int $version, int $actor): InvestmentZone
{
return DB::transaction(function () use ($id, $version, $actor) {
$zone = InvestmentZone::whereKey($id)->lockForUpdate()->firstOrFail();
abort_if((int) $zone->version_no !== $version, 409, 'Versão alterada.');
$zone->fill(['version_no' => $version + 1, 'publication_status' => 'suspended'])->save();
$this->snapshot($zone, $actor, 'suspended');
return $zone;
});
}
private function snapshot(InvestmentZone $zone, int $actor, string $action): void
{
$json = $zone->toJson(JSON_THROW_ON_ERROR);
DB::table('investment_zone_revisions')->insert(['id' => (string) Str::uuid(), 'zone_id' => $zone->id,
'version_no' => $zone->version_no, 'actor_id' => $actor, 'action' => $action,
'snapshot' => Crypt::encryptString($json), 'sha256' => hash('sha256', $json)]);
DB::table('audit_events')->insert(['source_system' => 'investment-zones', 'entity_type' => 'investment_zones',
'entity_id' => $zone->id, 'actor_user_id' => $actor, 'action' => $action,
'after_data' => json_encode(['version_no' => $zone->version_no, 'sha256' => hash('sha256', $json)], JSON_THROW_ON_ERROR)]);
}
}