Overview
Projects (proyectos) are the core organizational unit in Core Projects. Each project represents a real estate development with its own towers, properties, pricing policies, and sales targets. The system supports complex hierarchical structures from project level down to individual units.
Project Model
The Proyecto model encapsulates all project-related data and relationships:
class Proyecto extends Model
{
protected $table = 'proyectos' ;
protected $primaryKey = 'id_proyecto' ;
protected $fillable = [
'nombre' ,
'descripcion' ,
'fecha_inicio' ,
'fecha_finalizacion' ,
'presupuesto_inicial' ,
'presupuesto_final' ,
'metros_construidos' ,
'cantidad_locales' ,
'cantidad_apartamentos' ,
'cantidad_parqueaderos_vehiculo' ,
'cantidad_parqueaderos_moto' ,
'estrato' ,
'numero_pisos' ,
'numero_torres' ,
'id_estado' ,
'id_ubicacion' ,
'prima_altura_base' ,
'prima_altura_incremento' ,
'prima_altura_activa' ,
'porcentaje_cuota_inicial_min' ,
'plazo_cuota_inicial_meses' ,
'valor_min_separacion' ,
'plazo_max_separacion_dias' ,
'activo' ,
];
}
Projects use custom primary keys (id_proyecto) rather than Laravel’s default id convention for domain-specific clarity.
Project Hierarchy
Core Projects implements a four-level hierarchy for organizing properties:
Level 1: Project
Top-level container for the entire development:
$proyecto = Proyecto :: create ([
'nombre' => 'Edificio Panorama' ,
'descripcion' => 'Desarrollo residencial de lujo' ,
'fecha_inicio' => '2025-01-01' ,
'estrato' => 5 ,
'numero_torres' => 2 ,
'numero_pisos' => 15 ,
'id_ubicacion' => 1 ,
'activo' => true
]);
Level 2: Towers
Projects can contain multiple towers:
class Torre extends Model
{
protected $table = 'torres' ;
protected $primaryKey = 'id_torre' ;
protected $fillable = [
'nombre_torre' ,
'numero_pisos' ,
'nivel_inicio_prima' , // Floor where height premium starts
'id_proyecto' ,
'id_estado'
];
public function proyecto ()
{
return $this -> belongsTo ( Proyecto :: class , 'id_proyecto' , 'id_proyecto' );
}
public function pisos ()
{
return $this -> hasMany ( PisoTorre :: class , 'id_torre' , 'id_torre' );
}
}
Example:
$torre = Torre :: create ([
'nombre_torre' => 'Torre Norte' ,
'numero_pisos' => 15 ,
'nivel_inicio_prima' => 8 , // Floors 8+ have height premium
'id_proyecto' => $proyecto -> id_proyecto
]);
Level 3: Floors
Each tower contains multiple floors:
class PisoTorre extends Model
{
protected $table = 'pisos_torre' ;
protected $primaryKey = 'id_piso_torre' ;
protected $fillable = [
'numero_piso' ,
'id_torre'
];
public function torre ()
{
return $this -> belongsTo ( Torre :: class , 'id_torre' , 'id_torre' );
}
public function apartamentos ()
{
return $this -> hasMany ( Apartamento :: class , 'id_piso_torre' , 'id_piso_torre' );
}
public function locales ()
{
return $this -> hasMany ( Local :: class , 'id_piso_torre' , 'id_piso_torre' );
}
}
Level 4: Units (Apartments & Locals)
Floors contain the actual saleable properties. See Properties for detailed information.
Project Relationships
Core Relationships
public function torres ()
{
return $this -> hasMany ( Torre :: class , 'id_proyecto' , 'id_proyecto' );
}
public function zonasSociales ()
{
return $this -> hasMany ( ZonaSocial :: class , 'id_proyecto' , 'id_proyecto' );
}
public function tiposApartamento ()
{
return $this -> hasMany ( TipoApartamento :: class , 'id_proyecto' , 'id_proyecto' );
}
public function ubicacion ()
{
return $this -> belongsTo ( Ubicacion :: class , 'id_ubicacion' , 'id_ubicacion' );
}
public function estado_proyecto ()
{
return $this -> belongsTo ( Estado :: class , 'id_estado' , 'id_estado' );
}
Policy Relationships
public function politicasPrecio ()
{
return $this -> hasMany ( PoliticaPrecioProyecto :: class , 'id_proyecto' , 'id_proyecto' );
}
public function politicasComisiones ()
{
return $this -> hasMany ( PoliticaComision :: class , 'id_proyecto' , 'id_proyecto' );
}
public function metasComerciales ()
{
return $this -> hasMany ( ProyectoMetaComercial :: class , 'id_proyecto' , 'id_proyecto' );
}
Active Pricing Policy
Get the currently active pricing policy:
public function politicaVigente ()
{
return $this -> hasOne ( PoliticaPrecioProyecto :: class , 'id_proyecto' , 'id_proyecto' )
-> where ( 'estado' , true )
-> where ( function ( $query ) {
$query -> whereNull ( 'aplica_desde' )
-> orWhere ( 'aplica_desde' , '<=' , now ());
})
-> latest ( 'aplica_desde' );
}
Project States
Projects transition through various states during their lifecycle:
$estados = Estado :: all ();
// Common states: Planificación, En Construcción, En Venta, Finalizado
Planificación
En Construcción
En Venta
Finalizado
Initial planning phase. Property inventory can be modified freely.
Active construction. Units may be available for pre-sale.
Primary sales phase. All units available for purchase.
Construction complete. Limited to final unit sales.
Pricing Policies
Projects support dynamic pricing through escalation policies:
app/Models/PoliticaPrecioProyecto.php
class PoliticaPrecioProyecto extends Model
{
protected $table = 'politicas_precio_proyecto' ;
protected $primaryKey = 'id_politica_precio' ;
protected $fillable = [
'id_proyecto' ,
'ventas_por_escalon' , // Sales required to trigger increase
'porcentaje_aumento' , // Percentage increase
'aplica_desde' , // Optional start date
'estado' // Active/inactive
];
public function proyecto ()
{
return $this -> belongsTo ( Proyecto :: class , 'id_proyecto' , 'id_proyecto' );
}
}
How Pricing Escalation Works
Define policy blocks - Set sales thresholds and price increases
Track sales - System counts completed sales and separations
Automatic recalculation - Prices increase when thresholds are met
Example:
// Policy 1: After 10 sales, increase 3%
PoliticaPrecioProyecto :: create ([
'id_proyecto' => 1 ,
'ventas_por_escalon' => 10 ,
'porcentaje_aumento' => 3.0 ,
'estado' => true
]);
// Policy 2: After next 10 sales (20 total), increase another 3%
PoliticaPrecioProyecto :: create ([
'id_proyecto' => 1 ,
'ventas_por_escalon' => 10 ,
'porcentaje_aumento' => 3.0 ,
'estado' => true
]);
Pricing Service
The ProyectoPricingService automatically recalculates prices:
app/Services/ProyectoPricingService.php
class ProyectoPricingService
{
public function recalcPreciosProyecto ( int $idProyecto ) : void
{
DB :: transaction ( function () use ( $idProyecto ) {
$proyecto = Proyecto :: with ( 'politicasPrecio' )
-> lockForUpdate ()
-> findOrFail ( $idProyecto );
// Count active sales
$ventasActivas = Venta :: where ( 'id_proyecto' , $idProyecto )
-> whereIn ( 'tipo_operacion' , [ 'venta' , 'separacion' ])
-> count ();
// Determine which policy blocks should be applied
$politicas = $proyecto -> politicasPrecio ()
-> orderBy ( 'id_politica_precio' )
-> get ();
// Apply new price increases to available properties
foreach ( $bloquesNuevos as $bloque ) {
$incremento = $bloque -> porcentaje_aumento / 100 ;
Apartamento :: whereDisponible ( $proyecto -> id_proyecto )
-> each ( function ( $apto ) use ( $incremento ) {
$apto -> update ([
'valor_final' => $apto -> valor_final * ( 1 + $incremento )
]);
});
}
});
}
}
Price increases only apply to available properties. Sold or reserved units maintain their original price.
Height Premium
Projects can apply premiums for higher floors:
$proyecto = Proyecto :: create ([
'nombre' => 'Torre Skyline' ,
'prima_altura_activa' => true ,
'prima_altura_base' => 2000000 , // Base premium amount
'prima_altura_incremento' => 500000 , // Additional per floor
]);
// Formula: prima = prima_altura_base + (floor - nivel_inicio_prima) * prima_altura_incremento
Example Calculation:
For a unit on floor 10, where height premium starts at floor 8:
$prima = 2000000 + ( 10 - 8 ) * 500000 ;
// $prima = 3,000,000
$precioFinal = $valorBase + $prima + $valorPolitica ;
Separation Settings
Projects define separation (reservation) constraints:
$proyecto -> update ([
'valor_min_separacion' => 5000000 , // Minimum separation payment
'plazo_max_separacion_dias' => 30 , // Maximum reservation period
]);
These settings are enforced during separation creation:
app/Services/VentaService.php
protected function validarSeparacion ( array $data , Proyecto $proyecto ) : void
{
$valorSep = ( float )( $data [ 'valor_separacion' ] ?? 0 );
if ( $valorSep < $proyecto -> valor_min_separacion ) {
throw new ValidationException ([
'valor_separacion' => [ 'El valor de separación es menor al mínimo permitido.' ]
]);
}
$maxDias = ( int ) $proyecto -> plazo_max_separacion_dias ;
$fechaMax = now () -> addDays ( $maxDias ) -> toDateString ();
if ( $data [ 'fecha_limite_separacion' ] > $fechaMax ) {
throw new ValidationException ([
'fecha_limite_separacion' => [ 'La fecha excede el máximo permitido.' ]
]);
}
}
Down Payment Settings
Projects configure installment payment terms:
$proyecto -> update ([
'porcentaje_cuota_inicial_min' => 20 , // Minimum 20% down payment
'plazo_cuota_inicial_meses' => 24 , // Up to 24 months to pay
]);
Query Scopes
Convenient query scopes for common filters:
public function scopeActivos ( $query )
{
return $query -> where ( 'activo' , true );
}
Usage:
// Get only active projects
$proyectos = Proyecto :: activos () -> get ();
// Get active projects with sales count
$proyectos = Proyecto :: activos ()
-> withCount ( 'torres' )
-> with ( 'ubicacion' )
-> get ();
Loading Project Data
Efficient Eager Loading
$proyecto = Proyecto :: with ([
'torres' => function ( $query ) {
$query -> with ([
'pisos.apartamentos.estadoInmueble' ,
'pisos.apartamentos.tipoApartamento' ,
]);
},
'ubicacion.ciudad.departamento' ,
'politicaVigente' ,
'metasComerciales'
]) -> findOrFail ( $id );
Property Availability Summary
$proyecto = Proyecto :: find ( $id );
$estadisticas = [
'total_apartamentos' => $proyecto -> torres -> sum ( function ( $torre ) {
return $torre -> apartamentos -> count ();
}),
'disponibles' => $proyecto -> torres -> sum ( function ( $torre ) {
return $torre -> apartamentos -> where ( 'id_estado_inmueble' , $estadoDisponible ) -> count ();
}),
'vendidos' => $proyecto -> torres -> sum ( function ( $torre ) {
return $torre -> apartamentos -> where ( 'id_estado_inmueble' , $estadoVendido ) -> count ();
}),
];
Social Areas
Projects can define common amenities:
app/Models/ZonaSocial.php
class ZonaSocial extends Model
{
protected $table = 'zonas_sociales' ;
protected $primaryKey = 'id_zona_social' ;
protected $fillable = [
'nombre' ,
'descripcion' ,
'id_proyecto'
];
}
Examples:
Swimming pool
Gym
BBQ area
Children’s playground
Co-working space
Event room
Best Practices
Immutable Sales Terms Once a project starts selling, avoid changing pricing rules that affect existing commitments
Version Policies Create new pricing policies instead of modifying active ones
Validate Hierarchy Ensure tower/floor structure is complete before activating sales
Monitor Thresholds Track sales velocity relative to pricing escalation triggers
Properties Understanding apartments, locals, and parking
Sales Workflow How sales interact with projects
Architecture System design and patterns