simply-code/storage/snippets/nextcloud-banda-dynamic-pri...

2764 lines
101 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* PMPro Dynamic Pricing para Nextcloud Banda - VERSIÓN SINCRONIZADA v2.8.0
*
* Nombre del archivo: nextcloud-banda-dynamic-pricing.php
*
* RESPONSABILIDAD: Lógica de checkout, campos dinámicos y cálculos de precio
* CORREGIDO: Sincronización completa con theme-scripts.php y JavaScript
*
* @version 2.8.0
*/
if (!defined('ABSPATH')) {
exit('Acceso directo no permitido');
}
// ====
// CONFIGURACIÓN GLOBAL Y CONSTANTES - SINCRONIZADAS
// ====
define('NEXTCLOUD_BANDA_PLUGIN_VERSION', '2.8.0');
define('NEXTCLOUD_BANDA_CACHE_GROUP', 'nextcloud_banda_dynamic');
define('NEXTCLOUD_BANDA_CACHE_EXPIRY', HOUR_IN_SECONDS);
// CORREGIDO: Definir constante que será usada en JavaScript
if (!defined('NEXTCLOUD_BANDA_BASE_PRICE')) {
define('NEXTCLOUD_BANDA_BASE_PRICE', 70.00); // Precio base del plan (1TB + 2 usuarios)
}
/**
* FUNCIÓN CRÍTICA - Normaliza configuración Banda
*/
if (!function_exists('normalize_banda_config')) {
function normalize_banda_config($config_data) {
if (!is_array($config_data)) {
return [
'storage_space' => '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(
'<div class="notice notice-error"><p><strong>PMPro Banda Dynamic:</strong> Los siguientes plugins son requeridos: %s</p></div>',
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');
?>
<div class="pmpro_account-profile-field">
<h3>Detalhes do plano <strong><?php echo esc_html($membership->name); ?></strong></h3>
<?php if ($is_estimated): ?>
<div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 10px; border-radius: 4px; margin: 10px 0; font-size: 0.9em;">
<strong> Informação:</strong> Os dados abaixo são estimados baseados na sua assinatura.
Para configurar seu plano personalizado, entre em contato com o suporte.
</div>
<?php endif; ?>
<div style="background: #f9f9f9; padding: 20px; border-radius: 8px; margin: 15px 0; border-left: 4px solid #ff6b35;">
<div style="margin-bottom: 15px;">
<p><strong>🗄️ Armazenamento:</strong>
<?php echo esc_html($storage_options[$display_storage] ?? $display_storage); ?>
<?php if ($is_estimated && $real_config['source'] === 'none'): ?>
<em style="color: #666; font-size: 0.85em;">(estimado)</em>
<?php endif; ?>
</p>
<?php if ($used_space_tb > 0): ?>
<p style="margin-left: 20px; color: #666; font-size: 0.9em;">
<em>Espaço usado: <?php echo number_format_i18n($used_space_tb, 2); ?> TB</em>
</p>
<?php endif; ?>
</div>
<div style="margin-bottom: 15px;">
<p><strong>👥 Usuários:</strong>
<?php echo esc_html($user_options[strval($display_users)] ?? "{$display_users} usuários"); ?>
<?php if ($is_estimated && $real_config['source'] === 'none'): ?>
<em style="color: #666; font-size: 0.85em;">(estimado)</em>
<?php endif; ?>
</p>
<?php if ($additional_users > 0): ?>
<p style="margin-left: 20px; color: #666; font-size: 0.9em;">
<em><?php echo $base_users_included; ?> incluídos + <?php echo $additional_users; ?> adicionais</em>
</p>
<?php else: ?>
<p style="margin-left: 20px; color: #666; font-size: 0.9em;">
<em><?php echo $base_users_included; ?> usuários incluídos no plano base</em>
</p>
<?php endif; ?>
</div>
<div style="margin-bottom: 15px;">
<p><strong>💳 Ciclo de Pagamento:</strong>
<?php echo esc_html($cycle_label); ?>
<?php if ($is_estimated && $real_config['source'] === 'none'): ?>
<em style="color: #666; font-size: 0.85em;">(estimado)</em>
<?php endif; ?>
</p>
</div>
<?php if (!empty($display_amount)): ?>
<div style="margin-bottom: 15px;">
<p><strong>💰 Valor do plano:</strong>
R$ <?php echo number_format_i18n((float)$display_amount, 2); ?>
</p>
</div>
<?php endif; ?>
<?php if (!$is_estimated): ?>
<div style="background: #d4edda; border: 1px solid #c3e6cb; padding: 8px; border-radius: 4px; margin: 10px 0; font-size: 0.85em;">
<strong>✅ Configuração ativada</strong> -
<?php
$source_labels = [
'saved_config' => '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');
?>
</div>
<?php endif; ?>
<div style="border-top: 1px solid #ddd; padding-top: 15px; margin-top: 15px;">
<?php if (!empty($next_payment_date)): ?>
<p style="font-size: 0.9em; color: #666;">
<strong>🔄 Próximo pagamento:</strong>
<?php echo esc_html($next_payment_date); ?>
</p>
<?php endif; ?>
<p style="font-size: 0.9em; color: #666;">
<strong>📅 Cliente desde:</strong>
<?php echo wp_date('d/m/Y', nextcloud_banda_safe_ts($membership->startdate), wp_timezone()); ?>
</p>
</div>
<div style="border-top: 1px solid #eee; padding-top: 10px; margin-top: 10px;">
<p style="font-size: 0.8em; color: #999;">
Grupo Nextcloud: <strong>banda-<?php echo esc_html($user_id); ?></strong>
</p>
<p style="font-size: 0.8em; color: #999;">
ID do plano: <?php echo esc_html($membership->id); ?>
</p>
</div>
<div style="border-top: 1px solid #eee; padding-top: 10px; margin-top: 10px;">
<p style="font-size: 0.6em; color: #999;">
Versão: <?php echo esc_html(NEXTCLOUD_BANDA_PLUGIN_VERSION); ?> |
Fonte: <?php echo esc_html($real_config['source']); ?>
<?php if ($cycle_info && isset($cycle_info['cycle_start'])): ?>
| Ciclo: válido
<?php endif; ?>
</p>
</div>
</div>
</div>
<?php
nextcloud_banda_log_info("Banda member config displayed successfully for user {$user_id}", [
'source' => $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 '<div class="pmpro_account-profile-field">';
echo '<p style="color: red;"><strong>Erro:</strong> Não foi possível carregar os detalhes do plano Banda.</p>';
echo '</div>';
}
}
}
// ====
// 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')
]);