Documentation Index Fetch the complete documentation index at: https://mintlify.com/daecheverri9801/core-projects/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Core Projects handles two types of property transactions:
Separations (separaciones): Temporary reservations with deadlines
Sales (ventas): Full property purchases with payment plans
The system supports flexible payment structures, automatic price calculation, and parqueadero (parking) management.
Sales Workflow
1. Quotation
Sales begin in the quotation module where advisors can:
Browse Available Units View all available apartments and locals across active projects with real-time pricing
Calculate Payments Generate payment scenarios based on project configuration and available terms
public function index ( Request $request )
{
$empleado = $request -> user () -> load ( 'cargo' );
// Available apartments
$apartamentos = Apartamento :: with ([
'torre.proyecto' ,
'tipoApartamento' ,
'pisoTorre' ,
'estadoInmueble'
])
-> whereHas ( 'estadoInmueble' , fn ( $q ) =>
$q -> whereRaw ( 'LOWER(nombre) = ?' , [ 'disponible' ])
)
-> whereHas ( 'torre.proyecto' , function ( $q ) {
$q -> activos ();
})
-> get ();
// Available locals
$locales = Local :: with ([ 'torre.proyecto' , 'estadoInmueble' ])
-> whereHas ( 'estadoInmueble' , fn ( $q ) =>
$q -> whereRaw ( 'LOWER(nombre) = ?' , [ 'disponible' ])
)
-> whereHas ( 'torre.proyecto' , function ( $q ) {
$q -> activos ();
})
-> get ();
}
2. Creating a Separation
Separations reserve a unit for a limited time:
Separation Parameters
Client : Customer making the reservation
Employee : Sales advisor
Unit : Apartment or local
Separation value : Payment to reserve (typically project’s minimum)
Deadline : Date by which client must convert to full sale
Optional parking : Additional parqueadero can be added
public function store ( Request $request )
{
$validated = $request -> validate ([
'tipo_operacion' => 'required|in:venta,separacion' ,
'id_empleado' => 'required|exists:empleados,id_empleado' ,
'documento_cliente' => 'required|exists:clientes,documento' ,
'fecha_venta' => 'required|date' ,
'id_proyecto' => 'required|exists:proyectos,id_proyecto' ,
'inmueble_tipo' => 'required|in:apartamento,local' ,
'inmueble_id' => 'required|integer' ,
'id_forma_pago' => 'required|exists:formas_pago,id_forma_pago' ,
'id_parqueadero' => 'nullable|exists:parqueaderos,id_parqueadero' ,
'valor_separacion' => 'nullable|numeric|min:0' ,
'fecha_limite_separacion' => 'nullable|date|after_or_equal:today' ,
]);
// For separations, clear down payment fields
if ( $validated [ 'tipo_operacion' ] === 'separacion' ) {
$validated [ 'frecuencia_cuota_inicial_meses' ] = null ;
$validated [ 'plazo_cuota_inicial_meses' ] = null ;
$validated [ 'cuota_inicial' ] = null ;
}
$venta = $this -> ventaService -> crearOperacion ( $validated );
return redirect ()
-> route ( 'ventas.show' , $venta -> id_venta )
-> with ( 'success' , 'Operación registrada exitosamente.' );
}
3. Converting Separation to Sale
Separations can be converted to full sales:
public function convertirStore ( Request $request , $id )
{
$venta = Venta :: lockForUpdate () -> findOrFail ( $id );
if ( ! $venta -> esSeparacion ()) {
return back () -> withErrors ([
'operacion' => 'Esta operación no es una separación.'
]);
}
$validated = $request -> validate ([
'id_forma_pago' => 'required|exists:formas_pago,id_forma_pago' ,
'cuota_inicial' => 'required|numeric|min:0' ,
'plazo_cuota_inicial_meses' => 'required|integer|min:1' ,
'frecuencia_cuota_inicial_meses' => 'required|integer|min:1' ,
'id_parqueadero' => 'nullable|exists:parqueaderos,id_parqueadero' ,
]);
// Update to full sale
$venta -> update ([
'tipo_operacion' => 'venta' ,
'fecha_limite_separacion' => null ,
'estado_operacion' => 'convertida' ,
// ... additional fields
]);
// Generate amortization plan
app ( VentaService :: class ) -> regenerarPlanCuotaInicial ( $venta );
// Recalculate project prices
app ( PriceEngine :: class ) -> recalcularProyectoPorVenta ( $venta );
}
4. Creating Direct Sales
Direct sales skip the separation phase:
Sale Configuration
Down payment : Initial payment amount
Down payment term : Months to complete down payment
Payment frequency : Monthly, bimonthly, quarterly, etc.
Remaining value : Financed through mortgage/other means
Parking : Optional additional parqueadero
Payment Plans
Down Payment Amortization
Core Projects generates automatic amortization schedules for down payments:
class PlanAmortizacionVenta extends Model
{
protected $fillable = [
'id_venta' ,
'tipo_plan' , // 'cuota_inicial' or 'financiacion'
'valor_interes_anual' , // Annual interest rate
'plazo_meses' , // Term in months
'fecha_inicio' , // Start date
'observacion' , // Notes
];
public function cuotas ()
{
return $this -> hasMany ( PlanAmortizacionCuota :: class , 'id_plan' );
}
}
Payment Frequency
Payments can be scheduled at different frequencies:
Monthly (frecuencia = 1): Payment every month
Bimonthly (frecuencia = 2): Payment every 2 months
Quarterly (frecuencia = 3): Payment every 3 months
Custom : Any frequency from 1-12 months
// Number of payments = ceiling(term / frequency)
$numPagos = ( int ) ceil ( $plazo / $frecuencia );
// Amount per payment
$cuotaPorPago = $numPagos > 0 ? floor ( $saldoAmortizar / $numPagos ) : 0 ;
$residuo = $saldoAmortizar - ( $cuotaPorPago * $numPagos );
// Add residue to final payment
if ( $k === $numPagos ) {
$valorCuota += $residuo ;
}
Parking Management
Additional parking spaces (parqueaderos) can be added to apartment sales:
Parking Rules
Only parking spaces NOT already assigned to a unit (id_apartamento IS NULL) can be sold additionally.
Additional parking can only be added to apartment sales, not locals.
Parking price is added to the total sale value automatically.
When a parking space is sold with an apartment, it’s linked in the parqueaderos table.
if ( ! empty ( $idParqueadero )) {
if ( ! $validated [ 'id_apartamento' ]) {
throw new RuntimeException (
'El parqueadero adicional solo aplica para apartamentos.'
);
}
$p = Parqueadero :: where ( 'id_parqueadero' , $idParqueadero )
-> lockForUpdate ()
-> firstOrFail ();
// Validate it's additional (free or assigned to this apartment)
if ( ! empty ( $p -> id_apartamento ) &&
( int ) $p -> id_apartamento !== ( int ) $validated [ 'id_apartamento' ]) {
throw new RuntimeException (
'El parqueadero no es adicional o ya está asignado.'
);
}
$precioParqueadero = ( float )( $p -> precio ?? 0 );
}
$valorTotal = $valorBaseInmueble + $precioParqueadero ;
Price Calculation
Sale prices are calculated dynamically:
Price Components
Base Value : From unit type configuration
Height Premium : Based on floor level
Pricing Policy : Based on sales progress
Parking : If additional parking included
// 1. Base from unit type
$valorBase = ( float )( $tipoApartamento -> valor_estimado ?? 0 );
// 2. Add height premium
$primaAltura = $this -> calcularPrimaAltura ( $idPiso , $idTorre );
$valorConPrima = $valorBase + $primaAltura ;
// 3. Apply pricing policy
$politicaCalc = $this -> calcularValorConPolitica (
$valorConPrima ,
$idProyecto
);
$valorFinal = $politicaCalc [ 'valor_final' ];
// 4. Add parking if applicable
if ( $parqueadero ) {
$valorFinal += $parqueadero -> precio ;
}
Sales States
Sales and separations track state through:
Property State
Disponible : Available for sale
Separado : Reserved with separation
Vendido : Sold
Bloqueado : Administratively blocked
Operation State
vigente : Active/current
convertida : Separation converted to sale
cancelada : Cancelled
vencida : Expired (past deadline)
Cancelling Operations
Cancelling Separations
public function cancelarSeparacion ( $id )
{
$venta = Venta :: findOrFail ( $id );
if ( ! $venta -> esSeparacion ()) {
return back () -> withErrors ([
'operacion' => 'Esta operación no es una separación.'
]);
}
// Release property
$inmueble = $venta -> id_apartamento
? $venta -> apartamento
: $venta -> local ;
$estadoDisponible = EstadoInmueble :: where ( 'nombre' , 'Disponible' ) -> first ();
$inmueble -> update ([ 'id_estado_inmueble' => $estadoDisponible -> id_estado_inmueble ]);
// Release parking if assigned
if ( $venta -> id_parqueadero ) {
app ( VentaService :: class ) -> liberarParqueaderoDeApartamento (
( int ) $venta -> id_parqueadero ,
$venta -> id_apartamento ? ( int ) $venta -> id_apartamento : null
);
}
// Delete separation
$venta -> delete ();
// Recalculate project prices
if ( $proyecto = Proyecto :: find ( $venta -> id_proyecto )) {
app ( PriceEngine :: class ) -> recalcularProyecto ( $proyecto );
}
}
Payment Recording
Payments against sales are tracked separately:
class Pago extends Model
{
protected $fillable = [
'fecha' , // Payment date
'id_venta' , // Related sale
'referencia_pago' , // Payment reference/transaction ID
'id_concepto_pago' , // Payment concept (separation, down payment, etc.)
'id_medio_pago' , // Payment method (cash, transfer, check)
'descripcion' , // Description/notes
'valor' , // Amount
'id_cuota' , // Related installment (if applicable)
];
public function venta ()
{
return $this -> belongsTo ( Venta :: class , 'id_venta' );
}
public function cuota ()
{
return $this -> belongsTo ( PlanAmortizacionCuota :: class , 'id_cuota' );
}
}
Available Terms Calculation
The system calculates available payment terms based on project timeline:
private function calcularPlazosDisponibles ( Proyecto $proyecto )
{
if ( ! $proyecto -> fecha_inicio || ! $proyecto -> plazo_cuota_inicial_meses ) {
return [];
}
$inicio = Carbon :: parse ( $proyecto -> fecha_inicio );
$mesesTranscurridos = $inicio -> diffInMonths ( now ());
$max = $proyecto -> plazo_cuota_inicial_meses ;
$restantes = max ( $max - $mesesTranscurridos , 0 );
return range ( 1 , $restantes );
}
This ensures clients can’t select payment terms extending beyond the project’s configured timeline.
Validation Rules
[
'tipo_operacion' => 'required|in:venta,separacion' ,
'id_empleado' => 'required|exists:empleados,id_empleado' ,
'documento_cliente' => 'required|exists:clientes,documento' ,
'fecha_venta' => 'required|date' ,
'id_proyecto' => 'required|exists:proyectos,id_proyecto' ,
'inmueble_tipo' => 'required|in:apartamento,local' ,
'inmueble_id' => 'required|integer' ,
'id_forma_pago' => 'required|exists:formas_pago,id_forma_pago' ,
'id_parqueadero' => 'nullable|exists:parqueaderos,id_parqueadero' ,
'cuota_inicial' => 'nullable|numeric|min:0' ,
'valor_separacion' => 'nullable|numeric|min:0' ,
'fecha_limite_separacion' => 'nullable|date|after_or_equal:today' ,
'plazo_cuota_inicial_meses' => 'nullable|integer|min:0' ,
'frecuencia_cuota_inicial_meses' => 'nullable|integer|min:1' ,
]
Best Practices
Use separations for leads
Separations help secure client commitment while allowing time for financing approval.
Monitor separation deadlines
Track upcoming separation deadlines to follow up with clients before expiration.
Configure payment frequencies
Offer flexible payment frequencies to accommodate different client cash flows.
Validate parking availability
Always check parking availability before offering to clients to avoid conflicts.