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:
Base Price (valor_total): Area × Price per m²
Height Premium (prima_altura): Additional cost for higher floors
Policy Adjustment (valor_politica): Dynamic pricing based on sales velocity
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
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
Included Parking
Additional Parking
Motorcycle Parking
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
]);
Extra parking spots sold separately: // Available as add-on during sale
Parqueadero :: create ([
'numero' => 'B-205' ,
'tipo' => 'vehiculo' ,
'id_apartamento' => null , // Not pre-assigned
'id_proyecto' => $proyecto -> id_proyecto ,
'precio' => 25000000 // Additional cost
]);
Designated motorcycle parking: Parqueadero :: create ([
'numero' => 'M-015' ,
'tipo' => 'moto' ,
'id_apartamento' => $apartamento -> id_apartamento ,
'precio' => 0
]);
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:
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