Skip to main content

Overview

Core Projects manages three types of properties:
  • Apartments (apartamentos): Residential units with configurable types
  • Locals: Commercial units
  • Parking (parqueaderos): Vehicle and motorcycle parking spaces
All properties are organized within projects and towers, with real-time availability tracking and dynamic pricing.

Apartments

Apartments are residential units with rich configuration options.

Apartment Model

class Apartamento extends Model
{
    protected $table = 'apartamentos';
    protected $primaryKey = 'id_apartamento';
    
    protected $fillable = [
        'numero',                  // Unit number
        'id_tipo_apartamento',     // Apartment type
        'id_torre',                // Tower
        'id_piso_torre',           // Floor
        'id_estado_inmueble',      // Property state
        'valor_total',             // Base value
        'prima_altura',            // Height premium
        'valor_politica',          // Pricing policy adjustment
        'valor_final',             // Final calculated price
        'documento',               // Owner document (if sold)
    ];
    
    protected $casts = [
        'valor_total' => 'decimal:2',
        'valor_politica' => 'decimal:2',
        'valor_final' => 'decimal:2',
    ];
}

Apartment Types

Each apartment belongs to a type that defines its characteristics:
class TipoApartamento extends Model
{
    protected $fillable = [
        'id_proyecto',      // Project this type belongs to
        'nombre',           // Type name (e.g., "2BR-2BA")
        'descripcion',      // Description
        'area',             // Square meters
        'habitaciones',     // Number of bedrooms
        'banos',            // Number of bathrooms
        'valor_estimado',   // Base estimated value
        'imagen',           // Floor plan image
    ];
    
    public function apartamentos()
    {
        return $this->hasMany(Apartamento::class, 'id_tipo_apartamento');
    }
}

Type Attributes

  • Name and description
  • Square meters (area)
  • Number of bedrooms
  • Number of bathrooms
  • Base price estimate

Floor Plans

Upload floor plan images to show clients the layout and distribution of each apartment type.

Creating Apartments

Apartments can be created individually or in bulk:
public function store(Request $request)
{
    $isBulk = $request->has('apartamentos');
    
    $validated = $request->validate(
        $isBulk ? [
            'id_torre' => ['required', 'exists:torres,id_torre'],
            'apartamentos' => ['required', 'array', 'min:1'],
            'apartamentos.*.numero' => ['required', 'string', 'max:20'],
            'apartamentos.*.id_tipo_apartamento' => ['required', 'exists:tipos_apartamento'],
            'apartamentos.*.id_piso_torre' => ['required', 'exists:pisos_torre'],
            'apartamentos.*.id_estado_inmueble' => ['required', 'exists:estados_inmueble'],
        ] : [
            // Single apartment validation
        ]
    );
    
    // Calculate pricing for each apartment
    foreach ($rows as $r) {
        $tipo = TipoApartamento::find($r['id_tipo_apartamento']);
        $valorBase = (float)($tipo->valor_estimado ?? 0);
        
        // Height premium
        $primaAltura = $this->calcularPrimaAltura($r['id_piso_torre'], $validated['id_torre']);
        
        // Pricing policy adjustment
        $politicaCalc = $this->calcularValorConPolitica($valorBase, $proyecto->id_proyecto);
        
        Apartamento::create([
            'numero' => $r['numero'],
            'id_tipo_apartamento' => $r['id_tipo_apartamento'],
            'id_torre' => $validated['id_torre'],
            'id_piso_torre' => $r['id_piso_torre'],
            'id_estado_inmueble' => $r['id_estado_inmueble'],
            'prima_altura' => $primaAltura,
            'valor_total' => $valorBase,
            'valor_politica' => $politicaCalc['valor_politica'],
            'valor_final' => $politicaCalc['valor_final'] + $primaAltura,
        ]);
    }
}

Bulk Creation Benefits

Efficient Tower Setup

Create multiple apartments at once when setting up a tower:
  • Select tower and floor
  • Upload CSV or enter multiple units
  • System validates uniqueness
  • Prices calculated automatically
  • All units created in single transaction

Apartment Pricing

Apartment prices are calculated from multiple components:
// 1. Base price from type
$valorBase = $tipoApartamento->valor_estimado;

// 2. Height premium (based on floor)
$primaAltura = calcularPrimaAltura($piso, $torre);

// 3. Pricing policy adjustment (based on sales progress)
$valorPolitica = calcularValorConPolitica($valorBase, $proyecto);

// 4. Final price
$valorFinal = $valorBase + $primaAltura + $valorPolitica;
Prices are recalculated automatically when:
  • New sales are registered
  • Pricing policies change
  • Height premium configuration updates

Locals (Commercial Units)

Locals are commercial units with simpler configuration:
class Local extends Model
{
    protected $table = 'locales';
    protected $primaryKey = 'id_local';
    
    protected $fillable = [
        'numero',              // Unit number
        'id_torre',            // Tower
        'id_piso_torre',       // Floor
        'id_estado_inmueble',  // Property state
        'area',                // Square meters
        'valor_total',         // Total value
        'descripcion',         // Description
    ];
    
    protected $casts = [
        'valor_total' => 'decimal:2',
    ];
}

Locals vs Apartments

Locals

  • Simpler configuration
  • No type classification
  • Fixed pricing (no height premium)
  • Cannot have additional parking
  • Direct area specification

Apartments

  • Type-based configuration
  • Height premiums apply
  • Dynamic pricing policies
  • Can include parking
  • Bedrooms/bathrooms tracking

Parking (Parqueaderos)

Parking spaces come in two types and can be assigned or additional:
class Parqueadero extends Model
{
    protected $table = 'parqueaderos';
    protected $primaryKey = 'id_parqueadero';
    
    protected $fillable = [
        'numero',          // Parking number
        'tipo',            // 'Vehiculo' or 'Moto'
        'id_proyecto',     // Project
        'id_torre',        // Tower (optional)
        'id_apartamento',  // Assigned apartment (null if additional)
        'precio',          // Price (if additional)
    ];
    
    protected $casts = [
        'precio' => 'decimal:2',
    ];
}

Parking Types

Assigned Parking

Included with Unit
  • id_apartamento is set
  • Automatically comes with apartment
  • Price included in unit price
  • Cannot be sold separately

Additional Parking

Available for Purchase
  • id_apartamento is NULL
  • Can be added to sales
  • Has independent price
  • Tracked in ventas.id_parqueadero

Parking Management

// Get available additional parking
$parqueaderos = Parqueadero::query()
    ->whereNull('id_apartamento')  // Not assigned to a unit
    ->whereNotIn('id_parqueadero', function ($q) {
        $q->select('id_parqueadero')
            ->from('ventas')
            ->whereNotNull('id_parqueadero');
    })
    ->orderBy('numero')
    ->get();

// Assign parking to apartment
Parqueadero::where('id_parqueadero', $idParqueadero)
    ->where(function ($q) use ($idApartamento) {
        $q->whereNull('id_apartamento')
            ->orWhere('id_apartamento', $idApartamento);
    })
    ->update(['id_apartamento' => $idApartamento]);

// Release parking
Parqueadero::where('id_parqueadero', $idParqueadero)
    ->where('id_apartamento', $idApartamento)
    ->update(['id_apartamento' => null]);

Property States

All properties track their availability status:
class EstadoInmueble extends Model
{
    protected $table = 'estados_inmueble';
    
    // Common states:
    // - Disponible: Available for sale
    // - Separado: Reserved with separation
    // - Vendido: Sold
    // - Bloqueado: Administratively blocked
    // - Congelado: Frozen (temporarily unavailable)
}

Disponible

Available for sale. Appears in quotation and sales modules.

Separado

Reserved with a separation. Not available for other clients until separation expires or is cancelled.

Vendido

Sold. No longer available. Associated with a completed sale.

Bloqueado

Administratively blocked. Not available for sale until unblocked.

State Transitions

// When creating a separation
$inmueble->update([
    'id_estado_inmueble' => EstadoInmueble::where('nombre', 'Separado')->value('id_estado_inmueble')
]);

// When converting to sale
$inmueble->update([
    'id_estado_inmueble' => EstadoInmueble::where('nombre', 'Vendido')->value('id_estado_inmueble')
]);

// When cancelling separation
$inmueble->update([
    'id_estado_inmueble' => EstadoInmueble::where('nombre', 'Disponible')->value('id_estado_inmueble')
]);

Property Listing

The system provides filtered views of properties:
public function index(Request $request)
{
    $apartamentos = Apartamento::with([
        'tipoApartamento', 
        'torre.proyecto', 
        'pisoTorre', 
        'estadoInmueble'
    ])
    ->when($request->proyecto_id, fn($q) => 
        $q->whereHas('torre', fn($qq) => $qq->where('id_proyecto', $request->proyecto_id))
    )
    ->when($request->estado, fn($q) => 
        $q->whereHas('estadoInmueble', fn($qq) => $qq->where('nombre', $request->estado))
    )
    ->orderBy('numero', 'asc')
    ->get();
}

Filter Options

  • By project
  • By tower
  • By state (available, sold, etc.)
  • By floor
  • By apartment type
  • By price range

Property Details View

Detailed property information includes:

Basic Info

  • Unit number
  • Type (for apartments)
  • Area (square meters)
  • Floor and tower
  • Project name

Pricing

  • Base price
  • Height premium
  • Policy adjustment
  • Final price
  • Price history

Features

  • Bedrooms/bathrooms (apartments)
  • Included parking spaces
  • Floor plan image
  • Amenities access

Status

  • Current state
  • Sale/separation info
  • Client (if sold)
  • Sale date

Inventory Reports

Property inventory is tracked for reporting:
public function inventarioPorProyecto(array $filtros): array
{
    $proyectos = Proyecto::with([
        'torres.apartamentos.estadoInmueble',
        'torres.apartamentos.tipoApartamento',
        'torres.apartamentos.ventas',
        'torres.locales.estadoInmueble',
        'torres.locales.ventas',
    ])->get();
    
    foreach ($proyectos as $proyecto) {
        $items = [];
        
        foreach ($proyecto->torres as $torre) {
            foreach ($torre->apartamentos as $apto) {
                $items[] = [
                    'tipo' => 'Apartamento',
                    'etiqueta' => 'Apto ' . $apto->numero,
                    'precio_base' => (float)($apto->tipoApartamento->valor_estimado ?? 0),
                    'precio_vigente' => (float)($apto->valor_final ?? 0),
                    'estado' => $apto->estadoInmueble->nombre,
                ];
            }
        }
    }
}

Validation Rules

Apartment Validation

[
    'numero' => ['required', 'string', 'max:20'],
    'id_tipo_apartamento' => ['required', 'exists:tipos_apartamento,id_tipo_apartamento'],
    'id_torre' => ['required', 'exists:torres,id_torre'],
    'id_piso_torre' => ['required', 'exists:pisos_torre,id_piso_torre'],
    'id_estado_inmueble' => ['required', 'exists:estados_inmueble,id_estado_inmueble'],
]
Unique constraint: Apartment numbers must be unique within each tower. You can have apartment “101” in Tower A and Tower B, but not two “101” in Tower A.

Best Practices

Establish a consistent numbering scheme for units (e.g., floor + unit number like “501” for floor 5, unit 1).
Define all apartment types with floor plans and prices before creating the actual unit inventory.
When setting up a new tower, use bulk apartment creation to save time and ensure consistency.
Clearly distinguish between included and additional parking to avoid sales conflicts.
Never delete apartments or locals that have been sold. This would break historical records.

Build docs developers (and LLMs) love