312 lines
24 KiB
PHP
312 lines
24 KiB
PHP
<?php
|
||
|
||
namespace App\Services;
|
||
|
||
use App\Models\Opportunity;
|
||
use Illuminate\Support\Facades\DB;
|
||
|
||
/** §25 and common fields 415–422: explicit elaboration of opaque objects, shared by API and editor. */
|
||
class OpportunityCategoryFields
|
||
{
|
||
public const VERSION = 'semantic-fields-20261010-1';
|
||
|
||
/** @return array<string,mixed> */
|
||
private function text(): array
|
||
{
|
||
return ['type' => 'string', 'minLength' => 1, 'maxLength' => 1000];
|
||
}
|
||
|
||
/** @return array<string,mixed> */
|
||
private function doc(): array
|
||
{
|
||
return ['type' => 'string', 'format' => 'uuid'];
|
||
}
|
||
|
||
/** @return array<string,mixed> */
|
||
private function money(): array
|
||
{
|
||
return ['type' => 'number', 'minimum' => 0];
|
||
}
|
||
|
||
/** @param array<string,mixed> $properties
|
||
* @param list<string> $required
|
||
* @return array<string,mixed> */
|
||
private function object(array $properties, array $required = []): array
|
||
{
|
||
return ['type' => 'object', 'properties' => $properties, 'required' => $required, 'additionalProperties' => true];
|
||
}
|
||
|
||
/** @param array<string,mixed> $properties
|
||
* @param list<string> $required
|
||
* @return array<string,mixed> */
|
||
private function rows(array $properties, array $required = []): array
|
||
{
|
||
return ['type' => 'array', 'items' => $this->object($properties, $required)];
|
||
}
|
||
|
||
/** Preserve normative artifact and legacy keys; enrich its runtime adapter explicitly.
|
||
* @param array<string,mixed> $schema
|
||
* @return array<string,mixed> */
|
||
public function enrich(array $schema): array
|
||
{
|
||
$s = $this->text();
|
||
$d = $this->doc();
|
||
$n = $this->money();
|
||
$date = ['type' => 'string', 'format' => 'date'];
|
||
$strings = ['type' => 'array', 'items' => $s];
|
||
$bool = ['type' => 'boolean'];
|
||
$valuation = $this->object(['value' => $n, 'date' => $date, 'method' => $s, 'appraiser' => $s, 'document_id' => $d, 'currency' => ['type' => 'string', 'pattern' => '^[A-Z]{3}$'], 'divergence_justification' => $s], ['value', 'date', 'method', 'appraiser', 'document_id']);
|
||
$defs = &$schema['$defs'];
|
||
$common = &$defs['opportunityCreate']['properties'];
|
||
foreach (['markets' => 'marketCreate', 'risks' => 'riskCreate', 'licenses' => 'licenseCreate', 'milestones' => 'milestoneCreate'] as $field => $def) {
|
||
$common[$field]['items'] = ['$ref' => '#/$defs/'.$def];
|
||
}
|
||
// Reuse the existing canonical object definition in both editor and validation.
|
||
// The extracted Create property was opaque, preventing completion through the UI.
|
||
$common['environmental_social'] = ['$ref' => '#/$defs/environmentalSocialCreate'];
|
||
$common['maturity_stage']['enum'] = [...array_keys(OpportunityMaturity::STAGES), null];
|
||
$common['financial_indicators'] = $this->object(['npv' => ['type' => 'number'], 'irr' => ['type' => 'number'], 'payback_years' => $n, 'scenario' => $s, 'horizon_years' => ['type' => 'integer', 'minimum' => 1], 'currency' => ['type' => 'string', 'pattern' => '^[A-Z]{3}$'], 'discount_rate' => $n, 'model_document_id' => $d]);
|
||
// OP-9f173ed3-0043: a historical scenario is not a completed feasibility model.
|
||
$defs['opportunityCreate']['allOf'][] = [
|
||
'if' => ['properties' => ['maturity_stage' => ['enum' => ['feasibility', 'structured', 'investment_ready', 'implementation', 'operational']]], 'required' => ['maturity_stage']],
|
||
'then' => ['required' => ['financial_indicators'], 'properties' => ['financial_indicators' => ['required' => ['npv', 'irr', 'payback_years', 'scenario', 'horizon_years', 'currency', 'discount_rate', 'model_document_id']]]],
|
||
];
|
||
$defs['milestoneCreate']['properties'] += ['start_date' => $date, 'end_date' => $date, 'responsible_name' => $s, 'dependencies' => $strings];
|
||
$defs['milestoneCreate']['required'] = array_values(array_unique([...$defs['milestoneCreate']['required'], 'start_date', 'end_date', 'responsible_name']));
|
||
$defs['riskCreate']['properties']['mitigation']['minLength'] = 1;
|
||
$re = &$defs['realEstateCreate']['properties'];
|
||
$re['valuation'] = $valuation;
|
||
$re['utilities'] = $this->rows(['type' => ['enum' => ['electricity', 'water', 'sanitation', 'telecommunications', 'access']], 'status' => ['enum' => ['existing', 'planned', 'unavailable']]], ['type', 'status']);
|
||
$re['encumbrances'] = $this->rows(['type' => $s, 'details' => $s, 'document_id' => $d], ['type']);
|
||
$re += ['title_document_id' => $d, 'title_holder_name' => $s, 'issuing_authority_id' => $d, 'expected_completion_date' => $date, 'occupancy_details' => $s, 'occupancy_document_id' => $d, 'amenities_other' => $s, 'urban_limit_document_id' => $d, 'max_built_area' => ['type' => 'number', 'exclusiveMinimum' => 0]];
|
||
$re['viewing'] = $this->object(['mode' => $s, 'contact_id' => $d, 'window' => $s, 'instructions' => $s]);
|
||
$defs['realEstateCreate']['required'] = [...$defs['realEstateCreate']['required'], 'title_document_id', 'title_holder_name', 'issuing_authority_id'];
|
||
$lr = &$defs['landRightsCreate']['properties'];
|
||
$lr['overlaps_conflicts'] = $this->rows(['type' => $s, 'status' => $s, 'details' => $s, 'resolution_document_id' => $d], ['type', 'status']);
|
||
$lr['required_authorizations'] = $this->rows(['name' => $s, 'authority_id' => $d, 'status' => ['enum' => ['pending', 'approved', 'rejected', 'not_required']], 'document_id' => $d, 'condition' => $s], ['name', 'status']);
|
||
$lr['community_consultation'] = $this->object(['status' => $s, 'minutes_document_id' => $d, 'participants' => $strings, 'map_document_id' => $d, 'commitments' => $s, 'date' => $date], ['status', 'minutes_document_id', 'participants', 'map_document_id', 'commitments', 'date']);
|
||
$lr['consideration'] = $this->object(['object' => ['enum' => ['improvements', 'assets', 'operation', 'partnership']], 'amount' => $n, 'currency' => ['type' => 'string', 'pattern' => '^[A-Z]{3}$']], ['object', 'amount', 'currency']);
|
||
$lr += ['holder_document_id' => $d, 'communities_affected' => $bool, 'exploitation_plan_required' => $bool];
|
||
// Explicit optional unit of the parcel's existing area. Older versions stay unknown;
|
||
// do not assign the location's unit or convert the recorded numeric area.
|
||
$defs['parcelCreate']['properties']['area_unit'] = ['enum' => [...OpportunityResources::AREA_UNITS, null]];
|
||
$defs['landRightsCreate']['required'] = [...$defs['landRightsCreate']['required'], 'holder_document_id', 'communities_affected', 'exploitation_plan_required'];
|
||
$bt = &$defs['businessTransactionCreate']['properties'];
|
||
$bt['valuation'] = $valuation;
|
||
$bt['entity_registration'] = $this->object(['tax_identifier' => $s, 'registration_number' => $s, 'legal_document_id' => $d, 'beneficial_owner_document_id' => $d], ['tax_identifier', 'registration_number', 'legal_document_id', 'beneficial_owner_document_id']);
|
||
$bt['financial_periods'] = $this->rows(['year' => ['type' => 'integer', 'minimum' => 1900], 'currency' => ['type' => 'string', 'pattern' => '^[A-Z]{3}$'], 'revenue' => $n, 'profit' => ['type' => 'number'], 'ebitda' => ['type' => 'number'], 'assets' => $n, 'liabilities' => $n, 'cash_flow' => ['type' => 'number'], 'document_id' => $d], ['year', 'currency', 'revenue', 'profit', 'assets', 'liabilities', 'cash_flow', 'document_id']);
|
||
$bt['assets_liabilities'] = $this->object(['assets' => $s, 'excluded_assets' => $s, 'liabilities' => $s, 'guarantees' => $s, 'reconciliation_document_id' => $d], ['assets', 'excluded_assets', 'liabilities', 'guarantees', 'reconciliation_document_id']);
|
||
$bt['employees'] = $this->object(['total' => ['type' => 'integer', 'minimum' => 0], 'contracts' => $s, 'obligations' => $s, 'labor_liabilities' => $s], ['total', 'contracts', 'obligations', 'labor_liabilities']);
|
||
$bt['contracts_licenses'] = $this->rows(['name' => $s, 'transferability' => ['enum' => ['transferable', 'consent_required', 'not_transferable']], 'expiry_date' => $date, 'consent_document_id' => $d, 'document_id' => $d], ['name', 'transferability', 'expiry_date', 'document_id']);
|
||
$bt['litigation_contingencies'] = $this->rows(['type' => $s, 'details' => $s, 'value' => $n, 'status' => $s, 'probability' => ['type' => 'number', 'minimum' => 0, 'maximum' => 100], 'entity' => $s, 'document_id' => $d], ['type']);
|
||
$bt['partner_contribution'] = $this->object(['capital' => $n, 'assets' => $s, 'technology' => $s, 'market' => $s, 'management' => $s, 'calendar' => $s], ['capital', 'assets', 'technology', 'market', 'management', 'calendar']);
|
||
$bt['governance_rights'] = $this->object(['voting' => $s, 'board' => $s, 'reserved_matters' => $s, 'dividends' => $s, 'exit' => $s, 'deadlock' => $s], ['voting', 'board', 'reserved_matters', 'dividends', 'exit', 'deadlock']);
|
||
$bt['franchise_terms'] = $this->object(['brand' => $s, 'territory' => $s, 'term' => $s, 'initial_fee' => $n, 'royalties' => $s, 'support' => $s, 'exclusivity' => $s, 'renewal' => $s, 'termination' => $s, 'brand_license_document_id' => $d], ['brand', 'territory', 'term', 'initial_fee', 'royalties', 'support', 'exclusivity', 'renewal', 'termination', 'brand_license_document_id']);
|
||
$bt += ['available_financial_years' => ['type' => 'integer', 'minimum' => 1], 'nda_document_id' => $d, 'teaser' => $s];
|
||
$defs['businessTransactionCreate']['required'] = [...$defs['businessTransactionCreate']['required'], 'entity_registration', 'available_financial_years'];
|
||
|
||
return $schema;
|
||
}
|
||
|
||
/** @param array<string,mixed> $p
|
||
* @param array<string,mixed> $resources
|
||
* @param array<string,string> $errors */
|
||
public function validate(array $p, Opportunity $record, array $resources, array &$errors): void
|
||
{
|
||
$fail = function (bool $ok, string $path, string $message) use (&$errors) {
|
||
if (! $ok) {
|
||
$errors['canonical.'.$path] = $message;
|
||
}
|
||
};
|
||
// Every nested reference belongs to this record, not merely to an existing document.
|
||
$walk = function (array $node, string $path = '') use (&$walk, $resources, $fail) {
|
||
foreach ($node as $key => $value) {
|
||
$here = ltrim($path.'.'.$key, '.');
|
||
if (is_array($value)) {
|
||
$walk($value, $here);
|
||
} elseif (is_string($key) && (str_ends_with($key, 'document_id') || in_array($key, ['image_id', 'location_id'], true)) && $value !== null && $value !== '') {
|
||
$kind = $key === 'image_id' ? 'images' : ($key === 'location_id' ? 'locations' : 'documents');
|
||
$fail(isset($resources[$kind][$value]), $here, 'Selecione um recurso ativo desta oportunidade, incluído no contrato.');
|
||
}
|
||
if ($key === 'authority_id' || $key === 'issuing_authority_id') {
|
||
$fail($value === null || DB::table('authorities')->where('id', $value)->where('status', 'published')->where('valid_from', '<=', today())->where(fn ($q) => $q->whereNull('valid_to')->orWhere('valid_to', '>=', today()))->exists(), $here, 'Selecione uma entidade emissora publicada e aplicável nesta data.');
|
||
}
|
||
}
|
||
};
|
||
$walk($p);
|
||
if (isset($p['real_estate']) || isset($p['business_transaction'])) {
|
||
$fail(isset($p['currency']) && is_string($p['currency']) && preg_match('/^[A-Z]{3}$/D', $p['currency']) === 1, 'currency', 'Indique a moeda de todos os valores desta versão.');
|
||
}
|
||
if ($p['opportunity_category'] === 'investment_project') {
|
||
$milestones = $p['milestones'];
|
||
$fail(count($milestones) >= 2, 'milestones', 'Indique pelo menos dois marcos.');
|
||
$titles = array_column($milestones, 'title');
|
||
$fail(count($titles) === count(array_unique($titles)), 'milestones', 'Os nomes dos marcos devem identificar cada dependência sem ambiguidade.');
|
||
$edges = [];
|
||
foreach ($milestones as $i => $m) {
|
||
$fail($m['end_date'] >= $m['start_date'], "milestones.$i.end_date", 'O fim não pode anteceder o início.');
|
||
$edges[$m['title']] = $m['dependencies'] ?? [];
|
||
$fail(array_diff($edges[$m['title']], $titles) === [], "milestones.$i.dependencies", 'Uma dependência deve indicar outro marco deste cronograma.');
|
||
}
|
||
$seen = [];
|
||
$active = [];
|
||
$visit = function ($title) use (&$visit, &$seen, &$active, $edges): bool {
|
||
if (isset($active[$title])) {
|
||
return false;
|
||
}if (isset($seen[$title])) {
|
||
return true;
|
||
}$active[$title] = true;
|
||
foreach ($edges[$title] ?? [] as $parent) {
|
||
if (! $visit($parent)) {
|
||
return false;
|
||
}
|
||
}unset($active[$title]);
|
||
$seen[$title] = true;
|
||
|
||
return true;
|
||
};
|
||
foreach ($titles as $title) {
|
||
$fail($visit($title), 'milestones', 'As dependências não podem formar um ciclo.');
|
||
}
|
||
foreach ($p['risks'] as $i => $risk) {
|
||
$fail(($risk['likelihood'] < 4 && $risk['severity'] < 4) || ! empty($risk['mitigation']), "risks.$i.mitigation", 'Risco com probabilidade ou gravidade alta exige mitigação.');
|
||
}
|
||
if (app(OpportunityInitiativeApplicability::class)->applicable($p, 'unit_investment') && in_array($p['maturity_stage'], ['feasibility', 'structured', 'investment_ready', 'implementation', 'operational'], true)) {
|
||
$fail(! empty($p['financial_indicators']), 'financial_indicators', 'A partir de viabilidade, indique cenário, indicadores e modelo financeiro.');
|
||
$fail(count(array_filter($p['evidences'], fn ($e) => in_array($e['evidence_type'], ['feasibility_study', 'feasibility'], true))) > 0, 'evidences', 'Associe o estudo de viabilidade desta etapa.');
|
||
}
|
||
}
|
||
if (isset($p['real_estate'])) {
|
||
$re = $p['real_estate'];
|
||
$types = array_column($re['utilities'], 'type');
|
||
$fail(count($types) === 5 && count(array_unique($types)) === 5, 'real_estate.utilities', 'Declare os cinco serviços: eletricidade, água, saneamento, telecomunicações e acesso.');
|
||
if ($re['occupancy_status'] !== 'vacant') {
|
||
foreach (['occupancy_details', 'occupancy_document_id'] as $field) {
|
||
$fail(! empty($re[$field]), 'real_estate.'.$field, 'Ocupação exige detalhe contratual restrito e documento.');
|
||
}
|
||
}
|
||
if (in_array($re['construction_status'], ['planned', 'under_construction'], true)) {
|
||
$fail(! empty($re['expected_completion_date']) && $re['expected_completion_date'] >= today()->toDateString(), 'real_estate.expected_completion_date', 'Indique prazo de conclusão futuro ou atual.');
|
||
}
|
||
if (in_array('other', $re['amenities'] ?? [], true)) {
|
||
$fail(! empty($re['amenities_other']), 'real_estate.amenities_other', 'Descreva a comodidade indicada como outra.');
|
||
}
|
||
if (isset($re['max_built_area'])) {
|
||
$fail($re['built_area'] <= $re['max_built_area'], 'real_estate.built_area', 'A área excede o limite urbanístico declarado.');
|
||
$fail(! empty($re['urban_limit_document_id']), 'real_estate.urban_limit_document_id', 'Associe a fonte do limite urbanístico.');
|
||
}
|
||
foreach ($re['encumbrances'] as $i => $row) {
|
||
if ($row['type'] !== 'none') {
|
||
foreach (['details', 'document_id'] as $field) {
|
||
$fail(! empty($row[$field]), "real_estate.encumbrances.$i.$field", 'Ónus ou restrição exige descrição e documento.');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if (isset($p['land_rights'])) {
|
||
$lr = $p['land_rights'];
|
||
// §27: UNIQUE (opportunity_id, cadastral_number); absent numbers remain unknown.
|
||
$cadastralNumbers = [];
|
||
foreach ($lr['parcels'] ?? [] as $i => $parcel) {
|
||
$number = $parcel['cadastral_number'] ?? null;
|
||
if ($number !== null && $number !== '') {
|
||
$fail(! in_array($number, $cadastralNumbers, true), "land_rights.parcels.$i.cadastral_number", 'Número cadastral repetido nesta oportunidade; recupere e reconcilie a parcela existente.');
|
||
$cadastralNumbers[] = $number;
|
||
}
|
||
}
|
||
if (in_array($lr['duat_status'], ['provisional', 'definitive'], true)) {
|
||
$fail(! empty($lr['issuing_authority_id']), 'land_rights.issuing_authority_id', 'DUAT provisório/definitivo exige entidade emissora.');
|
||
if (! empty($lr['duat_number']) && ! empty($lr['issuing_authority_id'])) {
|
||
$fail(! Opportunity::whereKeyNot($record->id)->whereNotNull('owner_user_id')->whereRaw("management_draft->'canonical'->'land_rights'->>'duat_number'=?", [$lr['duat_number']])->whereRaw("management_draft->'canonical'->'land_rights'->>'issuing_authority_id'=?", [$lr['issuing_authority_id']])->whereNotIn('status', ['archived', 'rejected'])->exists(), 'land_rights.duat_number', 'Este número já está associado à mesma entidade emissora; reconciliar antes de submeter.');
|
||
}
|
||
}
|
||
if (in_array($lr['operation_type'], ['transfer_of_improvements', 'lease_of_improvements'], true)) {
|
||
$fail(! empty($lr['improvements']), 'land_rights.improvements', 'A operação sobre benfeitorias exige a respetiva descrição/evidência.');
|
||
}
|
||
foreach ($lr['improvements'] ?? [] as $i => $row) {
|
||
foreach (['assessed_value', 'currency', 'valuation_date', 'document_id', 'image_id'] as $field) {
|
||
$fail(isset($row[$field]), "land_rights.improvements.$i.$field", 'Benfeitoria exige valor, data, moeda, documento e fotografia.');
|
||
}
|
||
}
|
||
$fail(! $lr['exploitation_plan_required'] || ! empty($lr['exploitation_plan_document_id']), 'land_rights.exploitation_plan_document_id', 'Plano de exploração aplicável exige documento.');
|
||
$fail(! $lr['communities_affected'] || (! empty($lr['communities']) && ! empty($lr['community_consultation'])), 'land_rights.community_consultation', 'Comunidade afetada exige comunidade, representantes e consulta documentada.');
|
||
$communityKeys = [];
|
||
foreach ($lr['communities'] ?? [] as $i => $row) {
|
||
$key = json_encode([$row['name'] ?? null, $row['location_id'] ?? null]);
|
||
$fail(! isset($communityKeys[$key]), "land_rights.communities.$i.name", 'A mesma comunidade/localização já está incluída (§27).');
|
||
$communityKeys[$key] = true;
|
||
foreach (['representative_name', 'consultation_date', 'location_id'] as $field) {
|
||
$fail(! empty($row[$field]), "land_rights.communities.$i.$field", 'Identifique representante, data e localização da comunidade.');
|
||
}
|
||
}
|
||
}
|
||
if (isset($p['business_transaction'])) {
|
||
$bt = $p['business_transaction'];
|
||
$type = $bt['transaction_type'];
|
||
$allowed = match ($p['opportunity_category']) {
|
||
'business_sale' => ['full_sale', 'partial_sale', 'succession'],'business_partnership' => ['partnership', 'joint_venture'],'franchise' => ['franchise'],default => []
|
||
};
|
||
$fail(in_array($type, $allowed, true), 'business_transaction.transaction_type', 'Modalidade incompatível com esta categoria.');
|
||
$years = array_column($bt['financial_periods'], 'year');
|
||
if (($bt['history_status'] ?? '') === 'no_completed_years') {
|
||
$fail(app(OpportunityModularProfiles::class)->enabled() && ($p['module_profile_version'] ?? '') === OpportunityModularProfiles::HISTORY_VERSION,
|
||
'module_profile_version', 'Ausência de histórico requer a extensão QA v2 explícita; preserve o contrato original.');
|
||
$fail($bt['available_financial_years'] === 0 && $bt['financial_periods'] === [], 'business_transaction.financial_periods', 'Não misture projeções com exercícios financeiros realizados.');
|
||
foreach (['history_declaration', 'history_declaration_document_id', 'projections'] as $field) {
|
||
$fail(! empty($bt[$field]), 'business_transaction.'.$field, 'Entidade sem exercícios completos requer declaração e projeções identificadas.');
|
||
}
|
||
} else {
|
||
$fail($bt['available_financial_years'] >= 1 && $bt['financial_periods'] !== [], 'business_transaction.financial_periods', 'Use os exercícios existentes ou a declaração versionada de ausência de histórico.');
|
||
}
|
||
$fail(count($years) >= min(3, $bt['available_financial_years']) && count($years) === count(array_unique($years)), 'business_transaction.financial_periods', 'Inclua pelo menos três exercícios quando existentes, sem repetir anos.');
|
||
foreach ($years as $i => $year) {
|
||
$fail($year <= (int) today()->format('Y'), "business_transaction.financial_periods.$i.year", 'Não apresente um exercício futuro como demonstração realizada.');
|
||
}
|
||
$fail($bt['litigation_contingencies'] !== [], 'business_transaction.litigation_contingencies', 'Declare contingências, incluindo explicitamente a ausência.');
|
||
foreach ($bt['litigation_contingencies'] as $i => $row) {
|
||
if ($row['type'] !== 'none') {
|
||
foreach (['value', 'status', 'probability', 'entity', 'document_id'] as $field) {
|
||
$fail(isset($row[$field]), "business_transaction.litigation_contingencies.$i.$field", 'Contingência exige valor, estado, probabilidade, responsável/evidência restrita.');
|
||
}
|
||
}
|
||
}
|
||
if ($bt['confidentiality_level'] === 'nda_required') {
|
||
$fail(! empty($bt['nda_document_id']), 'business_transaction.nda_document_id', 'Associe o acordo de confidencialidade aplicável.');
|
||
}
|
||
if ($p['visibility'] === 'public') {
|
||
$fail(! empty($bt['teaser']), 'business_transaction.teaser', 'Prepare o texto público sem informação confidencial.');
|
||
}
|
||
}
|
||
foreach (['real_estate', 'business_transaction'] as $module) {
|
||
if (isset($p[$module])) {
|
||
$v = $p[$module]['valuation'];
|
||
$fail($v['value'] > 0, "$module.valuation.value", 'Avaliação deve ter valor positivo.');
|
||
$fail($v['date'] <= today()->toDateString(), "$module.valuation.date", 'A data da avaliação não pode ser futura.');
|
||
// No arbitrary materiality threshold: reviewer determines divergence and its evidence.
|
||
}
|
||
}
|
||
}
|
||
|
||
/** @param array<string,mixed> $p
|
||
* @param array<string,string> $errors */
|
||
public function publication(array $p, array &$errors): void
|
||
{
|
||
if (isset($p['business_transaction'])) {
|
||
foreach ($p['business_transaction']['contracts_licenses'] as $i => $row) {
|
||
if ($row['expiry_date'] < today()->toDateString() || $row['transferability'] === 'not_transferable' || ($row['transferability'] === 'consent_required' && empty($row['consent_document_id']))) {
|
||
$errors["business_transaction.contracts_licenses.$i"] = 'Contrato/licença vencido, intransmissível ou sem consentimento exigido.';
|
||
}
|
||
}
|
||
}
|
||
if (isset($p['land_rights'])) {
|
||
$lr = $p['land_rights'];
|
||
if (! empty($lr['expiry_date']) && $lr['expiry_date'] < today()->toDateString()) {
|
||
$errors['land_rights.expiry_date'] = 'Direito expirado; decisão interna não renova o ato externo.';
|
||
}
|
||
if ($lr['communities_affected'] && ! in_array($lr['community_consultation']['status'] ?? '', ['completed', 'approved'], true)) {
|
||
$errors['land_rights.community_consultation'] = 'Consulta necessária ainda não concluída/documentada.';
|
||
}
|
||
}
|
||
}
|
||
}
|