Files
Investidor-V2/app/Services/PrivateOfferDetails.php
T

84 lines
4.5 KiB
PHP

<?php
namespace App\Services;
use App\Models\Opportunity;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
/** Optional commercial fields confirmed by Tempo for phase four; no title certification. */
final class PrivateOfferDetails
{
/** @param array<string,mixed> $input
* @return array<string,mixed> */
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<string,mixed> $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<string,array<string,mixed>> */
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;
}
}