,when?:array>}> */ 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> */ 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 $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; } } } }