'1tb', 'num_users' => 2, 'payment_frequency' => 'monthly' ]; } // Validar y normalizar storage $storage_space = sanitize_text_field($config_data['storage_space'] ?? '1tb'); $valid_storage = ['1tb', '2tb', '3tb', '4tb', '5tb', '6tb', '7tb', '8tb', '9tb', '10tb', '15tb', '20tb']; if (!in_array($storage_space, $valid_storage, true)) { $storage_space = '1tb'; } // Validar y normalizar usuarios (mínimo 2, máximo 20) $num_users = max(2, min(20, intval($config_data['num_users'] ?? 2))); // Validar y normalizar frecuencia $payment_frequency = sanitize_text_field($config_data['payment_frequency'] ?? 'monthly'); $valid_frequencies = ['monthly', 'semiannual', 'annual', 'biennial', 'triennial', 'quadrennial', 'quinquennial']; if (!in_array($payment_frequency, $valid_frequencies, true)) { $payment_frequency = 'monthly'; } return [ 'storage_space' => $storage_space, 'num_users' => $num_users, 'payment_frequency' => $payment_frequency ]; } } /** * Obtiene configuración real del usuario desde múltiples fuentes */ function nextcloud_banda_get_user_real_config($user_id, $membership = null) { $real_config = [ 'storage_space' => null, 'num_users' => null, 'payment_frequency' => null, 'final_amount' => null, 'source' => 'none' ]; // 1. Intentar obtener desde configuración guardada (JSON) $config_json = get_user_meta($user_id, 'nextcloud_banda_config', true); if (!empty($config_json)) { $config = json_decode($config_json, true); if (is_array($config) && json_last_error() === JSON_ERROR_NONE && !isset($config['auto_created'])) { $real_config['storage_space'] = $config['storage_space'] ?? null; $real_config['num_users'] = $config['num_users'] ?? null; $real_config['payment_frequency'] = $config['payment_frequency'] ?? null; $real_config['final_amount'] = $config['final_amount'] ?? null; $real_config['source'] = 'saved_config'; nextcloud_banda_log_debug("Real config found from saved JSON for user {$user_id}", $real_config); return $real_config; } } // 2. Intentar obtener desde campos personalizados de PMPro Register Helper if (function_exists('pmprorh_getProfileField')) { $storage_field = pmprorh_getProfileField('storage_space', $user_id); $users_field = pmprorh_getProfileField('num_users', $user_id); $frequency_field = pmprorh_getProfileField('payment_frequency', $user_id); if (!empty($storage_field) || !empty($users_field) || !empty($frequency_field)) { $real_config['storage_space'] = $storage_field ?: null; $real_config['num_users'] = $users_field ? intval($users_field) : null; $real_config['payment_frequency'] = $frequency_field ?: null; $real_config['source'] = 'profile_fields'; nextcloud_banda_log_debug("Real config found from profile fields for user {$user_id}", $real_config); return $real_config; } } // 3. Intentar obtener desde user_meta directo $storage_meta = get_user_meta($user_id, 'storage_space', true); $users_meta = get_user_meta($user_id, 'num_users', true); $frequency_meta = get_user_meta($user_id, 'payment_frequency', true); if (!empty($storage_meta) || !empty($users_meta) || !empty($frequency_meta)) { $real_config['storage_space'] = $storage_meta ?: null; $real_config['num_users'] = $users_meta ? intval($users_meta) : null; $real_config['payment_frequency'] = $frequency_meta ?: null; $real_config['source'] = 'user_meta'; nextcloud_banda_log_debug("Real config found from user meta for user {$user_id}", $real_config); return $real_config; } // 4. Intentar deducir desde información de membresía if ($membership && !empty($membership->initial_payment)) { $real_config['final_amount'] = (float)$membership->initial_payment; $real_config['source'] = 'membership_deduction'; nextcloud_banda_log_debug("Config deduced from membership for user {$user_id}", $real_config); } // Verificar si el usuario tiene una membresía activa if (!pmpro_hasMembershipLevel($user_id)) { // Forzar valores por defecto si no hay membresía activa return [ 'storage_space' => '1tb', 'num_users' => 2, 'payment_frequency' => 'monthly', 'final_amount' => null, 'source' => 'defaults_no_membership' ]; } nextcloud_banda_log_debug("No real config found for user {$user_id}, returning empty", $real_config); return $real_config; } /** * Configuración centralizada - SINCRONIZADA */ function nextcloud_banda_get_config($key = null) { static $config = null; if ($config === null) { $config = [ 'allowed_levels' => [2], // ID del nivel Nextcloud Banda 'price_per_tb' => 70.00, // Precio por TB adicional 'price_per_additional_user' => 10.00, // Precio por usuario adicional 'base_users_included' => 2, // Usuarios incluidos en precio base 'base_storage_included' => 1, // TB incluidos en precio base 'base_price_default' => NEXTCLOUD_BANDA_BASE_PRICE, // CORREGIDO: Usar constante 'min_users' => 2, 'max_users' => 20, 'min_storage' => 1, 'max_storage' => 20, 'frequency_multipliers' => [ 'monthly' => 1.0, 'semiannual' => 5.7, 'annual' => 10.8, 'biennial' => 20.4, 'triennial' => 28.8, 'quadrennial' => 36.0, 'quinquennial' => 42.0 ], 'storage_options' => [ '1tb' => '1 Terabyte', '2tb' => '2 Terabytes', '3tb' => '3 Terabytes', '4tb' => '4 Terabytes', '5tb' => '5 Terabytes', '6tb' => '6 Terabytes', '7tb' => '7 Terabytes', '8tb' => '8 Terabytes', '9tb' => '9 Terabytes', '10tb' => '10 Terabytes', '15tb' => '15 Terabytes', '20tb' => '20 Terabytes' ], 'user_options' => [ '2' => '2 usuários (incluídos)', '3' => '3 usuários', '4' => '4 usuários', '5' => '5 usuários', '6' => '6 usuários', '7' => '7 usuários', '8' => '8 usuários', '9' => '9 usuários', '10' => '10 usuários', '15' => '15 usuários', '20' => '20 usuários' ] ]; } return $key ? ($config[$key] ?? null) : $config; } // ==== // SISTEMA DE LOGGING // ==== function nextcloud_banda_log($level, $message, $context = []) { static $log_level = null; if ($log_level === null) { if (defined('WP_DEBUG') && WP_DEBUG) { $log_level = 4; // DEBUG } elseif (defined('WP_DEBUG_LOG') && WP_DEBUG_LOG) { $log_level = 3; // INFO } else { $log_level = 1; // ERROR only } } $levels = [1 => 'ERROR', 2 => 'WARNING', 3 => 'INFO', 4 => 'DEBUG']; if ($level > $log_level) return; $log_message = sprintf( '[PMPro Banda %s] %s', $levels[$level], $message ); if (!empty($context)) { $log_message .= ' | Context: ' . wp_json_encode($context, JSON_UNESCAPED_UNICODE); } error_log($log_message); } function nextcloud_banda_log_error($message, $context = []) { nextcloud_banda_log(1, $message, $context); } function nextcloud_banda_log_info($message, $context = []) { nextcloud_banda_log(3, $message, $context); } function nextcloud_banda_log_debug($message, $context = []) { nextcloud_banda_log(4, $message, $context); } // ==== // SISTEMA DE CACHÉ // ==== function nextcloud_banda_cache_get($key, $default = false) { $cached = wp_cache_get($key, NEXTCLOUD_BANDA_CACHE_GROUP); if ($cached !== false) { nextcloud_banda_log_debug("Cache hit for key: {$key}"); return $cached; } nextcloud_banda_log_debug("Cache miss for key: {$key}"); return $default; } function nextcloud_banda_cache_set($key, $data, $expiry = NEXTCLOUD_BANDA_CACHE_EXPIRY) { $result = wp_cache_set($key, $data, NEXTCLOUD_BANDA_CACHE_GROUP, $expiry); nextcloud_banda_log_debug("Cache set for key: {$key}", ['success' => $result]); return $result; } function nextcloud_banda_invalidate_user_cache($user_id) { $keys = [ "banda_config_{$user_id}", "pmpro_membership_{$user_id}", "last_payment_date_{$user_id}", "used_space_{$user_id}" ]; foreach ($keys as $key) { wp_cache_delete($key, NEXTCLOUD_BANDA_CACHE_GROUP); } nextcloud_banda_log_info("User cache invalidated", ['user_id' => $user_id]); } // ==== // FUNCIONES DE API DE NEXTCLOUD - CORREGIDAS // ==== function nextcloud_banda_api_get_group_used_space_mb($user_id) { // CORREGIDO: Usar get_option en lugar de hardcoded $site_url = get_option('siteurl'); $nextcloud_api_url = 'https://cloud.' . parse_url($site_url, PHP_URL_HOST); // Obtener credenciales de variables de entorno $nextcloud_api_admin = getenv('NEXTCLOUD_API_ADMIN'); $nextcloud_api_pass = getenv('NEXTCLOUD_API_PASS'); // Verificar que las credenciales estén disponibles if (empty($nextcloud_api_admin) || empty($nextcloud_api_pass)) { nextcloud_banda_log_error('Las credenciales de la API de Nextcloud no están definidas en variables de entorno.'); return false; } // Obtener el nombre de usuario de WordPress, que se usará como el ID del grupo en Nextcloud $wp_user = get_userdata($user_id); if (!$wp_user) { nextcloud_banda_log_error("No se pudo encontrar el usuario de WordPress con ID: {$user_id}"); return false; } $group_id = 'banda-' . $user_id; // Argumentos base para las peticiones a la API $api_args = [ 'headers' => [ 'Authorization' => 'Basic ' . base64_encode($nextcloud_api_admin . ':' . $nextcloud_api_pass), 'OCS-APIRequest' => 'true', 'Accept' => 'application/json', ], 'timeout' => 20, ]; // Obtener la lista de usuarios del grupo $users_url = sprintf('%s/ocs/v2.php/cloud/groups/%s/users', $nextcloud_api_url, urlencode($group_id)); $response_users = wp_remote_get($users_url, $api_args); if (is_wp_error($response_users)) { nextcloud_banda_log_error('Error en la conexión a la API de Nextcloud (obteniendo usuarios)', ['error' => $response_users->get_error_message()]); return false; } $status_code_users = wp_remote_retrieve_response_code($response_users); if ($status_code_users !== 200) { nextcloud_banda_log_error("La API de Nextcloud devolvió un error al obtener usuarios del grupo '{$group_id}'", ['status_code' => $status_code_users]); return false; } $users_body = wp_remote_retrieve_body($response_users); $users_data = json_decode($users_body, true); if (empty($users_data['ocs']['data']['users'])) { nextcloud_banda_log_info("El grupo '{$group_id}' no tiene usuarios o no existe en Nextcloud. Se devuelve 0MB."); return 0.0; } $nextcloud_user_ids = $users_data['ocs']['data']['users']; $total_used_bytes = 0; // Obtener el espacio usado por cada usuario y sumarlo foreach ($nextcloud_user_ids as $nc_user_id) { $user_detail_url = sprintf('%s/ocs/v2.php/cloud/users/%s', $nextcloud_api_url, urlencode($nc_user_id)); $response_user = wp_remote_get($user_detail_url, $api_args); if (is_wp_error($response_user) || wp_remote_retrieve_response_code($response_user) !== 200) { nextcloud_banda_log_error("No se pudo obtener la información del usuario de Nextcloud: {$nc_user_id}"); continue; } $user_body = wp_remote_retrieve_body($response_user); $user_data = json_decode($user_body, true); if (isset($user_data['ocs']['data']['quota']['used'])) { $total_used_bytes += (int) $user_data['ocs']['data']['quota']['used']; } } // Convertir bytes a Megabytes $total_used_mb = $total_used_bytes / (1024 * 1024); nextcloud_banda_log_debug("Cálculo de espacio finalizado para el grupo '{$group_id}'", [ 'total_bytes' => $total_used_bytes, 'total_mb' => $total_used_mb, 'users_count' => count($nextcloud_user_ids) ]); return $total_used_mb; } // ==== // VERIFICACIÓN DE DEPENDENCIAS // ==== function nextcloud_banda_check_dependencies() { static $dependencies_checked = false; static $dependencies_ok = false; if ($dependencies_checked) { return $dependencies_ok; } $missing_plugins = []; if (!function_exists('pmprorh_add_registration_field')) { $missing_plugins[] = 'PMPro Register Helper'; nextcloud_banda_log_error('PMPro Register Helper functions not found'); } if (!function_exists('pmpro_getOption')) { $missing_plugins[] = 'Paid Memberships Pro'; nextcloud_banda_log_error('PMPro core functions not found'); } if (!class_exists('PMProRH_Field')) { $missing_plugins[] = 'PMProRH_Field class'; nextcloud_banda_log_error('PMProRH_Field class not available'); } if (!empty($missing_plugins) && is_admin() && current_user_can('manage_options')) { add_action('admin_notices', function() use ($missing_plugins) { $plugins_list = implode(', ', $missing_plugins); printf( '

PMPro Banda Dynamic: Los siguientes plugins son requeridos: %s

', esc_html($plugins_list) ); }); } $dependencies_ok = empty($missing_plugins); $dependencies_checked = true; return $dependencies_ok; } // ==== // FUNCIONES AUXILIARES // ==== /** * Convierte un valor de fecha a timestamp (segundos) de forma segura. * Acepta: int (unix), string (Y-m-d H:i:s, Y-m-d, d/m/Y, ISO) o DateTimeInterface. * Si no puede convertir, retorna time(). * Además, recorta a un rango razonable (1970-01-01 a 2100-01-01). */ function nextcloud_banda_safe_ts($value) { // int ya válido if (is_int($value) && $value > 0 && $value < 4102444800) { // ~2100-01-01 return $value; } // DateTime/Immutable if ($value instanceof DateTimeInterface) { $ts = $value->getTimestamp(); return ($ts > 0 && $ts < 4102444800) ? $ts : time(); } // String if (is_string($value) && $value !== '') { // Intentar formatos explícitos primero $formats = ['Y-m-d H:i:s', 'Y-m-d', 'd/m/Y', DateTimeInterface::ATOM, DATE_RFC3339]; foreach ($formats as $fmt) { $dt = DateTimeImmutable::createFromFormat($fmt, $value); if ($dt instanceof DateTimeImmutable) { $ts = $dt->getTimestamp(); return ($ts > 0 && $ts < 4102444800) ? $ts : time(); } } // Fallback general $ts = strtotime($value); if ($ts !== false && $ts > 0 && $ts < 4102444800) { return $ts; } } return time(); } /** * Obtiene info del ciclo actual basado estrictamente en cycle_number/cycle_period. * Usa DateTimeImmutable y la zona horaria de WordPress para evitar desajustes. * Devuelve timestamps (según timezone de WP) y next_payment_date como timestamp del final del ciclo actual (próximo cobro). */ function nextcloud_banda_get_next_payment_info($user_id) { global $wpdb; nextcloud_banda_log_debug("=== GET NEXT PAYMENT INFO START ==="); nextcloud_banda_log_debug("User ID: $user_id"); $current_level = pmpro_getMembershipLevelForUser($user_id); if (empty($current_level) || empty($current_level->id)) { nextcloud_banda_log_debug("ERROR: Usuario sin membresía activa"); return false; } $cycle_number = (int) ($current_level->cycle_number ?? 0); $cycle_period = strtolower((string) ($current_level->cycle_period ?? '')); nextcloud_banda_log_debug("Level Cycle: $cycle_number $cycle_period"); if ($cycle_number <= 0 || empty($cycle_period)) { nextcloud_banda_log_debug("ERROR: Ciclo inválido en el nivel"); return false; } // Zona horaria de WP $tz_string = get_option('timezone_string'); if (!$tz_string) { $gmt_offset = (float) get_option('gmt_offset', 0); $tz_string = $gmt_offset >= 0 ? "UTC+$gmt_offset" : "UTC$gmt_offset"; } try { $tz = new DateTimeZone($tz_string); } catch (Exception $e) { $tz = new DateTimeZone('UTC'); } // NOW en zona WP $now_ts = current_time('timestamp'); $now = (new DateTimeImmutable('@' . $now_ts))->setTimezone($tz); // Buscar última orden "success" $last_order = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->pmpro_membership_orders} WHERE user_id = %d AND status = 'success' ORDER BY timestamp DESC LIMIT 1", $user_id )); if (!$last_order) { $last_order = $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->pmpro_membership_orders} WHERE user_id = %d AND status = 'cancelled' ORDER BY timestamp DESC LIMIT 1", $user_id )) ?: $wpdb->get_row($wpdb->prepare( "SELECT * FROM {$wpdb->pmpro_membership_orders} WHERE user_id = %d AND status = 'refunded' ORDER BY timestamp DESC LIMIT 1", $user_id )); } // Determinar cycle_start_anchor if ($last_order && !empty($last_order->timestamp)) { try { $anchor_dt = new DateTimeImmutable($last_order->timestamp, $tz); } catch (Exception $e) { $anchor_dt = $now; nextcloud_banda_log_debug("WARN: No se pudo parsear timestamp de orden, usando NOW"); } nextcloud_banda_log_debug("Última orden: ID {$last_order->id}, Timestamp: " . $anchor_dt->format('Y-m-d H:i:s')); } elseif (!empty($current_level->startdate)) { $start_ts = nextcloud_banda_safe_ts($current_level->startdate); $anchor_dt = (new DateTimeImmutable('@' . $start_ts))->setTimezone($tz); nextcloud_banda_log_debug("Usando startdate: " . $anchor_dt->format('Y-m-d H:i:s')); } else { $anchor_dt = $now; nextcloud_banda_log_debug("No hay órdenes ni startdate, usando NOW como ancla"); } // Normalizar a 00:00 para consistencia en cálculos diarios $cycle_start_dt = $anchor_dt->setTime(0, 0, 0); nextcloud_banda_log_debug("Cycle Start (normalizado): " . $cycle_start_dt->format('Y-m-d H:i:s')); // Construir intervalo del ciclo switch ($cycle_period) { case 'day': case 'days': $interval_spec = 'P' . $cycle_number . 'D'; break; case 'week': case 'weeks': $interval_spec = 'P' . ($cycle_number * 7) . 'D'; break; case 'month': case 'months': $interval_spec = 'P' . $cycle_number . 'M'; break; case 'year': case 'years': $interval_spec = 'P' . $cycle_number . 'Y'; break; default: nextcloud_banda_log_debug("ERROR: Período desconocido: $cycle_period"); return false; } try { $interval = new DateInterval($interval_spec); } catch (Exception $e) { nextcloud_banda_log_debug("ERROR: No se pudo crear DateInterval: $interval_spec"); return false; } // Calcular el ciclo actual de forma eficiente $cycle_end_dt = $cycle_start_dt; // Para meses y años, usar cálculo más preciso if ($cycle_period === 'month' || $cycle_period === 'months' || $cycle_period === 'year' || $cycle_period === 'years') { // Estimar ciclos pasados $estimate_cycles = 0; if ($cycle_period === 'month' || $cycle_period === 'months') { $diff = $cycle_start_dt->diff($now); $months_total = $diff->y * 12 + $diff->m; $estimate_cycles = (int) floor($months_total / $cycle_number); } else { $diff = $cycle_start_dt->diff($now); $years_total = $diff->y; $estimate_cycles = (int) floor($years_total / $cycle_number); } // Avanzar por bloques grandes si hay muchos ciclos if ($estimate_cycles > 0) { try { $block_interval_spec = ($cycle_period === 'month' || $cycle_period === 'months') ? 'P' . ($estimate_cycles * $cycle_number) . 'M' : 'P' . ($estimate_cycles * $cycle_number) . 'Y'; $block_interval = new DateInterval($block_interval_spec); $cycle_start_dt = $cycle_start_dt->add($block_interval); $cycle_end_dt = $cycle_start_dt->add($interval); } catch (Exception $e) { // fallback a lógica simple $cycle_end_dt = $cycle_start_dt->add($interval); } } else { $cycle_end_dt = $cycle_start_dt->add($interval); } // Ajustar hasta encontrar el ciclo correcto $guard = 0; while ($cycle_end_dt <= $now && $guard < 24) { $cycle_start_dt = $cycle_end_dt; $cycle_end_dt = $cycle_start_dt->add($interval); $guard++; } if ($guard >= 24) { nextcloud_banda_log_debug("ERROR: Guardia excedida al ajustar ciclos"); return false; } } else { // Para días y semanas usar aritmética directa $days_per_cycle = ($cycle_period === 'week' || $cycle_period === 'weeks') ? $cycle_number * 7 : $cycle_number; $days_since_start = (int) floor(($now->getTimestamp() - $cycle_start_dt->getTimestamp()) / DAY_IN_SECONDS); $cycles_passed = ($days_since_start > 0) ? (int) floor($days_since_start / $days_per_cycle) : 0; if ($cycles_passed > 0) { $cycle_start_dt = $cycle_start_dt->add(new DateInterval('P' . ($cycles_passed * $days_per_cycle) . 'D')); } $cycle_end_dt = $cycle_start_dt->add(new DateInterval('P' . $days_per_cycle . 'D')); // Ajuste final si necesario if ($cycle_end_dt <= $now) { $cycle_start_dt = $cycle_end_dt; $cycle_end_dt = $cycle_start_dt->add(new DateInterval('P' . $days_per_cycle . 'D')); } } $cycle_start_ts = $cycle_start_dt->getTimestamp(); $cycle_end_ts = $cycle_end_dt->getTimestamp(); // Calcular días totales y restantes $total_seconds = max(1, $cycle_end_ts - $cycle_start_ts); $remaining_seconds = max(0, $cycle_end_ts - $now->getTimestamp()); $total_days = max(1, (int)round($total_seconds / DAY_IN_SECONDS)); $days_remaining = max(0, (int)floor($remaining_seconds / DAY_IN_SECONDS)); if ($remaining_seconds > 0 && $days_remaining === 0) { $days_remaining = 1; } nextcloud_banda_log_debug("Cycle Start: " . $cycle_start_dt->format('Y-m-d H:i:s')); nextcloud_banda_log_debug("Cycle End (Next Payment): " . $cycle_end_dt->format('Y-m-d H:i:s')); nextcloud_banda_log_debug("Days remaining: $days_remaining, Total days: $total_days"); nextcloud_banda_log_debug("=== GET NEXT PAYMENT INFO END ==="); return [ 'next_payment_date' => $cycle_end_ts, 'cycle_start' => $cycle_start_ts, 'cycle_end' => $cycle_end_ts, 'days_remaining' => $days_remaining, 'total_days' => $total_days ]; } // ==== // NUEVOS AUXILIARES // ==== /** * Obtiene información de prorrateo con cache */ function nextcloud_banda_get_cached_proration($user_id, $level_id, $storage, $users, $frequency) { $cache_key = "proration_{$user_id}_{$level_id}_{$storage}_{$users}_{$frequency}"; $cached = nextcloud_banda_cache_get($cache_key); if ($cached !== false) { return $cached; } $proration = nextcloud_banda_calculate_proration_core_aligned( $user_id, $level_id, $storage, $users, $frequency ); // Cache por 5 minutos nextcloud_banda_cache_set($cache_key, $proration, 300); return $proration; } /** * Formatea valores monetarios de forma consistente */ function nextcloud_banda_format_currency($amount) { return 'R$ ' . number_format((float)$amount, 2, ',', '.'); } /** * Formatea porcentajes de descuento */ function nextcloud_banda_format_discount($frequency) { $discounts = [ 'monthly' => 0, 'semiannual' => 5, 'annual' => 10, 'biennial' => 15, 'triennial' => 20, 'quadrennial' => 25, 'quinquennial' => 30 ]; $discount = $discounts[$frequency] ?? 0; return $discount > 0 ? "(-{$discount}%)" : ""; } /** * Valida configuración antes del checkout */ function nextcloud_banda_validate_checkout_config() { $required_fields = ['storage_space', 'num_users', 'payment_frequency']; foreach ($required_fields as $field) { if (!isset($_REQUEST[$field]) || empty($_REQUEST[$field])) { return new WP_Error('missing_field', "Campo requerido: {$field}"); } } // Validar rangos $num_users = (int)$_REQUEST['num_users']; $config = nextcloud_banda_get_config(); if ($num_users < $config['min_users'] || $num_users > $config['max_users']) { return new WP_Error('invalid_users', "Número de usuarios fuera de rango"); } $storage_space = sanitize_text_field($_REQUEST['storage_space']); $valid_storage = array_keys($config['storage_options']); if (!in_array($storage_space, $valid_storage, true)) { return new WP_Error('invalid_storage', "Espacio de almacenamiento inválido"); } $payment_frequency = strtolower(sanitize_text_field($_REQUEST['payment_frequency'])); $valid_frequencies = array_keys($config['frequency_multipliers']); if (!in_array($payment_frequency, $valid_frequencies, true)) { return new WP_Error('invalid_frequency', "Frecuencia de pago inválida"); } return true; } // ==== // NUEVOS AUXILIARES - FIN // ==== /** * Suma un período usando DateTimeImmutable para evitar errores de strtotime/DST. */ function nextcloud_banda_add_period($timestamp, $number, $period) { $number = (int)$number ?: 1; $period = strtolower($period); $dt = (new DateTimeImmutable('@' . nextcloud_banda_safe_ts($timestamp)))->setTimezone(wp_timezone()); switch ($period) { case 'day': case 'days': $interval = new DateInterval('P' . $number . 'D'); break; case 'week': case 'weeks': $interval = new DateInterval('P' . $number . 'W'); break; case 'month': case 'months': $interval = new DateInterval('P' . $number . 'M'); break; case 'year': case 'years': $interval = new DateInterval('P' . $number . 'Y'); break; default: $interval = new DateInterval('P' . $number . 'M'); break; } $res = $dt->add($interval); $ts = $res->getTimestamp(); // Saneo de rango if ($ts <= 0 || $ts >= 4102444800) { $ts = time(); } return $ts; } /** * Resta un período usando DateTimeImmutable. */ function nextcloud_banda_sub_period($timestamp, $number, $period) { $number = (int)$number ?: 1; $period = strtolower($period); $dt = (new DateTimeImmutable('@' . nextcloud_banda_safe_ts($timestamp)))->setTimezone(wp_timezone()); switch ($period) { case 'day': case 'days': $interval = new DateInterval('P' . $number . 'D'); break; case 'week': case 'weeks': $interval = new DateInterval('P' . $number . 'W'); break; case 'month': case 'months': $interval = new DateInterval('P' . $number . 'M'); break; case 'year': case 'years': $interval = new DateInterval('P' . $number . 'Y'); break; default: $interval = new DateInterval('P' . $number . 'M'); break; } $res = $dt->sub($interval); $ts = $res->getTimestamp(); if ($ts <= 0 || $ts >= 4102444800) { $ts = time(); } return $ts; } function nextcloud_banda_get_used_space_tb($user_id) { $cache_key = "used_space_{$user_id}"; $cached = nextcloud_banda_cache_get($cache_key); if ($cached !== false) { return $cached; } // Llamada a la función de API real $used_space_mb = nextcloud_banda_api_get_group_used_space_mb($user_id); // Si la llamada a la API falla, usar 0 como valor por defecto if ($used_space_mb === false) { nextcloud_banda_log_error("Fallo al obtener el espacio usado desde la API para user_id: {$user_id}. Se utilizará 0 como valor por defecto."); $used_space_mb = 0; } // Convierte el valor de MB a TB y redondea a 2 decimales $used_space_tb = round($used_space_mb / 1024, 2); // Guarda el resultado en caché por 5 minutos nextcloud_banda_cache_set($cache_key, $used_space_tb, 300); nextcloud_banda_log_debug("Espacio calculado desde API para user {$user_id}", [ 'used_space_mb' => $used_space_mb, 'used_space_tb' => $used_space_tb ]); return $used_space_tb; } function nextcloud_banda_get_current_level_id() { static $cached_level_id = null; if ($cached_level_id !== null) { return $cached_level_id; } // CORREGIDO: Usar filter_input para mayor seguridad $sources = [ filter_input(INPUT_GET, 'level', FILTER_VALIDATE_INT), filter_input(INPUT_GET, 'pmpro_level', FILTER_VALIDATE_INT), filter_input(INPUT_POST, 'level', FILTER_VALIDATE_INT), filter_input(INPUT_POST, 'pmpro_level', FILTER_VALIDATE_INT), isset($_SESSION['pmpro_level']) ? (int)$_SESSION['pmpro_level'] : null, isset($GLOBALS['pmpro_checkout_level']->id) ? (int)$GLOBALS['pmpro_checkout_level']->id : null, isset($GLOBALS['pmpro_level']->id) ? (int)$GLOBALS['pmpro_level']->id : null, ]; foreach ($sources as $source) { if ($source > 0) { $cached_level_id = $source; nextcloud_banda_log_debug("Nivel detectado: {$source}"); return $source; } } $cached_level_id = 0; return 0; } // ==== // CAMPOS DINÁMICOS - CORREGIDOS // ==== // Actualizar la función de campos dinámicos para manejar mejor el estado function nextcloud_banda_add_dynamic_fields() { $user_id = get_current_user_id(); // Verificar membresía activa usando next_payment_info if ($user_id) { $user_levels = pmpro_getMembershipLevelsForUser($user_id); $allowed_levels = nextcloud_banda_get_config('allowed_levels'); $has_active_banda_membership = false; if (!empty($user_levels)) { foreach ($user_levels as $level) { if (in_array((int)$level->id, $allowed_levels, true)) { // Verificar usando next_payment_info $cycle_info = nextcloud_banda_get_next_payment_info($user_id); if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { $has_active_banda_membership = true; break; } } } } // Si no tiene membresía Banda activa, limpiar caché if (!$has_active_banda_membership) { nextcloud_banda_invalidate_user_cache($user_id); } } static $fields_added = false; if ($fields_added) { return true; } if (!nextcloud_banda_check_dependencies()) { return false; } $current_level_id = nextcloud_banda_get_current_level_id(); $allowed_levels = nextcloud_banda_get_config('allowed_levels'); if (!in_array($current_level_id, $allowed_levels, true)) { nextcloud_banda_log_info("Level {$current_level_id} not in allowed levels, skipping fields"); return false; } try { $config = nextcloud_banda_get_config(); $fields = []; // Obtener configuración real del usuario si existe $real_config = []; $current_banda_level = null; if ($user_id) { $user_levels = pmpro_getMembershipLevelsForUser($user_id); if (!empty($user_levels)) { foreach ($user_levels as $lvl) { if (in_array((int)$lvl->id, $allowed_levels, true)) { $current_banda_level = $lvl; break; } } } // CORREGIDO: Usar la función base sin mejoras para obtener config real if ($current_banda_level) { $real_config = nextcloud_banda_get_user_real_config($user_id, $current_banda_level); } } // Definir opciones de frecuencia $frequency_options = [ 'monthly' => 'Mensal', 'semiannual' => 'Semestral (-5%)', 'annual' => 'Anual (-10%)', 'biennial' => 'Bienal (-15%)', 'triennial' => 'Trienal (-20%)', 'quadrennial' => 'Quadrienal (-25%)', 'quinquennial'=> 'Quinquenal (-30%)' ]; // CORREGIDO: Definir valores por defecto verificando source $default_storage = '1tb'; $default_users = '2'; $default_frequency = 'monthly'; // Si hay configuración real guardada, usarla if (!empty($real_config) && isset($real_config['source']) && !in_array($real_config['source'], ['none', 'defaults_no_active_membership', 'membership_deduction'], true)) { $default_storage = $real_config['storage_space'] ?? '1tb'; $default_users = isset($real_config['num_users']) ? strval($real_config['num_users']) : '2'; $default_frequency = $real_config['payment_frequency'] ?? 'monthly'; nextcloud_banda_log_debug("Usando configuración real del usuario", [ 'user_id' => $user_id, 'source' => $real_config['source'], 'storage' => $default_storage, 'users' => $default_users, 'frequency' => $default_frequency ]); } else { nextcloud_banda_log_debug("Usando valores por defecto (sin config guardada)", [ 'user_id' => $user_id, 'source' => $real_config['source'] ?? 'empty' ]); } // Campo de almacenamiento $fields[] = new PMProRH_Field( 'storage_space', 'select', [ 'label' => 'Espaço de armazenamento', 'options' => $config['storage_options'], 'profile' => true, 'required' => false, 'memberslistcsv' => true, 'addmember' => true, 'location' => 'after_level', 'default' => $default_storage ] ); // Campo de número de usuários $fields[] = new PMProRH_Field( 'num_users', 'select', [ 'label' => 'Número de usuários', 'options' => $config['user_options'], 'profile' => true, 'required' => false, 'memberslistcsv' => true, 'addmember' => true, 'location' => 'after_level', 'default' => $default_users ] ); // Campo de ciclo $fields[] = new PMProRH_Field( 'payment_frequency', 'select', [ 'label' => 'Ciclo de pagamento', 'options' => $frequency_options, 'profile' => true, 'required' => false, 'memberslistcsv' => true, 'addmember' => true, 'location' => 'after_level', 'default' => $default_frequency ] ); // Campo de precio total $fields[] = new PMProRH_Field( 'total_price_display', 'text', [ 'label' => 'Preço total', 'profile' => false, 'required' => false, 'memberslistcsv' => false, 'addmember' => false, 'readonly' => true, 'location' => 'after_level', 'showrequired' => false, 'divclass' => 'pmpro_checkout-field-price-display', 'default' => 'R$ ' . number_format(NEXTCLOUD_BANDA_BASE_PRICE, 2, ',', '.') ] ); // Añadir campos foreach($fields as $field) { pmprorh_add_registration_field('Configuração do plano', $field); } $fields_added = true; nextcloud_banda_log_info("Dynamic fields added successfully", [ 'level_id' => $current_level_id, 'fields_count' => count($fields), 'base_price' => NEXTCLOUD_BANDA_BASE_PRICE, 'default_values' => [ 'storage' => $default_storage, 'users' => $default_users, 'frequency' => $default_frequency ] ]); return true; } catch (Exception $e) { nextcloud_banda_log_error('Exception adding dynamic fields', [ 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine() ]); return false; } } // ==== // CÁLCULOS DE PRECIO - CORREGIDOS // ==== function nextcloud_banda_calculate_pricing($storage_space, $num_users, $payment_frequency, $base_price) { if (empty($storage_space) || empty($num_users) || empty($payment_frequency)) { nextcloud_banda_log_error('Missing parameters for price calculation'); return $base_price ?: NEXTCLOUD_BANDA_BASE_PRICE; // CORREGIDO: Usar constante } // Verificar caché $cache_key = "pricing_{$storage_space}_{$num_users}_{$payment_frequency}_{$base_price}"; $cached_price = nextcloud_banda_cache_get($cache_key); if ($cached_price !== false) { return $cached_price; } $config = nextcloud_banda_get_config(); $price_per_tb = $config['price_per_tb']; $price_per_user = $config['price_per_additional_user']; $base_users_included = $config['base_users_included']; $base_storage_included = $config['base_storage_included']; // CORREGIDO: Asegurar precio base válido if ($base_price <= 0) { $base_price = NEXTCLOUD_BANDA_BASE_PRICE; } // Calcular precio de almacenamiento (1TB incluido en base_price) $storage_tb = (int)str_replace('tb', '', $storage_space); $additional_tb = max(0, $storage_tb - $base_storage_included); $storage_price = $base_price + ($price_per_tb * $additional_tb); // Calcular precio por usuarios (2 usuarios incluidos en base_price) $additional_users = max(0, (int)$num_users - $base_users_included); $user_price = $price_per_user * $additional_users; // Precio combinado $combined_price = $storage_price + $user_price; // Aplicar multiplicador de frecuencia $multipliers = $config['frequency_multipliers']; $frequency_multiplier = $multipliers[$payment_frequency] ?? 1.0; // Calcular precio total $total_price = ceil($combined_price * $frequency_multiplier); // Guardar en caché nextcloud_banda_cache_set($cache_key, $total_price, 300); nextcloud_banda_log_debug('Price calculated', [ 'storage_space' => $storage_space, 'storage_tb' => $storage_tb, 'additional_tb' => $additional_tb, 'num_users' => $num_users, 'additional_users' => $additional_users, 'payment_frequency' => $payment_frequency, 'base_price' => $base_price, 'storage_price' => $storage_price, 'user_price' => $user_price, 'combined_price' => $combined_price, 'total_price' => $total_price ]); return $total_price; } function nextcloud_banda_configure_billing_period($level, $payment_frequency, $total_price) { if (empty($level) || !is_object($level)) { nextcloud_banda_log_error('Invalid level object provided'); return $level; } // Mapa de frecuencias a ciclo PMPro $billing_cycles = [ 'monthly' => ['number' => 1, 'period' => 'Month'], 'semiannual' => ['number' => 6, 'period' => 'Month'], 'annual' => ['number' => 12, 'period' => 'Month'], 'biennial' => ['number' => 24, 'period' => 'Month'], 'triennial' => ['number' => 36, 'period' => 'Month'], 'quadrennial' => ['number' => 48, 'period' => 'Month'], 'quinquennial' => ['number' => 60, 'period' => 'Month'], ]; $freq = strtolower($payment_frequency ?: 'monthly'); $cycle = $billing_cycles[$freq] ?? $billing_cycles['monthly']; // Establecer ciclo real en el level (PMPro/gateway usarán estos valores) $level->cycle_number = (int)$cycle['number']; $level->cycle_period = $cycle['period']; $level->billing_amount = (float)$total_price; $level->initial_payment = (float)$total_price; $level->trial_amount = 0; $level->trial_limit = 0; $level->recurring = true; return $level; } // ==== // HOOK PRINCIPAL DE MODIFICACIÓN DE PRECIO (MODIFICADO CON PRORRATEO) // ==== /** * Ajusta dinámicamente el precio del nivel seleccionado en el checkout * con base en la configuración de Nextcloud Banda y adjunta la información * de prorrateo para su consumo posterior en la interfaz. * * @param stdClass $level * @param int|null $user_id * * @return stdClass */ function nextcloud_banda_modify_level_pricing($level, $user_id = null) { if (empty($level) || empty($level->id)) { return $level; } if (empty($user_id)) { $user_id = get_current_user_id(); } if (empty($user_id)) { nextcloud_banda_log_debug('modify_level_pricing: sin user_id, devolviendo nivel sin cambios.', []); return $level; } nextcloud_banda_log_debug('=== MODIFY LEVEL PRICING START ===', [ 'user_id' => $user_id, 'level_id' => $level->id, 'level_name' => $level->name ?? '', ]); // Obtener configuración del usuario $user_config = nextcloud_banda_get_user_config($user_id); // Valores por defecto $config = nextcloud_banda_get_config(); $default_storage = '1tb'; $default_users = 2; $default_frequency = 'monthly'; // Obtener valores del request o configuración guardada $storage_space = isset($_REQUEST['storage_space']) ? sanitize_text_field($_REQUEST['storage_space']) : ($user_config['storage'] ? $user_config['storage'] . 'tb' : $default_storage); $num_users = isset($_REQUEST['num_users']) ? (int) $_REQUEST['num_users'] : ($user_config['users'] ?: $default_users); $payment_frequency = isset($_REQUEST['payment_frequency']) ? strtolower(sanitize_text_field($_REQUEST['payment_frequency'])) : ($user_config['frequency'] ?: $default_frequency); // Validar frecuencia $multipliers = $config['frequency_multipliers'] ?? ['monthly' => 1.0]; if (!isset($multipliers[$payment_frequency])) { $payment_frequency = 'monthly'; } // Obtener precio base de referencia $base_price_reference = isset($level->initial_payment) ? (float) $level->initial_payment : NEXTCLOUD_BANDA_BASE_PRICE; // Calcular precio $calculated_price = nextcloud_banda_calculate_pricing( $storage_space, $num_users, $payment_frequency, $base_price_reference ); // Aplicar el precio calculado al nivel $level->initial_payment = round((float) $calculated_price, 2); $level->billing_amount = round((float) $calculated_price, 2); $level->recurring = true; $level->trial_amount = 0; $level->trial_limit = 0; // Configurar el ciclo de facturación $level = nextcloud_banda_configure_billing_period($level, $payment_frequency, $calculated_price); // Calcular y adjuntar información de prorrateo solo para usuarios con membresía activa if ($user_id && pmpro_hasMembershipLevel($level->id, $user_id)) { $proration = nextcloud_banda_calculate_proration_core_aligned( $user_id, $level->id, $storage_space, $num_users, $payment_frequency ); // Adjuntar información de prorrateo al nivel $level->nextcloud_banda_proration = $proration; nextcloud_banda_log_debug('Proration info attached to level', [ 'user_id' => $user_id, 'proration_attached' => !empty($proration), 'prorated_amount' => $proration['prorated_amount'] ?? 0 ]); } nextcloud_banda_log_debug('=== MODIFY LEVEL PRICING END ===', [ 'calculated_price' => $level->initial_payment, 'storage_space' => $storage_space, 'num_users' => $num_users, 'payment_frequency' => $payment_frequency ]); return $level; } // ==== // >>> PRORATION SYSTEM: begin // ==== /** * Obtiene la configuración actual del usuario de forma precisa */ function nextcloud_banda_get_user_config($user_id) { $user_levels = pmpro_getMembershipLevelsForUser($user_id); $allowed_levels = nextcloud_banda_get_config('allowed_levels'); $current_banda_level = null; if (!empty($user_levels)) { foreach ($user_levels as $level) { if (in_array((int)$level->id, $allowed_levels, true)) { $cycle_info = nextcloud_banda_get_next_payment_info($user_id); if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { $current_banda_level = $level; break; } } } } // Si no hay membresía activa, devolver valores por defecto if (!$current_banda_level) { return [ 'storage' => 1, 'users' => 2, 'frequency' => 'monthly' ]; } // Obtener configuración real usando la función mejorada $real_config = nextcloud_banda_get_user_real_config_improved($user_id, $current_banda_level); // Convertir storage_space a número $storage_tb = 1; if (!empty($real_config['storage_space'])) { $storage_tb = (int)str_replace('tb', '', strtolower($real_config['storage_space'])); } return [ 'storage' => $storage_tb, 'users' => isset($real_config['num_users']) ? (int)$real_config['num_users'] : 2, 'frequency' => $real_config['payment_frequency'] ?? 'monthly' ]; } /** * Calcula el precio para una configuración específica */ function nextcloud_banda_calculate_price($level_id, $storage_tb, $num_users) { $config = nextcloud_banda_get_config(); $base_price = $config['base_price_default']; $price_per_tb = $config['price_per_tb']; $price_per_user = $config['price_per_additional_user']; $base_storage_included = $config['base_storage_included']; $base_users_included = $config['base_users_included']; // Calcular precio de almacenamiento $additional_tb = max(0, $storage_tb - $base_storage_included); $storage_price = $base_price + ($price_per_tb * $additional_tb); // Calcular precio por usuarios $additional_users = max(0, $num_users - $base_users_included); $user_price = $price_per_user * $additional_users; // Precio total (sin multiplicador de frecuencia) $total_price = $storage_price + $user_price; nextcloud_banda_log_debug('Price calculated for config', [ 'level_id' => $level_id, 'storage_tb' => $storage_tb, 'num_users' => $num_users, 'additional_tb' => $additional_tb, 'additional_users' => $additional_users, 'total_price' => $total_price ]); return $total_price; } /** * Calcula el prorrateo basado en el ciclo real del nivel PMPro * CORREGIDO: Cálculo matemático preciso de prorrateo */ function nextcloud_banda_calculate_proration_core_aligned($user_id, $new_level_id, $new_storage, $new_users, $new_frequency = 'monthly') { nextcloud_banda_log_debug("=== PRORATION CORE-ALIGNED START ===", [ 'user_id' => $user_id, 'new_level_id' => $new_level_id, 'new_storage' => $new_storage, 'new_users' => $new_users, 'new_frequency' => $new_frequency ]); $current_level = pmpro_getMembershipLevelForUser($user_id); if (empty($current_level) || empty($current_level->id)) { nextcloud_banda_log_debug("Usuario sin membresía activa, proration = 0"); return [ 'prorated_amount' => 0, 'days_remaining' => 0, 'total_days' => 1, 'next_payment_date' => '', 'message' => 'No active membership' ]; } $config = nextcloud_banda_get_config(); $multipliers = $config['frequency_multipliers'] ?? ['monthly' => 1.0]; // Normalizar storage $normalize_storage = function($value) { if (is_numeric($value)) { return max(1, (int)$value) . 'tb'; } $value = strtolower(trim((string)$value)); if ($value === '') { return '1tb'; } if (strpos($value, 'tb') === false && strpos($value, 'gb') === false) { $digits = preg_replace('/[^0-9]/', '', $value); return $digits !== '' ? $digits . 'tb' : '1tb'; } return $value; }; $extract_tb = function($slug) { $slug = strtolower((string)$slug); if (strpos($slug, 'gb') !== false) { $numeric = (float)preg_replace('/[^0-9.]/', '', $slug); return max(1, (int)ceil($numeric / 1024)); } $numeric = (int)preg_replace('/[^0-9]/', '', $slug); return max(1, $numeric); }; // Validar y normalizar frecuencias $new_frequency = strtolower($new_frequency ?: 'monthly'); if (!isset($multipliers[$new_frequency])) { $new_frequency = 'monthly'; } // Obtener configuraciones actuales - MEJORADO $user_config_simple = nextcloud_banda_get_user_config($user_id); $current_config = nextcloud_banda_get_user_real_config_improved($user_id, $current_level); // Extraer y normalizar configuración actual - MÁS ROBUSTO $current_storage_slug = $normalize_storage( $current_config['storage_space'] ?? ($user_config_simple['storage'] . 'tb' ?? '1tb') ); $current_users = (int)($current_config['num_users'] ?? $user_config_simple['users'] ?? 2); $current_users = max(2, $current_users); // Mínimo 2 usuarios $current_frequency = strtolower( $current_config['payment_frequency'] ?? ($user_config_simple['frequency'] ?? 'monthly') ); if (!isset($multipliers[$current_frequency])) { $current_frequency = 'monthly'; } // VALIDACIÓN CRÍTICA: Asegurar que tenemos los datos correctos nextcloud_banda_log_debug("CONFIGURACIÓN ACTUAL DETECTADA", [ 'from_real_config' => [ 'storage_space' => $current_config['storage_space'] ?? 'NOT_SET', 'num_users' => $current_config['num_users'] ?? 'NOT_SET', 'payment_frequency' => $current_config['payment_frequency'] ?? 'NOT_SET', 'final_amount' => $current_config['final_amount'] ?? 'NOT_SET', 'source' => $current_config['source'] ?? 'NOT_SET' ], 'from_simple_config' => $user_config_simple, 'normalized_values' => [ 'storage_slug' => $current_storage_slug, 'users' => $current_users, 'frequency' => $current_frequency ] ]); // Obtener precios base $base_price_reference = NEXTCLOUD_BANDA_BASE_PRICE; $level_obj = pmpro_getLevel($new_level_id); if ($level_obj && !empty($level_obj->initial_payment)) { $base_price_reference = (float)$level_obj->initial_payment; } // Normalizar nuevas configuraciones $new_storage_slug = $normalize_storage($new_storage); $new_users = max(2, (int)$new_users); $new_frequency = strtolower($new_frequency ?: 'monthly'); if (!isset($multipliers[$new_frequency])) { $new_frequency = 'monthly'; } // Calcular precios actuales y nuevos - MEJORADO $current_cycle_price = null; // PRIORIDAD 1: Usar precio final guardado if (!empty($current_config['final_amount']) && $current_config['final_amount'] > 0) { $current_cycle_price = round((float)$current_config['final_amount'], 2); nextcloud_banda_log_debug("Usando precio guardado de configuración", [ 'precio_guardado' => $current_cycle_price, 'source' => $current_config['source'] ]); } // PRIORIDAD 2: Usar initial_payment del nivel actual if (($current_cycle_price === null || $current_cycle_price <= 0) && !empty($current_level->initial_payment)) { $current_cycle_price = round((float)$current_level->initial_payment, 2); nextcloud_banda_log_debug("Usando initial_payment del nivel", [ 'precio_nivel' => $current_cycle_price ]); } // PRIORIDAD 3: Calcular basado en configuración if ($current_cycle_price === null || $current_cycle_price <= 0) { $current_cycle_price = (float)nextcloud_banda_calculate_pricing( $current_storage_slug, $current_users, $current_frequency, $base_price_reference ); nextcloud_banda_log_debug("Calculando precio basado en configuración", [ 'precio_calculado' => $current_cycle_price, 'storage' => $current_storage_slug, 'users' => $current_users, 'frequency' => $current_frequency, 'base_price' => $base_price_reference ]); } // Asegurar que tenemos un precio válido if ($current_cycle_price === null || $current_cycle_price <= 0) { $current_cycle_price = $base_price_reference; nextcloud_banda_log_debug("USANDO PRECIO BASE POR DEFECTO", [ 'precio_base' => $current_cycle_price ]); } $new_cycle_price = (float)nextcloud_banda_calculate_pricing( $new_storage_slug, $new_users, $new_frequency, $base_price_reference ); // VALIDACIÓN FINAL ANTES DE PRORRATEO nextcloud_banda_log_debug("VALIDACIÓN PRE-PRORRATEO", [ 'current_cycle_price' => $current_cycle_price, 'new_cycle_price' => $new_cycle_price, 'price_diff' => $new_cycle_price - $current_cycle_price, 'current_config' => [ 'storage' => $current_storage_slug, 'users' => $current_users, 'frequency' => $current_frequency ], 'new_config' => [ 'storage' => $new_storage_slug, 'users' => $new_users, 'frequency' => $new_frequency ] ]); // Verificar que los precios sean válidos if ($current_cycle_price <= 0) { nextcloud_banda_log_error("PRECIO ACTUAL INVÁLIDO", [ 'precio' => $current_cycle_price, 'configuracion' => $current_config ]); return [ 'prorated_amount' => 0, 'days_remaining' => 0, 'total_days' => 1, 'next_payment_date' => '', 'message' => 'Invalid current price', 'debug_info' => [ 'current_cycle_price' => $current_cycle_price, 'current_config_source' => $current_config['source'] ?? 'unknown' ] ]; } // CÁLCULO PRECISO DE PRORRATEO - CON VALIDACIONES $price_diff = round($new_cycle_price - $current_cycle_price, 2); nextcloud_banda_log_debug("ANÁLISIS DE DIFERENCIA DE PRECIO", [ 'precio_nuevo' => $new_cycle_price, 'precio_actual' => $current_cycle_price, 'diferencia' => $price_diff ]); // Si no hay diferencia de precio o es downgrade significativo, manejar adecuadamente if ($price_diff <= 0) { nextcloud_banda_log_debug("SIN DIFERENCIA O DOWNGRADE", [ 'price_diff' => $price_diff, 'current_price' => $current_cycle_price, 'new_price' => $new_cycle_price, 'mensaje' => $price_diff <= 0 ? 'Downgrade o mismo precio' : 'Upgrade positivo' ]); // Para downgrades, podemos devolver 0 o manejar según política de negocio return [ 'prorated_amount' => 0, 'days_remaining' => $days_remaining ?? 0, 'total_days' => $total_days ?? 1, 'next_payment_date' => '', 'message' => 'Downgrade or same price', 'current_price' => round($current_cycle_price, 2), 'new_price' => round($new_cycle_price, 2), 'price_diff' => $price_diff, 'current_frequency' => $current_frequency, 'new_frequency' => $new_frequency ]; } // Obtener información del ciclo de pago $payment_info = nextcloud_banda_get_next_payment_info($user_id); if (!$payment_info || empty($payment_info['next_payment_date'])) { nextcloud_banda_log_error("No se pudo obtener next_payment_date"); return [ 'prorated_amount' => 0, 'days_remaining' => 0, 'total_days' => 1, 'next_payment_date' => '', 'message' => 'Could not determine next payment date' ]; } // Calcular días restantes y totales $cycle_start_ts = (int)$payment_info['cycle_start']; $cycle_end_ts = (int)$payment_info['cycle_end']; $now = current_time('timestamp'); $total_seconds = max(1, $cycle_end_ts - $cycle_start_ts); $remaining_seconds = max(0, $cycle_end_ts - $now); $fraction_remaining = $total_seconds > 0 ? $remaining_seconds / $total_seconds : 0; $total_days = max(1, (int)round($total_seconds / DAY_IN_SECONDS)); $days_remaining = max(0, (int)floor($remaining_seconds / DAY_IN_SECONDS)); if ($remaining_seconds > 0 && $days_remaining === 0) { $days_remaining = 1; } // CÁLCULO MATEMÁTICO PRECISO DE PRORRATEO // Basado en tu ejemplo: R$ 1.055,00 - (R$ 1.055,00 × 180/182) = Crédito // Nuevo precio: R$ 2.862,00 - Crédito = Monto a pagar $daily_rate = $current_cycle_price / $total_days; $current_credit_precise = $daily_rate * $days_remaining; $prorated_amount_precise = $new_cycle_price - $current_credit_precise; // Redondeo cuidadoso para evitar errores de centavos $current_credit = round($current_credit_precise, 2); $prorated_amount = round($prorated_amount_precise, 2); // Asegurar que no sea negativo $prorated_amount = max(0, $prorated_amount); // Cálculo adicional para mostrar detalles $prorated_new = round($new_cycle_price * $fraction_remaining, 2); nextcloud_banda_log_debug("CÁLCULO PRORRATEO DETALLADO", [ 'metodo' => 'Precio nuevo - (Precio actual × fracción restante)', 'precio_nuevo' => $new_cycle_price, 'precio_actual' => $current_cycle_price, 'fraccion_restante' => $fraction_remaining, 'dias_restantes' => $days_remaining, 'dias_totales' => $total_days, 'tasa_diaria' => $daily_rate, 'credito_actual_preciso' => $current_credit_precise, 'credito_actual_redondeado' => $current_credit, 'monto_prorrateado_preciso' => $prorated_amount_precise, 'monto_prorrateado_final' => $prorated_amount ]); // Asegurar consistencia en los cálculos if ($days_remaining > 0 && $total_days > 0) { $fraction_check = $days_remaining / $total_days; nextcloud_banda_log_debug("VERIFICACIÓN DE FRACCIÓN", [ 'calculada' => $fraction_remaining, 'verificada' => $fraction_check, 'diferencia' => abs($fraction_remaining - $fraction_check) ]); } // Asegurar que el crédito no exceda el precio actual $current_credit = min($current_credit, $current_cycle_price); $prorated_amount = max(0, $new_cycle_price - $current_credit); nextcloud_banda_log_debug("VALORES FINALES AJUSTADOS", [ 'credito_final' => $current_credit, 'monto_a_pagar' => $prorated_amount, 'verificacion' => $new_cycle_price - $current_credit ]); // Generar etiquetas para las frecuencias $frequency_labels = [ 'monthly' => 'Mensal', 'semiannual' => 'Semestral', 'annual' => 'Anual', 'biennial' => 'Bienal', 'triennial' => 'Trienal', 'quadrennial' => 'Quadrienal', 'quinquennial' => 'Quinquenal' ]; $current_cycle_label = $frequency_labels[$current_frequency] ?? ucfirst($current_frequency); $new_cycle_label = $frequency_labels[$new_frequency] ?? ucfirst($new_frequency); $result = [ 'prorated_amount' => $prorated_amount, 'prorated_new_amount' => $prorated_new, 'current_prorated_amount' => $current_credit, 'price_diff' => $price_diff, 'fraction_remaining' => $fraction_remaining, 'remaining_seconds' => $remaining_seconds, 'total_seconds' => $total_seconds, 'days_remaining' => $days_remaining, 'total_days' => $total_days, 'next_payment_date' => date('Y-m-d', (int)$payment_info['next_payment_date']), 'current_price' => round($current_cycle_price, 2), 'new_price' => round($new_cycle_price, 2), 'current_frequency' => $current_frequency, 'current_cycle_label' => $current_cycle_label, 'new_frequency' => $new_frequency, 'new_cycle_label' => $new_cycle_label, 'message' => 'Success', 'debug_info' => [ 'calculation_method' => 'new_price - (current_price * fraction_remaining)', 'daily_rate' => round($daily_rate, 4), 'credit_calculation' => "{$current_cycle_price} × {$fraction_remaining} = {$current_credit}", 'final_calculation' => "{$new_cycle_price} - {$current_credit} = {$prorated_amount}", 'days_ratio' => "{$days_remaining}/{$total_days}" ] ]; nextcloud_banda_log_debug("RESULTADO FINAL DE PRORRATEO", $result); return $result; } /** * Determina si el usuario está actualizando su plan (considera membresía activa vía próxima fecha de pago). */ function nextcloud_banda_is_plan_upgrade($user_id, $new_storage, $new_users, $new_frequency) { $user_levels = pmpro_getMembershipLevelsForUser($user_id); $allowed_levels = nextcloud_banda_get_config('allowed_levels'); $current_banda_level = null; if (!empty($user_levels)) { foreach ($user_levels as $level) { if (in_array((int)$level->id, $allowed_levels, true)) { $cycle_info = nextcloud_banda_get_next_payment_info($user_id); if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { $current_banda_level = $level; break; } } } } if (!$current_banda_level) { nextcloud_banda_log_debug('No upgrade - no active membership by next payment', ['user_id' => $user_id]); return false; } $current_config = nextcloud_banda_get_user_real_config_improved($user_id, $current_banda_level); $current_storage = $current_config['storage_space'] ?: '1tb'; $current_users = $current_config['num_users'] ?: 2; $current_frequency = $current_config['payment_frequency'] ?: 'monthly'; $current_storage_tb = (int)str_replace('tb', '', $current_storage); $new_storage_tb = (int)str_replace('tb', '', $new_storage); $frequency_order = [ 'monthly' => 1, 'semiannual' => 2, 'annual' => 3, 'biennial' => 4, 'triennial' => 5, 'quadrennial' => 6, 'quinquennial' => 7 ]; $is_upgrade = ( $new_storage_tb > $current_storage_tb || $new_users > $current_users || (($frequency_order[$new_frequency] ?? 1) > ($frequency_order[$current_frequency] ?? 1)) ); return $is_upgrade; } /** * Info detallada de suscripción basada en próxima fecha de pago (sin enddate). */ function nextcloud_banda_get_detailed_subscription_info($user_id) { $user_levels = pmpro_getMembershipLevelsForUser($user_id); $allowed_levels = nextcloud_banda_get_config('allowed_levels'); $current_banda_level = null; if (!empty($user_levels)) { foreach ($user_levels as $level) { if (in_array((int)$level->id, $allowed_levels, true)) { $cycle_info = nextcloud_banda_get_next_payment_info($user_id); if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { $current_banda_level = $level; break; } } } } if (!$current_banda_level) { return false; } $current_config = nextcloud_banda_get_user_real_config_improved($user_id, $current_banda_level); $current_amount = $current_config['final_amount'] ?: (float)$current_banda_level->initial_payment; $cycle_info = nextcloud_banda_get_next_payment_info($user_id); if (!$cycle_info) { return false; } return [ 'current_amount' => $current_amount, 'days_remaining' => isset($cycle_info['days_remaining']) ? $cycle_info['days_remaining'] : 0, 'total_days' => isset($cycle_info['total_days']) ? $cycle_info['total_days'] : 1, 'start_date' => date('Y-m-d H:i:s', isset($cycle_info['cycle_start']) ? $cycle_info['cycle_start'] : time()), 'end_date' => date('Y-m-d H:i:s', isset($cycle_info['cycle_end']) ? $cycle_info['cycle_end'] : time()), 'next_payment_date' => date('Y-m-d H:i:s', isset($cycle_info['next_payment_date']) ? $cycle_info['next_payment_date'] : time()), 'current_config' => $current_config, 'source' => 'next_payment_info' ]; } /** * AJAX handler para cálculo de prorrateo en tiempo real - CORREGIDO */ function nextcloud_banda_ajax_calculate_proration() { // Verificar autenticación if (!is_user_logged_in()) { wp_send_json_error([ 'message' => __('Usuário não autenticado.', 'nextcloud-banda'), ], 401); return; } // Verificar nonce if (!check_ajax_referer('nextcloud_banda_proration', 'security', false)) { wp_send_json_error([ 'message' => __('Nonce inválido.', 'nextcloud-banda'), ], 403); return; } // Obtener y sanitizar parámetros $level_id = isset($_POST['level_id']) ? (int) $_POST['level_id'] : 0; $storage_space_raw = isset($_POST['storage']) ? sanitize_text_field($_POST['storage']) : ''; $num_users = isset($_POST['users']) ? (int) $_POST['users'] : 0; $payment_frequency = isset($_POST['frequency']) ? strtolower(sanitize_text_field($_POST['frequency'])) : 'monthly'; // Validar parámetros requeridos if ($level_id <= 0) { wp_send_json_error([ 'message' => __('ID do nível inválido.', 'nextcloud-banda'), ], 400); return; } // Logging para debugging nextcloud_banda_log_debug('AJAX proration request received', [ 'level_id' => $level_id, 'storage' => $storage_space_raw, 'users' => $num_users, 'frequency' => $payment_frequency, 'user_id' => get_current_user_id() ]); // Obtener configuración y validar frecuencia $config = nextcloud_banda_get_config(); $multipliers = $config['frequency_multipliers'] ?? ['monthly' => 1.0]; if (!isset($multipliers[$payment_frequency])) { $payment_frequency = 'monthly'; } // Normalizar parámetros $storage_space = !empty($storage_space_raw) ? strtolower($storage_space_raw) : '1tb'; if ($num_users < 1) { $num_users = 2; } // Obtener precio base $level = pmpro_getLevel($level_id); $base_price = $level && isset($level->initial_payment) ? (float) $level->initial_payment : NEXTCLOUD_BANDA_BASE_PRICE; // Calcular nuevo precio total $new_total_price = (float) nextcloud_banda_calculate_pricing( $storage_space, $num_users, $payment_frequency, $base_price ); // Obtener precio actual $current_membership = pmpro_getMembershipLevelForUser(get_current_user_id()); $current_base_price = !empty($current_membership) && isset($current_membership->initial_payment) ? (float) $current_membership->initial_payment : 0.0; nextcloud_banda_log_debug('--- AJAX PRORATION INPUT ---', [ 'user_id' => get_current_user_id(), 'level_id' => $level_id, 'storage' => $storage_space, 'users' => $num_users, 'frequency' => $payment_frequency, 'base_price' => $base_price, 'new_total_price' => $new_total_price, 'current_base_price' => $current_base_price, ]); // Calcular prorrateo $proration = nextcloud_banda_calculate_proration_core_aligned( get_current_user_id(), $level_id, $storage_space, $num_users, $payment_frequency ); // Verificar que el proration tenga datos válidos if (!$proration || !is_array($proration)) { nextcloud_banda_log_error('Proration calculation failed or returned invalid data', [ 'user_id' => get_current_user_id(), 'proration_result' => $proration ]); wp_send_json_error([ 'message' => __('Falha no cálculo de prorrateo.', 'nextcloud-banda'), ], 500); return; } nextcloud_banda_log_debug('--- AJAX PRORATION RESULT ---', $proration); // Construir respuesta consistente $response = [ 'is_upgrade' => isset($proration['prorated_amount']) && (float)$proration['prorated_amount'] > 0, 'new_total_price' => round($new_total_price, 2), 'prorated_amount' => isset($proration['prorated_amount']) ? round((float)$proration['prorated_amount'], 2) : 0, 'days_remaining' => isset($proration['days_remaining']) ? (int)$proration['days_remaining'] : 0, 'total_days' => isset($proration['total_days']) ? (int)$proration['total_days'] : 1, 'next_payment_date' => isset($proration['next_payment_date']) ? $proration['next_payment_date'] : '', 'current_price' => isset($proration['current_price']) ? round((float)$proration['current_price'], 2) : round($current_base_price, 2), 'new_price' => round($new_total_price, 2), 'current_prorated_amount' => isset($proration['current_prorated_amount']) ? round((float)$proration['current_prorated_amount'], 2) : 0, 'new_prorated_amount' => isset($proration['prorated_new_amount']) ? round((float)$proration['prorated_new_amount'], 2) : 0, 'fraction_remaining' => isset($proration['fraction_remaining']) ? (float)$proration['fraction_remaining'] : 0, 'current_frequency' => isset($proration['current_frequency']) ? $proration['current_frequency'] : $payment_frequency, 'new_frequency' => isset($proration['new_frequency']) ? $proration['new_frequency'] : $payment_frequency, 'current_cycle_label' => isset($proration['current_cycle_label']) ? $proration['current_cycle_label'] : '', 'new_cycle_label' => isset($proration['new_cycle_label']) ? $proration['new_cycle_label'] : '', 'message' => isset($proration['message']) ? $proration['message'] : 'Success', 'success' => true ]; nextcloud_banda_log_debug('--- AJAX PRORATION RESPONSE ---', $response); wp_send_json_success($response); } // Registrar el endpoint AJAX add_action('wp_ajax_nextcloud_banda_calculate_proration', 'nextcloud_banda_ajax_calculate_proration'); // ==== // >>> PRORATION SYSTEM: end // ==== // ==== // GUARDADO DE CONFIGURACIÓN // ==== /** * Versión mejorada de guardado de configuración */ function nextcloud_banda_save_configuration($user_id, $morder) { if (!$user_id || !$morder) { nextcloud_banda_log_error('Invalid parameters for save configuration', [ 'user_id' => $user_id, 'morder' => !empty($morder) ]); return; } // Validar campos requeridos $required_fields = ['storage_space', 'num_users', 'payment_frequency']; $config_data = []; foreach ($required_fields as $field) { if (!isset($_REQUEST[$field])) { nextcloud_banda_log_error('Missing required field in request', [ 'user_id' => $user_id, 'missing_field' => $field ]); return; } $config_data[$field] = sanitize_text_field(wp_unslash($_REQUEST[$field])); } // Normalizar y validar configuración $normalized_config = normalize_banda_config($config_data); // Preparar datos finales para guardar $config = array_merge($normalized_config, [ 'created_at' => current_time('mysql'), 'updated_at' => current_time('mysql'), 'level_id' => intval($morder->membership_id), 'final_amount' => floatval($morder->InitialPayment), 'order_id' => $morder->id ?? null, 'version' => NEXTCLOUD_BANDA_PLUGIN_VERSION ]); $config_json = wp_json_encode($config); if ($config_json === false) { nextcloud_banda_log_error('Failed to encode configuration JSON', [ 'user_id' => $user_id, 'config' => $config ]); return; } // Limpiar datos anteriores antes de guardar nuevos nextcloud_banda_delete_all_user_data($user_id); // Guardar nueva configuración $result = update_user_meta($user_id, 'nextcloud_banda_config', $config_json); if ($result === false) { nextcloud_banda_log_error('Failed to update user meta for configuration', [ 'user_id' => $user_id ]); return; } // Invalidar caché nextcloud_banda_invalidate_user_cache($user_id); nextcloud_banda_log_info('Configuration saved successfully', [ 'user_id' => $user_id, 'config' => $config ]); } // ==== // VISUALIZACIÓN DE CONFIGURACIÓN DEL MIEMBRO // ==== /** * Mapea cycle_number y cycle_period de PMPro a etiqueta legible en portugués * CORREGIDO: Manejo de plurales y casos especiales */ function nextcloud_banda_map_cycle_label($cycle_number, $cycle_period) { $period = strtolower((string)$cycle_period); $num = (int)$cycle_number; // Normalizar períodos if (strpos($period, 'month') !== false) { $period = 'month'; } elseif (strpos($period, 'year') !== false) { $period = 'year'; } elseif (strpos($period, 'week') !== false) { $period = 'week'; } elseif (strpos($period, 'day') !== false) { $period = 'day'; } if ($period === 'month') { switch ($num) { case 1: return 'Mensal'; case 2: return 'Bimestral'; case 3: return 'Trimestral'; case 4: return 'Quadrimensal'; case 5: return 'Quinquemestral'; case 6: return 'Semestral'; case 7: return 'Setembral'; case 8: return 'Octomestral'; case 9: return 'Nonomestral'; case 10: return 'Decamensual'; case 11: return 'Undecamensual'; case 12: return 'Anual'; case 18: return 'Anual e meio'; case 24: return 'Bienal'; case 30: return 'Biênio e meio'; case 36: return 'Trienal'; case 48: return 'Quadrienal'; case 60: return 'Quinquenal'; default: if ($num > 1) { return "{$num} meses"; } else { return "Mensal"; } } } if ($period === 'year') { if ($num === 1) { return 'Anual'; } else { return "{$num} anos"; } } if ($period === 'week') { if ($num === 1) { return 'Semanal'; } else { return "{$num} semanas"; } } if ($period === 'day') { if ($num === 1) { return 'Diário'; } else { return "{$num} dias"; } } // Fallback genérico return ucfirst($cycle_period) . " (" . $cycle_number . ")"; } /** * Deriva payment_frequency canónico desde cycle_number/cycle_period * CORREGIDO: Manejo más preciso de conversiones */ function nextcloud_banda_derive_frequency_from_cycle($cycle_number, $cycle_period) { $period = strtolower((string)$cycle_period); $num = (int)$cycle_number; // Normalizar períodos if (strpos($period, 'month') !== false) { $period = 'month'; } elseif (strpos($period, 'year') !== false) { $period = 'year'; } elseif (strpos($period, 'week') !== false) { $period = 'week'; } elseif (strpos($period, 'day') !== false) { $period = 'day'; } if ($period === 'month') { switch ($num) { case 1: return 'monthly'; case 6: return 'semiannual'; case 12: return 'annual'; case 24: return 'biennial'; case 36: return 'triennial'; case 48: return 'quadrennial'; case 60: return 'quinquennial'; default: return 'monthly'; // fallback para otros meses } } if ($period === 'year') { switch ($num) { case 1: return 'annual'; case 2: return 'biennial'; case 3: return 'triennial'; case 4: return 'quadrennial'; case 5: return 'quinquennial'; default: return 'annual'; // fallback para años } } if ($period === 'week') { if ($num === 1) { return 'weekly'; } elseif ($num === 2) { return 'biweekly'; } else { // Convertir semanas a meses aproximadamente $months = round($num / 4.33); // 4.33 semanas por mes promedio switch ($months) { case 1: return 'monthly'; case 3: return 'quarterly'; case 6: return 'semiannual'; case 12: return 'annual'; default: return 'monthly'; } } } if ($period === 'day') { if ($num === 1) { return 'daily'; } else { // Convertir días a meses aproximadamente $months = round($num / 30); // 30 días por mes aproximado switch ($months) { case 1: return 'monthly'; case 3: return 'quarterly'; case 6: return 'semiannual'; case 12: return 'annual'; default: return 'monthly'; } } } return 'monthly'; // fallback genérico } function nextcloud_banda_show_member_config_improved() { $user_id = get_current_user_id(); if (!$user_id) { nextcloud_banda_log_debug('No user logged in for member config display'); return; } try { $user_levels = pmpro_getMembershipLevelsForUser($user_id); if (empty($user_levels)) { nextcloud_banda_log_debug("No memberships found for user {$user_id}"); return; } $allowed_levels = nextcloud_banda_get_config('allowed_levels'); $banda_membership = null; foreach ($user_levels as $level) { if (in_array((int)$level->id, $allowed_levels, true)) { // Verificar que tenga ciclo activo usando next_payment_info $cycle_info = nextcloud_banda_get_next_payment_info($user_id); // CORREGIDO: Validar que cycle_info tenga las claves necesarias $cycle_end = isset($cycle_info['cycle_end']) ? (int)$cycle_info['cycle_end'] : 0; if ($cycle_end > time()) { $banda_membership = $level; break; } } } if (!$banda_membership) { nextcloud_banda_log_debug("No active Banda membership found for user {$user_id}"); return; } $membership = $banda_membership; $real_config = nextcloud_banda_get_user_real_config_improved($user_id, $membership); $storage_options = nextcloud_banda_get_config('storage_options'); $user_options = nextcloud_banda_get_config('user_options'); $frequency_labels = [ 'monthly' => 'Mensal', 'semiannual' => 'Semestral (-5%)', 'annual' => 'Anual (-10%)', 'biennial' => 'Bienal (-15%)', 'triennial' => 'Trienal (-20%)', 'quadrennial' => 'Quadrienal (-25%)', 'quinquennial' => 'Quinquenal (-30%)' ]; $used_space_tb = nextcloud_banda_get_used_space_tb($user_id); // Obtener próxima fecha de pago usando la nueva función $tz = wp_timezone(); $next_payment_date = ''; $cycle_info = nextcloud_banda_get_next_payment_info($user_id); // CORREGIDO: Validar que cycle_info tenga las claves necesarias $next_payment_ts = isset($cycle_info['next_payment_date']) ? (int)$cycle_info['next_payment_date'] : 0; if ($next_payment_ts > 0) { $next_payment_date = wp_date('d/m/Y', $next_payment_ts, $tz); } else { $next_payment_date = __('Assinatura ativa até cancelamento', 'pmpro-banda'); } $base_users_included = nextcloud_banda_get_config('base_users_included'); $display_storage = $real_config['storage_space'] ?: '1tb'; $display_users = $real_config['num_users'] ?: $base_users_included; // CORREGIDO: Derivar frecuencia desde el ciclo real del level (fuente de verdad) // y asegurar consistencia con la etiqueta mostrada $cycle_number = (int)($membership->cycle_number ?? 1); $cycle_period = (string)($membership->cycle_period ?? 'Month'); // Derivar la frecuencia canónica desde el ciclo $derived_frequency = nextcloud_banda_derive_frequency_from_cycle($cycle_number, $cycle_period); // Generar la etiqueta legible desde el ciclo real (esto es lo que se debe mostrar) $cycle_label = nextcloud_banda_map_cycle_label($cycle_number, $cycle_period); // Usar el ciclo real como fuente de verdad, no la frecuencia guardada $display_frequency = $derived_frequency; $display_amount = $real_config['final_amount'] ?: (float)$membership->initial_payment; $additional_users = max(0, (int)$display_users - $base_users_included); $is_estimated = ($real_config['source'] === 'none' || $real_config['source'] === 'membership_deduction'); ?>

Detalhes do plano name); ?>

ℹ️ Informação: Os dados abaixo são estimados baseados na sua assinatura. Para configurar seu plano personalizado, entre em contato com o suporte.

🗄️ Armazenamento: (estimado)

0): ?>

Espaço usado: TB

👥 Usuários: (estimado)

0): ?>

incluídos + adicionais

usuários incluídos no plano base

💳 Ciclo de Pagamento: (estimado)

💰 Valor do plano: R$

✅ Configuração ativada - 'dados salvos do seu pedido', 'profile_fields' => 'campos do seu perfil', 'user_meta' => 'configuração do sistema' ]; echo esc_html($source_labels[$real_config['source']] ?? 'fonte desconhecida'); ?>

🔄 Próximo pagamento:

📅 Cliente desde: startdate), wp_timezone()); ?>

Grupo Nextcloud: banda-

ID do plano: id); ?>

Versão: | Fonte: | Ciclo: válido

$real_config['source'], 'is_estimated' => $is_estimated, 'cycle_label' => $cycle_label, 'derived_frequency' => $derived_frequency, 'cycle_number' => $cycle_number, 'cycle_period' => $cycle_period ]); } catch (Exception $e) { nextcloud_banda_log_error('Exception in nextcloud_banda_show_member_config_improved', [ 'user_id' => $user_id, 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine() ]); if (defined('WP_DEBUG') && WP_DEBUG && current_user_can('manage_options')) { echo '
'; echo '

Erro: Não foi possível carregar os detalhes do plano Banda.

'; echo '
'; } } } // ==== // MANEJO DE ELIMINACIÓN COMPLETA DE DATOS // ==== /** * Elimina todos los datos de configuración de Banda para un usuario */ function nextcloud_banda_delete_all_user_data($user_id) { // Eliminar user meta específicos delete_user_meta($user_id, 'nextcloud_banda_config'); delete_user_meta($user_id, 'storage_space'); delete_user_meta($user_id, 'num_users'); delete_user_meta($user_id, 'payment_frequency'); // Eliminar campos de PMPro Register Helper si existen if (function_exists('pmprorh_getProfileFields')) { $fields = ['storage_space', 'num_users', 'payment_frequency']; foreach ($fields as $field_name) { delete_user_meta($user_id, $field_name); } } // Invalidar toda la caché del usuario nextcloud_banda_invalidate_user_cache($user_id); nextcloud_banda_log_info("Todos los datos de Banda eliminados para user_id: {$user_id}"); } /** * Hook para eliminar datos cuando se elimina un usuario de WordPress */ add_action('delete_user', 'nextcloud_banda_cleanup_on_user_deletion'); function nextcloud_banda_cleanup_on_user_deletion($user_id) { nextcloud_banda_delete_all_user_data($user_id); nextcloud_banda_log_info("Limpieza completada al eliminar usuario: {$user_id}"); } /** * Hook mejorado para eliminar configuración al cancelar membresía */ add_action('pmpro_after_cancel_membership_level', 'nextcloud_banda_clear_config_on_cancellation_improved', 10, 3); function nextcloud_banda_clear_config_on_cancellation_improved($user_id, $membership_level_id, $cancelled_levels) { $allowed_levels = nextcloud_banda_get_config('allowed_levels'); // Verificar si alguno de los niveles cancelados es de Banda $has_banda_level = false; if (is_array($cancelled_levels)) { foreach ($cancelled_levels as $level) { if (in_array((int)$level->membership_id, $allowed_levels, true)) { $has_banda_level = true; break; } } } else { // Compatibilidad con versiones anteriores if (is_object($cancelled_levels) && isset($cancelled_levels->membership_id)) { if (in_array((int)$cancelled_levels->membership_id, $allowed_levels, true)) { $has_banda_level = true; } } } if ($has_banda_level) { nextcloud_banda_delete_all_user_data($user_id); nextcloud_banda_log_info("Configuración eliminada tras cancelación de membresía Banda", [ 'user_id' => $user_id, 'cancelled_levels' => is_array($cancelled_levels) ? array_map(function($l) { return $l->membership_id; }, $cancelled_levels) : 'single_level' ]); } } /** * Hook para limpiar datos cuando se cambia completamente de nivel */ add_action('pmpro_after_change_membership_level', function($level_id, $user_id) { // Si el nuevo nivel no es de Banda, limpiar datos anteriores $allowed_levels = nextcloud_banda_get_config('allowed_levels'); if (!in_array((int)$level_id, $allowed_levels, true)) { nextcloud_banda_delete_all_user_data($user_id); } else { // Invalidar caché pero mantener datos nextcloud_banda_invalidate_user_cache($user_id); } }, 20, 2); // Prioridad más baja para ejecutarse después de otros hooks /** * Función mejorada para obtener configuración del usuario * Forzará valores por defecto si no hay membresía activa * Sincroniza payment_frequency con el ciclo real del level */ function nextcloud_banda_get_user_real_config_improved($user_id, $membership = null) { // Verificar si el usuario tiene membresía Banda activa $allowed_levels = nextcloud_banda_get_config('allowed_levels'); $user_levels = pmpro_getMembershipLevelsForUser($user_id); $has_active_banda_membership = false; $active_level = null; if (!empty($user_levels)) { foreach ($user_levels as $level) { if (in_array((int)$level->id, $allowed_levels, true)) { // Verificar usando next_payment_info $cycle_info = nextcloud_banda_get_next_payment_info($user_id); if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { $has_active_banda_membership = true; $active_level = $level; break; } } } } // Si no tiene membresía Banda activa, retornar valores por defecto if (!$has_active_banda_membership) { return [ 'storage_space' => '1tb', 'num_users' => 2, 'payment_frequency' => 'monthly', 'final_amount' => null, 'source' => 'defaults_no_active_membership' ]; } // Si tiene membresía activa, obtener configuración real $real_config = nextcloud_banda_get_user_real_config($user_id, $membership); // Sincronizar payment_frequency con el ciclo real del level (fuente de verdad) if ($active_level) { $cycle_number = (int)($active_level->cycle_number ?? 1); $cycle_period = (string)($active_level->cycle_period ?? 'Month'); $derived_freq = nextcloud_banda_derive_frequency_from_cycle($cycle_number, $cycle_period); // Sobrescribir payment_frequency con la derivada del ciclo real $real_config['payment_frequency'] = $derived_freq; $real_config['cycle_number'] = $cycle_number; $real_config['cycle_period'] = $cycle_period; nextcloud_banda_log('CONFIG_SYNC', [ 'user_id' => $user_id, 'level_id' => $active_level->id, 'cycle_number' => $cycle_number, 'cycle_period' => $cycle_period, 'derived_frequency' => $derived_freq, 'previous_frequency' => $real_config['payment_frequency'] ?? 'none' ]); } return $real_config; } // ==== // FUNCIONES DE LIMPIEZA PARA CASOS ESPECIALES // ==== /** * Función para limpiar datos de usuarios que ya no tienen membresía activa */ function nextcloud_banda_cleanup_inactive_users() { $users_with_config = get_users([ 'meta_key' => 'nextcloud_banda_config', 'fields' => ['ID'] ]); $cleaned_count = 0; $allowed_levels = nextcloud_banda_get_config('allowed_levels'); foreach ($users_with_config as $user) { $user_id = $user->ID; $user_levels = pmpro_getMembershipLevelsForUser($user_id); $has_active_banda_membership = false; if (!empty($user_levels)) { foreach ($user_levels as $level) { if (in_array((int)$level->id, $allowed_levels, true)) { // Verificar usando next_payment_info $cycle_info = nextcloud_banda_get_next_payment_info($user_id); if ($cycle_info && isset($cycle_info['cycle_end']) && $cycle_info['cycle_end'] > time()) { $has_active_banda_membership = true; break; } } } } // Si no tiene membresía Banda activa, limpiar sus datos if (!$has_active_banda_membership) { nextcloud_banda_delete_all_user_data($user_id); $cleaned_count++; } } nextcloud_banda_log_info("Limpieza de usuarios inactivos completada", [ 'usuarios_revisados' => count($users_with_config), 'usuarios_limpiados' => $cleaned_count ]); return $cleaned_count; } // Agregar endpoint para limpieza manual (solo para administradores) add_action('wp_ajax_nextcloud_banda_cleanup_inactive', 'nextcloud_banda_cleanup_inactive_endpoint'); function nextcloud_banda_cleanup_inactive_endpoint() { if (!current_user_can('manage_options')) { wp_die('Acceso denegado'); } $cleaned_count = nextcloud_banda_cleanup_inactive_users(); wp_send_json_success([ 'message' => "Limpieza completada. {$cleaned_count} usuarios procesados.", 'cleaned_count' => $cleaned_count ]); } // ==== // INICIALIZACIÓN Y HOOKS // ==== // Hook de inicialización único add_action('init', 'nextcloud_banda_add_dynamic_fields', 20); // Hook para validar antes del checkout add_filter('pmpro_registration_checks', 'nextcloud_banda_validate_before_checkout'); function nextcloud_banda_validate_before_checkout($pmpro_continue_registration) { if (!$pmpro_continue_registration) { return $pmpro_continue_registration; } $validation = nextcloud_banda_validate_checkout_config(); if (is_wp_error($validation)) { pmpro_setMessage($validation->get_error_message(), 'pmpro_error'); return false; } return true; } // Hook principal de modificación de precio add_filter('pmpro_checkout_level', 'nextcloud_banda_modify_level_pricing', 10, 2); // Mantiene el startdate original si el usuario permanece en el mismo level add_filter('pmpro_checkout_start_date', function($startdate, $user_id, $level) { if (empty($level) || empty($level->id)) { return $startdate; } if (!function_exists('pmpro_hasMembershipLevel')) { return $startdate; } if (!pmpro_hasMembershipLevel($level->id, $user_id)) { return $startdate; } global $wpdb; $old = $wpdb->get_var($wpdb->prepare( "SELECT startdate FROM $wpdb->pmpro_memberships_users WHERE user_id = %d AND membership_id = %d AND status = 'active' ORDER BY id DESC LIMIT 1", $user_id, $level->id )); if (!empty($old)) { return $old; } return $startdate; }, 10, 3); // Hooks de guardado add_action('pmpro_after_checkout', 'nextcloud_banda_save_configuration', 10, 2); // Hook para mostrar configuración en área de miembros // Modificar la función de visualización para usar la versión mejorada remove_action('pmpro_account_bullets_bottom', 'nextcloud_banda_show_member_config'); add_action('pmpro_account_bullets_bottom', 'nextcloud_banda_show_member_config_improved'); // Invalidación de caché add_action('pmpro_after_change_membership_level', function($level_id, $user_id) { nextcloud_banda_invalidate_user_cache($user_id); }, 10, 2); nextcloud_banda_log_info('PMPro Banda Dynamic Pricing loaded - SYNCHRONIZED VERSION', [ 'version' => NEXTCLOUD_BANDA_PLUGIN_VERSION, 'php_version' => PHP_VERSION, 'base_price_constant' => NEXTCLOUD_BANDA_BASE_PRICE, 'normalize_function_exists' => function_exists('normalize_banda_config'), 'real_config_function_exists' => function_exists('nextcloud_banda_get_user_real_config') ]);