2764 lines
101 KiB
PHP
2764 lines
101 KiB
PHP
<?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')
|
||
]);
|