[
'name' => 'Hialeah → Brickell (S por NW 27th Ave)',
'from' => 'Hialeah Market',
'to' => 'Brickell City Centre',
'wps' => [
[25.8576, -80.2781], [25.8450, -80.2700], [25.8320, -80.2620],
[25.8190, -80.2540], [25.8060, -80.2460], [25.7930, -80.2380],
[25.7800, -80.2300], [25.7680, -80.2220], [25.7617, -80.1918],
],
],
'B' => [
'name' => 'MIA Airport → Miami Beach (E por NW 36th St)',
'from' => 'Miami International Airport',
'to' => 'South Beach',
'wps' => [
[25.7959, -80.2870], [25.7959, -80.2650], [25.7940, -80.2430],
[25.7880, -80.2210], [25.7820, -80.1990], [25.7760, -80.1770],
[25.7700, -80.1550], [25.7640, -80.1330], [25.7617, -80.1320],
],
],
'C' => [
'name' => 'Kendall → Hialeah (N por FL Turnpike) — DESVÍO POSIBLE',
'from' => 'Kendall Drive',
'to' => 'Hialeah',
'wps' => [
[25.7090, -80.3560], [25.7230, -80.3420], [25.7370, -80.3280],
[25.7510, -80.3140], [25.7650, -80.3000], [25.7790, -80.2860],
[25.7930, -80.2720], [25.8070, -80.2580], [25.8210, -80.2440],
],
'deviation_start' => 3,
'deviation_path' => [
[25.7520, -80.2900], [25.7540, -80.2650],
[25.7560, -80.2400], [25.7580, -80.2150],
],
],
'D' => [
'name' => 'Aventura → Downtown Miami (S por Biscayne Blvd)',
'from' => 'Aventura Mall',
'to' => 'Downtown Miami',
'wps' => [
[25.9565, -80.1425], [25.9380, -80.1410], [25.9190, -80.1395],
[25.9000, -80.1380], [25.8810, -80.1365], [25.8620, -80.1350],
[25.8240, -80.1320], [25.7900, -80.1800], [25.7750, -80.1900],
],
],
'E' => [
'name' => 'Doral → Coral Gables (S por Palmetto Expy)',
'from' => 'Doral City Place',
'to' => 'Coral Gables Miracle Mile',
'wps' => [
[25.8196, -80.3556], [25.8100, -80.3400], [25.8000, -80.3250],
[25.7900, -80.3100], [25.7800, -80.2950], [25.7700, -80.2800],
[25.7600, -80.2650],
],
],
];
}
/* ══════════════════════════════════════════════════════════════════════
* CONFIGURACIÓN DE CONDUCTORES (12)
* ['login', 'nombre', ruta, step_inicial, empresa_corp|null]
* route FREE = disponible | route OFF = offline
* ══════════════════════════════════════════════════════════════════════ */
function ab_sim_driver_config() {
return [
['abtest_drv_a1', 'Andrés Pérez', 'A', 0, null],
['abtest_drv_a2', 'Beatriz Torres', 'A', 3, null],
['abtest_drv_b1', 'Carlos Méndez', 'B', 0, null],
['abtest_drv_b2', 'Diana López', 'B', 5, 'TechCorp'],
['abtest_drv_c1', 'Eduardo Ruiz', 'C', 0, null],
['abtest_drv_d1', 'Felipe Vargas', 'D', 0, 'LogiCorp'],
['abtest_drv_e1', 'Gabriela Suárez', 'E', 0, null],
['abtest_drv_f1', 'Hugo Ramírez', 'FREE', null, null],
['abtest_drv_f2', 'Isabel Castro', 'FREE', null, null],
['abtest_drv_f3', 'Julián Morales', 'FREE', null, null],
['abtest_drv_f4', 'Karen Reyes', 'FREE', null, null],
['abtest_drv_f5', 'Luis Herrera', 'OFF', null, null],
];
}
function ab_sim_haversine($lat1, $lng1, $lat2, $lng2) {
$R = 6371;
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat/2)**2 + cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng/2)**2;
return $R * 2 * atan2(sqrt($a), sqrt(1 - $a));
}
function ab_sim_free_positions() {
return [
[4.6700, -74.0550],
[4.6490, -74.0680],
[4.6120, -74.0760],
[4.6320, -74.1100],
];
}
/* ══════════════════════════════════════════════════════════════════════
* ESTADO EN wp_options
* ══════════════════════════════════════════════════════════════════════ */
function ab_sim_state() {
return get_option(AB_SIM_OPT, [
'running' => false,
'deviation' => false,
'drivers' => [],
'last_tick' => 0,
]);
}
function ab_sim_save($s) { update_option(AB_SIM_OPT, $s); }
/* ══════════════════════════════════════════════════════════════════════
* PÁGINA DE ADMINISTRACIÓN (Tools > AB Simulator)
* ══════════════════════════════════════════════════════════════════════ */
add_action('admin_menu', function () {
add_management_page('AB Simulator', 'AB Simulator', 'manage_options', 'ab-sim', 'ab_sim_page');
});
function ab_sim_page() {
if (isset($_POST['ab_sim_act']) && check_admin_referer('ab_sim')) {
$act = sanitize_key($_POST['ab_sim_act']);
if ($act === 'start') ab_sim_start();
if ($act === 'stop') ab_sim_stop();
if ($act === 'reset') ab_sim_reset();
if ($act === 'deviate') ab_sim_activate_deviation();
if ($act === 'tick') ab_sim_tick();
}
$s = ab_sim_state();
$routes = ab_sim_routes();
$running = $s['running'];
?>
🚗 AutoBooking Simulator
Estado: ▶ CORRIENDO'
: '⏸ DETENIDO'; ?>
| Último tick:
| Desvío ruta C: ⚠ ACTIVO'
: 'inactivo'; ?>
Conductores ()
| ID | Nombre | Ruta | Paso | Estado DB | Lat / Lng |
|
display_name : '—'); ?> |
|
|
|
|
Escenarios de prueba
| Escenario | Qué prueba | Pasos |
| Mapa en vivo |
Conductores disponibles y activos en el mapa del Command Center |
Iniciar → abrir Command Center |
| Viajes activos |
7 vehículos moviéndose en tiempo real (cada ~30s) |
Iniciar → refrescar mapa cada 30s |
| Desvío de ruta |
Conductor C abandona ruta Kennedy→Suba → alerta route_deviation en Command Center |
Iniciar → 2 ticks → Activar desvío → ver alertas |
| Viaje corporativo |
Diana López (TechCorp) y Felipe Vargas (LogiCorp) con company_id en sus viajes |
Iniciar → ver viajes activos con empresa asignada |
| Avance manual |
Control paso a paso para debugging de posiciones exactas |
Usar "Avanzar un paso" repetidamente |
get_col($wpdb->prepare(
"SELECT id FROM wp_ab_trips WHERE (driver_id=%d OR passenger_id=%d) AND trip_uuid LIKE 'sim\\_%'",
$uid, $uid
));
if ($trip_ids) {
$in = implode(',', array_map('intval', $trip_ids));
$wpdb->query("DELETE FROM wp_ab_trip_positions WHERE trip_id IN ($in)");
$wpdb->query("DELETE FROM wp_ab_incidents WHERE trip_id IN ($in)");
$wpdb->query("DELETE FROM wp_ab_trips WHERE id IN ($in)");
}
$wpdb->delete('wp_ab_driver_status', ['driver_id' => $uid]);
if (get_user_meta($uid, AB_SIM_MARKER, true) === '1') {
require_once ABSPATH . 'wp-admin/includes/user.php';
wp_delete_user($uid);
}
}
$wpdb->query("DELETE FROM wp_ab_companies WHERE name LIKE '%[SIM]%'");
delete_option(AB_SIM_OPT);
delete_option('ab_sim_extra_uids');
delete_option('ab_sim_company_ids');
}
/* ══════════════════════════════════════════════════════════════════════
* SEED — crear usuarios, empresas y viajes iniciales
* ══════════════════════════════════════════════════════════════════════ */
function ab_sim_seed(&$s) {
global $wpdb;
$routes = ab_sim_routes();
$drv_cfg = ab_sim_driver_config();
$free_pos = ab_sim_free_positions();
$now = current_time('mysql');
/* — 2 empresas institucionales — */
$company_ids = [];
foreach (['TechCorp [SIM]' => 'TechCorp', 'LogiCorp [SIM]' => 'LogiCorp'] as $cname => $ckey) {
$cid = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM wp_ab_companies WHERE name=%s LIMIT 1", $cname
));
if (!$cid) {
$wpdb->insert('wp_ab_companies', [
'name' => $cname,
'email' => strtolower($ckey) . '@sim.local',
'status' => 'active',
'created_at' => $now,
]);
$cid = $wpdb->insert_id;
}
$company_ids[$ckey] = (int) $cid;
}
update_option('ab_sim_company_ids', $company_ids);
/* — 20 pasajeros — */
$pass_names = [
'María García', 'José Rodríguez', 'Ana Martínez', 'Luis González',
'Carmen López', 'Pedro Sánchez', 'Laura Fernández', 'Miguel Torres',
'Isabel Ramírez', 'Carlos Flores', 'Sara Díaz', 'David Morales',
'Elena Jiménez', 'Roberto Ruiz', 'Patricia Herrera','Javier Medina',
'Sofía Delgado', 'Tomás Vargas', 'Valentina Cruz', 'Nicolás Ortiz',
];
$pass_ids = [];
foreach ($pass_names as $i => $pname) {
$login = 'abtest_pass_' . ($i + 1);
$existing = get_user_by('login', $login);
if ($existing) {
$pid = $existing->ID;
} else {
$pid = wp_insert_user([
'user_login' => $login,
'user_pass' => wp_generate_password(),
'display_name' => $pname,
'role' => 'subscriber',
]);
if (is_wp_error($pid)) continue;
update_user_meta($pid, AB_SIM_MARKER, '1');
// Empleados corporativos: índices 16-17 TechCorp, 18-19 LogiCorp
if ($i >= 16 && $i <= 17) update_user_meta($pid, 'ab_company_id', $company_ids['TechCorp']);
if ($i >= 18 && $i <= 19) update_user_meta($pid, 'ab_company_id', $company_ids['LogiCorp']);
}
$pass_ids[] = $pid;
}
update_option('ab_sim_extra_uids', $pass_ids);
/* — 12 conductores — */
$s['drivers'] = [];
$free_idx = 0;
foreach ($drv_cfg as $pi => $cfg) {
[$login, $dname, $route, $step, $corp] = $cfg;
$existing = get_user_by('login', $login);
if ($existing) {
$uid = $existing->ID;
} else {
$uid = wp_insert_user([
'user_login' => $login,
'user_pass' => wp_generate_password(),
'display_name' => $dname,
'role' => 'driver',
]);
if (is_wp_error($uid)) continue;
update_user_meta($uid, AB_SIM_MARKER, '1');
}
// Posición inicial y estado DB
if ($route === 'FREE') {
[$lat, $lng] = $free_pos[$free_idx % count($free_pos)];
$free_idx++;
$db_status = 'available';
} elseif ($route === 'OFF') {
$lat = 4.6500; $lng = -74.0600;
$db_status = 'offline';
} else {
[$lat, $lng] = $routes[$route]['wps'][$step ?? 0];
$db_status = 'on_trip';
}
// Insertar/actualizar wp_ab_driver_status
$exists = $wpdb->get_var($wpdb->prepare(
"SELECT driver_id FROM wp_ab_driver_status WHERE driver_id=%d", $uid
));
if ($exists) {
$wpdb->update('wp_ab_driver_status',
['last_lat' => $lat, 'last_lng' => $lng, 'online' => ($db_status === 'offline') ? 0 : 1, 'last_seen' => $now],
['driver_id' => $uid]
);
} else {
$wpdb->insert('wp_ab_driver_status', [
'driver_id' => $uid,
'last_lat' => $lat,
'last_lng' => $lng,
'online' => ($db_status === 'offline') ? 0 : 1,
'last_seen' => $now,
]);
}
// Crear viaje activo para conductores en ruta
$trip_id = null;
if ($route !== 'FREE' && $route !== 'OFF') {
$pass_id = $pass_ids[$pi] ?? $pass_ids[0];
$wps = $routes[$route]['wps'];
$last_wp = $wps[count($wps) - 1];
$company_id = ($corp && isset($company_ids[$corp])) ? $company_ids[$corp] : null;
$trip_id = $wpdb->get_var($wpdb->prepare(
"SELECT id FROM wp_ab_trips WHERE driver_id=%d AND notes LIKE '%[SIM]%' LIMIT 1", $uid
));
if (!$trip_id) {
$wps = $routes[$route]['wps'];
$last = $wps[count($wps) - 1];
$drv_user = get_userdata($uid);
$pax_user = get_userdata($pass_id);
$wpdb->insert('wp_ab_trips', [
'trip_uuid' => uniqid('sim_', true),
'driver_id' => $uid,
'passenger_id' => $pass_id,
'driver_name' => $drv_user ? $drv_user->display_name : 'Conductor SIM',
'passenger_name' => $pax_user ? $pax_user->display_name : 'Pasajero SIM',
'status' => 'in_progress',
'pickup_lat' => $wps[0][0],
'pickup_lng' => $wps[0][1],
'dropoff_lat' => $last[0],
'dropoff_lng' => $last[1],
'pickup_time' => $now,
'created_at' => $now,
]);
$trip_id = $wpdb->insert_id;
}
if ($trip_id) {
$wpdb->insert('wp_ab_trip_positions', [
'trip_id' => $trip_id,
'lat' => $lat,
'lng' => $lng,
'ts' => $now,
]);
}
}
$s['drivers'][] = [
'user_id' => $uid,
'route' => $route,
'step' => $step ?? 0,
'trip_id' => $trip_id,
'db_status' => $db_status,
'lat' => $lat,
'lng' => $lng,
'deviated' => false,
];
}
}
/* ══════════════════════════════════════════════════════════════════════
* TICK — avanzar todos los vehículos un paso
* ══════════════════════════════════════════════════════════════════════ */
add_action(AB_SIM_CRON, 'ab_sim_tick');
function ab_sim_tick() {
global $wpdb;
$s = ab_sim_state();
if (!$s['running']) return;
$routes = ab_sim_routes();
$now = current_time('mysql');
foreach ($s['drivers'] as &$d) {
$route = $d['route'];
if ($route === 'FREE') {
// Micromovimiento para conductores disponibles
$d['lat'] += (mt_rand(-4, 4) * 0.0001);
$d['lng'] += (mt_rand(-4, 4) * 0.0001);
$wpdb->update('wp_ab_driver_status',
['lat' => $d['lat'], 'lng' => $d['lng'], 'updated_at' => $now],
['user_id' => $d['user_id']]
);
continue;
}
if ($route === 'OFF') continue;
$wps = $routes[$route]['wps'];
$dev_start = $routes[$route]['deviation_start'] ?? PHP_INT_MAX;
$dev_path = $routes[$route]['deviation_path'] ?? [];
$is_deviating = $s['deviation'] && $route === 'C' && $d['step'] >= $dev_start;
if ($is_deviating) {
$di = $d['step'] - $dev_start;
if (isset($dev_path[$di])) {
[$d['lat'], $d['lng']] = $dev_path[$di];
// Primera vez que desvía: registrar separación pasajero-conductor
if (!$d['deviated'] && $d['trip_id']) {
$sep = $wps[$dev_start - 1] ?? $wps[0]; // último punto en ruta normal
$trip_row = $wpdb->get_row($wpdb->prepare(
"SELECT dropoff_lat, dropoff_lng, passenger_name FROM wp_ab_trips WHERE id = %d", $d['trip_id']
));
$dist_dest = ($trip_row && $trip_row->dropoff_lat)
? ab_sim_haversine($sep[0], $sep[1], $trip_row->dropoff_lat, $trip_row->dropoff_lng)
: 0;
$wpdb->insert('wp_ab_incidents', [
'trip_id' => $d['trip_id'],
'user_id' => $d['user_id'],
'type' => 'separacion_sospechosa',
'description' => sprintf(
'[SIM] Conductor abandonó ruta. Pasajero %s último punto conocido: %.5f, %.5f (a %.1f km del destino contratado). Conductor ahora en: %.5f, %.5f.',
$trip_row->passenger_name ?? 'desconocido',
$sep[0], $sep[1], $dist_dest,
$d['lat'], $d['lng']
),
'lat' => $d['lat'],
'lng' => $d['lng'],
'status' => 'open',
'created_at' => $now,
]);
$d['deviated'] = true;
}
$d['step']++;
} else {
// Agotó la ruta de desvío — cerrar viaje con alerta de separación fuera del destino
if ($d['trip_id']) {
$trip_row = $wpdb->get_row($wpdb->prepare(
"SELECT dropoff_lat, dropoff_lng, passenger_name FROM wp_ab_trips WHERE id = %d", $d['trip_id']
));
$sep = $wps[$dev_start - 1] ?? $wps[0];
if ($trip_row && $trip_row->dropoff_lat) {
$dist = ab_sim_haversine($sep[0], $sep[1], $trip_row->dropoff_lat, $trip_row->dropoff_lng);
$wpdb->insert('wp_ab_incidents', [
'trip_id' => $d['trip_id'],
'user_id' => $d['user_id'],
'type' => 'separacion_destino_incorrecto',
'description' => sprintf(
'[SIM] Viaje cerrado fuera del destino. Pasajero %s separado a %.1f km del destino contratado (%.5f, %.5f). Punto de separación: %.5f, %.5f.',
$trip_row->passenger_name ?? 'desconocido',
$dist,
$trip_row->dropoff_lat, $trip_row->dropoff_lng,
$sep[0], $sep[1]
),
'lat' => $sep[0],
'lng' => $sep[1],
'status' => 'open',
'created_at' => $now,
]);
}
$wpdb->update('wp_ab_trips',
['status' => 'completed', 'completed_at' => $now],
['id' => $d['trip_id']]
);
$d['trip_id'] = null;
}
$d['step'] = 0;
$d['deviated'] = false;
$d['db_status'] = 'available';
[$d['lat'], $d['lng']] = $wps[0];
$wpdb->update('wp_ab_driver_status',
['last_lat' => $d['lat'], 'last_lng' => $d['lng'], 'online' => 1, 'last_seen' => $now],
['driver_id' => $d['user_id']]
);
continue;
}
} else {
$next = $d['step'] + 1;
if ($next >= count($wps)) {
// Llegó al destino — verificar si coincide con el punto contratado
if ($d['trip_id']) {
$trip_row = $wpdb->get_row($wpdb->prepare(
"SELECT dropoff_lat, dropoff_lng, passenger_name FROM wp_ab_trips WHERE id = %d", $d['trip_id']
));
if ($trip_row && $trip_row->dropoff_lat) {
$dist = ab_sim_haversine($d['lat'], $d['lng'], $trip_row->dropoff_lat, $trip_row->dropoff_lng);
if ($dist > 0.5) {
$wpdb->insert('wp_ab_incidents', [
'trip_id' => $d['trip_id'],
'user_id' => $d['user_id'],
'type' => 'separacion_fuera_destino',
'description' => sprintf(
'[SIM] Viaje completado a %.1f km del destino contratado. Pasajero %s separado en: %.5f, %.5f. Destino original: %.5f, %.5f.',
$dist,
$trip_row->passenger_name ?? 'desconocido',
$d['lat'], $d['lng'],
$trip_row->dropoff_lat, $trip_row->dropoff_lng
),
'lat' => $d['lat'],
'lng' => $d['lng'],
'status' => 'open',
'created_at' => $now,
]);
}
}
$wpdb->update('wp_ab_trips',
['status' => 'completed', 'completed_at' => $now],
['id' => $d['trip_id']]
);
$d['trip_id'] = null;
}
$d['step'] = 0;
$d['deviated'] = false;
$d['db_status'] = 'available';
[$d['lat'], $d['lng']] = $wps[0];
$wpdb->update('wp_ab_driver_status',
['last_lat' => $d['lat'], 'last_lng' => $d['lng'], 'online' => 1, 'last_seen' => $now],
['driver_id' => $d['user_id']]
);
continue;
}
$d['step'] = $next;
[$d['lat'], $d['lng']] = $wps[$next];
}
$wpdb->update('wp_ab_driver_status',
['last_lat' => $d['lat'], 'last_lng' => $d['lng'], 'last_seen' => $now],
['driver_id' => $d['user_id']]
);
if ($d['trip_id']) {
$wpdb->insert('wp_ab_trip_positions', [
'trip_id' => $d['trip_id'],
'lat' => $d['lat'],
'lng' => $d['lng'],
'ts' => $now,
]);
}
}
unset($d);
$s['last_tick'] = time();
ab_sim_save($s);
}
/* ══════════════════════════════════════════════════════════════════════
* CRON — intervalo de 30 segundos
* ══════════════════════════════════════════════════════════════════════ */
add_filter('cron_schedules', function ($sc) {
$sc['ab_sim_30s'] = ['interval' => 30, 'display' => 'Cada 30s (AutoBooking Sim)'];
return $sc;
});
register_deactivation_hook(__FILE__, function () {
wp_clear_scheduled_hook(AB_SIM_CRON);
});