Skip to main content

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:
app/Models/Proyecto.php
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:
app/Models/Torre.php
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:
app/Models/PisoTorre.php
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

app/Models/Proyecto.php
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
Initial planning phase. Property inventory can be modified freely.

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

  1. Define policy blocks - Set sales thresholds and price increases
  2. Track sales - System counts completed sales and separations
  3. 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:
app/Models/Proyecto.php
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

Build docs developers (and LLMs) love