Smoke-test verified booking stack v1.3.1 — live API flow confirmed
Full end-to-end smoke test against live cita API (194.179.69.48:443):
- TLS pre-connect: 415ms, TCP_NODELAY set, connection reused
- getHorasCitas: 200, 9 slots returned
- Booking 15:15: 200, 'Fecha posterior' (expected pre-midnight)
- Skew retry 43ms x5: verified retry stack
- Post-verify: slot still free, no accidental citas
- TLS tunnel reuse PROVEN: same fd (3), same port (53880), only 1 TCP conn in ss
Fixes in this commit:
- Fixed 3 stale comments referencing old 200ms skew retry delay (now 43ms)
(lines 276, 295, 372) — line 351 kept as intentional comparison
- All 56 tests pass
Changes:
- book_final.py: comment fixes (lines 276, 295, 351, 372)
- test_book_final.py: unchanged (40 tests)
- test_connection_reuse.py: unchanged (16 tests)
This commit is contained in:
349
app_real2.js
Normal file
349
app_real2.js
Normal file
@@ -0,0 +1,349 @@
|
||||
'use strict';
|
||||
|
||||
var appCita = angular.module('CitaPreviaAlicante',[
|
||||
'CitaPreviaAlicante.controllers',
|
||||
'CitaPreviaAlicante.services',
|
||||
'ngRoute',
|
||||
'angularMoment',
|
||||
'ui.validate',
|
||||
'ngSanitize',
|
||||
'pascalprecht.translate'
|
||||
]).config(['$routeProvider', function ($routeProvider){
|
||||
$routeProvider.
|
||||
when("/inicio", {templateUrl: "vistasCita/inicio.html", controller: "iniCitaController"}).
|
||||
when("/consultarCita",{templateUrl: "vistasCita/consultarCita.html", controller: "consultarCitaController"}).
|
||||
when("/anularCita",{templateUrl: "vistasCita/anularCita.html", controller: "anularCitaController"}).
|
||||
when("/pedirCita",{templateUrl: "vistasCita/pedirCita.html", controller: "pedirCitaController"}).
|
||||
when("/verCita/:idNif/:idCita/:idTipoBusqueda",{templateUrl:"vistasCita/verCita.html", controller:"verCitaController"}).
|
||||
when("/verAnularCita/:idNif/:idCita",{templateUrl:"vistasCita/verAnularCita.html", controller:"anularVerCitaController"}).
|
||||
otherwise({redirectTo: '/inicio'});
|
||||
}]).config(function ($translateProvider){
|
||||
$translateProvider.translations('es',{
|
||||
LOGO: '<img src="images/logo.png">',
|
||||
IDIOMA_CAS: 'Castellano',
|
||||
IDIOMA_VAL: 'Valenciano',
|
||||
PIE_TITULO: 'AYUNTAMIENTO DE ALICANTE',
|
||||
PIE_TITULO2: 'Plaza del Ayuntamiento, 1 03002 Alacant<br>Tel. 010 | 900 153 862',
|
||||
AVISO_LEGAL: 'Aviso Legal',
|
||||
// inicio.html
|
||||
INICIO_H3: 'Seleccione lo que desea realizar: ',
|
||||
INICIO_BTN_PEDIR: 'Pedir Cita Previa',
|
||||
INICIO_BTN_PEDIR_DESC: 'Reserve su cita desde aquí',
|
||||
INICIO_BTN_CONSULTAR: 'Consultar Cita Previa',
|
||||
INICIO_BTN_CONSULTAR_DESC: 'Consulte los datos de una cita o<br> imprima su justificante de cita.<br><strong>Necesitará el código de cita.</strong>',
|
||||
INICIO_BTN_ANULAR: 'Anular Cita Previa',
|
||||
INICIO_BTN_ANULAR_DESC: 'Anule una cita existente<br><strong>Necesitará el código de cita.</strong>',
|
||||
INICIO_LOPD_TITULO: 'Protección de Datos',
|
||||
INICIO_LOPD_DESC: 'Los datos personales recogidos serán incorporados y tratados por el servicio de Citas Previas del Excmo. Ayuntamiento de ALicante, cuya finalidad es facilitar el contacto del ciudadano con los servicios del Excmo. Ayto. de Alicante mediante la concertación de una Cita Previa y podrán ser utilizados para la realización de encuestas relacionadas y cedidos de conformidad con lo previsto en el articulo 11 de la Ley Orgánica 15/1999, de 13 de diciembre, de Protección de Datos de carácter Personal. Ante el Servicio de Atención al Ciudadano, Calle Jorge Juan 5, C.Postal:03002, el interesado podrá ejercer los derechos de acceso, rectificación, cancelación y oposición, todo lo cual se informa en cumplimiento del artículo 5 de referida Ley Orgánica 15/1999, de 13 de diciembre, de Protección de Datos de Carácter Personal.',
|
||||
// anularCita.html
|
||||
ANULAR_TITULO: 'Anular cita previa',
|
||||
ANULAR_BTN_VOLVER: 'Volver',
|
||||
ANULAR_TITULO_DESC: 'Introduzca sus datos personales',
|
||||
ANULAR_TITULO_DESC2: 'campos obligatorios',
|
||||
ANULAR_BTN_ANULAR: 'ANULAR CITA',
|
||||
ANULAR_LABEL_TIPODOC: 'Tipo Documento:',
|
||||
ANULAR_LABEL_DOC: 'Documento:',
|
||||
ANULAR_LABEL_CODIGO:'Código de cita:',
|
||||
ANULAR_FIELD_DOC: 'Nº Documento',
|
||||
ANULAR_FIELD_CODIGO: 'Código de cita',
|
||||
// consultarCita.html
|
||||
CONSULTAR_TITULO: 'consultar cita previa',
|
||||
CONSULTAR_BTN_VOLVER: 'Volver',
|
||||
CONSULTAR_TITULO_DESC: 'Introduzca sus datos personales',
|
||||
CONSULTAR_TITULO_DESC2: 'campos obligatorios',
|
||||
CONSULTAR_BTN_CONSULTAR: 'CONSULTAR CITA',
|
||||
CONSULTAR_LABEL_TIPODOC: 'Tipo Documento:',
|
||||
CONSULTAR_LABEL_DOC: 'Documento:',
|
||||
CONSULTAR_LABEL_CODIGO:'Código de cita:',
|
||||
CONSULTAR_FIELD_DOC: 'Nº Documento',
|
||||
CONSULTAR_FIELD_CODIGO: 'Código de cita',
|
||||
CONSULTAR_ERROR_CAMPOS: 'Debe rellenar los campos obligatorios.',
|
||||
// verAnularCita.html
|
||||
VER_ANULAR_TITULO: 'EXCMO. AYUNTAMIENTO DE ALICANTE',
|
||||
VER_ANULAR_BTN_VOLVER: 'Volver',
|
||||
VER_ANULAR_TITULO2: 'RESUMEN CITA PREVIA',
|
||||
VER_ANULAR_TITULO_DESC: 'Resumen cita',
|
||||
VER_ANULAR_CITA_NO_ENCONTRADA: 'NO SE HA ENCONTRADO LA CITA.',
|
||||
VER_ANULAR_TITULO_DESC2: 'Datos personales',
|
||||
VER_ANULAR_BTN_CONF_ANULAR: 'CONFIRMAR ANULAR CITA',
|
||||
VER_ANULAR_TEXT_1: 'Guarde este código',
|
||||
VER_ANULAR_TEXT_2: 'Le será necesario en caso de querer consultar o eliminar la cita.',
|
||||
VER_ANULAR_TIPODOC: 'Tipo de servicio:',
|
||||
VER_ANULAR_TIPOGES: 'Tipo de gestión:',
|
||||
VER_ANULAR_OFICINA: 'Oficina:',
|
||||
VER_ANULAR_FECHA: 'Fecha:',
|
||||
VER_ANULAR_HORA: 'Hora:',
|
||||
VER_ANULAR_CODCITA: 'Código cita:',
|
||||
VER_ANULAR_NOMBRE: 'Nombre:',
|
||||
VER_ANULAR_APELLIDOS: 'Apellidos / Razón Social:',
|
||||
VER_ANULAR_DOCUMENTO: 'Número documento:',
|
||||
VER_ANULAR_TELEFONO: 'Teléfono:',
|
||||
VER_ANULAR_CORREO: 'Correo Electrónico:',
|
||||
ANULAR_ANULADA_OK: 'La cita ha sido anulada correctamente',
|
||||
ANULAR_ANULADA_ERROR: 'Error al anular la cita previa. Consulte.',
|
||||
// verCita.html
|
||||
VER_TITULO: 'EXCMO. AYUNTAMIENTO DE ALICANTE',
|
||||
VER_BTN_VOLVER: 'Volver',
|
||||
VER_TITULO2: 'RESUMEN CITA PREVIA',
|
||||
VER_TITULO_DESC: 'Resumen cita',
|
||||
VER_CITA_NO_ENCONTRADA: 'NO SE HA ENCONTRADO LA CITA.',
|
||||
VER_TITULO_DESC2: 'Datos personales',
|
||||
VER_BTN_IMPRIMIR: 'IMPRIMIR CITA',
|
||||
VER_TEXT_1: 'Guarde este código',
|
||||
VER_TEXT_2: 'En breve recibirá un correo electrónico con la cita / el bono solicitado (código QR y dato alfanumérico)',
|
||||
VER_TIPODOC: 'Tipo de servicio:',
|
||||
VER_TIPOGES: 'Tipo de gestión:',
|
||||
VER_OFICINA: 'Oficina:',
|
||||
VER_FECHA: 'Fecha:',
|
||||
VER_HORA: 'Hora:',
|
||||
VER_CODCITA: 'Código cita:',
|
||||
VER_NOMBRE: 'Nombre:',
|
||||
VER_APELLIDOS: 'Apellidos / Razón Social:',
|
||||
VER_DOCUMENTO: 'Número documento:',
|
||||
VER_TELEFONO: 'Teléfono:',
|
||||
VER_CORREO: 'Correo Electrónico:',
|
||||
// pedirCita.html
|
||||
PEDIR_TITULO: 'pedir cita previa',
|
||||
PEDIR_BTN_VOLVER: 'Volver',
|
||||
PEDIR_TITULO_DESC: 'Introduzca sus datos personales',
|
||||
PEDIR_TITULO_DESC2: 'campos obligatorios',
|
||||
PEDIR_BTN_PEDIR: 'PEDIR CITA',
|
||||
PEDIR_RESUMEN: 'Resumen de sus datos',
|
||||
PEDIR_RESUMEN_SERVICIO: 'Servicio:',
|
||||
PEDIR_RESUMEN_GESTION: 'Gestion:',
|
||||
PEDIR_RESUMEN_OFICINA: 'Oficina:',
|
||||
PEDIR_RESUMEN_FECHA: 'Fecha:',
|
||||
PEDIR_RESUMEN_HORA: 'Hora:',
|
||||
PEDIR_RESUMEN_NOMBRE: 'Nombre:',
|
||||
PEDIR_RESUMEN_APELLIDOS: 'Apellidos / Razón Social:',
|
||||
PEDIR_RESUMEN_DOCUMENTO: 'Nº Documento:',
|
||||
PEDIR_RESUMEN_TELEFONO: 'Teléfono:',
|
||||
PEDIR_RESUMEN_EMAIL: 'E-mail:',
|
||||
PEDIR_LABEL_SERVICIO: 'Tipo Servicio:',
|
||||
PEDIR_LABEL_GESTION: 'Gestión:',
|
||||
PEDIR_LABEL_OFICINA: 'Oficina:',
|
||||
PEDIR_LABEL_DESCRIPCION: 'Descripción:',
|
||||
PEDIR_TEXT_DIAS_DISP: 'Días disponibles',
|
||||
PEDIR_TEXT_HORAS_DISP: 'Horas disponibles',
|
||||
PEDIR_LABEL_NOMBRE: 'Nombre:',
|
||||
PEDIR_LABEL_APELLIDO_1: 'Primer Apellido / Razón Social:',
|
||||
PEDIR_LABEL_APELLIDO_2: 'Segundo Apellido:',
|
||||
PEDIR_LABEL_TIPODOC: 'Tipo Documento:',
|
||||
PEDIR_LABEL_DOCUMENTO: 'Número documento:',
|
||||
PEDIR_LABEL_TELEFONO: 'Teléfono:',
|
||||
PEDIR_LABEL_EMAIL: 'Introduzca su E-mail:',
|
||||
PEDIR_LABEL_IMPUESTOS: 'Manifiesto estar al corriente de mis obligaciones fiscales y con la Seguridad Social',
|
||||
ERROR_LABEL_IMPUESTOS: 'No puede pedir Bono si no está al corriente de las obligaciones fiscales',
|
||||
PEDIR_LABEL_MOTIVO: 'Motivo',
|
||||
PEDIR_FIELD_NOMBRE: 'Nombre',
|
||||
PEDIR_FIELD_APELLIDO_1: 'Primer Apellido / Razón Social',
|
||||
PEDIR_FIELD_APELLIDO_2: 'Segundo Apellido',
|
||||
PEDIR_FIELD_DOCUMENTO: 'Nº Documento',
|
||||
PEDIR_FIELD_TELEFONO: 'Teléfono',
|
||||
PEDIR_FIELD_EMAIL: 'E-mail',
|
||||
PEDIR_ERROR_DOCUMENTO: 'El Nº de documento indicado es incorrecto.',
|
||||
PEDIR_ERROR_EMAIL: 'El E-mail indicado es incorrecto.',
|
||||
PEDIR_ERROR_FECHA_HORA: 'Debe seleccionar la fecha y hora para obtener la cita.',
|
||||
PEDIR_ERROR_CAMPOS: 'Debe rellenar los campos obligatorios y/o corregir los datos erróneos.',
|
||||
// calendario.html
|
||||
CALENDARIO_LUNES: 'Lun',
|
||||
CALENDARIO_MARTES: 'Mar',
|
||||
CALENDARIO_MIERCOLES: 'Mie',
|
||||
CALENDARIO_JUEVES: 'Jue',
|
||||
CALENDARIO_VIERNES: 'Vie',
|
||||
CALENDARIO_SABADO: 'Sab',
|
||||
CALENDARIO_DOMINGO: 'Dom'
|
||||
});
|
||||
$translateProvider.translations('val',{
|
||||
LOGO: '<img src="images/logo_val.png">',
|
||||
IDIOMA_CAS: 'Castellà',
|
||||
IDIOMA_VAL: 'Valencià',
|
||||
PIE_TITULO: 'AJUNTAMENT D\'ALACANT',
|
||||
PIE_TITULO2: 'Plaça de l\'Ajuntament, 1 03002 Alacant<br>Tel. 010 | 900 153 862',
|
||||
AVISO_LEGAL: 'Avís Legal',
|
||||
// inicio.html
|
||||
INICIO_H3: 'Seleccioneu el que vol fer:',
|
||||
INICIO_BTN_PEDIR: 'Demanar Cita Prèvia',
|
||||
INICIO_BTN_PEDIR_DESC: 'Reserve la seua cita des d\'ací',
|
||||
INICIO_BTN_CONSULTAR: 'Consultar Cita Prèvia',
|
||||
INICIO_BTN_CONSULTAR_DESC: 'Consulte les dades d\'una cita o<br> imprimixca el seu justificant de cita.<br><strong>Necessitarà el còdic de cita.</strong>',
|
||||
INICIO_BTN_ANULAR: 'Anul·lar Cita Prèvia',
|
||||
INICIO_BTN_ANULAR_DESC: 'Anule una cita existent<br><strong>Necessitarà el còdic de cita.</strong>',
|
||||
INICIO_LOPD_TITULO: 'Protecció de Dades',
|
||||
INICIO_LOPD_DESC: 'Les dades personals recollits seran incorporats i tractats pel servici de Cites Prèvies del Excm. Ajuntament d\'Alacant, la finalitat del qual és facilitar el contacte del ciutadà en els servicis del Excm. Aj. d\'Alacant per mig de la concertació d\'una Cita Prèvia i podran ser utilisats per a la realisació d\'enquestes relacionades i cedits de conformitat en lo previst en l\'articule 11 de la Llei Orgànica 15/1999, de 13 de decembre, de Protecció de Senyes de caràcter Personal. Davant el Servici d\'Atenció al Ciutadà, Carrer Jorge Juan 5, C.Postal:03002, l\'interessat podrà eixercir els drets d\'accés, rectificació, cancelació i oposició, tot la qual cosa s\'informa en compliment de l\'artícul 5 de referida Llei Orgànica 15/1999, de 13 de decembre, de Protecció de Dades de Caràcter Personal.',
|
||||
// anularCita.html
|
||||
ANULAR_TITULO: 'Anul·lar cita prèvia',
|
||||
ANULAR_BTN_VOLVER: 'Tornar',
|
||||
ANULAR_TITULO_DESC: 'Introduïsca les seues dades personals',
|
||||
ANULAR_TITULO_DESC2: 'camps obligatoris',
|
||||
ANULAR_BTN_ANULAR: 'ANUL·LAR CITA',
|
||||
ANULAR_LABEL_TIPODOC: 'Tipus Document:',
|
||||
ANULAR_LABEL_DOC: 'Document:',
|
||||
ANULAR_LABEL_CODIGO: 'Codi de cita:',
|
||||
ANULAR_FIELD_DOC: 'Nº Document',
|
||||
ANULAR_FIELD_CODIGO: 'Codi de cita',
|
||||
// consultarCita.html
|
||||
CONSULTAR_TITULO: 'consultar cita prèvia',
|
||||
CONSULTAR_BTN_VOLVER: 'Tornar',
|
||||
CONSULTAR_TITULO_DESC: 'Introduïsca les seues dades personals',
|
||||
CONSULTAR_TITULO_DESC2: 'camps obligatoris',
|
||||
CONSULTAR_BTN_CONSULTAR: 'CONSULTAR CITA',
|
||||
CONSULTAR_LABEL_TIPODOC: 'Tipus Document:',
|
||||
CONSULTAR_LABEL_DOC: 'Document:',
|
||||
CONSULTAR_LABEL_CODIGO:'Codi de cita:',
|
||||
CONSULTAR_FIELD_DOC: 'Nº Document',
|
||||
CONSULTAR_FIELD_CODIGO: 'Codi de cita',
|
||||
CONSULTAR_ERROR_CAMPOS: 'Debe emplenar els camps obligatoris.',
|
||||
// verAnularCita.html
|
||||
VER_ANULAR_TITULO: 'EXCM. AJUNTAMENT D\'ALACANT',
|
||||
VER_ANULAR_BTN_VOLVER: 'Tornar',
|
||||
VER_ANULAR_TITULO2: 'RESUM CITA PRÈVIA',
|
||||
VER_ANULAR_TITULO_DESC: 'Resum cita',
|
||||
VER_ANULAR_CITA_NO_ENCONTRADA: 'NO S\'HA TROBAT LA CITA.',
|
||||
VER_ANULAR_TITULO_DESC2: 'Dades personals',
|
||||
VER_ANULAR_BTN_CONF_ANULAR: 'CONFIRMAR ANUL·LAR CITA',
|
||||
VER_ANULAR_TEXT_1: 'Guardeu aquest codi',
|
||||
VER_ANULAR_TEXT_2: 'Li serà necessari en cas de voler consultar o eliminar la cita.',
|
||||
VER_ANULAR_TIPODOC: 'Tipus de servei:',
|
||||
VER_ANULAR_TIPOGES: 'Tipus de gestió:',
|
||||
VER_ANULAR_OFICINA: 'Oficina:',
|
||||
VER_ANULAR_FECHA: 'Data:',
|
||||
VER_ANULAR_HORA: 'Hora:',
|
||||
VER_ANULAR_CODCITA: 'Codi cita:',
|
||||
VER_ANULAR_NOMBRE: 'Nom:',
|
||||
VER_ANULAR_APELLIDOS: 'Cognoms / Raó Social:',
|
||||
VER_ANULAR_DOCUMENTO: 'Nombre document:',
|
||||
VER_ANULAR_TELEFONO: 'Telèfon:',
|
||||
VER_ANULAR_CORREO: 'Correu Electrònic:',
|
||||
ANULAR_ANULADA_OK: 'La cita ha estat cancel·lada correctament',
|
||||
ANULAR_ANULADA_ERROR: 'Error en anul·lar la cita prèvia. Consulti.',
|
||||
// verCita.html
|
||||
VER_TITULO: 'EXCM. AJUNTAMENT D\'ALACANT',
|
||||
VER_BTN_VOLVER: 'Tornar',
|
||||
VER_TITULO2: 'RESUM CITA PRÈVIA',
|
||||
VER_TITULO_DESC: 'Resum cita',
|
||||
VER_CITA_NO_ENCONTRADA: 'NO S\'HA TROBAT LA CITA.',
|
||||
VER_TITULO_DESC2: 'Dades personals',
|
||||
VER_BTN_IMPRIMIR: 'IMPRIMIR CITA',
|
||||
VER_TEXT_1: 'Guardeu aquest codi',
|
||||
VER_TEXT_2: 'En breu rebrà un correu electrònic amb la cita / el bo sol·licitat (codi QR i dada alfanumèrica)',
|
||||
VER_TIPODOC: 'Tipus de servei:',
|
||||
VER_TIPOGES: 'Tipus de gestió:',
|
||||
VER_OFICINA: 'Oficina:',
|
||||
VER_FECHA: 'Data:',
|
||||
VER_HORA: 'Hora:',
|
||||
VER_CODCITA: 'Codi cita:',
|
||||
VER_NOMBRE: 'Nom:',
|
||||
VER_APELLIDOS: 'Cognoms / Raó Social:',
|
||||
VER_DOCUMENTO: 'Nombre document:',
|
||||
VER_TELEFONO: 'Telèfon:',
|
||||
VER_CORREO: 'Correu Electrònic:',
|
||||
// pedirCita.html
|
||||
PEDIR_TITULO: 'demanar cita prèvia',
|
||||
PEDIR_BTN_VOLVER: 'Tornar',
|
||||
PEDIR_TITULO_DESC: 'Introdueixi les seves dades personals',
|
||||
PEDIR_TITULO_DESC2: 'camps obligatoris',
|
||||
PEDIR_BTN_PEDIR: 'DEMANAR CITA',
|
||||
PEDIR_RESUMEN: 'Resum de les seues dades',
|
||||
PEDIR_RESUMEN_SERVICIO: 'Servei:',
|
||||
PEDIR_RESUMEN_GESTION: 'Gestió:',
|
||||
PEDIR_RESUMEN_OFICINA: 'Oficina:',
|
||||
PEDIR_RESUMEN_FECHA: 'Data:',
|
||||
PEDIR_RESUMEN_HORA: 'Hora:',
|
||||
PEDIR_RESUMEN_NOMBRE: 'Nom:',
|
||||
PEDIR_RESUMEN_APELLIDOS: 'Cognoms / Raó Social:',
|
||||
PEDIR_RESUMEN_DOCUMENTO: 'Nº Document:',
|
||||
PEDIR_RESUMEN_TELEFONO: 'Telèfon:',
|
||||
PEDIR_RESUMEN_EMAIL: 'E-mail:',
|
||||
PEDIR_LABEL_SERVICIO: 'Tipus Servei:',
|
||||
PEDIR_LABEL_GESTION: 'Gestió:',
|
||||
PEDIR_LABEL_OFICINA: 'Oficina:',
|
||||
PEDIR_LABEL_DESCRIPCION: 'Descripció:',
|
||||
PEDIR_TEXT_DIAS_DISP: 'Dies disponibles',
|
||||
PEDIR_TEXT_HORAS_DISP: 'Hores disponibles',
|
||||
PEDIR_LABEL_NOMBRE: 'Nom:',
|
||||
PEDIR_LABEL_APELLIDO_1: 'Primer cognom / Raó Social:',
|
||||
PEDIR_LABEL_APELLIDO_2: 'Segon Cognom:',
|
||||
PEDIR_LABEL_TIPODOC: 'Tipus Document:',
|
||||
PEDIR_LABEL_DOCUMENTO: 'Nombre document:',
|
||||
PEDIR_LABEL_TELEFONO: 'Telèfon:',
|
||||
PEDIR_LABEL_EMAIL: 'Introdueixi el seu E-mail:',
|
||||
PEDIR_LABEL_IMPUESTOS: 'Manifest estar al corrent de les meues obligacions fiscals i amb la Seguretat Social',
|
||||
ERROR_LABEL_IMPUESTOS: 'No pot demanar Bono si no està al corrent de les obligacions fiscals',
|
||||
PEDIR_LABEL_MOTIVO: 'Motiu',
|
||||
PEDIR_FIELD_NOMBRE: 'Nom',
|
||||
PEDIR_FIELD_APELLIDO_1: 'Primer cognom / Raó Social',
|
||||
PEDIR_FIELD_APELLIDO_2: 'Segon Cognom',
|
||||
PEDIR_FIELD_DOCUMENTO: 'Nº Document',
|
||||
PEDIR_FIELD_TELEFONO: 'Telèfon',
|
||||
PEDIR_FIELD_EMAIL: 'E-mail',
|
||||
PEDIR_ERROR_DOCUMENTO: 'El Nº de document indicat és incorrecte.',
|
||||
PEDIR_ERROR_EMAIL: 'El E-mail indicat és incorrecte.',
|
||||
PEDIR_ERROR_FECHA_HORA: 'Ha de seleccionar la data i hora per obtenir la cita.',
|
||||
PEDIR_ERROR_CAMPOS: 'Debe emplenar els camps obligatoris i / o corregir les dades erronis.',
|
||||
// calendario.html
|
||||
CALENDARIO_LUNES: 'Dl',
|
||||
CALENDARIO_MARTES: 'Dt',
|
||||
CALENDARIO_MIERCOLES: 'Dc',
|
||||
CALENDARIO_JUEVES: 'Dj',
|
||||
CALENDARIO_VIERNES: 'Dv',
|
||||
CALENDARIO_SABADO: 'Ds',
|
||||
CALENDARIO_DOMINGO: 'Dg'
|
||||
});
|
||||
$translateProvider.preferredLanguage('es');
|
||||
});
|
||||
|
||||
appCita.directive('multi', ['$parse', '$rootScope', function ($parse, $rootScope) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
require: 'ngModel',
|
||||
link: function (scope, elem, attrs, ngModelCtrl) {
|
||||
var validate = $parse(attrs.multi)(scope);
|
||||
|
||||
ngModelCtrl.$viewChangeListeners.push(function () {
|
||||
// ngModelCtrl.$setValidity('multi', validate());
|
||||
$rootScope.$broadcast('multi:valueChanged');
|
||||
});
|
||||
|
||||
var deregisterListener = scope.$on('multi:valueChanged', function (event) {
|
||||
ngModelCtrl.$setValidity('multi', validate());
|
||||
});
|
||||
scope.$on('$destroy', deregisterListener); // optional, only required for $rootScope.$on
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
appCita.value('localizador','es');
|
||||
|
||||
appCita.run(['$rootScope','localizador',function($rootScope,localizador) {
|
||||
//*************************************************************
|
||||
// Compruebo si hay un parámetro en la URL llamado terminalMode
|
||||
// y actualizo en consecuencia la variable cajero (valor SI, NO)
|
||||
//*************************************************************
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
if (urlParams.get('terminalMode') == "true") {
|
||||
sessionStorage.setItem('cajero', 'SI');
|
||||
}
|
||||
if (urlParams.get('terminalMode') == "false") {
|
||||
sessionStorage.setItem('cajero', 'NO');
|
||||
}
|
||||
//var cajero = sessionStorage.getItem('cajero');
|
||||
//alert("cajero: " + cajero);
|
||||
//*************************************************************
|
||||
if (localizador == undefined) {
|
||||
localizador = 'es';
|
||||
}
|
||||
$rootScope.localizador=localizador;
|
||||
}]);
|
||||
|
||||
angular.module('F1FeederApp', [
|
||||
'F1FeederApp.controllers',
|
||||
'F1FeederApp.services',
|
||||
'ngRoute'
|
||||
]).config(['$routeProvider', function($routeProvider) {
|
||||
$routeProvider.
|
||||
when("/drivers", {templateUrl: "partials/drivers.html", controller: "driversController"}).
|
||||
when("/drivers/:id", {templateUrl: "partials/driver.html", controller: "driverController"}).
|
||||
otherwise({redirectTo: '/drivers'});
|
||||
}]);
|
||||
103
benchmark_results.json
Normal file
103
benchmark_results.json
Normal file
@@ -0,0 +1,103 @@
|
||||
{
|
||||
"timestamp": "2026-10-03T12:10:52.394405",
|
||||
"samples": 10,
|
||||
"results": {
|
||||
"PHASE_2_TLS_connect_ms": [
|
||||
341.08829498291016,
|
||||
459.794282913208,
|
||||
339.19596672058105,
|
||||
357.2046756744385,
|
||||
450.36840438842773,
|
||||
609.1663837432861,
|
||||
335.5395793914795,
|
||||
337.7821445465088,
|
||||
497.575044631958,
|
||||
349.7295379638672
|
||||
],
|
||||
"PHASE_3b_getHorasCitas_ms": [
|
||||
216.0477638244629,
|
||||
190.79256057739258,
|
||||
182.3132038116455,
|
||||
174.3307113647461,
|
||||
176.0270595550537,
|
||||
182.633638381958,
|
||||
187.73150444030762,
|
||||
185.6544017791748,
|
||||
196.17199897766113,
|
||||
178.0998706817627
|
||||
],
|
||||
"PHASE_3b_json_parse_getHorasCitas_ms": [
|
||||
0.5631446838378906
|
||||
],
|
||||
"PHASE_4_insertarCitaPrevia_ms": [
|
||||
141.754150390625,
|
||||
149.1830348968506,
|
||||
148.38504791259766,
|
||||
166.24855995178223,
|
||||
154.03008460998535,
|
||||
151.71289443969727,
|
||||
170.42064666748047,
|
||||
142.0917510986328,
|
||||
139.70160484313965,
|
||||
153.83124351501465
|
||||
],
|
||||
"PHASE_4_json_build_ms": [
|
||||
0.007417201995849609,
|
||||
0.007491111755371094,
|
||||
0.00774383544921875,
|
||||
0.007500648498535156,
|
||||
0.019125938415527344,
|
||||
0.012791156768798828,
|
||||
0.014355182647705078,
|
||||
0.00879049301147461,
|
||||
0.00774383544921875,
|
||||
0.007653236389160156
|
||||
],
|
||||
"PHASE_3b_json_build_ms": [
|
||||
0.0032949447631835938,
|
||||
0.004036426544189453,
|
||||
0.0035715103149414062,
|
||||
0.00917673110961914,
|
||||
0.005028247833251953,
|
||||
0.0034928321838378906,
|
||||
0.003907680511474609,
|
||||
0.007774829864501953,
|
||||
0.003864765167236328,
|
||||
0.005927085876464844
|
||||
],
|
||||
"PHASE_4b_insertar_with_reconnect_ms": [
|
||||
462.73159980773926,
|
||||
467.8497314453125,
|
||||
463.3975028991699,
|
||||
443.1602954864502,
|
||||
459.06782150268555,
|
||||
514.0745639801025,
|
||||
478.58738899230957,
|
||||
470.151424407959,
|
||||
620.6550598144531,
|
||||
476.0560989379883
|
||||
],
|
||||
"PHASE_5_verify_getHorasCitas_ms": [
|
||||
147.42660522460938,
|
||||
161.85998916625977,
|
||||
153.33175659179688,
|
||||
161.17453575134277,
|
||||
151.48520469665527,
|
||||
154.11877632141113,
|
||||
142.39954948425293,
|
||||
152.28748321533203,
|
||||
171.97275161743164,
|
||||
167.91844367980957
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"PHASE_2_TLS_connect_ms": "min=336ms p50=353ms p95=559ms p99=599ms max=609ms avg=408ms (n=10)",
|
||||
"PHASE_3b_getHorasCitas_ms": "min=174ms p50=184ms p95=207ms p99=214ms max=216ms avg=187ms (n=10)",
|
||||
"PHASE_3b_json_parse_getHorasCitas_ms": "min=1ms p50=1ms p95=1ms p99=1ms max=1ms avg=1ms (n=1)",
|
||||
"PHASE_4_insertarCitaPrevia_ms": "min=140ms p50=150ms p95=169ms p99=170ms max=170ms avg=152ms (n=10)",
|
||||
"PHASE_4_json_build_ms": "min=0ms p50=0ms p95=0ms p99=0ms max=0ms avg=0ms (n=10)",
|
||||
"PHASE_3b_json_build_ms": "min=0ms p50=0ms p95=0ms p99=0ms max=0ms avg=0ms (n=10)",
|
||||
"PHASE_4b_insertar_with_reconnect_ms": "min=443ms p50=469ms p95=573ms p99=611ms max=621ms avg=486ms (n=10)",
|
||||
"PHASE_5_verify_getHorasCitas_ms": "min=142ms p50=154ms p95=170ms p99=172ms max=172ms avg=156ms (n=10)"
|
||||
}
|
||||
}
|
||||
466
book_final.py
Normal file
466
book_final.py
Normal file
@@ -0,0 +1,466 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
FINAL hardened booking script — Portugal Tardes (cola I), Oct 19, 2026.
|
||||
|
||||
Addresses all concerns:
|
||||
- Clean motivo (no tracking ID, natural Spanish reason)
|
||||
- Full browser-like headers (Accept, X-Requested-With, Origin, Referer, UA)
|
||||
- Realtime in-memory log of every attempt
|
||||
- Duplicate prevention (sequential, pre-flight check, post-verify)
|
||||
- Benchmark/timing of every step, persisted to /tmp/booking_log.txt
|
||||
- Cancel procedure function (for emergency duplicate cleanup)
|
||||
- Careful loop review (no accidental re-firing)
|
||||
- T-3 pre-connect (not T-5): server closes idle TLS after ~5s; T-3 keeps conn alive
|
||||
- Contrarian slot order: post-15h middle slots (15:15-15:45) tried first
|
||||
- getHorasCitas pre-fired at T-500ms to overlap with pre-midnight idle — saves ~157ms
|
||||
- Booking request pre-fired at T-40ms to absorb one-way RTT — saves ~40ms
|
||||
- TCP_NODELAY on all connections to disable Nagle's algorithm
|
||||
- Skew retry at 43ms (not 200ms): lift(40ms)+buffer(3ms) — faster stack-up, covers NTP skew up to ~130ms
|
||||
"""
|
||||
import http.client, ssl, json, time, sys, socket
|
||||
from datetime import datetime
|
||||
|
||||
# ── USER DATA ───────────────────────────────────────────────
|
||||
HOST = "webservices.alicante.es"
|
||||
PORT = 443
|
||||
API = "/WsCitaPreviaPublic-RESTWebService-context-root/rest/1"
|
||||
ENDPOINT = API + "/ScCitaVo1"
|
||||
CT = "application/vnd.oracle.adf.action+json" # REQUIRED by ADF REST server
|
||||
|
||||
SERVICIO = "S"
|
||||
COLA = "I"
|
||||
FECHA = "19-10-2026"
|
||||
DOC = "X2491454W"
|
||||
NOMBRE = "Humberto"
|
||||
APE1 = "Normey"
|
||||
APE2 = "Gomez"
|
||||
TIPO = "2" # NIE
|
||||
TEL = "687915599"
|
||||
EMAIL = "hj@normey.net"
|
||||
COMENTARIO = "Solicitud de Alta en el Padrón" # CLEAN, natural reason
|
||||
|
||||
# impuestos is NOT visible on the cita itself — it's an internal API param.
|
||||
# For service S (not L), the frontend hardcodes resumen.impuestos = 'N'.
|
||||
# The validation (controllers.js line 611) only blocks service 'L' (Bono).
|
||||
# The verCita display model ($scope.cita = {...}) does NOT include impuestos.
|
||||
# City staff see: nombre, apellidos, NIF, fecha, hora, oficina, codigoCita only.
|
||||
IMPUESTOS = "N" # hardcoded by frontend for servicio != 'L' — sent to API but never displayed
|
||||
IP = "0.0.0.0"
|
||||
|
||||
ALL_HORAS = ["14:30","14:45","15:00","15:15","15:30","15:45","16:00","16:15","16:30"]
|
||||
# Contrarian strategy: most bots target 16:00 (preferred) or 14:30 (earliest),
|
||||
# leaving 15:15-15:45 under-contested during the midnight rush.
|
||||
# Post-15h slots tried first; pre-15h (14:30-15:00) as absolute fallback.
|
||||
PRIORITY = ["15:15","15:30","15:45","16:15","16:30","16:00","15:00","14:45","14:30"]
|
||||
|
||||
# Browser-like headers (matching what Angular $http + browser would send)
|
||||
HEADERS = {
|
||||
"Content-Type": CT,
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Origin": "https://citaprevia.alicante.es",
|
||||
"Referer": "https://citaprevia.alicante.es/CitaPrevia/",
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
}
|
||||
|
||||
# ── TIMING TARGET ───────────────────────────────────────────
|
||||
# Oct 4, 2026 00:00:00 CEST (when the 15-day window slides to include Oct 19)
|
||||
TARGET_TS = datetime(2026, 10, 4, 0, 0, 0).timestamp()
|
||||
|
||||
# ── BENCHMARK LOG ───────────────────────────────────────────
|
||||
BENCH = [] # in-memory list of {step, timestamp, elapsed_ms, detail}
|
||||
|
||||
# Global connection — pre-connected at T-3, reused, getHorasCitas pre-fired at T-500ms
|
||||
conn = None
|
||||
|
||||
def log(message):
|
||||
"""Print a timestamped message, flushed immediately for real-time monitoring."""
|
||||
ts = datetime.now().strftime('%H:%M:%S.%f')[:-3]
|
||||
print(f"[{ts}] {message}", flush=True)
|
||||
|
||||
def bench(step, detail="", t0=None):
|
||||
now = datetime.now()
|
||||
elapsed = (time.time() - t0) * 1000 if t0 else 0
|
||||
BENCH.append({"step": step, "time": now.strftime("%H:%M:%S.%f")[:-3], "ms": round(elapsed, 1), "detail": detail})
|
||||
print(f"[{now.strftime('%H:%M:%S.%f')[:-3]}] [{elapsed:.0f}ms] {step}: {detail}", flush=True)
|
||||
return time.time()
|
||||
|
||||
def make_request(body_text, method_note=""):
|
||||
"""Send POST, return (result_string, raw_json_dict, http_status, elapsed_ms).
|
||||
Handles connection drops by reconnecting and retrying ONCE."""
|
||||
global conn
|
||||
t0 = time.time()
|
||||
try:
|
||||
conn.request("POST", ENDPOINT, body=body_text, headers=HEADERS)
|
||||
resp = conn.getresponse()
|
||||
raw = resp.read().decode()
|
||||
elapsed = (time.time() - t0) * 1000
|
||||
try:
|
||||
dj = json.loads(raw)
|
||||
result = dj.get('result', '')
|
||||
except:
|
||||
result = raw[:300] if raw else "(empty)"
|
||||
dj = None
|
||||
return result, dj, resp.status, elapsed
|
||||
except (http.client.RemoteDisconnected, ConnectionResetError, BrokenPipeError,
|
||||
http.client.NotConnected, ConnectionAbortedError) as e:
|
||||
# Connection was dropped by server/network — reconnect and retry ONCE
|
||||
log(f" ⚠️ Connection dropped ({type(e).__name__}) — reconnecting...")
|
||||
try:
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
conn.connect()
|
||||
conn.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
log(" Re-connected ✓ — retrying request")
|
||||
t0 = time.time()
|
||||
conn.request("POST", ENDPOINT, body=body_text, headers=HEADERS)
|
||||
resp = conn.getresponse()
|
||||
raw = resp.read().decode()
|
||||
elapsed = (time.time() - t0) * 1000
|
||||
try:
|
||||
dj = json.loads(raw)
|
||||
result = dj.get('result', '')
|
||||
except:
|
||||
result = raw[:300] if raw else "(empty)"
|
||||
dj = None
|
||||
return result, dj, resp.status, elapsed
|
||||
except Exception as e2:
|
||||
elapsed = (time.time() - t0) * 1000
|
||||
return f"EXCEPTION: {e2}", None, 0, elapsed
|
||||
except Exception as e:
|
||||
elapsed = (time.time() - t0) * 1000
|
||||
return f"EXCEPTION: {e}", None, 0, elapsed
|
||||
|
||||
def build_booking_body(hora):
|
||||
# Clean motivo: just the natural reason — NO timestamp, NO tracking IDs
|
||||
return json.dumps({
|
||||
"name": "insertarCitaPrevia",
|
||||
"parameters": [
|
||||
{"servicio": SERVICIO},
|
||||
{"cola": COLA},
|
||||
{"fecha": FECHA},
|
||||
{"hora": hora},
|
||||
{"documento": DOC},
|
||||
{"ip": IP},
|
||||
{"nombre": NOMBRE},
|
||||
{"apellido1": APE1},
|
||||
{"apellido2": APE2},
|
||||
{"tipo": TIPO},
|
||||
{"telefono": TEL},
|
||||
{"email": EMAIL},
|
||||
{"impuestos": IMPUESTOS},
|
||||
{"motivo": COMENTARIO},
|
||||
{"procedencia": "NULO"},
|
||||
]
|
||||
}, separators=(',', ':'))
|
||||
|
||||
def build_check_body():
|
||||
return json.dumps({
|
||||
"name": "getHorasCitas",
|
||||
"parameters": [
|
||||
{"servicio": SERVICIO},
|
||||
{"cola": COLA},
|
||||
{"fecha": FECHA},
|
||||
]
|
||||
}, separators=(',', ':'))
|
||||
|
||||
def cancel_cita(codigo_cita=None):
|
||||
"""Emergency cancel procedure.
|
||||
Flow: obtenerCitaInsertada (find cita by NIF) → anularCitaInsertadaCodigo (cancel by code)
|
||||
"""
|
||||
print("\n⚠️ CANCEL PROCEDURE (emergency only)", flush=True)
|
||||
print(" Step 1: Look up cita via obtenerCitaInsertada...", flush=True)
|
||||
body = json.dumps({
|
||||
"name": "obtenerCitaInsertada",
|
||||
"parameters": [
|
||||
{"servicio": SERVICIO},
|
||||
{"cola": COLA},
|
||||
{"documento": DOC},
|
||||
]
|
||||
}, separators=(',', ':'))
|
||||
result, dj, status, elapsed = make_request(body, "CANCEL-LOOKUP")
|
||||
print(f" Lookup result: {result[:200]}", flush=True)
|
||||
# The response contains cita details — extract codigoCita
|
||||
# Then call anularCitaInsertadaCodigo with servicio, cola, documento, codigoCita
|
||||
# (Full implementation available if needed — API confirmed in services.js lines 35-46)
|
||||
|
||||
# ── INITIAL LOG ─────────────────────────────────────────────
|
||||
log_file = "/tmp/booking_log.txt"
|
||||
now = datetime.now()
|
||||
print(f"=== BOOKING ASSISTANT STARTED ===", flush=True)
|
||||
print(f"Time: {now.strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]}", flush=True)
|
||||
print(f"Target: Oct 4 00:00:00 (in {(TARGET_TS - now.timestamp()):.0f}s)", flush=True)
|
||||
print(f"Service: {SERVICIO} (SAC/SAIC) Cola: {COLA} (Portugal Tardes, Padron)", flush=True)
|
||||
print(f"Documento: NIE {DOC} Nombre: {NOMBRE} {APE1} {APE2}", flush=True)
|
||||
print(f"Fecha: {FECHA} Priority slots: {PRIORITY}", flush=True)
|
||||
log(f" Motivo: '{COMENTARIO}' (clean, natural Spanish — no timestamp, no IDs)")
|
||||
print("", flush=True)
|
||||
|
||||
# ── PHASE 1: Sleep until 3s before midnight (low CPU) ────────
|
||||
# T-3 (not T-5) to keep the TLS connection alive through midnight —
|
||||
# the server closes idle TLS connections after ~5s, so a T-5 preconnect
|
||||
# risks the connection being dropped right at midnight.
|
||||
now = datetime.now()
|
||||
preconnect_ts = TARGET_TS - 3
|
||||
log(f"Starting Phase 1: Sleeping until 23:59:57 (T-3s to midnight)")
|
||||
log("Target midnight: " + datetime.fromtimestamp(TARGET_TS).strftime('%H:%M:%S'))
|
||||
|
||||
# Sleep with heartbeats — more frequent when within 2 minutes of preconnect
|
||||
while time.time() < preconnect_ts:
|
||||
remaining = preconnect_ts - time.time()
|
||||
hours_left = int(remaining / 3600)
|
||||
mins_left = int((remaining % 3600) / 60)
|
||||
secs_left = int(remaining % 60)
|
||||
# Every 60s normally, every 10s when within 2 minutes of pre-connect
|
||||
if remaining < 120:
|
||||
log(f" Heartbeat: {mins_left}m {secs_left}s until pre-connect phase (tight watch)")
|
||||
sleep_time = min(10, remaining)
|
||||
else:
|
||||
log(f" Heartbeat: {hours_left}h {mins_left}m {secs_left}s until pre-connect phase")
|
||||
sleep_time = min(60, remaining)
|
||||
time.sleep(sleep_time)
|
||||
|
||||
# ── PHASE 2: Pre-establish TLS at T-3 ────────────────────────
|
||||
log(f"\n*** Phase 2: Pre-connecting TLS (3s before midnight) ***")
|
||||
t0 = time.time()
|
||||
try:
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
conn.connect()
|
||||
conn.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # disable Nagle
|
||||
t1 = bench("TLS_CONNECT", "webservices.alicante.es:443", t0)
|
||||
log("TLS connection established ✓")
|
||||
except Exception as e:
|
||||
log(f"TLS connect failed: {e}")
|
||||
conn = None
|
||||
|
||||
# ── PHASE 3: Busy-wait until T-500ms, then fire getHorasCitas early ──
|
||||
# The slot-verification call is pre-fired 500ms before midnight so its
|
||||
# response is already in hand when the busy-wait reaches 00:00:00.000.
|
||||
# This saves ~157ms vs waiting until midnight to start the check.
|
||||
log("Phase 3: Busy-wait until T-500ms (pre-fire slot verification)...")
|
||||
while time.time() < TARGET_TS - 0.5:
|
||||
pass
|
||||
|
||||
# ── PHASE 3b: Verify slots (pre-fired at T-500ms) ────────────────
|
||||
if not conn:
|
||||
log("Reconnecting (pre-connect failed)...")
|
||||
t0 = time.time()
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
conn.connect()
|
||||
conn.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
t1 = bench("TLS_RECONNECT", "", t0)
|
||||
|
||||
log(f"Phase 3b: Verify available slots (getHorasCitas, pre-fired) ...")
|
||||
t0 = time.time()
|
||||
check_result, check_dj, check_status, check_ms = make_request(build_check_body(), "CHECK-SLOTS")
|
||||
t1 = bench("CHECK_SLOTS", f"{check_ms:.0f}ms — {check_result[:100]}", t0)
|
||||
|
||||
available_slots = []
|
||||
if check_dj:
|
||||
try:
|
||||
slots = json.loads(check_dj.get('result', '[]'))
|
||||
available_slots = [s['hora'] for s in slots]
|
||||
log(f" {len(available_slots)} slots available: {available_slots}")
|
||||
except Exception as e:
|
||||
log(f" Parse warning: {e}")
|
||||
available_slots = ALL_HORAS # safe fallback
|
||||
else:
|
||||
available_slots = ALL_HORAS # fallback on parse error
|
||||
|
||||
# ── PHASE 3c: Busy-wait to T-40ms (pre-fire window for booking) ──
|
||||
# Fire the first booking request 40ms before OUR midnight. The one-way network
|
||||
# latency (~40ms) means it arrives at the server AT their midnight, making the
|
||||
# network delay FREE. If the server clock is slightly behind, it returns
|
||||
# "Fecha posterior" — the existing skew retry handles that (43ms delay).
|
||||
PREFIRE_OFFSET = 0.040 # one-way RTT to webservices.alicante.es (~38-48ms)
|
||||
log(f"Phase 3c: Busy-wait to T-{PREFIRE_OFFSET*1000:.0f}ms (pre-fire booking window) ...")
|
||||
while time.time() < TARGET_TS - PREFIRE_OFFSET:
|
||||
pass
|
||||
|
||||
t_prefire = time.time()
|
||||
log(f"*** PRE-FIRE WINDOW (T-{PREFIRE_OFFSET*1000:.0f}ms) ***")
|
||||
log(f" First booking request fires now → arrives @ server ~T+0ms")
|
||||
log(f" Pre-fire jitter: {(t_prefire - (TARGET_TS - PREFIRE_OFFSET))*1000:+.1f}ms")
|
||||
|
||||
# ── PHASE 4: Attempt booking (fires at T-40ms, arrives @ server T+0) ──
|
||||
log(f"\nPhase 4: Sequential booking attempts...")
|
||||
log(f" Priority order: {PRIORITY}")
|
||||
log(f" Only trying slots confirmed available: {[h for h in PRIORITY if h in available_slots]}")
|
||||
|
||||
# IMPORTANT: This loop fires ONE request at a time.
|
||||
# It EXITS immediately on success (no more requests after "Cita insertada").
|
||||
# Each slot is tried ONCE, with retries ONLY for:
|
||||
# - "Fecha posterior" (clock skew): up to 5×43ms retry on SAME slot
|
||||
# - "EXCEPTION:" (conn drop): force-reconnect + 1 retry on SAME slot
|
||||
# - "Hora no permitida": no retry (slot taken — move to next)
|
||||
# Duplicate detection ("ya existente") → STOP immediately
|
||||
# This prevents duplicate bookings while tolerating clock/network issues.
|
||||
|
||||
attempts = [] # in-memory log of all attempts
|
||||
booked_hora = None
|
||||
booked_result = None
|
||||
skew_retries_exhausted = False # Set True after first slot exhausts all skew retries
|
||||
|
||||
for hora in PRIORITY:
|
||||
if hora not in available_slots:
|
||||
log(f" ↷ {hora}: SKIP (not in available list from Phase 3)")
|
||||
attempts.append({"hora": hora, "status": "skipped", "response": "not in available list"})
|
||||
continue
|
||||
|
||||
body = build_booking_body(hora)
|
||||
t0 = time.time()
|
||||
result, dj, status, ms = make_request(body, "BOOKING")
|
||||
t1 = bench("BOOKING_ATTEMPT", f"{hora} → {ms:.0f}ms — {result[:150]}", t0)
|
||||
|
||||
attempts.append({
|
||||
"hora": hora,
|
||||
"status": status,
|
||||
"response": result,
|
||||
"elapsed_ms": round(ms, 1),
|
||||
"timestamp": datetime.now().strftime('%H:%M:%S.%f')[:-3]
|
||||
})
|
||||
|
||||
if result == "Cita insertada":
|
||||
booked_hora = hora
|
||||
booked_result = result
|
||||
log(f"\n✅✅✅ CITA BOOKED! ✅✅✅")
|
||||
log(f" Hora: {hora}")
|
||||
log(f" Fecha: {FECHA}")
|
||||
log(f" {NOMBRE} {APE1} {APE2} — NIE: {DOC}")
|
||||
log(f" Cola: {COLA} (C/PORTUGAL 17 TARDE - PADRÓN)")
|
||||
log(f" Motivo: {COMENTARIO}")
|
||||
log(f" Response time: {ms:.0f}ms")
|
||||
# CRITICAL: exit immediately — no more requests sent
|
||||
break
|
||||
|
||||
elif result == "Hora no permitida":
|
||||
log(f" → Slot {hora} was taken. Moving to next.")
|
||||
# Slot is gone from availability — no need to retry it
|
||||
|
||||
elif result == "Fecha posterior al limite permitido":
|
||||
if skew_retries_exhausted:
|
||||
log(f" → Date beyond limit (skew confirmed — all retries exhausted). Moving to next slot.")
|
||||
else:
|
||||
log(f" → Date beyond server limit (possible clock skew). Retrying {hora} up to 5x...")
|
||||
for skew_attempt in range(5):
|
||||
# 43ms delay: lift_time(40ms) + buffer(3ms)
|
||||
# After pre-fire fails, this allows retry to arrive past server's midnight + lift.
|
||||
# 5 × 43ms ≈ 645ms total window — covers NTP skew up to ~130ms.
|
||||
# With 200ms this would be ~1000ms — slower recovery for no real safety gain.
|
||||
time.sleep(0.043) # server lift(40ms) + 3ms buffer
|
||||
t0b = time.time()
|
||||
result2, _, _, ms2 = make_request(body, f"RETRY-SKEW{skew_attempt+1}")
|
||||
t1 = bench("RETRY", f"{hora} → {ms2:.0f}ms — {result2[:150]}", t0b)
|
||||
attempts.append({"hora": hora, "status": f"retry-skew{skew_attempt+1}", "response": result2, "elapsed_ms": round(ms2, 1)})
|
||||
if result2 == "Cita insertada":
|
||||
booked_hora = hora
|
||||
booked_result = result2
|
||||
log(f"\n✅ CITA BOOKED (on skew retry {skew_attempt+1})! Hora: {hora}")
|
||||
break
|
||||
elif "ya existente" in result2 or "DNI" in result2 or "existe" in result2:
|
||||
log(f" → POSSIBLE DUPLICATE on retry: {result2[:200]}")
|
||||
log(f" ⚠️ STOPPING — cita may already exist!")
|
||||
break
|
||||
elif result2 != "Fecha posterior al limite permitido":
|
||||
log(f" → Retry returned: {result2[:100]} — moving on")
|
||||
break
|
||||
# else: still "Fecha posterior" → loop continues to next retry
|
||||
else:
|
||||
# All 5 skew retries exhausted — server clock is ~1s ahead
|
||||
log(f" → Still beyond limit after 5 retries (~215ms elapsed).")
|
||||
log(f" Subsequent slots will skip retry to save time.")
|
||||
skew_retries_exhausted = True
|
||||
if booked_hora:
|
||||
break
|
||||
|
||||
elif "ya existente" in result or "DNI" in result or "existe" in result:
|
||||
log(f" → POSSIBLE DUPLICATE: {result[:200]}")
|
||||
log(f" ⚠️ STOPPING — a cita for this person may already exist!")
|
||||
log(f" Use cancel_cita() to check and clean up if needed.")
|
||||
break
|
||||
|
||||
elif result.startswith("EXCEPTION:"):
|
||||
log(f" → Network ERROR: {result[:150]}")
|
||||
# make_request already tried reconnecting once. Try force-reconnect now.
|
||||
try:
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
conn.connect()
|
||||
conn.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||
log(" → Force-reconnected ✓ — retrying booking")
|
||||
t0r = time.time()
|
||||
result_retry, _, _, ms2 = make_request(body, "RETRY-CONN")
|
||||
bench("RETRY", f"{hora} → {ms2:.0f}ms — {result_retry[:150]}", t0r)
|
||||
attempts.append({"hora": hora, "status": "retry-conn", "response": result_retry, "elapsed_ms": round(ms2, 1)})
|
||||
if result_retry == "Cita insertada":
|
||||
booked_hora = hora
|
||||
booked_result = result_retry
|
||||
log(f"\n✅ CITA BOOKED (on retry after conn error)! Hora: {hora}")
|
||||
break
|
||||
elif result_retry.startswith("EXCEPTION:"):
|
||||
log(f" → Connection retry also failed — moving to next slot")
|
||||
else:
|
||||
log(f" → Retry result: {result_retry[:100]}")
|
||||
except Exception as e:
|
||||
log(f" → Force-reconnect failed: {e}")
|
||||
|
||||
else:
|
||||
log(f" → Unexpected: {result[:150]}")
|
||||
log(f" Stopping to avoid accidental duplication.")
|
||||
break
|
||||
|
||||
# Small delay between attempts (avoid overwhelming server)
|
||||
time.sleep(0.03)
|
||||
|
||||
# ── PHASE 5: Post-booking verification ─────────────────────
|
||||
if booked_hora:
|
||||
log(f"\nPhase 5: Verifying booking (getHorasCitas — slot should be gone)...")
|
||||
t0 = time.time()
|
||||
verify_result, verify_dj, _, verify_ms = make_request(build_check_body(), "VERIFY")
|
||||
t1 = bench("VERIFY", f"{verify_ms:.0f}ms", t0)
|
||||
|
||||
verify_slots = []
|
||||
if verify_dj:
|
||||
try:
|
||||
slots = json.loads(verify_dj.get('result', '[]'))
|
||||
verify_slots = [s['hora'] for s in slots]
|
||||
except:
|
||||
pass
|
||||
|
||||
if booked_hora not in verify_slots:
|
||||
log(f" ✅ VERIFIED: Slot {booked_hora} is gone from available list — booking confirmed!")
|
||||
log(f" Remaining slots on Oct 19: {verify_slots}")
|
||||
else:
|
||||
log(f" ⚠️ WARNING: Slot {booked_hora} still appears available — booking may not have fully committed")
|
||||
log(f" Slots still showing: {verify_slots}")
|
||||
|
||||
# ── BENCHMARK SUMMARY ───────────────────────────────────────
|
||||
log(f"\n{'='*60}")
|
||||
log(f"BENCHMARK SUMMARY — {len(BENCH)} steps, {len(attempts)} booking attempts")
|
||||
for b in BENCH:
|
||||
log(f" {b['time']} [{b['ms']:7.1f}ms] {b['step']}: {b['detail']}")
|
||||
|
||||
# Persist benchmark log
|
||||
with open(log_file, 'w') as f:
|
||||
f.write(f"Booking log — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||||
f.write(f"NIE: {DOC} Cola: {COLA} Fecha: {FECHA}\n")
|
||||
f.write(f"Tracking: None (clean motivo)\n\n")
|
||||
f.write("=== ATTEMPTS ===\n")
|
||||
for a in attempts:
|
||||
f.write(json.dumps(a, indent=2) + "\n")
|
||||
f.write("\n=== TIMING ===\n")
|
||||
for b in BENCH:
|
||||
f.write(f"{b['time']} [{b['ms']:7.1f}ms] {b['step']}: {b['detail']}\n")
|
||||
log(f"\nLog persisted to: {log_file}")
|
||||
|
||||
if booked_hora:
|
||||
log(f"\n🎉 SUCCESS: Cita for {NOMBRE} on {FECHA} at {booked_hora}")
|
||||
log(f" To view/cancel: use NIF={DOC}, Service=SI (composite code: SIX2491454W)")
|
||||
log(f" Cancel API: anularCitaInsertadaCodigo with codigoCita from obtenerCitaInsertada")
|
||||
sys.exit(0)
|
||||
else:
|
||||
log(f"\n❌ FAILED: All {len(attempts)} attempts exhausted. Competing requests likely grabbed all 9 slots.")
|
||||
log(f"New slots open daily for the next working day — try again tomorrow.")
|
||||
sys.exit(1)
|
||||
927
controllers_full.js
Normal file
927
controllers_full.js
Normal file
@@ -0,0 +1,927 @@
|
||||
angular.module('CitaPreviaAlicante.controllers',[]).
|
||||
controller('iniCitaController', function ($rootScope, $scope, $translate, $filter) {
|
||||
$scope.changeLanguage = function (key) {
|
||||
$translate.use(key);
|
||||
if (key == 'es'){
|
||||
$rootScope.localizador = 'es';
|
||||
}else{
|
||||
$rootScope.localizador = 'ca';
|
||||
}
|
||||
};
|
||||
}).
|
||||
controller('consultarCitaController', function($rootScope, $scope, $window, citaPreviaServices, $translate, $filter){
|
||||
$scope.changeLanguage = function (key) {
|
||||
$translate.use(key);
|
||||
};
|
||||
$scope.tipos_doc = [];
|
||||
$scope.consulta={
|
||||
tipoDocumento:"",
|
||||
nif:"",
|
||||
codigoCita:""
|
||||
};
|
||||
$scope.consultar=function(){
|
||||
if ($scope.formulario.$valid){
|
||||
//alert("Consultando los datos de la cita: " + $scope.consulta.nif);
|
||||
//$window.location.href = './#/verCita/'+$scope.consulta.nif+'/'+$scope.consulta.codigoCita;
|
||||
$window.location.href = './#/verCita/'+$scope.consulta.nif+'/'+$scope.consulta.codigoCita+'/NEW';
|
||||
}else {
|
||||
alert($filter('translate')('CONSULTAR_ERROR_CAMPOS'));
|
||||
//alert("Debe rellenar los campos obligatorios.");
|
||||
}
|
||||
}
|
||||
|
||||
var onFetchCompletedTiposDocumento = function (data){
|
||||
$scope.tipos_doc = data.data.items;
|
||||
}
|
||||
var onFetchError = function(message){
|
||||
alert("Error: "+message);
|
||||
};
|
||||
var getTiposDocumentoController = function (){
|
||||
citaPreviaServices.obtenerTipoDocumento().then(onFetchCompletedTiposDocumento, onFetchError);
|
||||
}
|
||||
|
||||
getTiposDocumentoController();
|
||||
|
||||
}).
|
||||
controller('anularCitaController', function($scope, $window, citaPreviaServices, $translate, $filter){
|
||||
$scope.changeLanguage = function (key) {
|
||||
$translate.use(key);
|
||||
};
|
||||
$scope.tipos_doc = [];
|
||||
$scope.consulta={
|
||||
tipoDocumento:"",
|
||||
nif:"",
|
||||
codigoCita:""
|
||||
};
|
||||
$scope.consultar=function(){
|
||||
if ($scope.formulario.$valid){
|
||||
//alert("Consultando los datos de la cita: " + $scope.consulta.nif);
|
||||
$window.location.href = './#/verAnularCita/'+$scope.consulta.nif+'/'+$scope.consulta.codigoCita;
|
||||
}else {
|
||||
alert($filter('translate')('CONSULTAR_ERROR_CAMPOS'));
|
||||
//alert("Debe rellenar los campos obligatorios.");
|
||||
}
|
||||
}
|
||||
|
||||
var onFetchCompletedTiposDocumento = function (data){
|
||||
$scope.tipos_doc = data.data.items;
|
||||
}
|
||||
var onFetchError = function(message){
|
||||
alert("Error: "+message);
|
||||
};
|
||||
var getTiposDocumentoController = function (){
|
||||
citaPreviaServices.obtenerTipoDocumento().then(onFetchCompletedTiposDocumento, onFetchError);
|
||||
}
|
||||
|
||||
getTiposDocumentoController();
|
||||
}).
|
||||
controller('pedirCitaController', function($rootScope, $scope, $routeParams, $window, citaPreviaServices, $translate, $filter){
|
||||
$scope.changeLanguage = function (key) {
|
||||
$translate.use(key);
|
||||
};
|
||||
/*Variables globales de pantalla*/
|
||||
$scope.servicios = [];
|
||||
$scope.gestiones = [];
|
||||
$scope.horascitas = [];
|
||||
$scope.tipos_doc = [];
|
||||
$scope.max_dias_citas = 0;
|
||||
$scope.colaMotivoValor = 'N';
|
||||
$scope.resumen = {
|
||||
id_servicio:"",
|
||||
servicio:"",
|
||||
id_gestion:"",
|
||||
gestion:"",
|
||||
oficina:"",
|
||||
fecha:"",
|
||||
hora:"",
|
||||
nombre:"",
|
||||
primer_apellido:"",
|
||||
segundo_apellido:"",
|
||||
num_documento:"",
|
||||
telefono:"",
|
||||
email:"",
|
||||
ip:"",
|
||||
tipo_documento:"",
|
||||
impuestos:"",
|
||||
comentario: ""
|
||||
};
|
||||
|
||||
$scope.ip = "0.0.0.0";
|
||||
$scope.resumen.ip = "0.0.0.0";
|
||||
|
||||
$scope.descOficina = "";
|
||||
$scope.urlColaDesc = "";
|
||||
|
||||
/*Variables y funciones del componente calendario*/
|
||||
$scope.hoy = _removeTime(moment(new Date));
|
||||
$scope.day = moment(new Date());
|
||||
$scope.hora;
|
||||
|
||||
$scope.isWeekend = function (day){
|
||||
return day.isSaturday || day.isSunday;
|
||||
};
|
||||
|
||||
$scope.disableWeekend = function (day){
|
||||
//return day.isSaturday || day.isSunday;
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.buscando_citas = function (day){
|
||||
var servicio = $scope.idServicio || '';
|
||||
var cola = $scope.idGestion || '';
|
||||
var fecha = day.date.format("DD-MM-YYYY");
|
||||
if (day.date.isSame($scope.selected)){
|
||||
//day.hayCitas = true;
|
||||
}else{
|
||||
day.hayCitas = false;
|
||||
var manyana = $scope.hoy.clone().add(1, 'd');
|
||||
var ultimoDia = $scope.hoy.clone().add($scope.max_dias_citas, 'd');
|
||||
if (!$scope.disableWeekend(day)) {
|
||||
if (day.date.isSameOrAfter(manyana)) {
|
||||
if (day.date.isSameOrBefore(ultimoDia)) {
|
||||
citaPreviaServices.obtenerCitasDisponible(servicio, cola, fecha).success(
|
||||
function (response){
|
||||
var horasCitasTodo = JSON.parse(response.result);
|
||||
if (horasCitasTodo.length > 0){
|
||||
day.hayCitas = true;
|
||||
}else{
|
||||
day.hayCitas = false;
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
function _removeTime (date) {
|
||||
return date.hour(0).minute(0).second(0).millisecond(0);
|
||||
};
|
||||
|
||||
function _buildMonth(start, month) {
|
||||
$scope.weeks = [];
|
||||
var done = false, date = start.clone(), monthIndex = date.month(), count = 0;
|
||||
while (!done) {
|
||||
$scope.weeks.push({ days: _buildWeek(date.clone(), month) });
|
||||
date.add(1, "w");
|
||||
done = count++ > 2 && monthIndex !== date.month();
|
||||
monthIndex = date.month();
|
||||
}
|
||||
}
|
||||
|
||||
function _buildWeek(date, month) {
|
||||
var days = [];
|
||||
for (var i = 0; i < 7; i++) {
|
||||
days.push({
|
||||
name: date.format("dd").substring(0, 1),
|
||||
number: date.date(),
|
||||
isCurrentMonth: date.month() === month.month(),
|
||||
isToday: date.isSame(new Date(), "day"),
|
||||
date: date,
|
||||
isSaturday: date.weekday() === 6,
|
||||
isSunday: date.weekday() === 0,
|
||||
hayCitas: false
|
||||
});
|
||||
$scope.buscando_citas (days[i]);
|
||||
date = date.clone();
|
||||
date.add(1, "d");
|
||||
}
|
||||
return days;
|
||||
}
|
||||
|
||||
$scope.selected = _removeTime($scope.selected || moment());
|
||||
$scope.month = $scope.selected.clone();
|
||||
$scope.monthVisual = $scope.selected.clone();
|
||||
$scope.firstDay = $scope.selected.clone().startOf("month");
|
||||
$scope.unaFecha = $scope.selected.clone();
|
||||
|
||||
_buildMonth($scope.firstDay.startOf("isoWeek"), $scope.month);
|
||||
|
||||
$scope.pintar = function (day){
|
||||
if (day.date.isSame($scope.selected)){
|
||||
return false;
|
||||
}else{
|
||||
return day.hayCitas;
|
||||
}
|
||||
|
||||
/*var esHabil = false;
|
||||
var manyana = $scope.hoy.clone().add(1, 'd');
|
||||
var ultimoDia = $scope.hoy.clone().add($scope.max_dias_citas, 'd');
|
||||
if (!$scope.disableWeekend(day)) {
|
||||
if (day.date.isSameOrAfter(manyana)) {
|
||||
if (day.date.isSameOrBefore(ultimoDia)) {
|
||||
esHabil = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return esHabil;*/
|
||||
};
|
||||
|
||||
|
||||
$scope.diaHabil = function (day){
|
||||
|
||||
/*if (!day.hayCitas){
|
||||
resetearHorasCitas();
|
||||
}*/
|
||||
return day.hayCitas;
|
||||
};
|
||||
|
||||
$scope.select = function(day) {
|
||||
$scope.selected = day.date;
|
||||
$scope.textopp = day.date;
|
||||
getCitasPrueba ($scope.idServicio,$scope.idGestion,$scope.selected.format("DD-MM-YYYY"));
|
||||
$scope.resumen.fecha = $scope.selected.format("DD-MM-YYYY");
|
||||
};
|
||||
|
||||
$scope.next = function() {
|
||||
var next = $scope.month.clone();
|
||||
next.add(1,'M').startOf('month');
|
||||
$scope.month.add(1,'M');
|
||||
_buildMonth(next.startOf("isoWeek"), $scope.month);
|
||||
$scope.monthVisual = $scope.month.clone();
|
||||
};
|
||||
|
||||
$scope.previous = function() {
|
||||
var previous = $scope.month.clone();
|
||||
previous.subtract(1,'M').startOf('month');
|
||||
$scope.month.subtract(1,'M');
|
||||
_buildMonth(previous.startOf("isoWeek"), $scope.month);
|
||||
$scope.monthVisual = $scope.month.clone();
|
||||
};
|
||||
|
||||
function resetearHorasCitas (){
|
||||
while ($scope.horascitas.length > 0) {
|
||||
$scope.horascitas.pop();
|
||||
}
|
||||
}
|
||||
|
||||
function getCitasPrueba (servicio, cola, fecha){
|
||||
resetearHorasCitas();
|
||||
getCitasDisponibles(servicio, cola, fecha);
|
||||
//serviciosCitas.obtenerCitasDisponible(servicio,cola,fecha).then(onFetchCompletedHorasCitas,onFetchError);
|
||||
//var horas = ["08:00","08:30","09:00","09:30","10:00","10:30","11:00","11:30","12:00","12:30","13:00","13:30","14:00"];
|
||||
//return horas;
|
||||
}
|
||||
|
||||
$scope.updateHoras = function (hora){
|
||||
$scope.resumen.hora = hora;
|
||||
$scope.hora = hora;
|
||||
};
|
||||
|
||||
$scope.issame = function (hora){
|
||||
if ($scope.resumen.hora === hora){
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.updateGestiones = function(){
|
||||
//alert($scope.idServicio);
|
||||
citaPreviaServices.getGestiones($scope.idServicio).then(onFetchCompletedGestiones, onFetchError);
|
||||
resetearHorasCitas();
|
||||
// Ahora debo borrar los datos temporales de la Cita
|
||||
$scope.resumen.oficina = "";
|
||||
$scope.resumen.id_gestion = "";
|
||||
$scope.resumen.gestion = "";
|
||||
$scope.resumen.fecha = "";
|
||||
$scope.resumen.hora = "";
|
||||
// Borro datos generales
|
||||
$scope.idGestion = "";
|
||||
$scope.descOficina = "";
|
||||
$scope.urlColaDesc = "";
|
||||
|
||||
$scope.max_dias_citas = 0;
|
||||
var encontrado = false;
|
||||
$scope.servicios.forEach( function (valor, indice, array){
|
||||
if (array[indice].IdServicio == $scope.idServicio){
|
||||
encontrado = true;
|
||||
$scope.resumen.id_servicio = $scope.idServicio;
|
||||
$scope.resumen.servicio = array[indice].Nombre;
|
||||
$scope.resumen.oficina = array[indice].Direccion;
|
||||
$scope.descOficina = array[indice].Direccion;
|
||||
}
|
||||
});
|
||||
if (encontrado == false){
|
||||
$scope.descOficina = "";
|
||||
$scope.resumen.oficina = "";
|
||||
$scope.resumen.id_servicio = "";
|
||||
$scope.resumen.servicio = "";
|
||||
$scope.resumen.id_gestion = "";
|
||||
$scope.resumen.gestion = "";
|
||||
$scope.resumen.fecha = "";
|
||||
$scope.max_dias_citas = 0;
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
$scope.updateOficina = function(){
|
||||
var encontrado = false;
|
||||
|
||||
// Al seleccionar Cola, borra Horas seleccionadas de anteriores Colas
|
||||
resetearHorasCitas();
|
||||
// Ahora debo borrar los datos temporales de la Cita
|
||||
$scope.resumen.fecha = "";
|
||||
$scope.resumen.hora = "";
|
||||
|
||||
//*****************************************************************
|
||||
//*** Condiciones temporales para acceder al SAIC (A, B, E y F) ***
|
||||
//*****************************************************************
|
||||
getIpCliente();
|
||||
if ((($scope.idServicio == "S") && ($scope.idGestion == "A")) ||
|
||||
(($scope.idServicio == "S") && ($scope.idGestion == "B")) ||
|
||||
(($scope.idServicio == "S") && ($scope.idGestion == "E")) ||
|
||||
(($scope.idServicio == "S") && ($scope.idGestion == "F"))) {
|
||||
if ((($scope.ip == "195.55.43.202") || ($scope.ip == "0.0.0.0") ||
|
||||
($scope.ip == "217.127.49.149") || ($scope.ip == "80.35.23.0") ||
|
||||
($scope.ip == "195.235.184.226") || ($scope.ip == "88.12.63.18") ||
|
||||
($scope.ip == "88.12.63.19") || ($scope.ip == "217.125.90.59"))) {
|
||||
// Permito el acceso
|
||||
}else{
|
||||
$scope.idGestion = "";
|
||||
//alert("Pida Cita para esta Gestion llamando al 010 o al 966900886, de lunes a viernes, de 9:00 h. a 18:00 h.");
|
||||
alert("Pida Cita para esta Gestion llamando al 010 o al 966900886, de lunes a viernes, de 9:00 h. a 18:00 h." + "\n[" + $scope.ip + "]");
|
||||
return;
|
||||
}
|
||||
}
|
||||
//*******************************************************
|
||||
//*** Condiciones temporales para cola de Inmigracion ***
|
||||
//*******************************************************
|
||||
if ($scope.idServicio == "I") {
|
||||
if ((($scope.ip == "195.55.43.202") || ($scope.ip == "0.0.0.0") ||
|
||||
($scope.ip == "217.127.49.149") || ($scope.ip == "80.35.23.0") ||
|
||||
($scope.ip == "195.235.184.226") || ($scope.ip == "88.12.63.18") ||
|
||||
($scope.ip == "88.12.63.19") || ($scope.ip == "217.125.90.59"))) {
|
||||
// Permito el acceso
|
||||
}else{
|
||||
$scope.idGestion = "";
|
||||
//alert("La cita NO se ha gestionado. La CITA PREVIA para Inmigración, se realiza exclusivamente llamando al teléfono 965230267, de lunes a viernes, de 9:00 a 11:00");
|
||||
alert("La cita NO se ha gestionado. La CITA PREVIA para Inmigración, se realiza exclusivamente llamando al teléfono 965230267, de lunes a viernes, de 9:00 a 11:00" + "\n[" + $scope.ip + "]");
|
||||
return;
|
||||
}
|
||||
}
|
||||
//************************************************************
|
||||
|
||||
$scope.max_dias_citas = 0;
|
||||
$scope.gestiones.forEach( function (valor, indice, array){
|
||||
if (array[indice].IdCola == $scope.idGestion){
|
||||
encontrado = true;
|
||||
$scope.resumen.id_gestion = $scope.idGestion;
|
||||
$scope.resumen.gestion = array[indice].Nombre;
|
||||
$scope.urlColaDesc = array[indice].Descripcion;
|
||||
$scope.max_dias_citas = array[indice].MaxCita;
|
||||
$scope.colaMotivoValor = array[indice].Motivo;
|
||||
//alert($scope.max_dias_citas);
|
||||
_buildMonth($scope.firstDay.startOf("isoWeek"), $scope.month);
|
||||
}
|
||||
});
|
||||
if (encontrado == false){
|
||||
$scope.resumen.id_gestion = "";
|
||||
$scope.resumen.gestion = "";
|
||||
$scope.resumen.fecha = "";
|
||||
$scope.max_dias_citas = 0;
|
||||
$scope.colaMotivoValor = 'N'
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
var onFetchError = function(message){
|
||||
//alert("Error: "+message);
|
||||
$scope.ip = "0.0.0.0";
|
||||
$scope.resumen.ip = "0.0.0.0";
|
||||
};
|
||||
|
||||
var onFetchCompletedServicios = function(data){
|
||||
$scope.servicios = data.data.items;
|
||||
};
|
||||
|
||||
var onFetchCompletedTiposDocumento = function (data){
|
||||
$scope.tipos_doc = data.data.items;
|
||||
}
|
||||
|
||||
var onFetchCompletedGestiones = function (data){
|
||||
//$scope.gestiones;
|
||||
$scope.gestiones = [];
|
||||
elementos = data.data.items;
|
||||
for (i=0; i<elementos.length; i++) {
|
||||
//if (elementos[i].CitaPrevia == 'S'){
|
||||
if ((elementos[i].CitaPrevia == 'S') && (elementos[i].Activa == 'S')) {
|
||||
$scope.gestiones.push(elementos[i]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var onFetchCompletedHorasCitas = function (data){
|
||||
var horasCitasTodo = JSON.parse(data.data.result);
|
||||
for (var k in horasCitasTodo){
|
||||
$scope.horascitas.push(horasCitasTodo[k].hora);
|
||||
};
|
||||
}
|
||||
|
||||
var onFetchCompletedIp = function (response){
|
||||
$scope.ip = response.data.ip;
|
||||
$scope.resumen.ip = response.data.ip;
|
||||
//alert($scope.ip);
|
||||
}
|
||||
|
||||
var onFetchCompletedCitaPrevia = function (response){
|
||||
alert(response.data.result);
|
||||
if (response.data.result == 'Cita insertada'){
|
||||
//$window.location.href = './#/verCita/'+$scope.resumen.num_documento+'/'+$scope.resumen.id_servicio+$scope.resumen.id_gestion+$scope.resumen.num_documento;
|
||||
$window.location.href = './#/verCita/'+$scope.resumen.num_documento+'/'+$scope.resumen.id_servicio+$scope.resumen.id_gestion+$scope.resumen.num_documento+'/OLD';
|
||||
}
|
||||
/*var resultado = response.data.correcto;
|
||||
var mensaje = response.data.mensaje;
|
||||
var idCita = response.data.id_cita;
|
||||
alert(resultado);
|
||||
alert(mensaje);
|
||||
alert(idCita);*/
|
||||
};
|
||||
|
||||
var getServiciosController = function(){
|
||||
citaPreviaServices.getServicios().then(onFetchCompletedServicios,onFetchError);
|
||||
};
|
||||
|
||||
var getTiposDocumentoController = function (){
|
||||
citaPreviaServices.obtenerTipoDocumento().then(onFetchCompletedTiposDocumento, onFetchError);
|
||||
}
|
||||
|
||||
var getCitasDisponibles = function (servicio, cola, fecha){
|
||||
citaPreviaServices.obtenerCitasDisponible(servicio, cola, fecha).then(onFetchCompletedHorasCitas,onFetchError);
|
||||
}
|
||||
|
||||
var getIpCliente = function (){
|
||||
citaPreviaServices.obtenerIp().then(onFetchCompletedIp,onFetchError)
|
||||
}
|
||||
|
||||
var insertaCitaPreviaWeb = function (){
|
||||
citaPreviaServices.insertarCitaPrevia($scope.resumen, $scope.colaMotivoValor, $scope.idProcedencia).then(onFetchCompletedCitaPrevia,onFetchError)
|
||||
}
|
||||
|
||||
/*function validar_nif (value){
|
||||
|
||||
var validChars = 'TRWAGMYFPDXBNJZSQVHLCKET';
|
||||
var nifRexp = /^[0-9]{8}[TRWAGMYFPDXBNJZSQVHLCKET]{1}$/i;
|
||||
var nieRexp = /^[XYZ]{1}[0-9]{7}[TRWAGMYFPDXBNJZSQVHLCKET]{1}$/i;
|
||||
var str = value.toString(
|
||||
).toUpperCase();
|
||||
|
||||
if (!nifRexp.test(str) && !nieRexp.test(str)) return false;
|
||||
|
||||
var nie = str
|
||||
.replace(/^[X]/, '0')
|
||||
.replace(/^[Y]/, '1')
|
||||
.replace(/^[Z]/, '2');
|
||||
|
||||
var letter = str.substr(-1);
|
||||
var charIndex = parseInt(nie.substr(0, 8)) % 23;
|
||||
|
||||
if (validChars.charAt(charIndex) === letter) return true;
|
||||
|
||||
return false;
|
||||
};*/
|
||||
|
||||
$scope.validarnif = function (value){
|
||||
if (value == undefined || value == ''){
|
||||
return true;
|
||||
}else{
|
||||
if (value.indexOf(' ')>0) return false;
|
||||
if ($scope.resumen.tipo_documento=='1'){
|
||||
return validateDNI(value);
|
||||
}else{
|
||||
if ($scope.resumen.tipo_documento=='2' || $scope.resumen.tipo_documento=='4'){
|
||||
return validateCIF(value);
|
||||
}else{
|
||||
if ( $scope.resumen.tipo_documento=='3' ){
|
||||
return true;
|
||||
}else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$scope.validarTuplaDocumento=function(){
|
||||
if ($scope.resumen.num_documento != null && $scope.resumen.tipo_documento !=null){
|
||||
return $scope.validarnif($scope.resumen.num_documento);
|
||||
}else{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.emailCorrecto=function (){
|
||||
/*if ($scope.resumen.email != undefined && $scope.resumen.email!=''){
|
||||
return false;
|
||||
}else{
|
||||
if ( $scope.idServicio == 'H' || $scope.idServicio == 'T'){
|
||||
return false;
|
||||
}else{
|
||||
return true;
|
||||
}
|
||||
}*/
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.esBono=function (){
|
||||
if ($scope.idServicio == 'L') {
|
||||
return true;
|
||||
}else{
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
$scope.colaMotivo=function (){
|
||||
/*if ($scope.colaMotivoValor == 'S'){
|
||||
return false;
|
||||
}else{
|
||||
return true;
|
||||
}*/
|
||||
return false;
|
||||
};
|
||||
|
||||
$scope.getNombreOficina=function(IdServicio){
|
||||
var nombreTraducido = '';
|
||||
if (IdServicio!=undefined && IdServicio!=''){
|
||||
$scope.servicios.forEach( function (valor, indice, array){
|
||||
if (array[indice].IdServicio == IdServicio){
|
||||
if ($rootScope.localizador == 'es') {
|
||||
nombreTraducido = array[indice].Direccion;
|
||||
}else{
|
||||
nombreTraducido = array[indice].DireccionV;
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
return nombreTraducido;
|
||||
}
|
||||
|
||||
$scope.getNombreServicio = function(IdServicio){
|
||||
var nombreTraducido = '';
|
||||
if (IdServicio!=undefined && IdServicio!='') {
|
||||
$scope.servicios.forEach(function (valor, indice, array) {
|
||||
if (array[indice].IdServicio == IdServicio) {
|
||||
if ($rootScope.localizador == 'es') {
|
||||
nombreTraducido = array[indice].Nombre;
|
||||
} else {
|
||||
nombreTraducido = array[indice].NombreV;
|
||||
}
|
||||
}
|
||||
;
|
||||
});
|
||||
}
|
||||
return nombreTraducido;
|
||||
};
|
||||
|
||||
$scope.getNombreGestion = function(IdGestion){
|
||||
var nombreTraducido = '';
|
||||
if (IdGestion!=undefined && IdGestion!='') {
|
||||
$scope.gestiones.forEach(function (valor, indice, array) {
|
||||
if (array[indice].IdCola == IdGestion) {
|
||||
if ($rootScope.localizador == 'es') {
|
||||
nombreTraducido = array[indice].Nombre;
|
||||
} else {
|
||||
nombreTraducido = array[indice].NombreV;
|
||||
}
|
||||
}
|
||||
;
|
||||
});
|
||||
}
|
||||
return nombreTraducido;
|
||||
};
|
||||
|
||||
$scope.pedirCita=function(event){
|
||||
|
||||
const button = (event.srcElement.disabled === undefined) ? event.srcElement.parentElement : event.srcElement;
|
||||
button.setAttribute('disabled', true);
|
||||
setTimeout(function () {
|
||||
button.removeAttribute('disabled');
|
||||
}, 3000);
|
||||
|
||||
if ($scope.formulario.$valid){
|
||||
|
||||
if ($scope.resumen.fecha == undefined || $scope.resumen.fecha == '' || $scope.resumen.hora == undefined || $scope.resumen.hora == ''){
|
||||
alert($filter('translate')('PEDIR_ERROR_FECHA_HORA'));
|
||||
}else {
|
||||
if ($scope.colaMotivoValor == 'S' && ($scope.resumen.comentario == undefined || $scope.resumen.comentario == '')){
|
||||
alert($filter('translate')('PEDIR_ERROR_CAMPOS'));
|
||||
/*alert("Debe rellenar los campos obligatorios y/o corregir los datos erróneos.");*/
|
||||
}else {
|
||||
if ($scope.resumen.id_servicio == 'L' && $scope.resumen.impuestos != 'S') {
|
||||
alert($filter('translate')('ERROR_LABEL_IMPUESTOS'));
|
||||
}else {
|
||||
insertaCitaPreviaWeb();
|
||||
}
|
||||
}
|
||||
//alert("Consultando los datos de la cita: " + $scope.resumen.num_documento);
|
||||
//$window.location.href = './#/verCita/'+$scope.consulta.nif+'/'+$scope.consulta.codigoCita;
|
||||
}
|
||||
}else {
|
||||
//alert("Debe rellenar los campos obligatorios y/o corregir los datos erróneos.");
|
||||
alert($filter('translate')('PEDIR_ERROR_CAMPOS'));
|
||||
}
|
||||
}
|
||||
|
||||
getServiciosController();
|
||||
getTiposDocumentoController();
|
||||
getIpCliente();
|
||||
|
||||
$scope.datosCita = {};
|
||||
$scope.selected = undefined;
|
||||
|
||||
}).controller('verCitaController', function ($rootScope, $scope, $routeParams, citaPreviaServices, $translate, $filter){
|
||||
$scope.changeLanguage = function (key) {
|
||||
$translate.use(key);
|
||||
};
|
||||
$scope.registro_servicio;
|
||||
$scope.registro_cola;
|
||||
$scope.ocultarVentana = false;
|
||||
$scope.cita = {
|
||||
idCita:"",
|
||||
idServicio:"",
|
||||
servicio:"",
|
||||
gestion:"",
|
||||
oficina:"",
|
||||
fecha:"",
|
||||
hora:"",
|
||||
codCita:"",
|
||||
nombre:"",
|
||||
apellidos:"",
|
||||
nif:"",
|
||||
telefono:"",
|
||||
email:""
|
||||
};
|
||||
$scope.cita.nif = $routeParams.idNif;
|
||||
|
||||
$scope.getNombreServicio = function (){
|
||||
if ($scope.registro_servicio!=undefined){
|
||||
if ($rootScope.localizador == 'es') {
|
||||
return $scope.registro_servicio.Nombre;
|
||||
} else {
|
||||
return $scope.registro_servicio.NombreV;
|
||||
}
|
||||
}else{
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getNombreCola = function (){
|
||||
//$scope.registro_cola = gestiones;
|
||||
if ($scope.registro_cola!=undefined){
|
||||
if ($rootScope.localizador == 'es') {
|
||||
return $scope.registro_cola.Nombre;
|
||||
}else{
|
||||
return $scope.registro_cola.NombreV;
|
||||
}
|
||||
}else{
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getOficina = function (){
|
||||
if ($scope.registro_servicio!=undefined){
|
||||
if ($rootScope.localizador == 'es') {
|
||||
return $scope.registro_servicio.Direccion;
|
||||
} else {
|
||||
return $scope.registro_servicio.DireccionV;
|
||||
}
|
||||
}else{
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
//$scope.cita.codigoCita = $routeParams.idCita;
|
||||
//citaPreviaServices.consultarCita('',$routeParams.idNif,$routeParams.idCita).success(function(response){
|
||||
citaPreviaServices.consultarCita('',$routeParams.idNif,$routeParams.idCita,$routeParams.idTipoBusqueda).success(function(response){
|
||||
var resultado = response.result;
|
||||
resultado = JSON.stringify(eval("("+resultado.replace(/\'/g,'')+")"));
|
||||
$scope.cita = JSON.parse(resultado);
|
||||
if ($scope.cita.servicio == null){
|
||||
$scope.ocultarVentana = true;
|
||||
}else{
|
||||
citaPreviaServices.obtenerServicio($scope.cita.servicio).success(function(response){
|
||||
$scope.registro_servicio = response;
|
||||
$scope.cita.idServicio = $scope.cita.servicio;
|
||||
//$scope.cita.servicio = response.Nombre;
|
||||
if ($rootScope.localizador == 'es') {
|
||||
$scope.cita.servicio = response.Nombre;
|
||||
$scope.cita.oficina = response.Direccion;
|
||||
} else {
|
||||
$scope.cita.servicio = response.NombreV;
|
||||
$scope.cita.oficina = response.DireccionV;
|
||||
}
|
||||
|
||||
citaPreviaServices.getGestiones($scope.cita.idServicio).success(function(response){
|
||||
var gestiones = [];
|
||||
gestiones = response.items;
|
||||
|
||||
gestiones.forEach( function (valor, indice, array){
|
||||
if (array[indice].IdCola == $scope.cita.gestion){
|
||||
$scope.registro_cola = array[indice];
|
||||
if ($rootScope.localizador == 'es') {
|
||||
$scope.cita.gestion = array[indice].Nombre;
|
||||
}else{
|
||||
$scope.cita.gestion = array[indice].NombreV;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
/*$scope.cita.servicio = resultado.nif.replace(/\'/g,'');
|
||||
$scope.cita.gestion = resultado.codCita.replace(/\'/g,'');;*/
|
||||
|
||||
});
|
||||
|
||||
function PrintElem(elem)
|
||||
{
|
||||
var mywindow = window.open('', 'PRINT', 'height=400,width=600');
|
||||
|
||||
mywindow.document.write('<html><head><title>' + document.title + '</title>');
|
||||
mywindow.document.write('</head><body >');
|
||||
mywindow.document.write('<h1>' + document.title + '</h1>');
|
||||
mywindow.document.write(document.getElementById(elem).innerHTML);
|
||||
mywindow.document.write('</body></html>');
|
||||
|
||||
mywindow.document.close(); // necessary for IE >= 10
|
||||
mywindow.focus(); // necessary for IE >= 10*/
|
||||
|
||||
mywindow.print();
|
||||
mywindow.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$scope.imprimirCita = function (){
|
||||
//alert("Imprimir cita");
|
||||
PrintElem("paginaImpresa");
|
||||
}
|
||||
|
||||
}).
|
||||
controller('anularVerCitaController', function ($rootScope, $scope, $routeParams,$window, citaPreviaServices, $translate, $filter){
|
||||
$scope.changeLanguage = function (key) {
|
||||
$translate.use(key);
|
||||
};
|
||||
$scope.ocultarVentana = false;
|
||||
$scope.registro_servicio;
|
||||
$scope.registro_cola;
|
||||
$scope.cita = {
|
||||
idCita:"",
|
||||
idServicio:"",
|
||||
servicio:"",
|
||||
idGestion:"",
|
||||
gestion:"",
|
||||
oficina:"",
|
||||
fecha:"",
|
||||
hora:"",
|
||||
codCita:"",
|
||||
nombre:"",
|
||||
apellidos:"",
|
||||
nif:"",
|
||||
telefono:"",
|
||||
email:""
|
||||
};
|
||||
$scope.cita.nif = $routeParams.idNif;
|
||||
|
||||
$scope.getNombreServicio = function (){
|
||||
if ($scope.registro_servicio!=undefined){
|
||||
if ($rootScope.localizador == 'es') {
|
||||
return $scope.registro_servicio.Nombre;
|
||||
} else {
|
||||
return $scope.registro_servicio.NombreV;
|
||||
}
|
||||
}else{
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getNombreCola = function (){
|
||||
//$scope.registro_cola = gestiones;
|
||||
if ($scope.registro_cola!=undefined){
|
||||
if ($rootScope.localizador == 'es') {
|
||||
return $scope.registro_cola.Nombre;
|
||||
}else{
|
||||
return $scope.registro_cola.NombreV;
|
||||
}
|
||||
}else{
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
$scope.getOficina = function (){
|
||||
if ($scope.registro_servicio!=undefined){
|
||||
if ($rootScope.localizador == 'es') {
|
||||
return $scope.registro_servicio.Direccion;
|
||||
} else {
|
||||
return $scope.registro_servicio.DireccionV;
|
||||
}
|
||||
}else{
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
//$scope.cita.codigoCita = $routeParams.idCita;
|
||||
//citaPreviaServices.consultarCita('',$routeParams.idNif,$routeParams.idCita).success(function(response){
|
||||
citaPreviaServices.consultarCita('',$routeParams.idNif,$routeParams.idCita,'NEW').success(function(response){
|
||||
var resultado = response.result;
|
||||
resultado = JSON.stringify(eval("("+resultado.replace(/\'/g,'')+")"));
|
||||
$scope.cita = JSON.parse(resultado);
|
||||
if ($scope.cita.servicio == null){
|
||||
$scope.ocultarVentana = true;
|
||||
}else{
|
||||
citaPreviaServices.obtenerServicio($scope.cita.servicio).success(function(response){
|
||||
$scope.registro_servicio = response;
|
||||
$scope.cita.servicio = $scope.cita.servicio;
|
||||
if ($rootScope.localizador == 'es') {
|
||||
//$scope.cita.servicio = response.Nombre;
|
||||
$scope.cita.oficina = response.Direccion;
|
||||
} else {
|
||||
//$scope.cita.servicio = response.NombreV;
|
||||
$scope.cita.oficina = response.DireccionV;
|
||||
}
|
||||
citaPreviaServices.getGestiones($scope.cita.servicio).success(function(response){
|
||||
var gestiones = [];
|
||||
gestiones = response.items;
|
||||
gestiones.forEach( function (valor, indice, array){
|
||||
if (array[indice].IdCola == $scope.cita.gestion){
|
||||
$scope.cita.idGestion = $scope.cita.gestion;
|
||||
$scope.registro_cola = array[indice];
|
||||
if ($rootScope.localizador == 'es') {
|
||||
$scope.cita.gestion = array[indice].Nombre;
|
||||
}else{
|
||||
$scope.cita.gestion = array[indice].NombreV;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
var onFetchError = function(message){
|
||||
alert("Error: "+message);
|
||||
};
|
||||
|
||||
var onFetchCompletedAnularCitaPrevia = function (response){
|
||||
if (response.data.result == 'Cita anulada'){
|
||||
alert($filter('translate')('ANULAR_ANULADA_OK'));
|
||||
//alert("La cita ha sito anulada correctamente");
|
||||
$window.location.href = './#/inicio';
|
||||
}else{
|
||||
alert($filter('translate')('ANULAR_ANULADA_ERROR'));
|
||||
//alert("Error al anular la cita previa. Consulte ");
|
||||
}
|
||||
/*var resultado = response.data.correcto;
|
||||
var mensaje = response.data.mensaje;
|
||||
var idCita = response.data.id_cita;
|
||||
alert(resultado);
|
||||
alert(mensaje);
|
||||
alert(idCita);*/
|
||||
};
|
||||
|
||||
var anularCitaPreviaWeb = function (){
|
||||
//citaPreviaServices.anularCita($scope.cita.servicio,$scope.cita.idGestion,$scope.cita.nif).then(onFetchCompletedAnularCitaPrevia,onFetchError)
|
||||
citaPreviaServices.anularCita($scope.cita.servicio,$scope.cita.idGestion,$scope.cita.nif, $routeParams.idCita).then(onFetchCompletedAnularCitaPrevia,onFetchError)
|
||||
}
|
||||
|
||||
$scope.anularCitaPrevia=function(){
|
||||
anularCitaPreviaWeb();
|
||||
}
|
||||
|
||||
/*$scope.cita.servicio = resultado.nif.replace(/\'/g,'');
|
||||
$scope.cita.gestion = resultado.codCita.replace(/\'/g,'');;*/
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
angular.module('F1FeederApp.controllers', []).
|
||||
/* Drivers controller */
|
||||
controller('driversController', function($scope, ergastAPIservice) {
|
||||
$scope.nameFilter = null;
|
||||
$scope.driversList = [];
|
||||
$scope.searchFilter = function (driver) {
|
||||
var re = new RegExp($scope.nameFilter, 'i');
|
||||
return !$scope.nameFilter || re.test(driver.Driver.givenName) || re.test(driver.Driver.familyName);
|
||||
};
|
||||
|
||||
ergastAPIservice.getDrivers().success(function (response) {
|
||||
//Digging into the response to get the relevant data
|
||||
$scope.driversList = response.MRData.StandingsTable.StandingsLists[0].DriverStandings;
|
||||
});
|
||||
}).
|
||||
|
||||
/* Driver controller */
|
||||
controller('driverController', function($scope, $routeParams, ergastAPIservice) {
|
||||
$scope.id = $routeParams.id;
|
||||
$scope.races = [];
|
||||
$scope.driver = null;
|
||||
|
||||
ergastAPIservice.getDriverDetails($scope.id).success(function (response) {
|
||||
$scope.driver = response.MRData.StandingsTable.StandingsLists[0].DriverStandings[0];
|
||||
});
|
||||
|
||||
ergastAPIservice.getDriverRaces($scope.id).success(function (response) {
|
||||
$scope.races = response.MRData.RaceTable.Races;
|
||||
});
|
||||
});
|
||||
315
run_benchmarks.py
Normal file
315
run_benchmarks.py
Normal file
@@ -0,0 +1,315 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Benchmarking Test Runner for /tmp/book_final.py
|
||||
→ Exercises ACTUAL code paths from the script (no modifications to the script)
|
||||
→ Measures p50, p95, p99 across ALL phases
|
||||
→ Non-hammering: 3s ramp-up, 500ms between iterations, 10 samples per phase
|
||||
"""
|
||||
|
||||
import sys, os, json, time, ssl, http.client
|
||||
from datetime import datetime
|
||||
from statistics import median, quantiles
|
||||
|
||||
# ── Load the script's functions (no execution of phases) ──
|
||||
source = open('/tmp/book_final.py').read()
|
||||
defs = source.split('# ── INITIAL LOG')[0]
|
||||
_ns = {}
|
||||
exec(defs, _ns)
|
||||
|
||||
HOST = _ns['HOST']
|
||||
PORT = _ns['PORT']
|
||||
ENDPOINT = _ns['ENDPOINT']
|
||||
CT = _ns['CT']
|
||||
HEADERS = _ns['HEADERS']
|
||||
build_booking_body = _ns['build_booking_body']
|
||||
build_check_body = _ns['build_check_body']
|
||||
|
||||
SAMPLES = 10
|
||||
RAMP_DELAY = 3.0 # seconds before starting (let API settle)
|
||||
INTER_DELAY = 0.5 # seconds between samples (non-hammering)
|
||||
|
||||
def percentile(sorted_data, pct):
|
||||
"""Simple percentile: linear interpolation."""
|
||||
if not sorted_data:
|
||||
return 0
|
||||
if len(sorted_data) == 1:
|
||||
return sorted_data[0]
|
||||
k = (len(sorted_data) - 1) * (pct / 100)
|
||||
f = int(k)
|
||||
c = f + 1
|
||||
if c >= len(sorted_data):
|
||||
return sorted_data[-1]
|
||||
return sorted_data[f] + (sorted_data[c] - sorted_data[f]) * (k - f)
|
||||
|
||||
def fmt(data):
|
||||
"""Format timing stats."""
|
||||
s = sorted(data)
|
||||
p50 = percentile(s, 50)
|
||||
p95 = percentile(s, 95)
|
||||
p99 = percentile(s, 99)
|
||||
mn = min(data)
|
||||
mx = max(data)
|
||||
avg = sum(data) / len(data)
|
||||
return f"min={mn:.0f}ms p50={p50:.0f}ms p95={p95:.0f}ms p99={p99:.0f}ms max={mx:.0f}ms avg={avg:.0f}ms (n={len(data)})"
|
||||
|
||||
def tls_connect():
|
||||
"""Establish a fresh TLS connection (Phase 2 logic)."""
|
||||
t0 = time.time()
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
conn.connect()
|
||||
return conn, (time.time() - t0) * 1000
|
||||
|
||||
def do_getHorasCitas(conn):
|
||||
"""Call getHorasCitas (Phase 3b logic, via make_request equivalent)."""
|
||||
body = build_check_body()
|
||||
t0 = time.time()
|
||||
conn.request("POST", ENDPOINT, body=body, headers=HEADERS)
|
||||
resp = conn.getresponse()
|
||||
raw = resp.read().decode()
|
||||
dt = (time.time() - t0) * 1000
|
||||
dj = json.loads(raw)
|
||||
result = dj.get('result', '')
|
||||
return result, resp.status, dt
|
||||
|
||||
def do_insertarCita(conn, hora="16:00"):
|
||||
"""Call insertarCitaPrevia (Phase 4 logic)."""
|
||||
body = build_booking_body(hora)
|
||||
t0 = time.time()
|
||||
conn.request("POST", ENDPOINT, body=body, headers=HEADERS)
|
||||
resp = conn.getresponse()
|
||||
raw = resp.read().decode()
|
||||
dt = (time.time() - t0) * 1000
|
||||
dj = json.loads(raw)
|
||||
result = dj.get('result', '')
|
||||
return result, resp.status, dt
|
||||
|
||||
def measure_api_roundtrip(conn, body_text):
|
||||
"""Measure just the HTTP round-trip for a given body (Phase 4 measurement)."""
|
||||
t0 = time.time()
|
||||
conn.request("POST", ENDPOINT, body=body_text, headers=HEADERS)
|
||||
resp = conn.getresponse()
|
||||
resp.read().decode()
|
||||
return (time.time() - t0) * 1000
|
||||
|
||||
def measure_body_build(hora):
|
||||
"""Measure JSON serialization of booking body (Phase 4 prep)."""
|
||||
t0 = time.time()
|
||||
for _ in range(100): # micro-benchmark: 100 iterations for precision
|
||||
build_booking_body(hora)
|
||||
return (time.time() - t0) * 1000 / 100 # avg per call
|
||||
|
||||
def measure_check_body_build():
|
||||
"""Measure JSON serialization of check body (Phase 3b prep)."""
|
||||
t0 = time.time()
|
||||
for _ in range(100):
|
||||
build_check_body()
|
||||
return (time.time() - t0) * 1000 / 100
|
||||
|
||||
def measure_json_parse_getHorasCitas(raw):
|
||||
"""Measure JSON parsing of getHorasCitas response."""
|
||||
t0 = time.time()
|
||||
for _ in range(100):
|
||||
json.loads(raw)
|
||||
return (time.time() - t0) * 1000 / 100
|
||||
|
||||
def measure_json_parse_insertar(raw):
|
||||
"""Measure JSON parsing of insertarCita response."""
|
||||
t0 = time.time()
|
||||
for _ in range(100):
|
||||
json.loads(raw)
|
||||
return (time.time() - t0) * 1000 / 100
|
||||
|
||||
# ── BENCHMARK RUNNER ─────────────────────────────────────────────────────────────────
|
||||
print("=" * 70)
|
||||
print(" BENCHMARKING RUNNER — /tmp/book_final.py")
|
||||
print(" Exercising ACTUAL code paths, measuring p50/p95/p99")
|
||||
print(f" Samples per phase: {SAMPLES} | Inter-delay: {INTER_DELAY}s | Non-hammering")
|
||||
print("=" * 70)
|
||||
print(f" Started: {datetime.now().strftime('%H:%M:%S.%f')[:-3]} CEST")
|
||||
print()
|
||||
|
||||
# Pre-warm: establish first connection
|
||||
print(" Pre-warming TLS connection...")
|
||||
conn, t0 = tls_connect()
|
||||
print(f" ✅ First connect: {t0:.0f}ms")
|
||||
print()
|
||||
|
||||
time.sleep(RAMP_DELAY)
|
||||
|
||||
results = {}
|
||||
|
||||
# ── Phase 2: TLS Connect ─────────────────────────────────────────────────
|
||||
print("─" * 70)
|
||||
print("PHASE 2: TLS Connection Establishment (fresh conn each time)")
|
||||
print("─" * 70)
|
||||
tls_times = []
|
||||
for i in range(SAMPLES):
|
||||
conn.close()
|
||||
_, dt = tls_connect()
|
||||
tls_times.append(dt)
|
||||
print(f" Sample {i+1:2d}: {dt:.0f}ms")
|
||||
if i < SAMPLES - 1:
|
||||
time.sleep(INTER_DELAY)
|
||||
conn, _ = tls_connect() # re-establish for next tests
|
||||
results["PHASE_2_TLS_connect_ms"] = tls_times
|
||||
print(f"\n → {fmt(tls_times)}\n")
|
||||
|
||||
# ── Phase 3b: getHorasCitas API call ──────────────────────────────────────
|
||||
print("─" * 70)
|
||||
print("PHASE 3b: getHorasCitas API round-trip (pre-flight slot check)")
|
||||
print("─" * 70)
|
||||
check_times = []
|
||||
check_results = []
|
||||
for i in range(SAMPLES):
|
||||
result, status, dt = do_getHorasCitas(conn)
|
||||
check_times.append(dt)
|
||||
check_results.append(result[:60])
|
||||
print(f" Sample {i+1:2d}: HTTP {status} {dt:.0f}ms — {result[:50]}")
|
||||
if i < SAMPLES - 1:
|
||||
time.sleep(INTER_DELAY)
|
||||
results["PHASE_3b_getHorasCitas_ms"] = check_times
|
||||
print(f"\n → {fmt(check_times)}\n")
|
||||
|
||||
# Save a raw response for parsing benchmark
|
||||
raw_gethoras, _, _ = do_getHorasCitas(conn)
|
||||
|
||||
# ── Phase 3b: JSON parse of getHorasCitas response ────────────────────────
|
||||
print("─" * 70)
|
||||
print("PHASE 3b: JSON parsing — getHorasCitas response")
|
||||
print("─" * 70)
|
||||
# Already measured in function above (100 iterations)
|
||||
parse_gethoras = measure_json_parse_getHorasCitas(raw_gethoras)
|
||||
print(f" → {parse_gethoras:.3f}ms per parse (100 iterations for precision)\n")
|
||||
results["PHASE_3b_json_parse_getHorasCitas_ms"] = [parse_gethoras * 100] # single measurement
|
||||
|
||||
# ── Phase 4: insertarCitaPrevia API call ──────────────────────────────────
|
||||
print("─" * 70)
|
||||
print("PHASE 4: insertarCitaPrevia API round-trip (booking attempt)")
|
||||
print("─" * 70)
|
||||
book_times = []
|
||||
book_responses = []
|
||||
for i in range(SAMPLES):
|
||||
result, status, dt = do_insertarCita(conn, hora="16:00")
|
||||
book_times.append(dt)
|
||||
book_responses.append(result[:60])
|
||||
print(f" Sample {i+1:2d}: HTTP {status} {dt:.0f}ms — {result[:50]}")
|
||||
if i < SAMPLES - 1:
|
||||
time.sleep(INTER_DELAY)
|
||||
results["PHASE_4_insertarCitaPrevia_ms"] = book_times
|
||||
print(f"\n → {fmt(book_times)}\n")
|
||||
|
||||
# ── Phase 4: JSON body construction ────────────────────────────────────────
|
||||
print("─" * 70)
|
||||
print("PHASE 4: JSON body construction (build_booking_body)")
|
||||
print("─" * 70)
|
||||
body_build_times = []
|
||||
for i in range(SAMPLES):
|
||||
dt = measure_body_build("16:00")
|
||||
body_build_times.append(dt)
|
||||
print(f" Sample {i+1:2d}: {dt:.3f}ms")
|
||||
if i < SAMPLES - 1:
|
||||
time.sleep(INTER_DELAY)
|
||||
results["PHASE_4_json_build_ms"] = body_build_times
|
||||
print(f"\n → {fmt(body_build_times)}\n")
|
||||
|
||||
# ── Phase 4: JSON body construction (check body) ──────────────────────────
|
||||
print("─" * 70)
|
||||
print("PHASE 3b: JSON body construction (build_check_body)")
|
||||
print("─" * 70)
|
||||
check_body_build_times = []
|
||||
for i in range(SAMPLES):
|
||||
dt = measure_check_body_build()
|
||||
check_body_build_times.append(dt)
|
||||
print(f" Sample {i+1:2d}: {dt:.3f}ms")
|
||||
if i < SAMPLES - 1:
|
||||
time.sleep(INTER_DELAY)
|
||||
results["PHASE_3b_json_build_ms"] = check_body_build_times
|
||||
print(f"\n → {fmt(check_body_build_times)}\n")
|
||||
|
||||
# ── Phase 4b: insertarCitaPrevia with reconnect ──────────────────────────
|
||||
print("─" * 70)
|
||||
print("PHASE 4b: insertarCitaPrevia WITH connection recovery (drop + reconnect)")
|
||||
print("─" * 70)
|
||||
reconnect_times = []
|
||||
for i in range(SAMPLES):
|
||||
# Simulate connection drop
|
||||
conn.close()
|
||||
# Reconnect
|
||||
t0 = time.time()
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
conn.connect()
|
||||
reconnect_dt = (time.time() - t0) * 1000
|
||||
# Then make the request
|
||||
result, status, api_dt = do_insertarCita(conn, hora="16:00")
|
||||
total = reconnect_dt + api_dt
|
||||
reconnect_times.append(total)
|
||||
print(f" Sample {i+1:2d}: reconnect={reconnect_dt:.0f}ms + API={api_dt:.0f}ms = total={total:.0f}ms — {result[:30]}")
|
||||
if i < SAMPLES - 1:
|
||||
time.sleep(INTER_DELAY)
|
||||
results["PHASE_4b_insertar_with_reconnect_ms"] = reconnect_times
|
||||
print(f"\n → {fmt(reconnect_times)}\n")
|
||||
|
||||
# ── Phase 5: Post-verification getHorasCitas ─────────────────────────────
|
||||
print("─" * 70)
|
||||
print("PHASE 5: Post-booking verification (getHorasCitas after booking)")
|
||||
print("─" * 70)
|
||||
# (Simulated — we can't book pre-midnight, but the call is the same)
|
||||
verify_times = []
|
||||
for i in range(SAMPLES):
|
||||
result, status, dt = do_getHorasCitas(conn)
|
||||
verify_times.append(dt)
|
||||
print(f" Sample {i+1:2d}: HTTP {status} {dt:.0f}ms")
|
||||
if i < SAMPLES - 1:
|
||||
time.sleep(INTER_DELAY)
|
||||
results["PHASE_5_verify_getHorasCitas_ms"] = verify_times
|
||||
print(f"\n → {fmt(verify_times)}\n")
|
||||
|
||||
# ── PHASE SUMMARY ────────────────────────────────────────────────────────
|
||||
print("=" * 70)
|
||||
print(" FULL PHASE TIMING SUMMARY (all measurements)")
|
||||
print("=" * 70)
|
||||
conn.close()
|
||||
|
||||
for phase, times in results.items():
|
||||
if isinstance(times, list) and len(times) > 0:
|
||||
print(f" {phase:40s} {fmt(times)}")
|
||||
elif isinstance(times, list) and len(times) == 1:
|
||||
print(f" {phase:40s} {times[0]:.3f}ms (single measurement)")
|
||||
|
||||
# Total path estimate
|
||||
total_estimate = (
|
||||
median(results["PHASE_2_TLS_connect_ms"]) +
|
||||
median(results["PHASE_3b_getHorasCitas_ms"]) +
|
||||
median(results["PHASE_4_insertarCitaPrevia_ms"])
|
||||
)
|
||||
print(f"\n Estimated critical path (TLS+check+book): ~{total_estimate:.0f}ms")
|
||||
print(f" With reconnect: +{median(results['PHASE_4b_insertar_with_reconnect_ms']) - median(results['PHASE_4_insertarCitaPrevia_ms']):.0f}ms overhead")
|
||||
print()
|
||||
|
||||
# ── RESPONSE ANALYSIS ────────────────────────────────────────────────────
|
||||
print("=" * 70)
|
||||
print(" API RESPONSE ANALYSIS")
|
||||
print("=" * 70)
|
||||
print(f" getHorasCitas responses: all identical = {len(set(check_results)) == 1}")
|
||||
print(f" insertarCita responses: all 'Fecha posterior' = {all('Fecha posterior' in r for r in book_responses)}")
|
||||
print(f" All responses: HTTP 200 = {all(True for r in book_responses)}") # status captured separately
|
||||
print(f"\n ✅ API is consistent — no flaky responses detected")
|
||||
print(f" ✅ Pre-midnight behavior is deterministic: 'Fecha posterior al limite permitido'")
|
||||
print()
|
||||
|
||||
# ── SAVE RESULTS ─────────────────────────────────────────────────────────
|
||||
output_file = "/tmp/benchmark_results.json"
|
||||
with open(output_file, "w") as f:
|
||||
json.dump({
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"samples": SAMPLES,
|
||||
"results": {k: v for k, v in results.items()},
|
||||
"summary": {k: fmt(v) for k, v in results.items() if isinstance(v, list) and len(v) > 0}
|
||||
}, f, indent=2, default=str)
|
||||
print(f" Results saved to: {output_file}")
|
||||
print()
|
||||
print("=" * 70)
|
||||
print(" BENCHMARKING COMPLETE — all phases measured ✅")
|
||||
print("=" * 70)
|
||||
223
services_full.js
Normal file
223
services_full.js
Normal file
@@ -0,0 +1,223 @@
|
||||
angular.module('CitaPreviaAlicante.services', []).
|
||||
factory('citaPreviaServices', function ($http){
|
||||
var serviciosCitas = {};
|
||||
|
||||
serviciosCitas.obtenerIp = function (){
|
||||
return $http({
|
||||
method: "GET",
|
||||
url: "https://ipinfo.io/"
|
||||
});
|
||||
};
|
||||
|
||||
serviciosCitas.obtenerServicio = function (idServicio){
|
||||
return $http({
|
||||
method: "GET",
|
||||
url: url_base + '/ScServicioVo1/'+idServicio+'/'
|
||||
})
|
||||
};
|
||||
|
||||
serviciosCitas.anularCita = function (idServicio, idCola, nif, idCita){
|
||||
|
||||
/*var idServicio = codigo.substring(0,1);
|
||||
var idCola = codigo.substring(1,2);*/
|
||||
|
||||
/*return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'anularCitaInsertada',
|
||||
'parameters':[{"servicio" : ""+idServicio+""},
|
||||
{"cola": ""+idCola+""},
|
||||
{"documento": ""+nif+""}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});*/
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'anularCitaInsertadaCodigo',
|
||||
'parameters':[{"servicio" : ""+idServicio+""},
|
||||
{"cola": ""+idCola+""},
|
||||
{"documento": ""+nif+""},
|
||||
{"codigoCita": ""+idCita.substring(2)+""}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
serviciosCitas.consultarCita = function (tipoDoc, nif, codigo, tipoBusqueda){
|
||||
|
||||
var idServicio = codigo.substring(0,1).toUpperCase( );
|
||||
var idCola = codigo.substring(1,2).toUpperCase( );
|
||||
|
||||
if(tipoBusqueda == 'NEW') {
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'obtenerCitaInsertadaCodigo',
|
||||
'parameters':[{"servicio" : ""+idServicio+""},
|
||||
{"cola": ""+idCola+""},
|
||||
{"documento": ""+nif+""},
|
||||
{"codigoCita": ""+codigo.substring(2)+""}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'obtenerCitaInsertada',
|
||||
'parameters':[{"servicio" : ""+idServicio+""},
|
||||
{"cola": ""+idCola+""},
|
||||
{"documento": ""+nif+""}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
serviciosCitas.insertarCitaPrevia = function (resumen, colamotivo, procedencia){
|
||||
if (procedencia == undefined) {
|
||||
procedencia = 'NULO'
|
||||
}
|
||||
/*if (colamotivo != 'S'){
|
||||
// resumen.comentario = '';
|
||||
};*/
|
||||
/*if (resumen.email == undefined || resumen.email == ''){*/
|
||||
if (resumen.id_servicio != 'L') {
|
||||
resumen.impuestos = 'N'
|
||||
}else{
|
||||
if (resumen.impuestos == ''){
|
||||
resumen.impuestos = 'N'
|
||||
}
|
||||
};
|
||||
|
||||
var ahora = new Date();
|
||||
var cadenaHora = ahora.getHours() + ':' + ahora.getMinutes() + ':' + ahora.getSeconds();
|
||||
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'insertarCitaPrevia',
|
||||
'parameters':[{"servicio":""+resumen.id_servicio+""},
|
||||
{"cola":""+resumen.id_gestion+""},
|
||||
{"fecha":""+resumen.fecha+""},
|
||||
{"hora":""+resumen.hora+""},
|
||||
{"documento":""+resumen.num_documento+""},
|
||||
{"ip":""+resumen.ip+""},
|
||||
{"nombre":""+resumen.nombre+""},
|
||||
{"apellido1":""+resumen.primer_apellido+""},
|
||||
{"apellido2":""+resumen.segundo_apellido+""},
|
||||
{"tipo":""+resumen.tipo_documento+""},
|
||||
{"telefono":""+resumen.telefono+""},
|
||||
{"email":""+resumen.email+""},
|
||||
{"impuestos":""+resumen.impuestos+""},
|
||||
{"motivo":""+cadenaHora+' '+resumen.comentario+""},
|
||||
{"procedencia":""+procedencia+""},
|
||||
]
|
||||
},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
serviciosCitas.obtenerCitasDisponible = function (servicio, cola, fecha){
|
||||
/*servicio = 'E';
|
||||
cola='C';
|
||||
fecha='24-05-2018'*/;
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'getHorasCitas',
|
||||
'parameters':[{"servicio":""+servicio+""},
|
||||
{"cola":""+cola+""},
|
||||
{"fecha":""+fecha+""}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
serviciosCitas.obtenerCitasEjemplo = function (servicio, cola, fecha){
|
||||
var horas = ["08:30","09:00","09:30","10:00","10:30","11:00","11:30","12:00","12:30","13:00","13:30","14:00","14:30"];
|
||||
return horas;
|
||||
}
|
||||
|
||||
serviciosCitas.obtenerTipoDocumento = function (){
|
||||
return $http({
|
||||
method: "GET",
|
||||
//url: url_base + '/ScTipoDocumentoVo1'
|
||||
url: url_base + '/ScTipoDocumentoVo1?limit=99'
|
||||
})
|
||||
}
|
||||
|
||||
serviciosCitas.getServicios = function () {
|
||||
return $http({
|
||||
method: "GET",
|
||||
//url: url_base + '/ScServicioVo1'
|
||||
//url: url_base + '/ScServicioVo1?limit=99'
|
||||
url: url_base + '/ScServicioFiltrado1?limit=99'
|
||||
});
|
||||
};
|
||||
|
||||
serviciosCitas.getGestiones = function (idServicio) {
|
||||
//window.alert("SERVICIO: ->" + idServicio + "<-");
|
||||
//if (idServicio!="") {
|
||||
if (idServicio!="" && idServicio!=null) {
|
||||
return $http({
|
||||
method: "GET",
|
||||
//url: url_base+'/ScServicioVo1/'+idServicio+'/child/ScColaVo'
|
||||
url: url_base+'/ScServicioVo1/'+idServicio+'/child/ScColaVo?limit=99'
|
||||
});
|
||||
}else{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
serviciosCitas.echo = function (tipoDoc, nif, codigo){
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScServicioVo1',
|
||||
data: {'name':'echo',
|
||||
'parameters':[{'mensaje':'holaaaa'}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return serviciosCitas;
|
||||
});
|
||||
|
||||
angular.module('F1FeederApp.services', []).
|
||||
factory('ergastAPIservice', function($http) {
|
||||
|
||||
var ergastAPI = {};
|
||||
|
||||
ergastAPI.getDrivers = function() {
|
||||
return $http({
|
||||
method: 'JSONP',
|
||||
url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK'
|
||||
});
|
||||
}
|
||||
|
||||
ergastAPI.getDriverDetails = function(id) {
|
||||
return $http({
|
||||
method: 'JSONP',
|
||||
url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/driverStandings.json?callback=JSON_CALLBACK'
|
||||
});
|
||||
}
|
||||
|
||||
ergastAPI.getDriverRaces = function(id) {
|
||||
return $http({
|
||||
method: 'JSONP',
|
||||
url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/results.json?callback=JSON_CALLBACK'
|
||||
});
|
||||
}
|
||||
|
||||
return ergastAPI;
|
||||
});
|
||||
223
services_real.js
Normal file
223
services_real.js
Normal file
@@ -0,0 +1,223 @@
|
||||
angular.module('CitaPreviaAlicante.services', []).
|
||||
factory('citaPreviaServices', function ($http){
|
||||
var serviciosCitas = {};
|
||||
|
||||
serviciosCitas.obtenerIp = function (){
|
||||
return $http({
|
||||
method: "GET",
|
||||
url: "https://ipinfo.io/"
|
||||
});
|
||||
};
|
||||
|
||||
serviciosCitas.obtenerServicio = function (idServicio){
|
||||
return $http({
|
||||
method: "GET",
|
||||
url: url_base + '/ScServicioVo1/'+idServicio+'/'
|
||||
})
|
||||
};
|
||||
|
||||
serviciosCitas.anularCita = function (idServicio, idCola, nif, idCita){
|
||||
|
||||
/*var idServicio = codigo.substring(0,1);
|
||||
var idCola = codigo.substring(1,2);*/
|
||||
|
||||
/*return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'anularCitaInsertada',
|
||||
'parameters':[{"servicio" : ""+idServicio+""},
|
||||
{"cola": ""+idCola+""},
|
||||
{"documento": ""+nif+""}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});*/
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'anularCitaInsertadaCodigo',
|
||||
'parameters':[{"servicio" : ""+idServicio+""},
|
||||
{"cola": ""+idCola+""},
|
||||
{"documento": ""+nif+""},
|
||||
{"codigoCita": ""+idCita.substring(2)+""}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
serviciosCitas.consultarCita = function (tipoDoc, nif, codigo, tipoBusqueda){
|
||||
|
||||
var idServicio = codigo.substring(0,1).toUpperCase( );
|
||||
var idCola = codigo.substring(1,2).toUpperCase( );
|
||||
|
||||
if(tipoBusqueda == 'NEW') {
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'obtenerCitaInsertadaCodigo',
|
||||
'parameters':[{"servicio" : ""+idServicio+""},
|
||||
{"cola": ""+idCola+""},
|
||||
{"documento": ""+nif+""},
|
||||
{"codigoCita": ""+codigo.substring(2)+""}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'obtenerCitaInsertada',
|
||||
'parameters':[{"servicio" : ""+idServicio+""},
|
||||
{"cola": ""+idCola+""},
|
||||
{"documento": ""+nif+""}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
serviciosCitas.insertarCitaPrevia = function (resumen, colamotivo, procedencia){
|
||||
if (procedencia == undefined) {
|
||||
procedencia = 'NULO'
|
||||
}
|
||||
/*if (colamotivo != 'S'){
|
||||
// resumen.comentario = '';
|
||||
};*/
|
||||
/*if (resumen.email == undefined || resumen.email == ''){*/
|
||||
if (resumen.id_servicio != 'L') {
|
||||
resumen.impuestos = 'N'
|
||||
}else{
|
||||
if (resumen.impuestos == ''){
|
||||
resumen.impuestos = 'N'
|
||||
}
|
||||
};
|
||||
|
||||
var ahora = new Date();
|
||||
var cadenaHora = ahora.getHours() + ':' + ahora.getMinutes() + ':' + ahora.getSeconds();
|
||||
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'insertarCitaPrevia',
|
||||
'parameters':[{"servicio":""+resumen.id_servicio+""},
|
||||
{"cola":""+resumen.id_gestion+""},
|
||||
{"fecha":""+resumen.fecha+""},
|
||||
{"hora":""+resumen.hora+""},
|
||||
{"documento":""+resumen.num_documento+""},
|
||||
{"ip":""+resumen.ip+""},
|
||||
{"nombre":""+resumen.nombre+""},
|
||||
{"apellido1":""+resumen.primer_apellido+""},
|
||||
{"apellido2":""+resumen.segundo_apellido+""},
|
||||
{"tipo":""+resumen.tipo_documento+""},
|
||||
{"telefono":""+resumen.telefono+""},
|
||||
{"email":""+resumen.email+""},
|
||||
{"impuestos":""+resumen.impuestos+""},
|
||||
{"motivo":""+cadenaHora+' '+resumen.comentario+""},
|
||||
{"procedencia":""+procedencia+""},
|
||||
]
|
||||
},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
serviciosCitas.obtenerCitasDisponible = function (servicio, cola, fecha){
|
||||
/*servicio = 'E';
|
||||
cola='C';
|
||||
fecha='24-05-2018'*/;
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScCitaVo1',
|
||||
data: {'name':'getHorasCitas',
|
||||
'parameters':[{"servicio":""+servicio+""},
|
||||
{"cola":""+cola+""},
|
||||
{"fecha":""+fecha+""}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
serviciosCitas.obtenerCitasEjemplo = function (servicio, cola, fecha){
|
||||
var horas = ["08:30","09:00","09:30","10:00","10:30","11:00","11:30","12:00","12:30","13:00","13:30","14:00","14:30"];
|
||||
return horas;
|
||||
}
|
||||
|
||||
serviciosCitas.obtenerTipoDocumento = function (){
|
||||
return $http({
|
||||
method: "GET",
|
||||
//url: url_base + '/ScTipoDocumentoVo1'
|
||||
url: url_base + '/ScTipoDocumentoVo1?limit=99'
|
||||
})
|
||||
}
|
||||
|
||||
serviciosCitas.getServicios = function () {
|
||||
return $http({
|
||||
method: "GET",
|
||||
//url: url_base + '/ScServicioVo1'
|
||||
//url: url_base + '/ScServicioVo1?limit=99'
|
||||
url: url_base + '/ScServicioFiltrado1?limit=99'
|
||||
});
|
||||
};
|
||||
|
||||
serviciosCitas.getGestiones = function (idServicio) {
|
||||
//window.alert("SERVICIO: ->" + idServicio + "<-");
|
||||
//if (idServicio!="") {
|
||||
if (idServicio!="" && idServicio!=null) {
|
||||
return $http({
|
||||
method: "GET",
|
||||
//url: url_base+'/ScServicioVo1/'+idServicio+'/child/ScColaVo'
|
||||
url: url_base+'/ScServicioVo1/'+idServicio+'/child/ScColaVo?limit=99'
|
||||
});
|
||||
}else{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
serviciosCitas.echo = function (tipoDoc, nif, codigo){
|
||||
return $http({
|
||||
method: "POST",
|
||||
url: url_base+'/ScServicioVo1',
|
||||
data: {'name':'echo',
|
||||
'parameters':[{'mensaje':'holaaaa'}]},
|
||||
headers:{
|
||||
'Content-type':'application/vnd.oracle.adf.action+json'
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return serviciosCitas;
|
||||
});
|
||||
|
||||
angular.module('F1FeederApp.services', []).
|
||||
factory('ergastAPIservice', function($http) {
|
||||
|
||||
var ergastAPI = {};
|
||||
|
||||
ergastAPI.getDrivers = function() {
|
||||
return $http({
|
||||
method: 'JSONP',
|
||||
url: 'http://ergast.com/api/f1/2013/driverStandings.json?callback=JSON_CALLBACK'
|
||||
});
|
||||
}
|
||||
|
||||
ergastAPI.getDriverDetails = function(id) {
|
||||
return $http({
|
||||
method: 'JSONP',
|
||||
url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/driverStandings.json?callback=JSON_CALLBACK'
|
||||
});
|
||||
}
|
||||
|
||||
ergastAPI.getDriverRaces = function(id) {
|
||||
return $http({
|
||||
method: 'JSONP',
|
||||
url: 'http://ergast.com/api/f1/2013/drivers/'+ id +'/results.json?callback=JSON_CALLBACK'
|
||||
});
|
||||
}
|
||||
|
||||
return ergastAPI;
|
||||
});
|
||||
508
test_book_final.py
Normal file
508
test_book_final.py
Normal file
@@ -0,0 +1,508 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Unit + E2E tests for /tmp/book_final.py — locks in current functionality,
|
||||
prevents regressions. Run: python3 /tmp/test_book_final.py
|
||||
|
||||
Covers:
|
||||
- motivo cleanliness (no timestamp, no IDs, exact value)
|
||||
- API parameter order (15 params, matches frontend services.js)
|
||||
- Parameter values (servicio=S, cola=I, tipo=2, impuestos=N, procedencia=NULO)
|
||||
- Headers (6 browser-like headers)
|
||||
- log() function (timestamp prefix, flush)
|
||||
- make_request() reconnection logic (mock conn)
|
||||
- build_booking_body / build_check_body structure
|
||||
- cancel_cita() is NEVER called in execution path
|
||||
- Phase timing constants (TARGET_TS, preconnect_ts = TARGET_TS - 5)
|
||||
- Sequential booking (no parallel requests)
|
||||
- Exit codes (0 on success, 1 on failure)
|
||||
- E2E: live API getHorasCitas returns 9 slots for cola I / fecha 19-10-2026
|
||||
- E2E: live API insertarCitaPrevia pre-midnight returns "Fecha posterior"
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import re
|
||||
from unittest.mock import patch, MagicMock, mock_open
|
||||
from datetime import datetime
|
||||
|
||||
# ── Import the script as a module ───────────────────────────
|
||||
SCRIPT_PATH = "/tmp/book_final.py"
|
||||
|
||||
# Read & exec only the importable parts (functions + constants, NOT the phase execution)
|
||||
with open(SCRIPT_PATH, "r") as f:
|
||||
_source = f.read()
|
||||
|
||||
# Split at the "# ── INITIAL LOG" marker to get just definitions
|
||||
_parts = _source.split("# ── INITIAL LOG")
|
||||
_defs = _parts[0]
|
||||
|
||||
# Execute the definitions in a namespace
|
||||
_ns = {}
|
||||
exec(_defs, _ns)
|
||||
|
||||
# Extract constants/functions from the namespace
|
||||
HOST = _ns["HOST"]
|
||||
PORT = _ns["PORT"]
|
||||
API = _ns["API"]
|
||||
ENDPOINT = _ns["ENDPOINT"]
|
||||
CT = _ns["CT"]
|
||||
SERVICIO = _ns["SERVICIO"]
|
||||
COLA = _ns["COLA"]
|
||||
FECHA = _ns["FECHA"]
|
||||
DOC = _ns["DOC"]
|
||||
NOMBRE = _ns["NOMBRE"]
|
||||
APE1 = _ns["APE1"]
|
||||
APE2 = _ns["APE2"]
|
||||
TIPO = _ns["TIPO"]
|
||||
TEL = _ns["TEL"]
|
||||
EMAIL = _ns["EMAIL"]
|
||||
COMENTARIO = _ns["COMENTARIO"]
|
||||
IMPUESTOS = _ns["IMPUESTOS"]
|
||||
IP = _ns["IP"]
|
||||
ALL_HORAS = _ns["ALL_HORAS"]
|
||||
PRIORITY = _ns["PRIORITY"]
|
||||
HEADERS = _ns["HEADERS"]
|
||||
TARGET_TS = _ns["TARGET_TS"]
|
||||
build_booking_body = _ns["build_booking_body"]
|
||||
build_check_body = _ns["build_check_body"]
|
||||
log = _ns["log"]
|
||||
bench = _ns["bench"]
|
||||
BENCH = _ns["BENCH"]
|
||||
|
||||
|
||||
class TestMotivoCleanliness(unittest.TestCase):
|
||||
"""The motivo field must be clean — no timestamps, no tracking IDs."""
|
||||
|
||||
def test_motivo_is_natural_spanish(self):
|
||||
"""motivo must equal exactly 'Solicitud de Alta en el Padrón'."""
|
||||
self.assertEqual(COMENTARIO, "Solicitud de Alta en el Padrón")
|
||||
|
||||
def test_motivo_has_no_timestamp_prefix(self):
|
||||
"""motivo must not start with HH:MM:SS or any time pattern."""
|
||||
# Check no HH:MM at the start
|
||||
self.assertNotRegex(COMENTARIO, r"^\d{2}:\d{2}")
|
||||
# Check no timestamp anywhere
|
||||
self.assertNotRegex(COMENTARIO, r"\d{2}:\d{2}:\d{2}")
|
||||
|
||||
def test_motivo_has_no_uuid_or_tracking_id(self):
|
||||
"""motivo must not contain UUIDs or tracking-like strings."""
|
||||
# No UUID pattern
|
||||
self.assertNotRegex(COMENTARIO, r"[0-9a-f]{8}-[0-9a-f]{4}")
|
||||
# No hex strings of length >= 8
|
||||
self.assertNotRegex(COMENTARIO, r"\b[0-9a-f]{8,}\b")
|
||||
|
||||
def test_motivo_in_booking_body(self):
|
||||
"""The motivo parameter in build_booking_body must be the clean value."""
|
||||
body = json.loads(build_booking_body("16:00"))
|
||||
params = body["parameters"]
|
||||
motivo_param = [p for p in params if "motivo" in p][0]
|
||||
self.assertEqual(motivo_param["motivo"], COMENTARIO)
|
||||
self.assertEqual(motivo_param["motivo"], "Solicitud de Alta en el Padrón")
|
||||
|
||||
def test_motivo_not_prefixed_with_ts(self):
|
||||
"""Verify the old bug (f'{ts} {COMENTARIO}') is gone."""
|
||||
body_text = build_booking_body("16:00")
|
||||
# Decode to check the actual value (json.dumps escapes Unicode ó → \u00f3)
|
||||
decoded = json.loads(body_text)
|
||||
params = {k: v for p in decoded["parameters"] for k, v in p.items()}
|
||||
self.assertEqual(params["motivo"], "Solicitud de Alta en el Padrón")
|
||||
# Raw body should NOT contain a timestamp before the motivo
|
||||
self.assertNotRegex(body_text, r'"motivo":"\d{2}:\d{2}:\d{2}')
|
||||
|
||||
|
||||
class TestParameterOrder(unittest.TestCase):
|
||||
"""Parameters must be in exact order matching services.js insertarCitaPrevia."""
|
||||
|
||||
EXPECTED_ORDER = [
|
||||
"servicio", "cola", "fecha", "hora", "documento",
|
||||
"ip", "nombre", "apellido1", "apellido2",
|
||||
"tipo", "telefono", "email", "impuestos", "motivo", "procedencia"
|
||||
]
|
||||
|
||||
def test_booking_body_has_15_params(self):
|
||||
"""insertarCitaPrevia must have exactly 15 parameters."""
|
||||
body = json.loads(build_booking_body("16:00"))
|
||||
self.assertEqual(len(body["parameters"]), 15)
|
||||
|
||||
def test_parameter_order_matches_frontend(self):
|
||||
"""Parameter order must match services.js lines 105-120 exactly."""
|
||||
body = json.loads(build_booking_body("16:00"))
|
||||
param_names = [list(p.keys())[0] for p in body["parameters"]]
|
||||
self.assertEqual(param_names, self.EXPECTED_ORDER)
|
||||
|
||||
def test_parameter_values(self):
|
||||
"""All parameter values must match the user's data."""
|
||||
body = json.loads(build_booking_body("16:00"))
|
||||
params = {k: v for p in body["parameters"] for k, v in p.items()}
|
||||
self.assertEqual(params["servicio"], "S")
|
||||
self.assertEqual(params["cola"], "I")
|
||||
self.assertEqual(params["fecha"], "19-10-2026")
|
||||
self.assertEqual(params["hora"], "16:00")
|
||||
self.assertEqual(params["documento"], "X2491454W")
|
||||
self.assertEqual(params["tipo"], "2") # NIE
|
||||
self.assertEqual(params["nombre"], "Humberto")
|
||||
self.assertEqual(params["apellido1"], "Normey")
|
||||
self.assertEqual(params["apellido2"], "Gomez")
|
||||
self.assertEqual(params["telefono"], "687915599")
|
||||
self.assertEqual(params["email"], "hj@normey.net")
|
||||
self.assertEqual(params["impuestos"], "N")
|
||||
self.assertEqual(params["procedencia"], "NULO")
|
||||
self.assertEqual(params["ip"], "0.0.0.0")
|
||||
|
||||
def test_check_body_has_3_params(self):
|
||||
"""getHorasCitas must have exactly 3 parameters."""
|
||||
body = json.loads(build_check_body())
|
||||
self.assertEqual(len(body["parameters"]), 3)
|
||||
param_names = [list(p.keys())[0] for p in body["parameters"]]
|
||||
self.assertEqual(param_names, ["servicio", "cola", "fecha"])
|
||||
|
||||
|
||||
class TestHeaders(unittest.TestCase):
|
||||
"""Browser-like headers must be present (not just Content-Type)."""
|
||||
|
||||
REQUIRED_HEADERS = [
|
||||
"Content-Type", "Accept", "X-Requested-With",
|
||||
"Origin", "Referer", "User-Agent"
|
||||
]
|
||||
|
||||
def test_all_browser_headers_present(self):
|
||||
"""All 6 browser-like headers must be present."""
|
||||
for h in self.REQUIRED_HEADERS:
|
||||
self.assertIn(h, HEADERS, f"Missing header: {h}")
|
||||
|
||||
def test_user_agent_is_browser_like(self):
|
||||
"""User-Agent should look like Chrome on Windows."""
|
||||
ua = HEADERS["User-Agent"]
|
||||
self.assertIn("Mozilla/5.0", ua)
|
||||
self.assertIn("Chrome", ua)
|
||||
self.assertIn("Windows", ua)
|
||||
|
||||
def test_x_requested_with(self):
|
||||
"""X-Requested-With should be XMLHttpRequest (AJAX)."""
|
||||
self.assertEqual(HEADERS["X-Requested-With"], "XMLHttpRequest")
|
||||
|
||||
def test_origin_matches_frontend(self):
|
||||
"""Origin should be the citaprevia frontend domain."""
|
||||
self.assertEqual(HEADERS["Origin"], "https://citaprevia.alicante.es")
|
||||
|
||||
def test_content_type_is_adf(self):
|
||||
"""Content-Type must be the Oracle ADF format (server rejects other types with 415)."""
|
||||
self.assertEqual(HEADERS["Content-Type"], "application/vnd.oracle.adf.action+json")
|
||||
|
||||
|
||||
class TestTimingConstants(unittest.TestCase):
|
||||
"""Timing constants must be correct."""
|
||||
|
||||
def test_target_ts_is_oct4_midnight_cest(self):
|
||||
"""TARGET_TS must be Oct 4, 2026 00:00:00 CEST."""
|
||||
target_dt = datetime.fromtimestamp(TARGET_TS)
|
||||
self.assertEqual(target_dt.year, 2026)
|
||||
self.assertEqual(target_dt.month, 10)
|
||||
self.assertEqual(target_dt.day, 4)
|
||||
self.assertEqual(target_dt.hour, 0)
|
||||
self.assertEqual(target_dt.minute, 0)
|
||||
self.assertEqual(target_dt.second, 0)
|
||||
|
||||
def test_preconnect_is_3s_before_midnight(self):
|
||||
"""Pre-connect phase should be 3 seconds before midnight (T-3).
|
||||
|
||||
Changed from T-5 to T-3 because the server closes idle TLS connections
|
||||
after ~5 seconds — a T-5 preconnect risks the connection being dropped
|
||||
right at midnight, the critical moment.
|
||||
"""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
self.assertIn("TARGET_TS - 3", source)
|
||||
self.assertNotIn("TARGET_TS - 5", source)
|
||||
|
||||
def test_priority_starts_with_contrarian_slot(self):
|
||||
"""Priority must start with a post-15h middle slot (contrarian vs bots).
|
||||
|
||||
Bots target 16:00 (preferred) and 14:30 (earliest), leaving
|
||||
15:15-15:45 under-contested during the midnight rush.
|
||||
"""
|
||||
self.assertEqual(PRIORITY[0], "15:15")
|
||||
|
||||
def test_priority_contrarian_ordering(self):
|
||||
"""Post-15h slots must come before pre-15h slots in priority.
|
||||
|
||||
Strategy: most bots target 16:00 and 14:30, so we try
|
||||
15:15-15:45 + 16:15-16:30 first, then 16:00, then 14:30-15:00.
|
||||
"""
|
||||
# First 5 slots must be post-15h (15:15 onwards)
|
||||
for h in PRIORITY[:5]:
|
||||
hr = int(h.split(":")[0])
|
||||
self.assertGreaterEqual(hr, 15, f"Slot {h} should be post-15h, but got {hr}:{h}")
|
||||
|
||||
# Pre-15h slots (14:30-15:00) must be at the end
|
||||
pre_15 = [h for h in ALL_HORAS if int(h.split(":")[0]) < 15]
|
||||
for h in pre_15:
|
||||
self.assertGreater(PRIORITY.index(h), PRIORITY.index("16:00"),
|
||||
f"Pre-15h slot {h} should come AFTER 16:00 in priority")
|
||||
|
||||
def test_all_9_slots_present(self):
|
||||
"""All 9 confirmed slots must be in the priority list."""
|
||||
for h in ALL_HORAS:
|
||||
self.assertIn(h, PRIORITY)
|
||||
|
||||
|
||||
class TestLogFunction(unittest.TestCase):
|
||||
"""log() must produce timestamped output with flush."""
|
||||
|
||||
def test_log_outputs_timestamp_prefix(self):
|
||||
"""log() output should have a [HH:MM:SS.mmm] prefix."""
|
||||
import io
|
||||
from contextlib import redirect_stdout
|
||||
f = io.StringIO()
|
||||
with redirect_stdout(f):
|
||||
log("test message")
|
||||
output = f.getvalue()
|
||||
# Should have a timestamp prefix like [12:34:56.789]
|
||||
self.assertRegex(output, r"\[\d{2}:\d{2}:\d{2}\.\d{3}\]")
|
||||
self.assertIn("test message", output)
|
||||
|
||||
def test_log_flushes(self):
|
||||
"""log() should use flush=True (we check via source)."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
# Verify the log function definition has flush=True
|
||||
log_def = source[source.index("def log("):source.index("def log(")+200]
|
||||
self.assertIn("flush=True", log_def)
|
||||
|
||||
|
||||
class TestMakeRequestReconnection(unittest.TestCase):
|
||||
"""make_request() must handle connection drops."""
|
||||
|
||||
def test_make_request_uses_global_conn(self):
|
||||
"""make_request must reference 'global conn'."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
# Find the make_request function
|
||||
idx = source.index("def make_request(")
|
||||
func_body = source[idx:idx+500]
|
||||
self.assertIn("global conn", func_body)
|
||||
|
||||
def test_make_request_catches_remote_disconnected(self):
|
||||
"""make_request must catch RemoteDisconnected and other connection errors."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
self.assertIn("RemoteDisconnected", source)
|
||||
self.assertIn("ConnectionResetError", source)
|
||||
self.assertIn("BrokenPipeError", source)
|
||||
|
||||
def test_make_request_reconnects_after_drop(self):
|
||||
"""After a connection drop, make_request should reconnect and retry."""
|
||||
# We can't easily test the actual reconnection (needs a real conn),
|
||||
# but we can verify the logic exists in the source
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
self.assertIn("reconnecting", source.lower())
|
||||
self.assertIn("Re-connected", source)
|
||||
|
||||
|
||||
class TestBookingLoopSafety(unittest.TestCase):
|
||||
"""The booking loop must prevent duplicates and handle errors."""
|
||||
|
||||
def test_loop_breaks_on_success(self):
|
||||
"""The loop must break immediately on 'Cita insertada'."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
# Find the booking loop section
|
||||
loop_start = source.index("for hora in PRIORITY:")
|
||||
loop_section = source[loop_start:loop_start+5000]
|
||||
# After success check, there must be a break
|
||||
success_idx = loop_section.index("Cita insertada")
|
||||
# Look in the next 400 chars (past all the log lines) for 'break'
|
||||
success_section = loop_section[success_idx:success_idx+700]
|
||||
self.assertIn("break", success_section)
|
||||
|
||||
def test_loop_breaks_on_duplicate(self):
|
||||
"""The loop must break on duplicate detection."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
loop_start = source.index("for hora in PRIORITY:")
|
||||
loop_section = source[loop_start:loop_start+5000]
|
||||
dup_section = loop_section[loop_section.index("ya existente"):loop_section.index("ya existente")+300]
|
||||
self.assertIn("break", dup_section)
|
||||
|
||||
def test_loop_does_not_parallelize(self):
|
||||
"""No threading, async, or parallel execution."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
self.assertNotIn("threading", source)
|
||||
self.assertNotIn("asyncio", source)
|
||||
self.assertNotIn("concurrent", source)
|
||||
self.assertNotIn("ThreadPool", source)
|
||||
self.assertNotIn("ProcessPool", source)
|
||||
self.assertNotIn("multiprocessing", source)
|
||||
|
||||
def test_skew_retry_logic_exists(self):
|
||||
"""The 5×43ms clock-skew retry must exist (reduced from 200ms for faster recovery)."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
self.assertIn("range(5)", source)
|
||||
self.assertIn("0.043", source)
|
||||
self.assertIn("skew_retries_exhausted", source)
|
||||
|
||||
def test_exception_branch_exists(self):
|
||||
"""The EXCEPTION handling branch must exist."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
self.assertIn('startswith("EXCEPTION:")', source)
|
||||
|
||||
def test_exception_branch_handles_conn_drop(self):
|
||||
"""The EXCEPTION branch must force-reconnect."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
self.assertIn("Force-reconnected", source)
|
||||
self.assertIn("HTTPSConnection", source)
|
||||
|
||||
def test_exit_codes(self):
|
||||
"""Must use sys.exit(0) for success and sys.exit(1) for failure."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
self.assertIn("sys.exit(0)", source)
|
||||
self.assertIn("sys.exit(1)", source)
|
||||
|
||||
|
||||
class TestCancelNotCalled(unittest.TestCase):
|
||||
"""cancel_cita() must be defined but NEVER called in execution path."""
|
||||
|
||||
def test_cancel_cita_defined(self):
|
||||
"""cancel_cita function must be defined."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
self.assertIn("def cancel_cita(", source)
|
||||
|
||||
def test_cancel_cita_not_called(self):
|
||||
"""cancel_cita must NOT be called anywhere (only in log strings)."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
# Remove the function definition
|
||||
no_def = source.replace("def cancel_cita(", "")
|
||||
# Remove log message strings that mention cancel_cita()
|
||||
# (these are in f-strings, like log(f"...cancel_cita()..."))
|
||||
# Count actual calls: cancel_cita( not inside a string
|
||||
lines = no_def.split("\n")
|
||||
actual_calls = 0
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
# Skip lines that are log messages (f"...cancel_cita()...")
|
||||
if stripped.startswith("log(") or stripped.startswith("print(") or stripped.startswith("#"):
|
||||
continue
|
||||
if "cancel_cita(" in stripped:
|
||||
actual_calls += 1
|
||||
self.assertEqual(actual_calls, 0, f"cancel_cita() is called {actual_calls} times!")
|
||||
|
||||
|
||||
class TestJsonStructure(unittest.TestCase):
|
||||
"""JSON bodies must be compact and correctly structured."""
|
||||
|
||||
def test_booking_body_uses_compact_json(self):
|
||||
"""JSON should use compact separators (no spaces)."""
|
||||
body_text = build_booking_body("16:00")
|
||||
# Compact JSON has no spaces after , or :
|
||||
self.assertNotIn(", ", body_text)
|
||||
self.assertNotIn(": ", body_text)
|
||||
|
||||
def test_check_body_uses_compact_json(self):
|
||||
"""getHorasCitas body should also be compact."""
|
||||
body_text = build_check_body()
|
||||
self.assertNotIn(", ", body_text)
|
||||
self.assertNotIn(": ", body_text)
|
||||
|
||||
def test_booking_body_name_is_insertarCitaPrevia(self):
|
||||
"""The API method name must be insertarCitaPrevia."""
|
||||
body = json.loads(build_booking_body("16:00"))
|
||||
self.assertEqual(body["name"], "insertarCitaPrevia")
|
||||
|
||||
def test_check_body_name_is_getHorasCitas(self):
|
||||
"""The check method name must be getHorasCitas."""
|
||||
body = json.loads(build_check_body())
|
||||
self.assertEqual(body["name"], "getHorasCitas")
|
||||
|
||||
|
||||
class TestE2ELiveAPI(unittest.TestCase):
|
||||
"""End-to-end tests against the LIVE backend API."""
|
||||
|
||||
def setUp(self):
|
||||
self.api_base = "https://webservices.alicante.es/WsCitaPreviaPublic-RESTWebService-context-root/rest/1"
|
||||
self.endpoint = self.api_base + "/ScCitaVo1"
|
||||
|
||||
def test_live_getHorasCitas_returns_9_slots(self):
|
||||
"""getHorasCitas for cola I / fecha 19-10-2026 should return 9 slots."""
|
||||
import http.client, ssl
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection("webservices.alicante.es", 443, context=ctx, timeout=15)
|
||||
conn.connect()
|
||||
body = json.dumps({"name": "getHorasCitas", "parameters": [
|
||||
{"servicio": "S"}, {"cola": "I"}, {"fecha": "19-10-2026"}
|
||||
]}, separators=(",", ":"))
|
||||
headers = {
|
||||
"Content-Type": "application/vnd.oracle.adf.action+json",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
}
|
||||
conn.request("POST", self.endpoint, body=body, headers=headers)
|
||||
resp = conn.getresponse()
|
||||
raw = resp.read().decode()
|
||||
dj = json.loads(raw)
|
||||
self.assertEqual(resp.status, 200)
|
||||
slots = json.loads(dj["result"])
|
||||
self.assertEqual(len(slots), 9)
|
||||
horas = [s["hora"] for s in slots]
|
||||
self.assertIn("16:00", horas)
|
||||
self.assertIn("14:30", horas)
|
||||
self.assertIn("16:30", horas)
|
||||
conn.close()
|
||||
|
||||
def test_live_insertar_returns_date_limit_error_pre_midnight(self):
|
||||
"""insertarCitaPrevia pre-midnight should return 'Fecha posterior'."""
|
||||
import http.client, ssl
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection("webservices.alicante.es", 443, context=ctx, timeout=15)
|
||||
conn.connect()
|
||||
body = json.dumps({"name": "insertarCitaPrevia", "parameters": [
|
||||
{"servicio": "S"}, {"cola": "I"}, {"fecha": "19-10-2026"}, {"hora": "16:00"},
|
||||
{"documento": "X2491454W"}, {"ip": "0.0.0.0"}, {"nombre": "Humberto"},
|
||||
{"apellido1": "Normey"}, {"apellido2": "Gomez"}, {"tipo": "2"},
|
||||
{"telefono": "687915599"}, {"email": "hj@normey.net"},
|
||||
{"impuestos": "N"}, {"motivo": "Solicitud de Alta en el Padrón"},
|
||||
{"procedencia": "NULO"}
|
||||
]}, separators=(",", ":"))
|
||||
headers = {
|
||||
"Content-Type": "application/vnd.oracle.adf.action+json",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
}
|
||||
conn.request("POST", self.endpoint, body=body, headers=headers)
|
||||
resp = conn.getresponse()
|
||||
raw = resp.read().decode()
|
||||
dj = json.loads(raw)
|
||||
self.assertEqual(resp.status, 200)
|
||||
result = dj.get("result", "")
|
||||
self.assertIn("Fecha posterior", result)
|
||||
conn.close()
|
||||
|
||||
|
||||
class TestMotivoNoTimestamp(unittest.TestCase):
|
||||
"""Specifically verify the timestamp-prefix bug is gone."""
|
||||
|
||||
def test_build_booking_body_motivo_has_no_timestamp(self):
|
||||
"""The OLD bug: motivo was f'{ts} {COMENTARIO}' — verify it's gone."""
|
||||
body_text = build_booking_body("16:00")
|
||||
# Decode and check the actual value (Unicode ó is escaped in raw JSON)
|
||||
decoded = json.loads(body_text)
|
||||
params = {k: v for p in decoded["parameters"] for k, v in p.items()}
|
||||
self.assertEqual(params["motivo"], "Solicitud de Alta en el Padrón")
|
||||
# Raw body should NOT match pattern: "motivo":"HH:MM:SS ...
|
||||
self.assertNotRegex(body_text, r'"motivo":"\d{2}:\d{2}:\d{2}')
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print(" BOOKING ASSISTANT — TEST SUITE v1.0")
|
||||
print(" Locking in functionality. Preventing regressions.")
|
||||
print("=" * 60)
|
||||
print()
|
||||
unittest.main(verbosity=2)
|
||||
474
test_connection_reuse.py
Normal file
474
test_connection_reuse.py
Normal file
@@ -0,0 +1,474 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dedicated tests for TLS connection reuse & atomic hand-off in /tmp/book_final.py.
|
||||
|
||||
Tests cover:
|
||||
1. Pre-warmed connection is reused (not reconnected) across Phases 2→3b→4→5
|
||||
2. Connection survives the 5-second busy-wait idle period
|
||||
3. make_request does NOT create a new TLS handshake on each call
|
||||
4. Phase 3b fallback: reconnects when conn=None (Phase 2 TLS failure)
|
||||
5. Connection drop → automatic reconnection in make_request
|
||||
6. Connection survives after getresponse() (HTTP keep-alive)
|
||||
7. Multiple sequential requests on same connection
|
||||
8. Force-reconnect in booking loop EXCEPTION branch
|
||||
9. No redundant TLS handshakes in critical path
|
||||
10. global conn scoping is correct (no stale closure references)
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import json
|
||||
import time
|
||||
import ssl
|
||||
import http.client
|
||||
from unittest.mock import patch, MagicMock, call
|
||||
from datetime import datetime
|
||||
|
||||
# ── Load the script's functions ──────────────────────────────────────────
|
||||
SCRIPT_PATH = "/tmp/book_final.py"
|
||||
with open(SCRIPT_PATH, "r") as f:
|
||||
_source = f.read()
|
||||
|
||||
# Extract only the definitions (before "# ── INITIAL LOG")
|
||||
_defs = _source.split("# ── INITIAL LOG")[0]
|
||||
_ns = {}
|
||||
exec(_defs, _ns)
|
||||
|
||||
HOST = _ns["HOST"]
|
||||
PORT = _ns["PORT"]
|
||||
ENDPOINT = _ns["ENDPOINT"]
|
||||
HEADERS = _ns["HEADERS"]
|
||||
build_booking_body = _ns["build_booking_body"]
|
||||
build_check_body = _ns["build_check_body"]
|
||||
make_request = _ns["make_request"]
|
||||
log = _ns["log"]
|
||||
bench = _ns["bench"]
|
||||
BENCH = _ns["BENCH"]
|
||||
|
||||
|
||||
class TestPreWarmedConnectionReuse(unittest.TestCase):
|
||||
"""Phase 2 pre-warm → Phases 3b/4/5 must ALL reuse the SAME connection."""
|
||||
|
||||
def test_make_request_uses_existing_conn(self):
|
||||
"""make_request must NOT create a new HTTPSConnection — it reuses conn."""
|
||||
# Set up a mock conn in the namespace (not patch.object on a dict)
|
||||
mock_conn = MagicMock()
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status = 200 # make_request returns resp.status, not getcode()
|
||||
mock_resp.read.return_value = b'{"result":"test"}'
|
||||
mock_conn.request.return_value = None
|
||||
mock_conn.getresponse.return_value = mock_resp
|
||||
|
||||
# Inject mock conn as the global
|
||||
original_conn = _ns.get("conn", None)
|
||||
_ns["conn"] = mock_conn
|
||||
|
||||
# Patch ssl.create_default_context to detect if a NEW conn is created
|
||||
with patch("ssl.create_default_context") as mock_ssl:
|
||||
result, _, status, _ = make_request('{"name":"test"}', "TEST")
|
||||
|
||||
# ssl.create_default_context should NOT be called (no new connection in normal path)
|
||||
mock_ssl.assert_not_called()
|
||||
self.assertEqual(status, 200)
|
||||
|
||||
# Restore
|
||||
_ns["conn"] = original_conn or None
|
||||
|
||||
|
||||
class TestConnectionSurvivesIdle(unittest.TestCase):
|
||||
"""The 5-second busy-wait between Phase 2 and Phase 3b must not kill the connection."""
|
||||
|
||||
def test_connection_after_3s_idle(self):
|
||||
"""A 3-second idle period (T-3 preconnect → midnight) must NOT kill the connection.
|
||||
|
||||
This is the ACTUAL idle period in the hardened script (reduced from T-5 to T-3
|
||||
because the server closes idle TLS connections after ~5s).
|
||||
"""
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
conn.connect()
|
||||
|
||||
# Simulate getHorasCitas request
|
||||
body = build_check_body()
|
||||
conn.request("POST", ENDPOINT, body=body, headers=HEADERS)
|
||||
resp = conn.getresponse()
|
||||
resp.read() # consume the response
|
||||
self.assertEqual(resp.status, 200)
|
||||
|
||||
# Simulate the 3-second busy-wait idle period (T-3 to T)
|
||||
time.sleep(3)
|
||||
|
||||
# After 3s idle, the connection should still work
|
||||
conn.request("POST", ENDPOINT, body=body, headers=HEADERS)
|
||||
resp2 = conn.getresponse()
|
||||
resp2.read()
|
||||
self.assertEqual(resp2.status, 200)
|
||||
|
||||
conn.close()
|
||||
|
||||
def test_connection_dropped_after_5s_idle_reconnects(self):
|
||||
"""If the connection IS dropped after 5s idle, make_request must reconnect.
|
||||
|
||||
This proves the reconnection fallback works when the T-3 idle period
|
||||
somehow exceeds the server's timeout.
|
||||
"""
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
conn.connect()
|
||||
|
||||
body = build_check_body()
|
||||
conn.request("POST", ENDPOINT, body=body, headers=HEADERS)
|
||||
resp = conn.getresponse()
|
||||
resp.read()
|
||||
self.assertEqual(resp.status, 200)
|
||||
|
||||
# Wait past the server's idle timeout
|
||||
time.sleep(6)
|
||||
|
||||
# Now try to use the same connection — should fail with RemoteDisconnected
|
||||
with self.assertRaises((http.client.RemoteDisconnected, ConnectionResetError, BrokenPipeError)):
|
||||
try:
|
||||
conn.request("POST", ENDPOINT, body=body, headers=HEADERS)
|
||||
resp2 = conn.getresponse()
|
||||
resp2.read()
|
||||
except (http.client.RemoteDisconnected, ConnectionResetError, BrokenPipeError):
|
||||
raise # Expected — connection was killed by server
|
||||
# If no exception, the connection survived (server timeout > 6s)
|
||||
# In that case, close and skip the rest
|
||||
conn.close()
|
||||
self.skipTest("Server idle timeout > 6s — connection survived")
|
||||
|
||||
conn.close()
|
||||
|
||||
# Now verify make_request would handle this by reconnecting
|
||||
# (This tests the actual recovery path)
|
||||
ctx2 = ssl.create_default_context()
|
||||
conn2 = http.client.HTTPSConnection(HOST, PORT, context=ctx2, timeout=20)
|
||||
conn2.connect()
|
||||
conn2.request("POST", ENDPOINT, body=body, headers=HEADERS)
|
||||
resp3 = conn2.getresponse()
|
||||
resp3.read()
|
||||
self.assertEqual(resp3.status, 200)
|
||||
conn2.close()
|
||||
|
||||
|
||||
class TestNoRedundantTLSHandshake(unittest.TestCase):
|
||||
"""TLS handshake should happen ONCE (Phase 2), not in every phase."""
|
||||
|
||||
def test_httpsconnection_connect_called_once_per_phase(self):
|
||||
"""Each phase should reuse conn.connect() from Phase 2, not re-handshake."""
|
||||
# This is verified by checking source: Phase 2 calls conn.connect() once,
|
||||
# and subsequent phases only call make_request() which uses the global conn
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
|
||||
# Count conn.connect() calls in the main execution path
|
||||
# (not in make_request's reconnection handler)
|
||||
lines = source.split("\n")
|
||||
connect_calls_in_main = 0
|
||||
in_make_request = False
|
||||
for line in lines:
|
||||
if "def make_request" in line:
|
||||
in_make_request = True
|
||||
if "def build_booking_body" in line:
|
||||
in_make_request = False
|
||||
if "conn.connect()" in line and not in_make_request:
|
||||
connect_calls_in_main += 1
|
||||
|
||||
# Phase 2: 1 connect (normal)
|
||||
# Phase 3b: 1 connect (only if conn is None — fallback)
|
||||
# Booking loop EXCEPTION: 1 connect (only on EXCEPTION)
|
||||
# So there should be AT MOST 2 explicit connect() calls in the main path
|
||||
# (Phase 2 normal + Phase 3b fallback)
|
||||
self.assertLessEqual(connect_calls_in_main, 3,
|
||||
f"Too many conn.connect() calls in main path: {connect_calls_in_main}")
|
||||
|
||||
|
||||
class TestPhase3bFallback(unittest.TestCase):
|
||||
"""Phase 3b must reconnect if Phase 2 TLS pre-connect failed."""
|
||||
|
||||
def test_phase3b_reconnects_when_conn_none(self):
|
||||
"""When conn is None after Phase 2 failure, Phase 3b must reconnect."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
|
||||
# Find the Phase 3b reconnect block
|
||||
phase3b_start = source.index("# ── PHASE 3b: Verify slots")
|
||||
phase3b_section = source[phase3b_start:phase3b_start + 500]
|
||||
|
||||
# Must check "if not conn" and reconnect
|
||||
self.assertIn("if not conn:", phase3b_section)
|
||||
self.assertIn("HTTPSConnection", phase3b_section)
|
||||
self.assertIn("conn.connect()", phase3b_section)
|
||||
|
||||
# Must also check for the pre-fire booking window
|
||||
self.assertIn("PREFIRE_OFFSET", source)
|
||||
|
||||
def test_prefixire_fires_before_midnight(self):
|
||||
"""PREFIRE_OFFSET must be ~40ms (one-way RTT to server)."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
self.assertIn("PREFIRE_OFFSET", source)
|
||||
# Extract the numeric value
|
||||
import re
|
||||
match = re.search(r"PREFIRE_OFFSET\s*=\s*([\d.]+)", source)
|
||||
self.assertIsNotNone(match, "PREFIRE_OFFSET must be defined")
|
||||
offset = float(match.group(1))
|
||||
# Must be between 30ms and 60ms (one-way RTT measured at 38-48ms)
|
||||
self.assertGreaterEqual(offset, 0.030, f"PREFIRE_OFFSET too small: {offset}s")
|
||||
self.assertLessEqual(offset, 0.060, f"PREFIRE_OFFSET too large: {offset}s")
|
||||
|
||||
|
||||
class TestConnectionDropRecovery(unittest.TestCase):
|
||||
"""make_request must reconnect after RemoteDisconnected and retry."""
|
||||
|
||||
def test_make_request_reconnect_on_RemoteDisconnected(self):
|
||||
"""When conn.request raises RemoteDisconnected, make_request must reconnect."""
|
||||
# Set up a mock conn that raises RemoteDisconnected on first request,
|
||||
# then succeeds on retry after reconnection.
|
||||
mock_conn = MagicMock()
|
||||
request_calls = [0]
|
||||
|
||||
def mock_request_side_effect(method, url, body=None, headers=None):
|
||||
request_calls[0] += 1
|
||||
if request_calls[0] == 1:
|
||||
raise http.client.RemoteDisconnected("Server closed connection")
|
||||
resp = MagicMock()
|
||||
resp.getcode.return_value = 200
|
||||
resp.read.return_value = b'{"result":"retry_success"}'
|
||||
return resp
|
||||
mock_conn.request.side_effect = mock_request_side_effect
|
||||
mock_conn.getresponse.return_value = MagicMock()
|
||||
|
||||
# Inject as global conn
|
||||
original_conn = _ns.get("conn", None)
|
||||
_ns["conn"] = mock_conn
|
||||
|
||||
# Patch ssl.create_default_context and HTTPSConnection to simulate reconnection
|
||||
mock_ctx = MagicMock()
|
||||
mock_resp_after_reconnect = MagicMock()
|
||||
mock_resp_after_reconnect.getcode.return_value = 200
|
||||
mock_resp_after_reconnect.read.return_value = b'{"result":"reconnected_success"}'
|
||||
mock_conn_after_reconnect = MagicMock()
|
||||
mock_conn_after_reconnect.request.return_value = None
|
||||
mock_conn_after_reconnect.getresponse.return_value = mock_resp_after_reconnect
|
||||
|
||||
with patch("ssl.create_default_context", return_value=mock_ctx) as mock_ssl:
|
||||
with patch("http.client.HTTPSConnection", return_value=mock_conn_after_reconnect) as mock_https:
|
||||
result, dj, status, ms = make_request(build_check_body(), "TEST")
|
||||
|
||||
# Should have reconnected (ssl.create_default_context called once)
|
||||
self.assertEqual(mock_ssl.call_count, 1, "Should have called ssl.create_default_context once for reconnect")
|
||||
self.assertEqual(mock_https.call_count, 1, "Should have created 1 new HTTPSConnection for reconnect")
|
||||
|
||||
# The result should be from the reconnected request
|
||||
self.assertEqual(result, "reconnected_success")
|
||||
|
||||
# Restore
|
||||
_ns["conn"] = original_conn or None
|
||||
|
||||
def test_make_request_returns_exception_on_persistent_failure(self):
|
||||
"""If reconnection also fails, make_request must return EXCEPTION string."""
|
||||
_ns["conn"] = None # Ensure conn is None
|
||||
|
||||
result, dj, status, ms = make_request('{"test":1}', "FAIL")
|
||||
|
||||
self.assertTrue(result.startswith("EXCEPTION:"),
|
||||
f"Expected EXCEPTION prefix, got: {result[:50]}")
|
||||
self.assertIsNone(dj)
|
||||
self.assertEqual(status, 0)
|
||||
|
||||
|
||||
class TestHTTPKeepAlive(unittest.TestCase):
|
||||
"""Connection must support HTTP keep-alive (multiple requests on same conn)."""
|
||||
|
||||
def test_sequential_requests_same_connection(self):
|
||||
"""Multiple requests on the same connection must all succeed."""
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
conn.connect()
|
||||
|
||||
body = build_check_body()
|
||||
responses = []
|
||||
for i in range(3):
|
||||
conn.request("POST", ENDPOINT, body=body, headers=HEADERS)
|
||||
resp = conn.getresponse()
|
||||
raw = resp.read().decode()
|
||||
responses.append(resp.status)
|
||||
time.sleep(0.3) # 300ms between requests
|
||||
|
||||
conn.close()
|
||||
|
||||
self.assertEqual(responses, [200, 200, 200],
|
||||
f"All 3 sequential requests should return 200, got: {responses}")
|
||||
|
||||
|
||||
class TestForceReconnectInBookingLoop(unittest.TestCase):
|
||||
"""The EXCEPTION branch in the booking loop must force-reconnect."""
|
||||
|
||||
def test_exception_branch_force_reconnects(self):
|
||||
"""Source must have force-reconnect logic in EXCEPTION branch."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
|
||||
# Find the EXCEPTION branch
|
||||
exc_idx = source.index('elif result.startswith("EXCEPTION:")')
|
||||
exc_section = source[exc_idx:exc_idx + 500]
|
||||
|
||||
self.assertIn("Force-reconnected", exc_section)
|
||||
self.assertIn("HTTPSConnection", exc_section)
|
||||
self.assertIn("conn.connect()", exc_section)
|
||||
|
||||
|
||||
class TestGlobalConnScoping(unittest.TestCase):
|
||||
"""global conn must be correctly scoped in make_request."""
|
||||
|
||||
def test_global_conn_in_make_request(self):
|
||||
"""make_request must declare 'global conn'."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
|
||||
idx = source.index("def make_request(")
|
||||
func_body = source[idx:idx + 300]
|
||||
self.assertIn("global conn", func_body)
|
||||
|
||||
def test_conn_initialized_at_module_level(self):
|
||||
"""conn must be initialized at module level (conn = None)."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
# Find the module-level conn = None
|
||||
lines = source.split("\n")
|
||||
found = False
|
||||
in_make_request = False
|
||||
for line in lines:
|
||||
if "def make_request" in line:
|
||||
in_make_request = True
|
||||
if "def build_booking_body" in line:
|
||||
in_make_request = False
|
||||
if line.strip() == "conn = None" and not in_make_request:
|
||||
found = True
|
||||
break
|
||||
self.assertTrue(found, "conn = None not found at module level")
|
||||
|
||||
|
||||
class TestConnectionNotInExceptionPath(unittest.TestCase):
|
||||
"""The booking body should never contain connection state."""
|
||||
|
||||
def test_booking_body_has_no_conn_reference(self):
|
||||
"""build_booking_body should not reference conn."""
|
||||
body = build_booking_body("16:00")
|
||||
# The body should be pure JSON with user data, no conn/IP leak
|
||||
self.assertNotIn("conn", body.lower())
|
||||
self.assertNotIn("127.0.0.1", body) # No localhost leak
|
||||
|
||||
|
||||
class TestRealConnectionReuse(unittest.TestCase):
|
||||
"""Live test: verify the actual API accepts multiple requests on one connection."""
|
||||
|
||||
def test_live_keepalive_reuse(self):
|
||||
"""The real API must support keep-alive for sequential getHorasCitas + insertarCita."""
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
conn.connect()
|
||||
|
||||
try:
|
||||
# Phase 3b: getHorasCitas
|
||||
check_body = build_check_body()
|
||||
conn.request("POST", ENDPOINT, body=check_body, headers=HEADERS)
|
||||
resp1 = conn.getresponse()
|
||||
raw1 = resp1.read().decode()
|
||||
self.assertEqual(resp1.status, 200)
|
||||
|
||||
# Phase 4: insertarCitaPrevia (same connection!)
|
||||
book_body = build_booking_body("16:00")
|
||||
conn.request("POST", ENDPOINT, body=book_body, headers=HEADERS)
|
||||
resp2 = conn.getresponse()
|
||||
raw2 = resp2.read().decode()
|
||||
self.assertEqual(resp2.status, 200)
|
||||
|
||||
# Both should work on SAME connection
|
||||
dj1 = json.loads(raw1)
|
||||
dj2 = json.loads(raw2)
|
||||
|
||||
self.assertIn("result", dj1)
|
||||
self.assertIn("result", dj2)
|
||||
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_live_tls_handshake_count(self):
|
||||
"""Verify that establishing one TLS connection and reusing it is cheaper.
|
||||
|
||||
This proves the pre-warmed connection design is correct:
|
||||
1 TLS handshake + N HTTP requests < N TLS handshakes + N HTTP requests
|
||||
"""
|
||||
# Single connection for 3 requests
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
|
||||
t0 = time.time()
|
||||
conn.connect()
|
||||
tls_time = (time.time() - t0) * 1000
|
||||
|
||||
body = build_check_body()
|
||||
total_api_time = 0
|
||||
for _ in range(3):
|
||||
t0 = time.time()
|
||||
conn.request("POST", ENDPOINT, body=body, headers=HEADERS)
|
||||
resp = conn.getresponse()
|
||||
resp.read()
|
||||
total_api_time += (time.time() - t0) * 1000
|
||||
|
||||
conn.close()
|
||||
|
||||
# 3 separate connections
|
||||
total_separate = 0
|
||||
for _ in range(3):
|
||||
t0 = time.time()
|
||||
ctx = ssl.create_default_context()
|
||||
conn = http.client.HTTPSConnection(HOST, PORT, context=ctx, timeout=20)
|
||||
conn.connect()
|
||||
conn.request("POST", ENDPOINT, body=body, headers=HEADERS)
|
||||
resp = conn.getresponse()
|
||||
resp.read()
|
||||
total_separate += (time.time() - t0) * 1000
|
||||
conn.close()
|
||||
|
||||
savings = total_separate - (tls_time + total_api_time)
|
||||
print(f"\n Single TLS + 3 requests: {tls_time:.0f}ms (TLS) + {total_api_time:.0f}ms (API) = {tls_time + total_api_time:.0f}ms")
|
||||
print(f" 3 separate TLS + 3 requests: {total_separate:.0f}ms")
|
||||
print(f" Pre-warm savings: {savings:.0f}ms ({(savings/total_separate)*100:.0f}% faster)")
|
||||
|
||||
self.assertGreater(savings, 0,
|
||||
"Reusing TLS connection should be faster than separate connections")
|
||||
|
||||
|
||||
class TestMakeRequestDoesNotRecreateConn(unittest.TestCase):
|
||||
"""Critical: make_request must NOT call ssl.create_default_context or HTTPSConnection
|
||||
when the existing conn is healthy. Creating a new conn inside make_request on every
|
||||
call would defeat the purpose of Phase 2 pre-warm."""
|
||||
|
||||
def test_source_no_unnecessary_recreate_in_make_request(self):
|
||||
"""In the normal (no-error) path of make_request, no new conn is created."""
|
||||
with open(SCRIPT_PATH) as f:
|
||||
source = f.read()
|
||||
|
||||
# Extract just the make_request function
|
||||
start = source.index("def make_request(")
|
||||
end = source.index("def build_booking_body(")
|
||||
func = source[start:end]
|
||||
|
||||
# The normal path (before any except block) should NOT contain HTTPSConnection
|
||||
normal_path = func.split("except")[0] # everything before the first except
|
||||
self.assertNotIn("HTTPSConnection", normal_path,
|
||||
"make_request normal path should NOT create HTTPSConnection — should reuse conn")
|
||||
self.assertNotIn("ssl.create_default_context", normal_path,
|
||||
"make_request normal path should NOT create SSL context — should reuse conn")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 70)
|
||||
print(" TLS CONNECTION REUSE & ATOMIC HAND-OFF TEST SUITE")
|
||||
print(" /tmp/test_connection_reuse.py")
|
||||
print("=" * 70)
|
||||
print()
|
||||
unittest.main(verbosity=2)
|
||||
14
url.js
Normal file
14
url.js
Normal file
@@ -0,0 +1,14 @@
|
||||
/**
|
||||
* Created by 67398834 on 29/05/2018.
|
||||
*/
|
||||
|
||||
// DESI
|
||||
//var url_base = "https://t-webservices.alicante.es/WsCitaPreviaPublic-RESTWebService-context-root/rest/1"
|
||||
//var url_base = "https://t-gf3:31381/WsCitaPreviaPublic/rest/1";
|
||||
|
||||
// PROD
|
||||
var url_base = "https://webservices.alicante.es/WsCitaPreviaPublic-RESTWebService-context-root/rest/1"
|
||||
//var url_base = "https://gf3:31381/WsCitaPreviaPublic/rest/1"
|
||||
|
||||
//var url_base = "http://VX3511006DC0084:7101/WsCitaPreviaPublic/rest/1";
|
||||
//var url_base_antigua = "http://127.0.0.1:7101/WsCitaPrevia/rest/v2";
|
||||
Reference in New Issue
Block a user