$input * @return array */ public function validate(array $input): array { $bytes = file_get_contents(resource_path('data/investmoz-opportunity-modular.schema.json')); if ($bytes === false) { throw new \RuntimeException('Opportunity schema unavailable.'); } $schema = json_decode($bytes, true, 512, JSON_THROW_ON_ERROR); $property = $schema['$defs']['realEstatePatch']['properties']; return Validator::make($input, [ 'business' => 'sometimes|nullable|array:sector_id,secondary_sector_id,revenue,cash_flow,currency,reason,availability', 'business.sector_id' => ['nullable', 'integer', Rule::exists('sectors', 'id')->where('is_active', true)], 'business.secondary_sector_id' => ['nullable', 'integer', Rule::exists('sectors', 'id')->where('is_active', true)], 'business.revenue' => ['nullable', 'string', 'regex:/^\d{1,16}(\.\d{1,2})?$/'], 'business.cash_flow' => ['nullable', 'string', 'regex:/^-?\d{1,16}(\.\d{1,2})?$/'], 'business.currency' => 'nullable|required_with:business.revenue,business.cash_flow|in:MZN,USD', 'business.reason' => 'nullable|string|max:1000', 'business.availability' => 'nullable|in:available,sold', 'property' => 'sometimes|nullable|array:transaction_type,property_type,land_area,built_area,area_unit,bedrooms,bathrooms,amenities,availability', 'property.transaction_type' => 'nullable|in:sale,lease', 'property.property_type' => ['nullable', Rule::in($property['property_type']['enum'])], 'property.land_area' => 'nullable|numeric|gt:0', 'property.built_area' => 'nullable|numeric|gt:0', 'property.area_unit' => ['nullable', Rule::in(OpportunityResources::AREA_UNITS)], 'property.bedrooms' => 'nullable|integer|min:0|max:10000', 'property.bathrooms' => 'nullable|integer|min:0|max:10000', 'property.amenities' => 'nullable|array|max:100', 'property.amenities.*' => 'string|max:250', 'property.availability' => 'nullable|in:available,sold,leased', ])->validate(); } /** @param array $offer */ public function assertCategory(array $offer, string $category): void { Validator::make($offer, [ 'business' => $category === 'business_sale' ? 'nullable|array' : 'prohibited', 'property' => $category === 'real_estate' ? 'nullable|array' : 'prohibited', ])->validate(); } /** Snapshot all active linked originals and metadata in the existing editorial version. * @return array> */ public function resources(Opportunity $record): array { $result = ['documents' => [], 'images' => [], 'locations' => []]; foreach (array_keys($result) as $kind) { foreach (app(OpportunityResources::class)->listing($record->id, $kind) as $row) { if ($row->deleted_at !== null) { continue; } if ($kind !== 'locations') { $file = $row->file; $doc = DB::table('documents')->where('id', $row->document_id)->first(); if (! $doc || $doc->deleted_at !== null || $doc->status !== 'active' || $doc->malware_scan_status !== 'clean' || ! hash_equals(trim($doc->sha256), trim((string) $doc->malware_scanned_sha256))) { throw ValidationException::withMessages(['attachments' => 'Aguarde uma análise de segurança válida dos anexos.']); } $result[$kind][$row->id] = ['version' => $row->version, 'document_id' => $row->document_id, 'payload' => $row->payload, 'file' => $file]; } else { $result[$kind][$row->id] = ['version' => $row->version, 'payload' => $row->payload]; } } ksort($result[$kind]); } if (count($result['images']) > 6) { throw ValidationException::withMessages(['images' => 'A oferta admite no máximo seis fotografias.']); } return $result; } }