Skip to main content

Overview

Properties (inmuebles) are the fundamental saleable assets in Core Projects. The system supports multiple property types with distinct characteristics, pricing models, and workflows. All properties track availability status and maintain relationships to their parent project structure.

Property Types

Core Projects manages four main property types:

Apartamentos

Residential apartment units with type-based configurations

Locales

Commercial spaces with area-based pricing

Parqueaderos

Parking spots for vehicles and motorcycles

Zonas Sociales

Common amenity areas (non-saleable)

Apartments (Apartamentos)

Model Definition

app/Models/Apartamento.php
class Apartamento extends Model
{
    protected $table = 'apartamentos';
    protected $primaryKey = 'id_apartamento';

    protected $fillable = [
        'numero',
        'id_tipo_apartamento',
        'id_torre',
        'id_piso_torre',
        'id_estado_inmueble',
        'valor_total',           // Base price
        'prima_altura',          // Height premium
        'valor_politica',        // Policy adjustment
        'valor_final',           // Final calculated price
        'documento',             // Owner's document (if sold)
    ];

    protected $casts = [
        'valor_total' => 'decimal:2',
        'valor_politica' => 'decimal:2',
        'valor_final' => 'decimal:2',
    ];
}

Relationships

public function tipoApartamento()
{
    return $this->belongsTo(TipoApartamento::class, 'id_tipo_apartamento', 'id_tipo_apartamento');
}

public function torre()
{
    return $this->belongsTo(Torre::class, 'id_torre', 'id_torre');
}

public function pisoTorre()
{
    return $this->belongsTo(PisoTorre::class, 'id_piso_torre', 'id_piso_torre');
}

public function estadoInmueble()
{
    return $this->belongsTo(EstadoInmueble::class, 'id_estado_inmueble', 'id_estado_inmueble');
}

public function parqueaderos()
{
    return $this->hasMany(Parqueadero::class, 'id_apartamento', 'id_apartamento');
}

public function ventas()
{
    return $this->hasMany(Venta::class, 'id_apartamento');
}

public function cliente()
{
    return $this->belongsTo(Cliente::class, 'documento', 'documento');
}

Apartment Types

Apartments are categorized by type with predefined characteristics:
app/Models/TipoApartamento.php
class TipoApartamento extends Model
{
    protected $fillable = [
        'nombre',              // e.g., "2 Habitaciones", "Penthouse"
        'area_total',          // Square meters
        'area_construida',     // Built area
        'habitaciones',        // Number of bedrooms
        'banos',              // Number of bathrooms
        'balcon',             // Has balcony
        'numero_parqueaderos', // Included parking spots
        'valor_base_m2',      // Base price per square meter
        'id_proyecto'
    ];
}
Creating an apartment type:
$tipo = TipoApartamento::create([
    'nombre' => '3 Habitaciones',
    'area_total' => 85.5,
    'area_construida' => 75.0,
    'habitaciones' => 3,
    'banos' => 2,
    'balcon' => true,
    'numero_parqueaderos' => 1,
    'valor_base_m2' => 4500000,
    'id_proyecto' => 1
]);

Price Calculation

Apartment pricing incorporates multiple factors:
public function getValorComercialAttribute()
{
    $base = $this->valor_total ?? 0;      // Base from area * rate
    $prima = $this->prima_altura ?? 0;     // Floor height premium
    $politica = $this->valor_politica ?? 0; // Dynamic pricing adjustment

    return $base + $prima + $politica;
}
Breakdown:
  1. Base Price (valor_total): Area × Price per m²
  2. Height Premium (prima_altura): Additional cost for higher floors
  3. Policy Adjustment (valor_politica): Dynamic pricing based on sales velocity
  4. Final Price (valor_final): Sum of all components
The valor_final field is automatically updated by the pricing engine when policies are applied.

Commercial Spaces (Locales)

Model Definition

app/Models/Local.php
class Local extends Model
{
    protected $table = 'locales';
    protected $primaryKey = 'id_local';

    protected $fillable = [
        'numero',
        'id_estado_inmueble',
        'area_total_local',
        'id_torre',
        'id_piso_torre',
        'valor_m2',              // Price per square meter
        'valor_total'            // Total price
    ];

    protected $casts = [
        'area_total_local' => 'decimal:2',
        'valor_m2' => 'decimal:2',
        'valor_total' => 'decimal:2'
    ];
}

Relationships

public function estadoInmueble()
{
    return $this->belongsTo(EstadoInmueble::class, 'id_estado_inmueble', 'id_estado_inmueble');
}

public function torre()
{
    return $this->belongsTo(Torre::class, 'id_torre', 'id_torre');
}

public function pisoTorre()
{
    return $this->belongsTo(PisoTorre::class, 'id_piso_torre', 'id_piso_torre');
}

public function ventas()
{
    return $this->hasMany(Venta::class, 'id_local');
}

Price Calculation

Commercial spaces use simpler area-based pricing:
public function getValorComercialAttribute()
{
    return $this->valor_total ?? 0;
}

// Typically calculated as:
// valor_total = area_total_local * valor_m2
Commercial spaces (locales) don’t receive height premiums or policy-based price adjustments in the current implementation.

Parking Spots (Parqueaderos)

Model Definition

app/Models/Parqueadero.php
class Parqueadero extends Model
{
    protected $table = 'parqueaderos';
    protected $primaryKey = 'id_parqueadero';

    protected $fillable = [
        'numero',
        'tipo',              // 'vehiculo' or 'moto'
        'id_apartamento',    // If assigned to apartment (included)
        'id_torre',
        'id_proyecto',
        'precio'             // Additional cost if sold separately
    ];

    protected $casts = [
        'precio' => 'decimal:2',
    ];
}

Relationships

public function apartamento()
{
    return $this->belongsTo(Apartamento::class, 'id_apartamento', 'id_apartamento');
}

public function torre()
{
    return $this->belongsTo(Torre::class, 'id_torre', 'id_torre');
}

public function proyecto()
{
    return $this->belongsTo(Proyecto::class, 'id_proyecto', 'id_proyecto');
}

Parking Assignment Types

Parking spots automatically assigned to apartment units:
// Created when apartment type includes parking
Parqueadero::create([
    'numero' => 'A-101',
    'tipo' => 'vehiculo',
    'id_apartamento' => $apartamento->id_apartamento,
    'id_torre' => $apartamento->id_torre,
    'precio' => 0  // No additional cost
]);

Parking in Sales

The VentaService handles parking assignment during sales:
app/Services/VentaService.php
public function asignarParqueaderoAApartamento(?int $idParqueadero, ?int $idApartamento): void
{
    if (!$idParqueadero || !$idApartamento) return;

    Parqueadero::where('id_parqueadero', $idParqueadero)
        ->whereNull('id_apartamento')  // Must be unassigned
        ->update(['id_apartamento' => $idApartamento]);
}

public function liberarParqueaderoDeApartamento(?int $idParqueadero, ?int $idApartamento): void
{
    if (!$idParqueadero) return;

    Parqueadero::where('id_parqueadero', $idParqueadero)
        ->when($idApartamento, fn($q) => $q->where('id_apartamento', $idApartamento))
        ->update(['id_apartamento' => null]);
}

Property States

All properties track their availability through the estados_inmueble table:
app/Models/EstadoInmueble.php
class EstadoInmueble extends Model
{
    protected $table = 'estados_inmueble';
    protected $primaryKey = 'id_estado_inmueble';

    protected $fillable = [
        'nombre',
        'descripcion'
    ];

    public function apartamentos()
    {
        return $this->hasMany(Apartamento::class, 'id_estado_inmueble', 'id_estado_inmueble');
    }

    public function locales()
    {
        return $this->hasMany(Local::class, 'id_estado_inmueble', 'id_estado_inmueble');
    }
}

Standard States

Property is ready for sale. Appears in catalog and can be reserved or sold.
$estadoDisponible = EstadoInmueble::where('nombre', 'Disponible')
    ->value('id_estado_inmueble');
Property has an active separation (reservation). Client has committed a deposit and has a limited time to complete the purchase.
$estadoSeparado = EstadoInmueble::where('nombre', 'Separado')
    ->value('id_estado_inmueble');
Property has been sold. No longer available for new transactions.
$estadoVendido = EstadoInmueble::where('nombre', 'Vendido')
    ->value('id_estado_inmueble');
Property is temporarily unavailable (construction issues, legal holds, etc.).
$estadoBloqueado = EstadoInmueble::where('nombre', 'Bloqueado')
    ->value('id_estado_inmueble');

State Transitions

State Management in Sales

app/Services/VentaService.php
// During sale/separation creation
$estadoDisponibleId = EstadoInmueble::where('nombre', 'Disponible')
    ->value('id_estado_inmueble');

// Verify property is available
if ($inmueble->id_estado_inmueble !== $estadoDisponibleId) {
    throw new RuntimeException('El inmueble ya no está disponible.');
}

// Update to new state
$estadoDestino = $data['tipo_operacion'] === 'venta' ? 'Vendido' : 'Separado';
$idEstadoDestino = EstadoInmueble::where('nombre', $estadoDestino)
    ->value('id_estado_inmueble');

$inmueble->update(['id_estado_inmueble' => $idEstadoDestino]);
Always use pessimistic locking (lockForUpdate()) when checking and updating property states to prevent race conditions in concurrent sales.

Property Availability Queries

Get Available Properties

$estadoDisponible = EstadoInmueble::where('nombre', 'Disponible')->first();

$apartamentosDisponibles = Apartamento::where('id_estado_inmueble', $estadoDisponible->id_estado_inmueble)
    ->with(['tipoApartamento', 'pisoTorre', 'torre.proyecto'])
    ->get();

$localesDisponibles = Local::where('id_estado_inmueble', $estadoDisponible->id_estado_inmueble)
    ->with(['pisoTorre', 'torre.proyecto'])
    ->get();

Get Project Property Summary

$proyecto = Proyecto::with(['torres.apartamentos.estadoInmueble'])
    ->findOrFail($id);

$resumen = [
    'total' => 0,
    'disponibles' => 0,
    'separados' => 0,
    'vendidos' => 0,
];

foreach ($proyecto->torres as $torre) {
    foreach ($torre->apartamentos as $apto) {
        $resumen['total']++;
        
        match($apto->estadoInmueble->nombre) {
            'Disponible' => $resumen['disponibles']++,
            'Separado' => $resumen['separados']++,
            'Vendido' => $resumen['vendidos']++,
            default => null
        };
    }
}

Polymorphic Property Handling

Sales can reference either apartments or commercial spaces:
app/Models/Venta.php
public function inmueble()
{
    if ($this->id_apartamento) {
        return $this->apartamento();
    }
    if ($this->id_local) {
        return $this->local();
    }
    return null;
}
Usage:
$venta = Venta::with(['apartamento', 'local'])->find($id);

$inmueble = $venta->inmueble();
$precio = $inmueble?->getValorComercialAttribute();
$estado = $inmueble?->estadoInmueble;

Property Catalog

The catalog presents available properties to sales team:
app/Http/Controllers/Ventas/CatalogoWebController.php
public function index()
{
    $estadoDisponible = EstadoInmueble::where('nombre', 'Disponible')->first();

    $apartamentos = Apartamento::where('id_estado_inmueble', $estadoDisponible->id_estado_inmueble)
        ->with(['tipoApartamento', 'torre.proyecto', 'pisoTorre'])
        ->get();

    $locales = Local::where('id_estado_inmueble', $estadoDisponible->id_estado_inmueble)
        ->with(['torre.proyecto', 'pisoTorre'])
        ->get();

    return Inertia::render('Ventas/Catalogo/Index', [
        'apartamentos' => $apartamentos,
        'locales' => $locales
    ]);
}

Best Practices

Atomic State Updates

Always use database transactions when changing property states

Pessimistic Locking

Lock properties during state verification to prevent double-booking

Eager Load Relationships

Load related data upfront to avoid N+1 query problems

Validate State Transitions

Verify current state before allowing transitions

Projects

Project hierarchy and structure

Sales Workflow

How properties are sold

Architecture

System design patterns

Build docs developers (and LLMs) love