Este documento establece los estándares de desarrollo que deben seguir todos los desarrolladores del proyecto RutaCC. El cumplimiento de estos estándares garantiza:
Todo archivo PHP debe seguir esta estructura. El encabezado es un comentario breve de qué hace el archivo —
en la práctica ningún archivo del proyecto usa tags @author/@version/@since
(ni siquiera security.php), así que no son parte real del estándar, solo el comentario descriptivo:
<?php
/**
* Descripción breve del archivo (qué hace, para qué endpoint/módulo es)
*/
// 1. Inclusión de dependencias
require_once __DIR__ . '/security.php';
// 2. Verificación de seguridad
requireAuth();
// 3. Obtención de datos de sesión
$tenant_id = getCurrentTenantId();
$user_id = getCurrentUserId();
// 4. Procesamiento de la petición
try {
// Lógica del negocio
} catch (Exception $e) {
secureLog("Error: " . $e->getMessage());
http_response_code(500);
echo json_encode(['success' => false, 'message' => 'Error interno']);
exit;
}
El try/catch de la petición es el patrón general para endpoints con lógica que puede lanzar excepciones
(por ejemplo, transacciones con varias tablas). Endpoints simples que delegan el manejo de errores a un
helper (como chat.php/chatv.php, que usan gemini_helper.php internamente
con su propio secureLog()) pueden omitirlo si no hay nada que de verdad pueda lanzar una excepción
no controlada.
$sql = "SELECT * FROM vehiculos WHERE id = " . $_GET['id'];
$result = $conn->query($sql);
$sql = "SELECT * FROM vehiculos WHERE id = ? AND tenant_id = ?";
$vehiculo = fetchOne($sql, "ii", [$_GET['id'], $tenant_id]);
Se deben usar las funciones helper centralizadas:
| Función | Uso | Ejemplo |
|---|---|---|
fetchOne() |
Obtener un registro | $user = fetchOne($sql, "i", [$id]); |
fetchAll() |
Obtener múltiples registros | $users = fetchAll($sql, "i", [$tenant]); |
executePreparedQuery() |
Ejecutar INSERT/UPDATE/DELETE | executePreparedQuery($sql, "s", [$name]); |
insertAndReturnId() |
Insertar y obtener ID | $id = insertAndReturnId($sql, "ss", [...]); |
sanitizeInput() |
Sanitizar datos de entrada | $name = sanitizeInput($_POST['name']); |
secureLog() |
Registrar eventos | secureLog("Usuario login", "INFO"); |
try {
$result = executePreparedQuery($sql, "i", [$id]);
echo json_encode(['success' => true, 'data' => $result]);
} catch (Exception $e) {
secureLog("Error en operación: " . $e->getMessage(), "ERROR");
http_response_code(500);
echo json_encode([
'success' => false,
'message' => 'Error al procesar la solicitud'
]);
}
Todas las respuestas deben incluir siempre success (booleano). En la práctica el resto de los
campos varían según lo que el endpoint necesite devolver — no hay un campo data fijo obligatorio:
un alta devuelve id, el chatbot devuelve reply, un listado devuelve el array
directamente, etc. Lo único consistente en todo el proyecto es la respuesta de error.
// Respuesta exitosa — el campo extra depende del endpoint
{ "success": true, "id": 42 } // ej: cobros_create.php
{ "success": true, "reply": "..." } // ej: chat.php
{ "success": true, "csrf_token": "..." } // ej: csrf_token.php
// Respuesta de error — esta forma sí es consistente en todo el proyecto
{
"success": false,
"message": "Descripción del error"
}
| Elemento | Convención | Ejemplo |
|---|---|---|
| Tablas | minúsculas, plural | vehiculos, despachos |
| Columnas | minúsculas, snake_case | tenant_id, fecha_creacion |
| Primary Keys | id |
id INT AUTO_INCREMENT |
| Foreign Keys | {tabla}_id |
tenant_id, vehiculo_id |
| Índices | idx_{tabla}_{columna} |
idx_tenant_estado |
| Constraints | fk_{tabla}_{referencia} |
fk_vehiculo_tenant |
id - Primary Key autoincrementaltenant_id - Foreign key a tenants (aislamiento multi-tenant)created_at - Timestamp de creación (cuando aplique)SELECT v.id, v.patente, v.marca, v.modelo,
c.nombre AS conductor_nombre
FROM vehiculos v
LEFT JOIN despachos d ON d.vehiculo_id = v.id
LEFT JOIN conductores c ON d.conductor_id = c.id
WHERE v.tenant_id = ?
AND v.estado = ?
ORDER BY v.patente ASC;
Cuando una operación involucra múltiples tablas, usar transacciones (la conexión es PDO, no mysqli — el
método es beginTransaction() en camelCase):
$conn = getDatabaseConnection();
$conn->beginTransaction();
try {
executePreparedQuery($sql1, "i", [$id]);
executePreparedQuery($sql2, "i", [$id]);
$conn->commit();
} catch (Exception $e) {
$conn->rollBack();
secureLog("Transacción fallida: " . $e->getMessage());
throw $e;
}
Recordar además el orden fijo de escritura entre tablas documentado al inicio de
queries.php (tenants → usuarios → bodegas → despachos → vehículos → conductores → pedidos → ...):
toda transacción que escribe en más de una tabla debe tocarlas en ese orden para evitar deadlocks.
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Título de la Página - RutaCC</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<!-- Contenido -->
<script src="js/api.js"></script>
</body>
</html>
| Elemento | Convención | Ejemplo |
|---|---|---|
| Clases | kebab-case | .card-header, .btn-primary |
| IDs | camelCase | #mainContainer |
| Variables | --prefijo-nombre | --color-primary |
alt<label><header>, <nav>, <main>,
<footer>)| Elemento | Convención | Ejemplo |
|---|---|---|
| Variables locales / computadas | camelCase | let usuarioActual; |
| Variables que reflejan un campo del payload o de la BD | snake_case, igual al nombre del campo | const bodega_id = this.dom.bodegaOrigen.value; (así están despachos_ficha.js y despachos_ficha_edicion.js, para que el nombre de la variable coincida 1:1 con la clave que se envía al backend) |
| Constantes | UPPER_SNAKE_CASE | const MAX_INTENTOS = 3; |
| Funciones | camelCase, verb + sustantivo | obtenerUsuarios() |
| Clases | PascalCase | class DespachoManager |
async function cargarDatos() {
try {
const response = await fetch('api/endpoint.php');
if (!response.ok) throw new Error('Error HTTP');
const data = await response.json();
if (data.success) {
procesarDatos(data.data);
} else {
mostrarError(data.message);
}
} catch (error) {
console.error('Error:', error);
mostrarError('Error de conexión');
}
}
Usar el patrón de módulo para organizar el código:
const Vehiculos = {
listar: async function() {
return await API.get('vehiculos_list.php');
},
crear: async function(datos) {
return await API.post('vehiculos_create.php', datos);
},
actualizar: async function(datos) {
return await API.post('vehiculos_update.php', datos);
}
};
| # | Regla | Justificación |
|---|---|---|
| 1 | NUNCA concatenar variables en SQL | Prevención de inyección SQL (OWASP A03) |
| 2 | SIEMPRE validar tenant_id en consultas |
Aislamiento multi-tenant |
| 3 | SIEMPRE incluir security.php |
Autenticación obligatoria |
| 4 | NUNCA mostrar errores detallados al usuario | Prevención de fuga de información |
| 5 | SIEMPRE sanitizar inputs del usuario | Prevención de XSS (OWASP A07) |
| 6 | NUNCA guardar contraseñas en texto plano | Usar password_hash() con bcrypt |
| 7 | NUNCA guardar credenciales en el código | Usar config/database.php |
| 8 | SIEMPRE usar HTTPS en producción | Cifrado de datos en tránsito |
// Validar tipo
$id = filter_var($data['id'], FILTER_VALIDATE_INT);
if ($id === false) {
throw new Exception('ID inválido');
}
// Validar rango
if ($id <= 0) {
throw new Exception('ID debe ser positivo');
}
// Validar email
$email = filter_var($data['email'], FILTER_VALIDATE_EMAIL);
if (!$email) {
throw new Exception('Email inválido');
}
| Tipo | Convención | Ejemplo |
|---|---|---|
| Listado | {modulo}_list.php | vehiculos_list.php |
| Crear | {modulo}_create.php | pedidos_create.php |
| Actualizar | {modulo}_update.php | conductores_update.php |
| Eliminar/Desactivar | {modulo}_disable.php | bodegas_disable.php |
| API/acción específica | {modulo}_{accion}.php | despachos_iniciar.php |
| Tipo | Convención | Ejemplo |
|---|---|---|
| Páginas principales | {modulo}.html | vehiculos.html |
| Fichas | {modulo}_ficha.html | despachos_ficha.html |
| Dashboard | dashboard.html | dashboard.html |
| Tipo | Convención | Ejemplo |
|---|---|---|
| Variables simples | camelCase | $tenantId, $vehiculoActual |
| Variables de BD | snake_case | $tenant_id, $user_id |
| Constantes | UPPER_SNAKE_CASE | DB_HOST, DEBUG_MODE |
| Arrays | plural | $vehiculos, $pedidos |
docker-rutacc/ # Raíz del entorno (fuera de este repositorio)
├── docker-compose.yml # Servicios: web, db, proxy, adminer
├── Dockerfile # Imagen del servicio web (php:8.2-apache + mod_headers)
├── .env # Secretos locales (API keys) — nunca se commitea
├── postgres/ # Build e init scripts de PostgreSQL
└── rutacc/ # ⬅ este repositorio (DocumentRoot del contenedor web)
├── config/
│ └── database.php # Config de BD + APP_ENV + API key de Gemini
├── css/ # Un archivo por página + compartidos (chat_widget.css, etc.)
├── js/
│ ├── api.js # Cliente HTTP central (fetch + CSRF), menú, chat de ayuda
│ ├── chat_widget.js # Motor genérico del widget de chat
│ ├── theme.js # Selector de tema oscuro/claro/azul
│ └── {modulo}[_ficha].js # Un archivo por página (64 en total)
├── components/ # Componentes HTML reutilizables (menu.html)
├── docs/ # Esta documentación
├── BBDD/ # Esquema SQL y migraciones
├── logs/ # Logs de depuración (protegido por .htaccess)
├── security.php # Seguridad central (auth, CSRF, headers, helpers de BD)
├── auth_functions.php # Funciones de autenticación
├── queries.php # Toda consulta SQL del sistema, como funciones nombradas
├── gemini_helper.php # Cliente de la API de Google Gemini
├── conocimiento_ayuda.php # Base de conocimiento del chat de ayuda
├── conocimiento_venta.php # Base de conocimiento del chat comercial
├── chat.php / chatv.php # Endpoints de los dos chatbots
├── *.php # Resto de endpoints (98 en total)
├── *.html # Vistas (40 en total)
└── .htaccess # Cabeceras de seguridad + protección de archivos sensibles
/**
* Calcula la distancia entre dos puntos usando la fórmula de Haversine
*
* @param float $lat1 Latitud del punto 1
* @param float $lon1 Longitud del punto 1
* @param float $lat2 Latitud del punto 2
* @param float $lon2 Longitud del punto 2
* @return float Distancia en kilómetros
*/
function calcularDistancia($lat1, $lon1, $lat2, $lon2) {
// Convertir grados a radianes
// ... código ...
}
// Incrementa i en 1
$i++;
// Retorna el resultado
return $resultado;
Toda API debe documentar:
| Rama | Propósito |
|---|---|
main |
Código en producción, estable |
develop |
Integración de features |
feature/{nombre} |
Nuevas funcionalidades |
bugfix/{nombre} |
Corrección de errores |
hotfix/{nombre} |
Correcciones urgentes en producción |
Formato de mensajes de commit:
tipo(scope): descripción breve
[cuerpo opcional con más detalles]
[issue reference opcional]
Tipos:
- feat: Nueva funcionalidad
- fix: Corrección de bug
- docs: Cambios en documentación
- style: Cambios de formato (sin cambio de lógica)
- refactor: Refactorización de código
- test: Agregado o corregido tests
- chore: Tareas de mantenimiento
feat(despachos): agregar cálculo automático de ruta óptima
Se implementó el algoritmo OSRM para calcular la mejor ruta
de entrega considerando los pedidos asignados al despacho.
Resuelve: #45
fix(vehiculos): corregir validación de patente única por tenant
La validación no consideraba el tenant_id, permitiendo
patentes duplicadas entre diferentes empresas.
Resuelve: #52
| Tipo | Descripción | Cobertura Mínima |
|---|---|---|
| Pruebas Unitarias | Funciones individuales | 80% del código crítico |
| Pruebas de Integración | Interacción entre módulos | Flujos principales |
| Pruebas Funcionales | Casos de uso completos | Todos los casos de uso |
| Pruebas de Seguridad | Vulnerabilidades | OWASP Top 10 |
| Pruebas de Rendimiento | Carga y estrés | 100 usuarios concurrentes |