'concession-qa-20261010-v1', 'financing' => 'financing-qa-20261010-v1', 'supply' => 'supply-qa-20261010-v1', 'other' => 'other-qa-20261010-v1', ]; public function enabled(): bool { if (! config('portal.opportunity_modular_profiles_qa_enabled') || ! app()->environment('testing')) { return false; } $connection = DB::connection(); if ($connection->getDriverName() !== 'pgsql' || (string) $connection->getConfig('port') !== '55441' || ! in_array($connection->getConfig('host'), ['127.0.0.1', 'localhost'], true) || $connection->getConfig('url') || ! str_ends_with($connection->getDatabaseName(), '_test')) { return false; } $actual = $connection->selectOne('SELECT current_database() AS database, inet_server_port() AS port'); return $actual->database === $connection->getDatabaseName() && (int) $actual->port === 55441; } /** @param array $schema * @return array */ public function extend(array $schema): array { if (! $this->enabled()) { return $schema; } $text = ['type' => 'string', 'minLength' => 1, 'maxLength' => 2000]; $doc = ['type' => 'string', 'format' => 'uuid']; $positive = ['type' => 'number', 'exclusiveMinimum' => 0]; $date = ['type' => 'string', 'format' => 'date']; $properties = [ 'mode' => ['enum' => ['intention_call', 'awarded_right', 'seeking_funding', 'funding_offer', 'purchase_request', 'supply_offer', 'classified_other']], 'conditions_status' => ['enum' => ['proposed', 'binding']], 'object' => $text, 'representation_document_id' => $doc, 'object_document_id' => $doc, 'terms' => $text, 'source' => $text, 'scenario' => $text, 'terms_date' => $date, 'commitment_document_id' => $doc, 'disclosure_authorization_document_id' => $doc, 'granting_entity' => $text, 'procedure_regime' => $text, 'scope' => $text, 'term_value' => $positive, 'term_unit' => ['enum' => ['days', 'months', 'years']], 'remuneration_obligations' => $text, 'participation_conditions' => $text, 'external_decision_document_id' => $doc, 'territorial_right_document_id' => $doc, 'land_required' => ['type' => 'boolean'], 'construction_required' => ['type' => 'boolean'], 'applicable_authorizations' => $text, 'construction_license_document_id' => $doc, 'start_date' => $date, 'end_date' => $date, 'instrument' => ['enum' => ['debt', 'equity', 'guarantee']], 'use_of_funds' => $text, 'disbursement_calendar' => $text, 'repayment_periodicity' => $text, 'financial_plan_document_id' => $doc, 'secured_evidence_document_id' => $doc, 'offered_capacity' => $positive, 'eligibility_criteria' => $text, 'capacity_document_id' => $doc, 'equity_percentage' => ['type' => 'number', 'exclusiveMinimum' => 0, 'maximum' => 100], 'proposed_rights' => $text, 'quantity' => $positive, 'unit' => $text, 'destination' => $text, 'acceptance_criteria' => $text, 'quotation_method' => $text, 'unit_price' => $positive, 'total_price' => $positive, 'declared_taxes' => $text, 'advance_or_guarantee' => ['type' => 'boolean'], 'subtype' => $text, 'purpose' => $text, 'recipients' => $text, 'expected_output' => $text, 'risks' => $text, ]; $schema['$defs']['opportunityCreate']['properties']['module_details'] = [ 'type' => 'object', 'properties' => $properties, 'additionalProperties' => false, ]; // Opt-in v2: a declaration/projection is not a fabricated completed financial year. $business = &$schema['$defs']['businessTransactionCreate']; $business['properties']['available_financial_years']['minimum'] = 0; $business['properties']['financial_periods']['minItems'] = 0; $business['properties'] += ['history_status' => ['enum' => ['existing_accounts', 'no_completed_years']], 'history_declaration' => $text, 'history_declaration_document_id' => $doc, 'projections' => ['type' => 'object', 'properties' => ['scenario' => $text, 'source' => $text, 'date' => $date, 'document_id' => $doc], 'required' => ['scenario', 'source', 'date', 'document_id'], 'additionalProperties' => false]]; $business['allOf'][] = ['if' => ['properties' => ['history_status' => ['const' => 'no_completed_years']], 'required' => ['history_status']], 'else' => ['properties' => ['financial_periods' => ['minItems' => 1], 'available_financial_years' => ['minimum' => 1]]]]; return $schema; } /** @param array $p * @param array $resources * @param array $errors */ public function validate(array $p, array $resources, array &$errors): void { $category = $p['opportunity_category'] ?? ''; if (! isset(self::PROFILES[$category])) { if (isset($p['module_details'])) { $errors['canonical.module_details'] = 'Detalhe modular incompatível com esta categoria.'; } return; } if (! $this->enabled() || ($p['module_profile_version'] ?? '') !== self::PROFILES[$category]) { $errors['canonical.module_profile_version'] = 'Use a versão QA explicitamente aprovada; versões anteriores permanecem rascunhos recuperáveis.'; return; } $m = $p['module_details'] ?? []; $fail = function (bool $ok, string $path, string $message) use (&$errors): void { if (! $ok) { $errors['canonical.'.$path] = $message; } }; $require = function (array $fields) use ($m, $fail): void { foreach ($fields as $field) { $fail(isset($m[$field]) && $m[$field] !== '', 'module_details.'.$field, 'Preencha este campo aplicável antes de submeter.'); } }; $require(['mode', 'object', 'representation_document_id', 'object_document_id']); $modes = match ($category) { 'concession' => ['intention_call', 'awarded_right'], 'financing' => ['seeking_funding', 'funding_offer'], 'supply' => ['purchase_request', 'supply_offer'], default => ['classified_other'], }; $fail(in_array($m['mode'] ?? '', $modes, true), 'module_details.mode', 'Selecione procura/oferta ou modalidade compatível com esta categoria.'); // Only references included in the current opportunity's verified resource snapshot are usable. foreach ($m as $key => $value) { if (str_ends_with($key, '_document_id') && $value !== null && $value !== '') { $fail(isset($resources['documents'][$value]), 'module_details.'.$key, 'Associe documento ativo desta oportunidade incluído no contrato.'); } } if (! empty($m['start_date']) && ! empty($m['end_date'])) { $fail($m['end_date'] >= $m['start_date'], 'module_details.end_date', 'O fim não pode anteceder o início.'); } if ($category === 'concession') { $require(['granting_entity', 'procedure_regime', 'scope', 'term_value', 'term_unit', 'remuneration_obligations', 'participation_conditions', 'land_required', 'construction_required']); $fail(($m['term_value'] ?? 0) > 0, 'module_details.term_value', 'Declare duração positiva.'); if (($m['mode'] ?? '') === 'awarded_right') { $require(['external_decision_document_id']); } if ($m['land_required'] ?? false) { $require(['applicable_authorizations']); if (($m['mode'] ?? '') === 'awarded_right') { $require(['territorial_right_document_id']); } } if ($m['construction_required'] ?? false) { $require(['applicable_authorizations']); } } if (in_array($category, ['financing', 'supply'], true)) { $require(['conditions_status', 'terms']); $fail(! empty($p['currency']), 'currency', 'Declare moeda sem converter valores implicitamente.'); if (($m['conditions_status'] ?? '') === 'proposed') { $require(['scenario', 'source', 'terms_date']); $fail(empty($m['terms_date']) || $m['terms_date'] <= today()->toDateString(), 'module_details.terms_date', 'A data da fonte não pode ser futura.'); } elseif (($m['conditions_status'] ?? '') === 'binding') { $require(['commitment_document_id']); } } if ($category === 'financing') { $require(['instrument', 'disbursement_calendar']); if (($m['mode'] ?? '') === 'seeking_funding') { $require(['use_of_funds', 'financial_plan_document_id']); foreach (['total_investment', 'investment_breakdown', 'secured_financing', 'funding_sought'] as $field) { $fail(array_key_exists($field, $p), $field, 'Complete o cenário do projeto para a procura de financiamento.'); } $total = $p['total_investment'] ?? 0; $secured = array_sum(array_column($p['secured_financing'] ?? [], 'amount')); $sum = array_sum(array_column($p['investment_breakdown'] ?? [], 'amount')); $fail($total > 0 && abs($sum - $total) <= 0.010001, 'investment_breakdown', 'Rubricas devem reconciliar com o investimento total positivo.'); $fail($secured <= $total, 'secured_financing', 'Financiamento assegurado não pode exceder o investimento.'); $sought = $p['funding_sought'] ?? 0; $fail($sought > 0 && $sought <= $total - $secured, 'funding_sought', 'Pedido positivo não pode exceder a necessidade remanescente do projeto.'); if ($secured > 0) { $require(['secured_evidence_document_id']); } foreach (['investment_breakdown', 'secured_financing'] as $field) { foreach ($p[$field] ?? [] as $i => $row) { $fail(($row['currency'] ?? '') === ($p['currency'] ?? ''), "$field.$i.currency", 'Moedas diferentes exigem cenário explícito; não somar automaticamente.'); } } } elseif (($m['mode'] ?? '') === 'funding_offer') { $require(['offered_capacity', 'eligibility_criteria', 'capacity_document_id']); $fail(($m['offered_capacity'] ?? 0) > 0, 'module_details.offered_capacity', 'Declare capacidade oferecida positiva e documentada, sem aprovação automática de beneficiários.'); foreach (['total_investment', 'investment_breakdown', 'secured_financing', 'funding_sought'] as $field) { $fail(! isset($p[$field]) || $p[$field] === [], $field, 'Oferta usa capacidade e critérios do financiador; preserve o cenário do projeto num percurso de procura.'); } } if (($m['instrument'] ?? '') === 'debt') { $require(['term_value', 'term_unit', 'repayment_periodicity']); $fail(($m['term_value'] ?? 0) > 0, 'module_details.term_value', 'Dívida exige prazo positivo.'); } elseif (($m['instrument'] ?? '') === 'equity') { $require(['equity_percentage', 'proposed_rights']); $fail(($m['equity_percentage'] ?? 0) > 0 && ($m['equity_percentage'] ?? 0) <= 100, 'module_details.equity_percentage', 'Percentagem deve estar entre zero exclusivo e 100.'); } } if ($category === 'supply') { $require(['quantity', 'unit', 'destination', 'start_date', 'end_date', 'acceptance_criteria', 'advance_or_guarantee']); $fail(($m['quantity'] ?? 0) > 0, 'module_details.quantity', 'Declare quantidade positiva.'); $fail(! empty($m['total_price']) || ! empty($m['quotation_method']), 'module_details.quotation_method', 'Declare preço total ou método de cotação.'); if (isset($m['unit_price'], $m['total_price'], $m['quantity'])) { $fail(abs($m['quantity'] * $m['unit_price'] - $m['total_price']) <= 0.010001, 'module_details.total_price', 'Reconcilie quantidade × preço unitário, separando impostos declarados.'); } if (($m['mode'] ?? '') === 'supply_offer') { $require(['capacity_document_id']); } } if ($category === 'other') { $require(['subtype', 'purpose', 'recipients', 'expected_output', 'risks']); $subtype = config('portal.opportunity_other_qa_subtypes.'.($m['subtype'] ?? '')); $fail(is_array($subtype) && ($subtype['approved'] ?? false) && ! empty($subtype['version']) && ! empty($subtype['gates']) && ! empty($subtype['decision_reference']), 'module_details.subtype', 'Subtipo ainda não aprovado: guarde e desenvolva a ideia; a submissão aguarda classificação funcional.'); } } /** @param array $p * @param array $errors */ public function publication(array $p, array &$errors): void { $m = $p['module_details'] ?? []; if (($p['opportunity_category'] ?? '') === 'concession') { if (($m['mode'] ?? '') === 'intention_call' && empty($m['disclosure_authorization_document_id'])) { $errors['module_details.disclosure_authorization_document_id'] = 'Chamada/intenção exige autorização de divulgação; não representa direito atribuído.'; } if (($m['mode'] ?? '') === 'awarded_right' && empty($m['external_decision_document_id'])) { $errors['module_details.external_decision_document_id'] = 'Direito atribuído exige o ato competente; aprovação interna não o substitui.'; } if (($m['construction_required'] ?? false) && ($m['mode'] ?? '') === 'awarded_right' && empty($m['construction_license_document_id'])) { $errors['module_details.construction_license_document_id'] = 'Construção aplicável exige licença antes da disponibilização como direito atribuído.'; } } } /** @return list */ public function gates(Opportunity $record): array { $p = app(OpportunityManagement::class)->values($record)['canonical'] ?? []; $m = $p['module_details'] ?? []; return match ($record->opportunity_category) { 'concession' => ['technical_review', 'legal_territorial_review'], 'financing' => ['technical_review', 'financial_review', 'legal_tax_due_diligence'], 'supply' => ['technical_review', 'legal_territorial_review', ...(($m['advance_or_guarantee'] ?? false) ? ['financial_review'] : [])], 'other' => config('portal.opportunity_other_qa_subtypes.'.($m['subtype'] ?? '').'.gates', []), default => [], }; } }