['Regadio', 'regadio|irrigação|irrigacao'], 'storage' => ['Silos e armazenagem', 'silos?|armazenagem|armazenamento'], 'cold' => ['Cadeia de frio', 'frio|refrigeração'], 'processing' => ['Processamento', 'processamento|transformação|moagem'], 'spinning' => ['Fiação', 'fiação|fiacao'], 'logistics' => ['Logística', 'logística|logistica|transporte'], 'certification' => ['Certificação', 'certificação|certificacao'], 'training' => ['Formação', 'formação|formacao'], 'finance' => ['Financiamento', 'financiamento|serviços financeiros'], ]; /** * @return array */ public function options(string $origin, ?string $territory = null): array { [$originKind, $originId] = array_pad(explode(':', $origin, 2), 2, ''); abort_unless(($originKind === 'portfolio' && Str::isUuid($originId)) || ($originKind === 'opportunity' && ctype_digit($originId)), 422, 'Origem complementar inválida.'); $principal = null; $relationEvidence = []; if (str_starts_with($origin, 'portfolio:')) { $record = InvestmentPortfolioController::visible()->addSelect('version_no')->whereKey(substr($origin, 10))->firstOrFail(); $text = trim(($record->content['focus'] ?? '')."\n".($record->content['gaps'] ?? '')."\n".($record->content['description'] ?? '')); $reference = ['key' => $origin, 'title' => $record->title, 'href' => '/carteira/'.$record->id, 'version' => $record->version_no]; $ids = app(CorridorAgribusinessOpportunities::class)->portfolioOpportunityIds($record); foreach (['projects' => 'project_id', 'clusters' => 'cluster_id', 'corridors' => 'corridor_id'] as $relation => $foreign) { if ($record->$foreign) { $ids = [...$ids, ...DB::table('opportunity_'.$relation)->where($foreign, $record->$foreign)->pluck('opportunity_id')->all()]; } } $basis = $record->source; foreach (array_unique($ids) as $id) { $relationEvidence[$id] = ['type' => 'portfolio', 'origin' => $origin, 'source' => $basis]; } $serviceOrigins = [['dossier', $record->id]]; foreach (['project_id' => 'project', 'cluster_id' => 'cluster'] as $foreign => $type) { if ($record->$foreign) { $serviceOrigins[] = [$type, $record->$foreign]; } } } else { $reference = app(InvestorContext::class)->reference($origin); abort_unless($reference['kind'] === 'opportunity', 422); // The context resolver has already checked the actor's private scope. // A private preparation is valid dossier input, not a public offer // from which this public-complement catalogue can infer services. if (($reference['visibility'] ?? 'public') === 'private') { return []; } $principal = Opportunity::published()->whereKey($reference['id'])->firstOrFail(); $text = $principal->summary; $basis = 'Resumo cadastrado da oportunidade '.$principal->reference; $projects = DB::table('opportunity_projects')->where('opportunity_id', $principal->id)->pluck('project_id'); $links = DB::table('complementary_project_links')->whereIn('anchor_project_id', $projects) ->whereNull('deleted_at')->where('data_classification', 'public')->get(['id', 'anchor_project_id', 'complementary_project_id', 'relation_type', 'version_no']); $targetProjects = DB::table('opportunity_projects')->whereIn('project_id', $links->pluck('complementary_project_id'))->get(); $ids = [$principal->id, ...$targetProjects->pluck('opportunity_id')->all()]; $relationEvidence[$principal->id] = ['type' => 'same_fiche', 'source' => $basis]; foreach ($targetProjects as $targetProject) { $relationEvidence[$targetProject->opportunity_id] = ['type' => 'complementary_project', 'links' => $links->where('complementary_project_id', $targetProject->project_id)->values()->map(fn ($link) => (array) $link)->all()]; } foreach (Opportunity::published()->whereIn('id', $ids)->where('id', '!=', $principal->id)->get() as $linked) { $text .= "\n".$linked->summary; } if (count($ids) > 1) { $basis .= '; relações complementares públicas entre projectos'; } $serviceOrigins = [['investment', (string) $principal->id]]; $classification = app(OpportunityClassification::class); $principalChains = collect($classification->publicRows($principal))->where('target_type', 'chain')->filter(fn ($r) => ! empty($r['source_reference'])); $chains = $principalChains->pluck('target_id')->all(); if ($chains) { Opportunity::published()->whereNot('id', $principal->id)->chunkById(100, function ($records) use (&$ids, &$relationEvidence, $chains, $principalChains, $classification) { foreach ($records as $candidate) { $related = collect($classification->publicRows($candidate))->where('target_type', 'chain')->filter(fn ($r) => ! empty($r['source_reference'])); $shared = array_intersect($chains, $related->pluck('target_id')->all()); if ($shared) { $ids[] = $candidate->id; $relationEvidence[$candidate->id] ??= ['type' => 'value_chain', 'principal' => $principalChains->whereIn('target_id', $shared)->values()->all(), 'target' => $related->whereIn('target_id', $shared)->values()->all()]; } } }); } } $territoryReference = $territory ? app(InvestorContext::class)->reference('territory:'.$territory) : null; $candidates = Opportunity::published()->whereIn('id', array_unique($ids))->get(); $items = []; foreach (self::TERMS as $code => [$label, $pattern]) { if (! preg_match('/\b(?:'.$pattern.')\b/iu', $text, $match)) { continue; } $offers = []; foreach ($candidates as $candidate) { // A common corridor alone does not prove the component. Keep the supporting excerpt. if (! preg_match('/\b(?:'.$pattern.')\b/iu', $candidate->title."\n".$candidate->summary, $evidence)) { continue; } // Mentioning a need for funding/training in a project's summary is not an offer. if (in_array($code, ['certification', 'training', 'finance'], true) && ! preg_match('/\b(?:'.$pattern.')\b/iu', $candidate->title)) { continue; } if (in_array($code, ['certification', 'training', 'finance'], true) && preg_match('/\b(?:necessidade|pedido|procura)\b/iu', $candidate->title)) { continue; } if ($territory && $candidate->primary_territory_id && ! $this->compatible($territory, $candidate->primary_territory_id)) { continue; } $offers[] = ['key' => 'opportunity:'.$candidate->id, 'title' => $candidate->title, 'href' => '/oportunidades/'.$candidate->slug, 'relationship' => $relationEvidence[$candidate->id] ?? null, 'version' => $candidate->management_version, 'evidence' => $evidence[0], 'location' => ($principal && $principal->id === $candidate->id ? 'Componente referido na própria ficha. ' : '').($candidate->primary_territory_id ? app(InvestorContext::class)->reference('territory:'.$candidate->primary_territory_id)['title'] : 'Implantação por definir')]; } // Services are concrete published offerings explicitly associated with this source. $services = []; if (in_array($code, ['certification', 'training', 'finance'], true) && Schema::hasTable('partnership_origins') && Schema::hasTable('partnership_interests')) { $services = DB::table('portal_partnerships as p')->join('partnership_origins as o', 'o.partnership_id', '=', 'p.id') ->where(function ($q) use ($serviceOrigins) { foreach ($serviceOrigins as [$type, $id]) { $q->orWhere(fn ($q) => $q->where('o.type', $type)->where('o.target_id', $id)); } })->where('p.status', 'published')->where('p.kind', 'offering') ->where('p.partnership_type', $code === 'finance' ? 'finance' : 'training') ->when($code !== 'finance', fn ($q) => $q->where(fn ($q) => $q->where('p.title', 'ilike', '%'.$match[0].'%')->orWhereRaw("p.payload->>'summary' ILIKE ?", ['%'.$match[0].'%'])))->distinct()->get(['p.id', 'p.title']) ->filter(function ($p) use ($territory) { if (! $territory) { return true; } $locations = DB::table('partnership_locations')->where('partnership_id', $p->id)->pluck('geo_location_id'); return $locations->isEmpty() || $locations->contains(fn ($id) => $this->compatible($territory, $id)); })->map(fn ($p) => ['key' => 'partnership:'.$p->id, 'title' => $p->title, 'href' => '/parcerias/'.$p->id, 'location' => 'Consultar âmbito na oferta'])->values()->all(); } $excerpt = collect(explode("\n", $text))->first(fn ($line) => preg_match('/\b(?:'.$pattern.')\b/iu', $line) === 1); $items[] = ['code' => $code, 'title' => $label, 'origin' => $reference, 'territory_id' => $territory, 'territory' => $territoryReference, 'source' => $basis, 'excerpt' => $excerpt, 'source_sha256' => hash('sha256', $text), 'opportunities' => $offers, 'services' => $services]; } return $items; } private function compatible(string $a, string $b): bool { if ($a === $b) { return true; } $geo = app(InvestmentGeography::class); return in_array($b, $geo->descendants($a), true) || in_array($a, $geo->descendants($b), true); } /** * @param array $input * @param array $previous * @return array */ public function resolve(array $input, array $previous = []): array { $resolved = []; foreach ($input as $choice) { $target = $choice['target'] ?? null; $old = collect($previous)->first(fn ($r) => $r['origin']['key'] === $choice['origin'] && $r['code'] === $choice['code'] && ($r['target']['key'] ?? null) === $target && ($r['territory_id'] ?? null) === ($choice['territory_id'] ?? null)); try { $item = collect($this->options($choice['origin'], $choice['territory_id'] ?? null))->firstWhere('code', $choice['code']); } catch (ModelNotFoundException $e) { if (! $old) { throw $e; } $item = null; } catch (HttpException $e) { if (! $old || $e->getStatusCode() !== 404) { throw $e; } $item = null; } $offer = $target && $item ? collect([...$item['opportunities'], ...$item['services']])->firstWhere('key', $target) : null; if ($old && (! $item || ($target && ! $offer) || (! $target && ($item['opportunities'] || $item['services'])))) { $resolved[$old['key']] = [...$old, 'availability' => 'review_required']; continue; } abort_unless($item, 422, 'Complemento sem referência documentada neste investimento.'); abort_if($target && ! $offer, 422, 'A oferta complementar deixou de estar disponível neste contexto.'); abort_if(! $target && ($item['opportunities'] || $item['services']), 422, 'Já existe uma oferta relacionada; seleccione a ficha correspondente.'); $key = hash('sha256', $choice['origin'].'|'.$choice['code'].'|'.($target ?? 'idea').'|'.($choice['territory_id'] ?? '')); $resolved[$key] = ['key' => $key, 'origin' => $item['origin'], 'code' => $item['code'], 'title' => $item['title'], 'kind' => $target ? (str_starts_with($target, 'opportunity:') ? 'opportunity' : 'service') : 'need', 'target' => $offer, 'territory_id' => $item['territory_id'], 'territory' => $item['territory'], 'source' => $item['source'], 'excerpt' => $item['excerpt'], 'source_sha256' => $item['source_sha256']]; } return array_values($resolved); } }