prepare. */ class Validafirma_Envios { /** * Estados posibles de un envío. * * @return array slug => etiqueta. */ public static function estados() { return array( 'enviando' => __( 'Enviando', 'validafirma' ), 'sin_enviar' => __( 'Sin enviar', 'validafirma' ), 'pendiente' => __( 'Pendiente', 'validafirma' ), 'firmado' => __( 'Firmado', 'validafirma' ), 'rechazado' => __( 'Rechazado', 'validafirma' ), 'vencido' => __( 'Vencido', 'validafirma' ), 'cancelado' => __( 'Cancelado', 'validafirma' ), 'no_emitido' => __( 'No emitido', 'validafirma' ), ); } /** * Etiquetas de los motivos de «Sin enviar». * * @return array código => etiqueta. */ public static function motivos() { return array( 'tope_dia' => __( 'tope del día', 'validafirma' ), 'saldo' => __( 'saldo', 'validafirma' ), 'plan' => __( 'plan', 'validafirma' ), 'rut_invalido' => __( 'RUT inválido', 'validafirma' ), 'rut_no_de_prueba' => __( 'RUT que no es de prueba', 'validafirma' ), 'desconectado' => __( 'sitio desconectado', 'validafirma' ), 'tope_prueba' => __( 'tope del modo de prueba', 'validafirma' ), 'carpeta' => __( 'carpeta no disponible', 'validafirma' ), 'sin_respuesta' => __( 'ValidaFirma no respondió', 'validafirma' ), 'falta_firmante_empresa' => __( 'falta el firmante de la empresa', 'validafirma' ), 'contacto_invalido' => __( 'contacto inválido', 'validafirma' ), 'error_de_api' => __( 'ValidaFirma no aceptó el envío', 'validafirma' ), 'archivo_no_disponible' => __( 'el PDF ya no está disponible', 'validafirma' ), 'no_encontrado' => __( 'ValidaFirma ya no tiene el documento', 'validafirma' ), ); } /** * Fecha y hora actuales en UTC para la base de datos. * * @param int|null $marca Marca de tiempo; ahora si es null. * @return string */ public static function ahora( $marca = null ) { return gmdate( 'Y-m-d H:i:s', null === $marca ? time() : $marca ); } /** * Convierte una fila de la base en un arreglo con tipos y JSON decodificado. * * @param array|null $fila Fila cruda. * @return array|null */ private static function decodificar( $fila ) { if ( ! is_array( $fila ) ) { return null; } $fila['id'] = (int) $fila['id']; $fila['entrada_ref'] = (int) $fila['entrada_ref']; $fila['es_prueba'] = (bool) $fila['es_prueba']; $fila['intentos'] = (int) $fila['intentos']; $fila['creado_por'] = (int) $fila['creado_por']; $firmantes = json_decode( (string) $fila['firmantes'], true ); $opciones = json_decode( (string) $fila['opciones'], true ); $fila['firmantes'] = is_array( $firmantes ) ? $firmantes : array(); $fila['opciones'] = is_array( $opciones ) ? $opciones : array(); return $fila; } /** * Crea un envío. * * @param array $datos Campos del envío (firmantes y opciones como arreglos). * @return int Id del envío, o 0 si no se pudo. */ public static function crear( array $datos ) { global $wpdb; $ahora = self::ahora(); $fila = wp_parse_args( $datos, array( 'origen' => 'panel', 'origen_ref' => '', 'entrada_ref' => 0, 'estado' => 'enviando', 'motivo' => null, 'es_prueba' => 0, 'documento_id' => null, 'titulo' => '', 'firmantes' => array(), 'opciones' => array(), 'clave_idempotencia' => '', 'archivo_original' => null, 'archivo_firmado' => null, 'correo_principal' => null, 'intentos' => 0, 'creado_por' => get_current_user_id(), ) ); $fila['firmantes'] = wp_json_encode( $fila['firmantes'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); $fila['opciones'] = wp_json_encode( $fila['opciones'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); $fila['es_prueba'] = $fila['es_prueba'] ? 1 : 0; $fila['creado_en'] = $ahora; $fila['actualizado_en'] = $ahora; $ok = $wpdb->insert( Validafirma_Instalador::tabla( 'envios' ), $fila ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery return $ok ? (int) $wpdb->insert_id : 0; } /** * Lee un envío. * * @param int $id Id del envío. * @return array|null */ public static function obtener( $id ) { global $wpdb; $tabla = Validafirma_Instalador::tabla( 'envios' ); $fila = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE id = %d', $tabla, (int) $id ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery return self::decodificar( $fila ); } /** * Busca el envío de un documento de ValidaFirma. * * @param string $documento_id Id del documento en ValidaFirma. * @return array|null */ public static function por_documento( $documento_id ) { global $wpdb; $tabla = Validafirma_Instalador::tabla( 'envios' ); $fila = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE documento_id = %s LIMIT 1', $tabla, (string) $documento_id ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery return self::decodificar( $fila ); } /** * Actualiza campos de un envío. * * @param int $id Id del envío. * @param array $campos Campos nuevos (firmantes y opciones pueden ir como arreglos). * @return bool */ public static function actualizar( $id, array $campos ) { global $wpdb; foreach ( array( 'firmantes', 'opciones' ) as $json ) { if ( isset( $campos[ $json ] ) && is_array( $campos[ $json ] ) ) { $campos[ $json ] = wp_json_encode( $campos[ $json ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); } } if ( isset( $campos['es_prueba'] ) ) { $campos['es_prueba'] = $campos['es_prueba'] ? 1 : 0; } $campos['actualizado_en'] = self::ahora(); return false !== $wpdb->update( Validafirma_Instalador::tabla( 'envios' ), $campos, array( 'id' => (int) $id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery } /** * Cambia el estado solo si el envío sigue en alguno de los estados de partida. * Es atómico: de dos procesos que lo intenten a la vez, solo uno lo logra (cada transición ocurre una vez). * * @param int $id Id del envío. * @param string[] $desde Estados desde los que se permite. * @param string $nuevo Estado nuevo. * @param array $extra Otros campos a poner en la misma sentencia. * @return bool Verdadero si este llamado hizo el cambio. */ public static function transicionar( $id, array $desde, $nuevo, array $extra = array() ) { global $wpdb; $tabla = Validafirma_Instalador::tabla( 'envios' ); $campos = array_merge( $extra, array( 'estado' => $nuevo, 'actualizado_en' => self::ahora(), ) ); $cambios = array(); foreach ( $campos as $columna => $valor ) { if ( preg_match( '/^[a-z_]+$/', $columna ) ) { $cambios[ $columna ] = null === $valor ? null : (string) $valor; } } // Un UPDATE por estado de origen, cada uno atómico y con su condición `id` + `estado` (WordPress escapa tabla, // columnas y valores): el primero que encuentra el envío en ese estado lo cambia y los demás ya no lo ven. $filas = 0; foreach ( array_unique( array_map( 'strval', $desde ) ) as $estado_origen ) { $filas = (int) $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $tabla, $cambios, array( 'id' => (int) $id, 'estado' => $estado_origen, ) ); if ( $filas > 0 ) { break; } } return 1 === $filas; } /** * Lista envíos para la pantalla. * * @param array $args vista (produccion|prueba|sin_enviar|todos), por_pagina, pagina. * @return array{items: array, total: int} */ public static function listar( array $args = array() ) { global $wpdb; $tabla = Validafirma_Instalador::tabla( 'envios' ); $args = wp_parse_args( $args, array( 'vista' => 'todos', 'por_pagina' => 20, 'pagina' => 1, ) ); $vista = self::parametros_de_vista( $args['vista'] ); $limite = max( 1, (int) $args['por_pagina'] ); $salto = max( 0, ( (int) $args['pagina'] - 1 ) * $limite ); $total = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE es_prueba IN (%d, %d) AND (estado = 'sin_enviar') IN (%d, %d)", $tabla, $vista[0], $vista[1], $vista[2], $vista[3] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $filas = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE es_prueba IN (%d, %d) AND (estado = 'sin_enviar') IN (%d, %d) ORDER BY id DESC LIMIT %d OFFSET %d", $tabla, $vista[0], $vista[1], $vista[2], $vista[3], $limite, $salto ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $items = array(); foreach ( is_array( $filas ) ? $filas : array() as $fila ) { $items[] = self::decodificar( $fila ); } return array( 'items' => $items, 'total' => $total, ); } /** * Cuenta de envíos por vista. * * @return array vista => cantidad. */ public static function contar_vistas() { global $wpdb; $tabla = Validafirma_Instalador::tabla( 'envios' ); $conteos = array(); foreach ( array( 'produccion', 'prueba', 'sin_enviar' ) as $vista ) { $valores = self::parametros_de_vista( $vista ); $conteos[ $vista ] = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM %i WHERE es_prueba IN (%d, %d) AND (estado = 'sin_enviar') IN (%d, %d)", $tabla, $valores[0], $valores[1], $valores[2], $valores[3] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery } return $conteos; } /** * Valores de cada vista para la condición `es_prueba IN (%d, %d) AND (estado = 'sin_enviar') IN (%d, %d)`: * los dos primeros son los valores de `es_prueba` que entran; los dos últimos, si entran los «Sin enviar» (1), * los demás (0) o ambos. La sentencia es una sola y fija; la vista solo elige números (no recibe texto de la persona). * * @param string $vista Nombre de la vista. * @return int[] */ private static function parametros_de_vista( $vista ) { switch ( $vista ) { case 'produccion': return array( 0, 0, 0, 0 ); case 'prueba': return array( 1, 1, 0, 0 ); case 'sin_enviar': return array( 0, 1, 1, 1 ); default: return array( 0, 1, 0, 1 ); } } /** * Envíos que siguen pendientes y toca revisar (RN-05): última revisión hace 15 minutos o más. * * @param int $limite Máximo por pasada. * @return array[] */ public static function para_revisar( $limite = 50 ) { global $wpdb; $tabla = Validafirma_Instalador::tabla( 'envios' ); $corte = self::ahora( time() - 15 * MINUTE_IN_SECONDS ); $filas = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery $wpdb->prepare( "SELECT * FROM %i WHERE estado = 'pendiente' AND documento_id IS NOT NULL AND (motivo IS NULL OR motivo <> 'no_encontrado') AND (ultima_revision_en IS NULL OR ultima_revision_en <= %s) ORDER BY ultima_revision_en IS NULL DESC, ultima_revision_en ASC LIMIT %d", $tabla, $corte, (int) $limite ), ARRAY_A ); return array_map( array( __CLASS__, 'decodificar' ), is_array( $filas ) ? $filas : array() ); } /** * Envíos en reintento (falla de red o de ValidaFirma). * * @return array[] */ public static function para_reintentar() { global $wpdb; $tabla = Validafirma_Instalador::tabla( 'envios' ); $filas = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE estado = 'enviando' AND primer_intento_en IS NOT NULL ORDER BY id ASC LIMIT 50", $tabla ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery return array_map( array( __CLASS__, 'decodificar' ), is_array( $filas ) ? $filas : array() ); } /** * Envíos firmados cuya copia local falta. * * @return array[] */ public static function firmados_sin_copia() { global $wpdb; $tabla = Validafirma_Instalador::tabla( 'envios' ); $filas = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM %i WHERE estado = 'firmado' AND archivo_firmado IS NULL AND documento_id IS NOT NULL LIMIT 50", $tabla ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery return array_map( array( __CLASS__, 'decodificar' ), is_array( $filas ) ? $filas : array() ); } /** * Busca los envíos donde figura un correo (para el exportador y el borrador de WordPress). * * @param string $correo Correo a buscar. * @return array[] */ public static function por_correo( $correo ) { global $wpdb; $tabla = Validafirma_Instalador::tabla( 'envios' ); $correo = strtolower( trim( (string) $correo ) ); if ( '' === $correo ) { return array(); } $patron = '%' . $wpdb->esc_like( wp_json_encode( $correo, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ) ) . '%'; $filas = $wpdb->get_results( $wpdb->prepare( 'SELECT * FROM %i WHERE correo_principal = %s OR LOWER(firmantes) LIKE %s ORDER BY id ASC', $tabla, $correo, $patron ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery $envios = array(); foreach ( is_array( $filas ) ? $filas : array() as $fila ) { $envio = self::decodificar( $fila ); // El LIKE puede traer falsos positivos: se confirma con la comparación exacta. foreach ( $envio['firmantes'] as $firmante ) { if ( isset( $firmante['email'] ) && strtolower( (string) $firmante['email'] ) === $correo ) { $envios[] = $envio; break; } } } return $envios; } /** * Borra un envío del sitio, con sus datos y las copias locales de los PDF (D-34). * No borra el PDF fijo de la Biblioteca de medios ni toca la copia de ValidaFirma. * * @param int $id Id del envío. * @return bool */ public static function eliminar( $id ) { global $wpdb; $envio = self::obtener( $id ); if ( ! $envio ) { return false; } Validafirma_Almacen_Privado::borrar( $envio['archivo_original'] ); Validafirma_Almacen_Privado::borrar( $envio['archivo_firmado'] ); return false !== $wpdb->delete( Validafirma_Instalador::tabla( 'envios' ), array( 'id' => (int) $id ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery } /** * Borra los «Sin enviar» de más de 30 días (RN-16). * * @return int Cantidad borrada. */ public static function purgar_sin_enviar() { global $wpdb; $tabla = Validafirma_Instalador::tabla( 'envios' ); $corte = self::ahora( time() - 30 * DAY_IN_SECONDS ); $ids = $wpdb->get_col( $wpdb->prepare( "SELECT id FROM %i WHERE estado = 'sin_enviar' AND actualizado_en < %s", $tabla, $corte ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery foreach ( is_array( $ids ) ? $ids : array() as $id ) { self::eliminar( (int) $id ); } return is_array( $ids ) ? count( $ids ) : 0; } /** * Versión pública de un envío: sin rutas de archivo ni datos internos. * * @param array $envio Envío completo. * @return array */ public static function publico( array $envio ) { unset( $envio['archivo_original'], $envio['clave_idempotencia'] ); $envio['tiene_firmado'] = ! empty( $envio['archivo_firmado'] ); unset( $envio['archivo_firmado'] ); return $envio; } }