207 lines
23 KiB
PHP
207 lines
23 KiB
PHP
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
use Illuminate\Support\Facades\Validator;
|
|
use Illuminate\Validation\Rules\In;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
final class BusinessFormSchema
|
|
{
|
|
/** @return array<string, array{step:int,pt:string,en:string,type:string,max:int,options?:array<string,string>,when?:array<string,list<string>>}> */
|
|
public static function fields(): array
|
|
{
|
|
return [
|
|
'proponent_type' => ['step' => 1, 'pt' => 'Quem apresenta a oportunidade?', 'en' => 'Who is presenting the opportunity?', 'type' => 'select', 'max' => 30, 'options' => ['organization' => 'Empresa ou organização / Company or organization', 'sole_trader' => 'Empresário em nome individual / Sole trader', 'person' => 'Pessoa singular / Individual', 'public_entity' => 'Entidade pública / Public entity']],
|
|
'proponent_name' => ['step' => 1, 'pt' => 'Nome ou denominação', 'en' => 'Name', 'type' => 'text', 'max' => 200],
|
|
'country' => ['step' => 1, 'pt' => 'País de origem ou constituição', 'en' => 'Country of origin or incorporation', 'type' => 'country', 'max' => 100],
|
|
'declared_origin_country_code' => ['step' => 1, 'pt' => 'País de origem declarado desta solicitação (opcional)', 'en' => 'Declared country of origin of this request (optional)', 'type' => 'country', 'max' => 2],
|
|
'tax_identifier' => ['step' => 1, 'pt' => 'NUIT ou identificador (interno, quando aplicável)', 'en' => 'NUIT or identifier (internal, when applicable)', 'type' => 'text', 'max' => 50],
|
|
'contact_name' => ['step' => 1, 'pt' => 'Nome da pessoa de contacto', 'en' => 'Contact person name', 'type' => 'text', 'max' => 200],
|
|
'contact_email' => ['step' => 1, 'pt' => 'Correio electrónico (indique email ou telefone)', 'en' => 'Email (provide email or phone)', 'type' => 'email', 'max' => 254],
|
|
'contact_phone' => ['step' => 1, 'pt' => 'Telefone (indique telefone ou email)', 'en' => 'Phone (provide phone or email)', 'type' => 'tel', 'max' => 40],
|
|
'contact_preference' => ['step' => 1, 'pt' => 'Preferência de contacto', 'en' => 'Contact preference', 'type' => 'select', 'max' => 30, 'options' => ['through_portal' => 'Receber pedidos através da InvestMoz', 'approved_business_contact' => 'Mostrar contacto empresarial aprovado']],
|
|
'acting_capacity' => ['step' => 2, 'pt' => 'Qualidade em que actua', 'en' => 'Acting capacity', 'type' => 'select', 'max' => 30, 'options' => ['holder' => 'Titular / Holder', 'representative' => 'Representante / Representative']],
|
|
'declared_holder' => ['step' => 2, 'pt' => 'Entidade ou pessoa representada', 'en' => 'Entity or person represented', 'type' => 'text', 'max' => 200],
|
|
'mandate_reference' => ['step' => 2, 'pt' => 'Referência do documento de autorização', 'en' => 'Authorization document reference', 'type' => 'text', 'max' => 500, 'when' => ['acting_capacity' => ['representative']]],
|
|
'mandate_document_reference' => ['step' => 2, 'pt' => 'Referência interna do documento de autorização (não carregue aqui)', 'en' => 'Internal authorization document reference (do not upload here)', 'type' => 'text', 'max' => 500, 'when' => ['acting_capacity' => ['representative']]],
|
|
'mandate_valid_until' => ['step' => 2, 'pt' => 'Validade do mandato', 'en' => 'Mandate expiry', 'type' => 'date', 'max' => 10, 'when' => ['acting_capacity' => ['representative']]],
|
|
'disclosure_scope' => ['step' => 2, 'pt' => 'Limites da autorização de divulgação', 'en' => 'Limits of disclosure authorization', 'type' => 'textarea', 'max' => 2000, 'when' => ['acting_capacity' => ['representative']]],
|
|
'legitimacy_declaration' => ['step' => 2, 'pt' => 'Declaro legitimidade para esta submissão', 'en' => 'I declare authority to submit', 'type' => 'select', 'max' => 3, 'options' => ['yes' => 'Sim / Yes', 'no' => 'Não / No']],
|
|
'title' => ['step' => 3, 'pt' => 'Título proposto', 'en' => 'Proposed title', 'type' => 'text', 'max' => 200],
|
|
'opportunity_type' => ['step' => 3, 'pt' => 'Tipo de oportunidade', 'en' => 'Opportunity type', 'type' => 'select', 'max' => 40, 'options' => ['investment_project' => 'Projecto de investimento', 'company_sale' => 'Venda de negócio existente', 'asset_sale' => 'Activo disponível', 'business_partnership' => 'Parceria empresarial', 'financing_request' => 'Procura de financiamento', 'concession_opportunity' => 'Concessão', 'supply_opportunity' => 'Oportunidade de fornecimento']],
|
|
'description' => ['step' => 3, 'pt' => 'Descrição resumida: o que está disponível, procura e condições principais', 'en' => 'Summary: what is available, sought and key conditions', 'type' => 'textarea', 'max' => 10000],
|
|
'subsector_activity' => ['step' => 3, 'pt' => 'Subsector e actividade (por designação)', 'en' => 'Subsector and activity (by name)', 'type' => 'text', 'max' => 500],
|
|
'declared_reason' => ['step' => 3, 'pt' => 'Motivo e objectivo', 'en' => 'Reason and objective', 'type' => 'textarea', 'max' => 2000],
|
|
'project_stage' => ['step' => 3, 'pt' => 'Situação do projecto ou negócio', 'en' => 'Project or business status', 'type' => 'select', 'max' => 40, 'options' => ['idea' => 'Ideia ou conceito', 'study' => 'Em estudo', 'preparation' => 'Em preparação', 'implementation' => 'Em implementação', 'operating' => 'Em funcionamento', 'suspended' => 'Actividade suspensa', 'other' => 'Outra situação']],
|
|
'project_stage_other' => ['step' => 3, 'pt' => 'Especifique a situação', 'en' => 'Specify the status', 'type' => 'text', 'max' => 200, 'when' => ['project_stage' => ['other']]],
|
|
'availability' => ['step' => 3, 'pt' => 'Disponibilidade', 'en' => 'Availability', 'type' => 'select', 'max' => 40, 'options' => ['available' => 'Disponível', 'from_date' => 'Disponível a partir de uma data', 'negotiating' => 'Em negociação; aceita manifestações de interesse']],
|
|
'available_from' => ['step' => 3, 'pt' => 'Disponível a partir de', 'en' => 'Available from', 'type' => 'date', 'max' => 10, 'when' => ['availability' => ['from_date']]],
|
|
'valid_until' => ['step' => 3, 'pt' => 'Prazo para manifestações de interesse (opcional)', 'en' => 'Interest deadline (optional)', 'type' => 'date', 'max' => 10],
|
|
'asking_value' => ['step' => 4, 'pt' => 'Valor', 'en' => 'Value', 'type' => 'decimal', 'max' => 24, 'when' => ['value_status' => ['defined', 'indicative']]],
|
|
'value_status' => ['step' => 4, 'pt' => 'O valor está definido?', 'en' => 'Is the value defined?', 'type' => 'select', 'max' => 30, 'options' => ['defined' => 'Sim', 'indicative' => 'Valor indicativo', 'consultation' => 'Sob consulta', 'unknown' => 'Ainda não definido']],
|
|
'value_meaning' => ['step' => 4, 'pt' => 'Significado do valor', 'en' => 'Meaning of the value', 'type' => 'select', 'max' => 30, 'options' => ['asking_price' => 'Preço pedido', 'investment_estimate' => 'Investimento estimado', 'financing_sought' => 'Financiamento procurado'], 'when' => ['value_status' => ['defined', 'indicative']]],
|
|
'value_nature' => ['step' => 4, 'pt' => 'Natureza do valor', 'en' => 'Value basis', 'type' => 'select', 'max' => 30, 'options' => ['asking' => 'Pedido', 'indicative' => 'Indicativo', 'appraised' => 'Avaliado, sujeito a prova'], 'when' => ['value_status' => ['defined', 'indicative']]],
|
|
'currency' => ['step' => 4, 'pt' => 'Moeda (código de três letras)', 'en' => 'Currency (three-letter code)', 'type' => 'text', 'max' => 3, 'when' => ['value_status' => ['defined', 'indicative']]],
|
|
'amount_secured' => ['step' => 4, 'pt' => 'Montante assegurado (projectos)', 'en' => 'Secured amount (projects)', 'type' => 'decimal', 'max' => 24, 'when' => ['opportunity_type' => ['investment_project']]],
|
|
'investment_total' => ['step' => 4, 'pt' => 'Investimento total estimado (projectos)', 'en' => 'Estimated total investment (projects)', 'type' => 'decimal', 'max' => 24, 'when' => ['opportunity_type' => ['investment_project']]],
|
|
'amount_sought' => ['step' => 4, 'pt' => 'Montante de financiamento procurado', 'en' => 'Financing sought', 'type' => 'decimal', 'max' => 24, 'when' => ['opportunity_type' => ['investment_project', 'financing_request']]],
|
|
'equity_percentage' => ['step' => 4, 'pt' => 'Participação disponível (%)', 'en' => 'Available equity (%)', 'type' => 'decimal', 'max' => 8],
|
|
'funds_use' => ['step' => 4, 'pt' => 'Utilização dos fundos', 'en' => 'Use of funds', 'type' => 'textarea', 'max' => 2000],
|
|
'asset_deal_kind' => ['step' => 4, 'pt' => 'Tipo de venda', 'en' => 'Type of sale', 'type' => 'select', 'max' => 30, 'options' => ['whole_business' => 'Venda total', 'participation' => 'Participação', 'assets' => 'Activos'], 'when' => ['opportunity_type' => ['company_sale', 'asset_sale']]],
|
|
'partnership_contribution' => ['step' => 4, 'pt' => 'Contribuição pretendida', 'en' => 'Contribution sought', 'type' => 'select', 'max' => 30, 'options' => ['capital' => 'Capital', 'technology' => 'Tecnologia', 'management' => 'Gestão', 'equipment' => 'Equipamento', 'distribution' => 'Distribuição', 'other' => 'Outra'], 'when' => ['opportunity_type' => ['business_partnership']]],
|
|
'asset_modality' => ['step' => 4, 'pt' => 'Modalidade do activo', 'en' => 'Asset arrangement', 'type' => 'select', 'max' => 30, 'options' => ['sale' => 'Venda', 'lease' => 'Arrendamento', 'other' => 'Outra'], 'when' => ['opportunity_type' => ['asset_sale']]],
|
|
'concession_entity' => ['step' => 4, 'pt' => 'Entidade responsável pela concessão', 'en' => 'Concession authority', 'type' => 'text', 'max' => 300, 'when' => ['opportunity_type' => ['concession_opportunity']]],
|
|
'concession_reference' => ['step' => 4, 'pt' => 'Referência oficial, procedimento e prazo', 'en' => 'Official reference, procedure and deadline', 'type' => 'textarea', 'max' => 2000, 'when' => ['opportunity_type' => ['concession_opportunity']]],
|
|
'supply_description' => ['step' => 4, 'pt' => 'Bem ou serviço, quantidade/capacidade, destino e prazo', 'en' => 'Good or service, quantity/capacity, destination and timing', 'type' => 'textarea', 'max' => 2000, 'when' => ['opportunity_type' => ['supply_opportunity']]],
|
|
'conditions' => ['step' => 4, 'pt' => 'Condições adicionais', 'en' => 'Additional conditions', 'type' => 'textarea', 'max' => 4000],
|
|
'geography_scope' => ['step' => 5, 'pt' => 'Âmbito geográfico', 'en' => 'Geographic scope', 'type' => 'select', 'max' => 30, 'options' => ['specific' => 'Localização específica', 'multiple' => 'Várias localizações', 'national' => 'Âmbito nacional', 'digital' => 'Digital/sem instalação definida']],
|
|
'province_id' => ['step' => 5, 'pt' => 'Província ou Cidade de Maputo', 'en' => 'Province or Maputo City', 'type' => 'territory', 'max' => 36, 'when' => ['geography_scope' => ['specific', 'multiple']]],
|
|
'district_id' => ['step' => 5, 'pt' => 'Distrito', 'en' => 'District', 'type' => 'territory', 'max' => 36, 'when' => ['geography_scope' => ['specific', 'multiple']]],
|
|
'admin_post_id' => ['step' => 5, 'pt' => 'Posto administrativo', 'en' => 'Administrative post', 'type' => 'territory', 'max' => 36, 'when' => ['geography_scope' => ['specific', 'multiple']]],
|
|
'locality_id' => ['step' => 5, 'pt' => 'Localidade', 'en' => 'Locality', 'type' => 'territory', 'max' => 36, 'when' => ['geography_scope' => ['specific', 'multiple']]],
|
|
'locations_json' => ['step' => 5, 'pt' => 'Localizações administrativas (várias)', 'en' => 'Administrative locations (multiple)', 'type' => 'location_list', 'max' => 16000, 'when' => ['geography_scope' => ['multiple']]],
|
|
'location_notes' => ['step' => 5, 'pt' => 'Endereço ou referência (interno)', 'en' => 'Address or reference (internal)', 'type' => 'textarea', 'max' => 2000],
|
|
'location_unknown' => ['step' => 5, 'pt' => 'Não conheço a localização exacta', 'en' => 'I do not know the exact location', 'type' => 'select', 'max' => 3, 'options' => ['yes' => 'Sim', 'no' => 'Não'], 'when' => ['geography_scope' => ['specific', 'multiple']]],
|
|
'disclosure_precision' => ['step' => 5, 'pt' => 'Precisão de divulgação', 'en' => 'Disclosure precision', 'type' => 'select', 'max' => 30, 'options' => ['exact' => 'Localização exacta (sujeita a aprovação)', 'approximate' => 'Localização aproximada', 'administrative' => 'Apenas território administrativo'], 'when' => ['geography_scope' => ['specific', 'multiple']]],
|
|
'asset_notes' => ['step' => 6, 'pt' => 'Resumo dos activos e direitos declarados', 'en' => 'Declared assets and rights summary', 'type' => 'textarea', 'max' => 2000],
|
|
'source_reference' => ['step' => 7, 'pt' => 'Referência da fonte', 'en' => 'Source reference', 'type' => 'textarea', 'max' => 2000],
|
|
'source_provider' => ['step' => 7, 'pt' => 'Quem forneceu os dados?', 'en' => 'Who supplied the information?', 'type' => 'text', 'max' => 500],
|
|
'source_date' => ['step' => 7, 'pt' => 'Data da fonte/informação', 'en' => 'Source/information date', 'type' => 'date', 'max' => 10],
|
|
'official_source_entity' => ['step' => 7, 'pt' => 'Entidade da fonte oficial (se existir)', 'en' => 'Official source authority (if any)', 'type' => 'text', 'max' => 500],
|
|
'official_source_document' => ['step' => 7, 'pt' => 'Documento oficial associado', 'en' => 'Related official document', 'type' => 'text', 'max' => 500],
|
|
'official_source_url' => ['step' => 7, 'pt' => 'Ligação da fonte oficial', 'en' => 'Official source link', 'type' => 'url', 'max' => 2000],
|
|
'official_source_period' => ['step' => 7, 'pt' => 'Período da fonte oficial', 'en' => 'Official source period', 'type' => 'text', 'max' => 200],
|
|
'document_reference' => ['step' => 7, 'pt' => 'Referências documentais (não são ficheiros validados)', 'en' => 'Document references (not validated files)', 'type' => 'textarea', 'max' => 2000],
|
|
'confirm_information' => ['step' => 8, 'pt' => 'Confirmo que a informação corresponde ao que conheço e identifiquei estimativas ou pendências.', 'en' => 'I confirm the information reflects what I know and identify estimates or pending items.', 'type' => 'checkbox', 'max' => 3, 'options' => ['yes' => 'yes']],
|
|
'confirm_disclosure_authority' => ['step' => 8, 'pt' => 'Confirmo que tenho autorização para apresentar a oportunidade e divulgar os conteúdos seleccionados.', 'en' => 'I confirm I am authorized to submit the opportunity and disclose selected content.', 'type' => 'checkbox', 'max' => 3, 'options' => ['yes' => 'yes']],
|
|
'confirm_no_publication_guarantee' => ['step' => 8, 'pt' => 'Compreendo que a submissão não constitui publicação, aprovação oficial ou garantia de investimento.', 'en' => 'I understand submission is not publication, official approval or an investment guarantee.', 'type' => 'checkbox', 'max' => 3, 'options' => ['yes' => 'yes']],
|
|
];
|
|
}
|
|
|
|
/** @return array<string, list<string|\Closure|In>> */
|
|
public static function rules(): array
|
|
{
|
|
$rules = [];
|
|
foreach (self::fields() as $key => $field) {
|
|
$rules['payload.'.$key] = ['nullable', 'string', 'max:'.$field['max']];
|
|
if (isset($field['options'])) {
|
|
$rules['payload.'.$key][] = 'in:'.implode(',', array_keys($field['options']));
|
|
}
|
|
}
|
|
$rules['payload.contact_email'][] = 'nullable';
|
|
$rules['payload.contact_email'][] = 'email';
|
|
$rules['payload.country'][] = app(CountryCatalogue::class)->legacyCountryRule();
|
|
$rules['payload.declared_origin_country_code'][] = app(CountryCatalogue::class)->rule();
|
|
$rules['payload.currency'][] = 'regex:/^[A-Z]{3}$/';
|
|
$rules['payload.asking_value'][] = 'regex:/^\d{1,15}(\.\d{1,4})?$/';
|
|
$rules['payload.amount_secured'][] = 'regex:/^\d{1,15}(\.\d{1,4})?$/';
|
|
$rules['payload.investment_total'][] = 'regex:/^\d{1,15}(\.\d{1,4})?$/';
|
|
$rules['payload.amount_sought'][] = 'regex:/^\d{1,15}(\.\d{1,4})?$/';
|
|
$rules['payload.source_date'][] = 'date_format:Y-m-d';
|
|
$rules['payload.official_source_url'][] = 'url';
|
|
// The field limit is a text length, not an 8% ceiling.
|
|
$rules['payload.equity_percentage'] = ['nullable', 'numeric', 'between:0,100',
|
|
function ($attribute, $value, $fail) {
|
|
if (strlen((string) $value) > 8) $fail('Indique a percentagem com até 8 caracteres.');
|
|
}];
|
|
foreach (['valid_until', 'mandate_valid_until'] as $date) {
|
|
$rules['payload.'.$date][] = 'date_format:Y-m-d';
|
|
}
|
|
$rules['payload.wizard_version'] = ['nullable', 'in:1,2'];
|
|
$rules['payload.wizard_step'] = ['nullable', 'integer', 'between:1,8'];
|
|
$rules['payload.locations_json'][] = 'json';
|
|
$rules['payload.title'] = ['required', 'string', 'max:200'];
|
|
|
|
return $rules;
|
|
}
|
|
|
|
/** @param array<string,mixed> $payload */
|
|
public static function validateSubmission(array $payload, ?int $sectorId, string $modality): void
|
|
{
|
|
$expectedType = ExistingBusinessProfileCategory::formType($modality);
|
|
if ($expectedType === null) {
|
|
throw ValidationException::withMessages(['opportunity_type' => 'Esta modalidade pode ser guardada em preparação. O formulário de submissão correspondente ainda não está definido; a modalidade não será alterada automaticamente.']);
|
|
}
|
|
if (($payload['opportunity_type'] ?? null) !== $expectedType) {
|
|
throw ValidationException::withMessages(['opportunity_type' => 'O tipo de oportunidade não corresponde à modalidade guardada. Recupere o formulário; a modalidade original foi preservada.']);
|
|
}
|
|
|
|
$rules = [];
|
|
foreach (['proponent_type', 'proponent_name', 'contact_name', 'country', 'acting_capacity', 'title', 'opportunity_type', 'description', 'project_stage', 'availability', 'geography_scope', 'source_reference', 'source_provider', 'source_date'] as $field) {
|
|
$rules[$field] = ['required'];
|
|
}
|
|
$rules['sector_id'] = ['required'];
|
|
$rules['country'] = ['required', 'string', app(CountryCatalogue::class)->rule()];
|
|
$rules['declared_origin_country_code'] = ['nullable', 'string', app(CountryCatalogue::class)->rule()];
|
|
$rules['contact_email'] = ['required_without:contact_phone', 'nullable', 'email'];
|
|
$rules['contact_phone'] = ['required_without:contact_email', 'nullable', 'string', 'regex:/^[+0-9() .-]{7,40}$/'];
|
|
$rules['legitimacy_declaration'] = ['required', 'in:yes'];
|
|
foreach (['confirm_information', 'confirm_disclosure_authority', 'confirm_no_publication_guarantee'] as $confirmation) {
|
|
$rules[$confirmation] = ['required', 'in:yes'];
|
|
}
|
|
$rules['mandate_reference'] = ['required_if:acting_capacity,representative'];
|
|
$rules['declared_holder'] = ['required_if:acting_capacity,representative'];
|
|
$rules['disclosure_scope'] = ['required_if:acting_capacity,representative'];
|
|
$rules['mandate_valid_until'] = ['nullable', 'date_format:Y-m-d', 'after_or_equal:today'];
|
|
$rules['mandate_document_reference'] = ['required_if:acting_capacity,representative'];
|
|
$rules['project_stage_other'] = ['required_if:project_stage,other'];
|
|
$rules['available_from'] = ['required_if:availability,from_date', 'nullable', 'date_format:Y-m-d', 'after_or_equal:today'];
|
|
$rules['value_status'] = ['required', 'in:defined,indicative,consultation,unknown'];
|
|
$rules['province_id'] = ['required_if:geography_scope,specific', 'nullable', 'uuid'];
|
|
foreach (['district_id', 'admin_post_id', 'locality_id'] as $locationField) {
|
|
$rules[$locationField] = ['nullable', 'uuid'];
|
|
}
|
|
$rules['locations_json'] = ['required_if:geography_scope,multiple', 'nullable', 'json', 'max:16000'];
|
|
$rules['asking_value'] = ['required_if:value_status,defined,indicative', 'nullable', 'numeric', 'min:0'];
|
|
$rules['currency'] = ['required_if:value_status,defined,indicative', 'nullable', 'regex:/^[A-Z]{3}$/'];
|
|
$rules['value_meaning'] = ['required_if:value_status,defined,indicative'];
|
|
$rules['amount_secured'] = ['nullable', 'numeric', 'min:0'];
|
|
$rules['investment_total'] = ['required_if:opportunity_type,investment_project', 'nullable', 'numeric', 'gt:0'];
|
|
$rules['amount_sought'] = ['nullable', 'numeric', 'min:0'];
|
|
$rules['equity_percentage'] = ['nullable', 'numeric', 'between:0,100'];
|
|
$rules['asset_deal_kind'] = ['required_if:opportunity_type,company_sale,asset_sale'];
|
|
$rules['equity_percentage'] = ['required_if:asset_deal_kind,participation', 'nullable', 'numeric', 'between:0,100'];
|
|
$rules['partnership_contribution'] = ['required_if:opportunity_type,business_partnership'];
|
|
$rules['amount_sought'] = ['required_if:opportunity_type,financing_request', 'nullable', 'numeric', 'min:0'];
|
|
$rules['asset_modality'] = ['required_if:opportunity_type,asset_sale'];
|
|
$rules['concession_entity'] = ['required_if:opportunity_type,concession_opportunity'];
|
|
$rules['concession_reference'] = ['required_if:opportunity_type,concession_opportunity'];
|
|
$rules['supply_description'] = ['required_if:opportunity_type,supply_opportunity'];
|
|
Validator::make([...$payload, 'sector_id' => $sectorId, 'modality' => $modality], $rules)->validate();
|
|
|
|
$geography = app(InvestmentGeography::class);
|
|
if (($payload['geography_scope'] ?? null) === 'specific') {
|
|
$geography->selection(array_intersect_key($payload, array_flip(InvestmentGeography::FIELDS)));
|
|
} elseif (($payload['geography_scope'] ?? null) === 'multiple') {
|
|
$locations = json_decode($payload['locations_json'], true, 512, JSON_THROW_ON_ERROR);
|
|
Validator::make(['locations' => $locations], [
|
|
'locations' => ['required', 'array', 'min:2', 'max:20'],
|
|
'locations.*' => ['required', 'array:province_id,district_id,admin_post_id,locality_id'],
|
|
'locations.*.province_id' => ['required', 'uuid'],
|
|
'locations.*.district_id' => ['nullable', 'uuid'],
|
|
'locations.*.admin_post_id' => ['nullable', 'uuid'],
|
|
'locations.*.locality_id' => ['nullable', 'uuid'],
|
|
])->validate();
|
|
|
|
$paths = [];
|
|
foreach ($locations as $index => $location) {
|
|
$path = array_map(fn ($node) => (string) $node->id, $geography->selection($location));
|
|
foreach ($paths as $existingPath) {
|
|
$sharedPrefixLength = 0;
|
|
while (isset($path[$sharedPrefixLength], $existingPath[$sharedPrefixLength])
|
|
&& $path[$sharedPrefixLength] === $existingPath[$sharedPrefixLength]) {
|
|
$sharedPrefixLength++;
|
|
}
|
|
if ($sharedPrefixLength === min(count($path), count($existingPath))) {
|
|
throw ValidationException::withMessages([
|
|
'locations_json' => 'As localizações devem ser distintas e não podem repetir ou conter o mesmo percurso administrativo.',
|
|
]);
|
|
}
|
|
}
|
|
$paths[] = $path;
|
|
}
|
|
}
|
|
}
|
|
}
|