138 lines
7.3 KiB
PHP
138 lines
7.3 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Http\Controllers\InvestmentPortfolioController;
|
|
use App\Models\Opportunity;
|
|
use App\Models\Sector;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Symfony\Component\HttpKernel\Exception\HttpException;
|
|
|
|
/** Entry adapter over existing sector, opportunity, portfolio and private dossier records. */
|
|
final class SectorInvestment
|
|
{
|
|
// Editorial bridges, not renamed masters or automatic opportunity classifications.
|
|
public const CONTENT = [
|
|
'mineracao' => ['mineracao', 'energia-mineracao'], 'energia-especifica' => ['energia', 'energia-mineracao'],
|
|
'agronegocio' => ['agronegocio'], 'turismo' => ['turismo'], 'infraestruturas' => ['infraestruturas'],
|
|
'tecnologias-de-informacao' => ['tech-fintech'], 'mercado-financeiro' => ['tech-fintech'],
|
|
'fintech' => ['tech-fintech'], 'imobiliario' => ['imobiliario'],
|
|
'bens-de-consumo-e-franquias' => ['bens-de-consumo-franquias'],
|
|
];
|
|
|
|
public function sector(string $slug): Sector
|
|
{
|
|
return Sector::where('slug', $slug)->where('is_active', true)->firstOrFail();
|
|
}
|
|
|
|
/**
|
|
* @return array<array-key, mixed>
|
|
*/
|
|
public function context(string $slug): array
|
|
{
|
|
$sector = $this->sector($slug);
|
|
|
|
return [...$sector->only(['id', 'name', 'slug']), 'href' => SectorOrganization::href($sector->slug)];
|
|
}
|
|
|
|
/** Public detail shared by the catalogue and the individual sector page.
|
|
* @return array<array-key, mixed>
|
|
*/
|
|
public function page(Sector $sector): array
|
|
{
|
|
return [
|
|
'sector' => $sector->only(['id', 'name', 'slug', 'description', 'short_description']),
|
|
'sectorEntry' => $this->publicView($sector),
|
|
'opportunities' => SectorOrganization::opportunities($sector)->latest('published_at')
|
|
->limit(12)->get(['id', 'title', 'slug', 'province', 'summary']),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<array-key, mixed>
|
|
*/
|
|
public function publicView(Sector $sector): array
|
|
{
|
|
$creation = app(ContentManagement::class)->access(request()->user())['opportunities']['create'] ?? null;
|
|
$query = SectorOrganization::opportunities($sector);
|
|
$articles = $this->guides($sector);
|
|
$relations = [];
|
|
foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $table => $foreign) {
|
|
$ids = DB::table('opportunity_'.$table)->whereIn('opportunity_id', (clone $query)->select('id'))->pluck($foreign);
|
|
foreach (InvestmentPortfolioController::visible()->whereIn($foreign, $ids)->orderBy('title')->get() as $dossier) {
|
|
$relations[] = ['id' => $dossier->id, 'title' => $dossier->title, 'href' => '/carteira/'.$dossier->id, 'kind' => $table,
|
|
'basis' => 'Associação cadastrada entre uma oportunidade pública deste setor e esta ficha publicada.', 'source' => $dossier->source];
|
|
}
|
|
}
|
|
$locations = [];
|
|
foreach ((clone $query)->orderBy('id')->get(['id', 'title', 'slug']) as $opportunity) {
|
|
foreach (app(InvestmentRelations::class)->opportunityLocations($opportunity->id) as $location) {
|
|
$locations[] = ['opportunity_title' => $opportunity->title, 'href' => '/oportunidades/'.$opportunity->slug, 'location' => $location];
|
|
}
|
|
}
|
|
|
|
return ['articles' => $articles, 'relations' => collect($relations)->unique('id')->values()->all(), 'locations' => $locations,
|
|
'start_href' => app(InvestorPath::class)->available() ? '/investimentos/a-partir-do-setor/'.$sector->slug : null,
|
|
'start_notice' => app(InvestorPath::class)->available() ? null : 'O percurso para guardar a ideia está temporariamente indisponível. A consulta e o apoio continuam disponíveis.',
|
|
'register_href' => app(PortalProfileAccess::class)->available() ? ($creation ? $creation.'?sector_id='.$sector->id : '/perfil/promotor') : null,
|
|
'register_label' => $creation ? 'Cadastrar oportunidade — escolher modalidade' : 'Pedir acesso para cadastrar oportunidades',
|
|
'register_notice' => app(PortalProfileAccess::class)->available() ? null : 'O cadastro por perfis está temporariamente indisponível; pode preparar a ideia e contactar o apoio.',
|
|
'specialization_href' => in_array($sector->slug, ['tecnologias-de-informacao', 'mercado-financeiro'], true) ? SectorOrganization::href('fintech') : null,
|
|
'content_notice' => $sector->slug === 'mercado-financeiro' ? 'O artigo geral de Mercado Financeiro permanece vazio e não foi publicado. A preparação de negócios neste setor está disponível.' : null];
|
|
}
|
|
|
|
/** Published guides for an explicit sector; never inferred from proximity.
|
|
* @return array<array-key, mixed>
|
|
*/
|
|
public function guides(?Sector $sector): array
|
|
{
|
|
if (! $sector) {
|
|
return [];
|
|
}
|
|
$articles = [];
|
|
if (app(LegacyContent::class)->available()) {
|
|
foreach (self::CONTENT[$sector->slug] ?? [] as $slug) {
|
|
try {
|
|
$a = app(LegacyContent::class)->article($slug, false);
|
|
$articles[] = Arr::only($a, ['title', 'href', 'image', 'sha256', 'summary', 'author', 'opportunities_href', 'opportunities_label', 'has_opportunities']);
|
|
} catch (HttpException $e) {
|
|
if ($e->getStatusCode() !== 404) {
|
|
throw $e;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return $articles;
|
|
}
|
|
|
|
/**
|
|
* @param array<array-key, mixed> $input
|
|
*/
|
|
public function attach(User $actor, string $id, array $input): void
|
|
{
|
|
$data = Validator::make($input, ['version' => 'required|integer|min:1', 'sector_slug' => 'required|string|max:150'])->validate();
|
|
$context = $this->context($data['sector_slug']);
|
|
abort_unless(app(InvestorPath::class)->available(), 404);
|
|
DB::transaction(function () use ($actor, $id, $data, $context) {
|
|
$desk = app(VirtualDesk::class);
|
|
$case = $desk->case($actor, $id, true, true);
|
|
abort_if($case->closed_at, 409, 'O dossier está encerrado.');
|
|
abort_unless(DB::table('investor_journeys')->where('id', $case->journey_id)->whereNotNull('path_definition_id')->whereNull('deleted_at')->exists(), 409, 'Este dossier conserva outro roteiro.');
|
|
$payload = $desk->decode($case->payload);
|
|
foreach ($payload['investor_path']['sector_contexts'] ?? [] as $existing) {
|
|
if ($existing['id'] === $context['id']) {
|
|
return;
|
|
}
|
|
}
|
|
abort_unless((int) $case->version === $data['version'], 409, 'O dossier foi alterado; recupere a versão atual.');
|
|
$payload['investor_path']['sector_contexts'][] = [...$context, 'actor_id' => $actor->id, 'saved_at' => now()->toIso8601String()];
|
|
DB::table('desk_cases')->where('id', $id)->update(['payload' => $desk->encode($payload), 'version' => $case->version + 1, 'updated_at' => now()]);
|
|
$desk->event($desk->case($actor, $id), $actor, 'sector_context_referenced', ['sector_id' => $context['id'], 'sector_slug' => $context['slug']]);
|
|
});
|
|
}
|
|
}
|