===== PADDLE WEBHOOK ===== $tol) throw new RuntimeException('Webhook fuera de tolerancia temporal.'); $exp=hash_hmac('sha256',$ts.':'.$raw,$secret); foreach($hs as $h){if(hash_equals($exp,$h))return;} throw new RuntimeException('Firma Paddle no coincide.'); } function paddle_appdb_pdo(): PDO { $p='/home/iotcuboc/.mypyme360_appdb.json'; if(!is_file($p)) throw new RuntimeException('Falta configuración BD.'); $c=json_decode((string)file_get_contents($p),true); $dsn='mysql:host='.$c['host'].';port='.(int)$c['port'].';dbname='.$c['database'].';charset=latin1'; return new PDO($dsn,$c['username'],$c['password'],[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION,PDO::ATTR_DEFAULT_FETCH_MODE=>PDO::FETCH_ASSOC,PDO::ATTR_EMULATE_PREPARES=>false]); } function paddle_event_seen(PDO $pdo,string $id): bool {$s=$pdo->prepare('SELECT id FROM saas_webhook_eventos WHERE paddle_event_id=? LIMIT 1');$s->execute([$id]);return (bool)$s->fetchColumn();} function paddle_log_event(PDO $pdo,array $e,string $raw): int {$s=$pdo->prepare('INSERT INTO saas_webhook_eventos (paddle_event_id,event_type,occurred_at,processed,payload,received_at) VALUES (?,?,?,0,?,NOW())');$o=!empty($e['occurred_at'])?date('Y-m-d H:i:s',strtotime($e['occurred_at'])):null;$s->execute([$e['event_id'],$e['event_type'],$o,$raw]);return (int)$pdo->lastInsertId();} function paddle_mark_event(PDO $pdo,int $id,bool $ok,?string $err=null): void {$s=$pdo->prepare('UPDATE saas_webhook_eventos SET processed=?,error_message=?,processed_at=NOW() WHERE id=?');$s->execute([$ok?1:0,$err,$id]);} function paddle_validate_ctx(PDO $pdo,?array $c): array { if(!$c) throw new RuntimeException('Evento sin custom_data.'); $tc=trim((string)($c['trial_code']??''));$eid=(int)($c['empresa_id']??0);$uid=(int)($c['usuario_id']??0);$plan=strtoupper(trim((string)($c['plan']??'')));$cycle=strtolower(trim((string)($c['billing_cycle']??''))); if($tc===''||$eid<=0||$uid<=0||$plan!=='INICIAL'||!in_array($cycle,['monthly','annual'],true)) throw new RuntimeException('custom_data inválido.'); $s=$pdo->prepare('SELECT empresa_id,usuario_id FROM saas_trials WHERE trial_code=? LIMIT 1');$s->execute([$tc]);$t=$s->fetch(); if(!$t||(int)$t['empresa_id']!==$eid||(int)$t['usuario_id']!==$uid) throw new RuntimeException('custom_data no coincide con trial.'); return ['trial_code'=>$tc,'empresa_id'=>$eid,'usuario_id'=>$uid,'plan'=>'INICIAL','billing_cycle'=>$cycle]; } function paddle_price_id(array $d): ?string {return (string)($d['items'][0]['price']['id']??'')?:null;} function paddle_dt($v): ?string {if(!$v)return null;$t=strtotime((string)$v);return $t?date('Y-m-d H:i:s',$t):null;} function paddle_upsert_sub(PDO $pdo,array $d,array $c): void { $sid=(string)($d['id']??''); if(!str_starts_with($sid,'sub_')) throw new RuntimeException('subscription id inválido.'); $s=$pdo->prepare("INSERT INTO saas_suscripciones (empresa_id,usuario_id,trial_code,plan_codigo,proveedor,paddle_customer_id,paddle_subscription_id,paddle_price_id,billing_cycle,currency,status,current_period_start,current_period_end,next_billed_at,canceled_at,created_at,updated_at) VALUES (?,?,?,?,'PADDLE',?,?,?,?,?,?,?,?,?,?,NOW(),NOW()) ON DUPLICATE KEY UPDATE empresa_id=VALUES(empresa_id),usuario_id=VALUES(usuario_id),trial_code=VALUES(trial_code),plan_codigo=VALUES(plan_codigo),paddle_customer_id=VALUES(paddle_customer_id),paddle_price_id=VALUES(paddle_price_id),billing_cycle=VALUES(billing_cycle),currency=VALUES(currency),status=VALUES(status),current_period_start=VALUES(current_period_start),current_period_end=VALUES(current_period_end),next_billed_at=VALUES(next_billed_at),canceled_at=VALUES(canceled_at),updated_at=NOW()"); $s->execute([$c['empresa_id'],$c['usuario_id'],$c['trial_code'],$c['plan'],$d['customer_id']??null,$sid,paddle_price_id($d),$c['billing_cycle'],$d['currency_code']??'USD',$d['status']??'unknown',paddle_dt($d['current_billing_period']['starts_at']??null),paddle_dt($d['current_billing_period']['ends_at']??null),paddle_dt($d['next_billed_at']??null),paddle_dt($d['canceled_at']??null)]); } function paddle_record_tx(PDO $pdo,array $d,array $c,string $type): void { $tx=(string)($d['id']??''); if(!str_starts_with($tx,'txn_')) throw new RuntimeException('transaction id inválido.'); $amt=$d['details']['totals']['total']??($d['details']['totals']['grand_total']??null); $s=$pdo->prepare("INSERT INTO saas_pagos (empresa_id,usuario_id,trial_code,proveedor,paddle_transaction_id,paddle_subscription_id,paddle_customer_id,amount_minor,currency,status,event_type,occurred_at,created_at,updated_at) VALUES (?,?,?,'PADDLE',?,?,?,?,?,?,?,?,NOW(),NOW()) ON DUPLICATE KEY UPDATE paddle_subscription_id=VALUES(paddle_subscription_id),paddle_customer_id=VALUES(paddle_customer_id),amount_minor=VALUES(amount_minor),currency=VALUES(currency),status=VALUES(status),event_type=VALUES(event_type),occurred_at=VALUES(occurred_at),updated_at=NOW()"); $s->execute([$c['empresa_id'],$c['usuario_id'],$c['trial_code'],$tx,$d['subscription_id']??null,$d['customer_id']??null,$amt,$d['currency_code']??'USD',$d['status']??($type==='transaction.payment_failed'?'payment_failed':'unknown'),$type,date('Y-m-d H:i:s')]); } function paddle_handle_event(PDO $pdo,array $e): void { $t=(string)($e['event_type']??'');$d=$e['data']??null;if(!is_array($d)) throw new RuntimeException('Evento sin data.'); if(in_array($t,['subscription.created','subscription.updated','subscription.canceled'],true)){ $c=paddle_validate_ctx($pdo,is_array($d['custom_data']??null)?$d['custom_data']:null);paddle_upsert_sub($pdo,$d,$c);return;} if(in_array($t,['transaction.completed','transaction.payment_failed'],true)){ $c=paddle_validate_ctx($pdo,is_array($d['custom_data']??null)?$d['custom_data']:null);paddle_record_tx($pdo,$d,$c,$t);return;} } ===== SAAS ACCESS ===== prepare(" SELECT id,trial_code,plan_codigo,proveedor,paddle_subscription_id, paddle_price_id,billing_cycle,currency,status, current_period_start,current_period_end,next_billed_at, canceled_at,created_at,updated_at FROM saas_suscripciones WHERE empresa_id=? ORDER BY COALESCE(updated_at,created_at) DESC,id DESC LIMIT 1 "); $st->execute([$empresaId]); $sub = $st->fetch(PDO::FETCH_ASSOC); if ($sub) { $status = strtolower((string)$sub['status']); $periodEnd = saas_dt($sub['current_period_end'] ?? null); $updatedAt = saas_dt($sub['updated_at'] ?? null) ?: saas_dt($sub['created_at'] ?? null) ?: $now; $base = [ 'source'=>'SUBSCRIPTION', 'empresa_id'=>$empresaId, 'trial_code'=>(string)($sub['trial_code'] ?? ''), 'plan'=>(string)($sub['plan_codigo'] ?? 'INICIAL'), 'billing_cycle'=>(string)($sub['billing_cycle'] ?? ''), 'currency'=>(string)($sub['currency'] ?? ''), 'paddle_subscription_id'=>(string)($sub['paddle_subscription_id'] ?? ''), 'paddle_price_id'=>(string)($sub['paddle_price_id'] ?? ''), 'current_period_start'=>$sub['current_period_start'] ?? null, 'current_period_end'=>$sub['current_period_end'] ?? null, 'next_billed_at'=>$sub['next_billed_at'] ?? null, 'raw_status'=>$status, 'grace_until'=>null, 'allowed'=>false, 'status'=>'SUBSCRIPTION_BLOCKED', 'message'=>'La suscripción no permite acceso.' ]; if ($status === 'active') { if ($periodEnd === null || $periodEnd >= $now) { return array_merge($base,[ 'allowed'=>true, 'status'=>'ACTIVE', 'message'=>'Suscripción activa.' ]); } return array_merge($base,[ 'status'=>'SUBSCRIPTION_EXPIRED', 'message'=>'El período pagado terminó y no se recibió una renovación vigente.' ]); } if ($status === 'canceled' && $periodEnd !== null && $periodEnd >= $now) { return array_merge($base,[ 'allowed'=>true, 'status'=>'CANCELED_UNTIL_END', 'message'=>'Suscripción cancelada; acceso disponible hasta el fin del período pagado.' ]); } if ($status === 'past_due') { $graceUntil = $updatedAt->modify('+'.MYPYME360_PAST_DUE_GRACE_DAYS.' days'); if ($graceUntil >= $now) { return array_merge($base,[ 'allowed'=>true, 'status'=>'PAST_DUE_GRACE', 'grace_until'=>$graceUntil->format('Y-m-d H:i:s'), 'message'=>'Pago pendiente dentro del período de gracia.' ]); } return array_merge($base,[ 'status'=>'PAST_DUE_EXPIRED', 'grace_until'=>$graceUntil->format('Y-m-d H:i:s'), 'message'=>'El período de gracia terminó.' ]); } if ($status === 'paused') { return array_merge($base,['status'=>'PAUSED','message'=>'La suscripción está pausada.']); } if ($status === 'canceled') { return array_merge($base,['status'=>'CANCELED','message'=>'La suscripción está cancelada.']); } return $base; } $st = $pdo->prepare(" SELECT id,trial_code,trial_start,trial_end,status,activated_at,created_at FROM saas_trials WHERE empresa_id=? ORDER BY id DESC LIMIT 1 "); $st->execute([$empresaId]); $trial = $st->fetch(PDO::FETCH_ASSOC); if (!$trial) { return [ 'source'=>'NONE', 'empresa_id'=>$empresaId, 'trial_code'=>'', 'allowed'=>false, 'status'=>'NO_COMMERCIAL_ACCESS', 'message'=>'La empresa no tiene trial ni suscripción.' ]; } $trialEnd = null; if (!empty($trial['trial_end'])) { try { $trialEnd = new DateTimeImmutable((string)$trial['trial_end'].' 23:59:59'); } catch (Throwable $e) {} } $trialStatus = strtoupper((string)($trial['status'] ?? '')); $base = [ 'source'=>'TRIAL', 'empresa_id'=>$empresaId, 'trial_code'=>(string)($trial['trial_code'] ?? ''), 'trial_start'=>$trial['trial_start'] ?? null, 'trial_end'=>$trial['trial_end'] ?? null, 'trial_db_status'=>$trialStatus, 'allowed'=>false, 'status'=>'TRIAL_EXPIRED', 'message'=>'El período de prueba terminó.' ]; if (in_array($trialStatus,['CANCELLED','EXPIRED'],true)) { return $base; } if ($trialEnd !== null && $trialEnd >= $now) { return array_merge($base,[ 'allowed'=>true, 'status'=>'TRIAL', 'message'=>'Período de prueba vigente.' ]); } return $base; } function saas_current_access(PDO $pdo): array { if (saas_is_master()) { return ['source'=>'MASTER','allowed'=>true,'status'=>'MASTER','message'=>'Acceso Master.']; } $empresaId = function_exists('empresa_id') ? (int)empresa_id() : (int)($_SESSION['empresa_id'] ?? 0); if ($empresaId <= 0) { return [ 'source'=>'NONE','allowed'=>false,'status'=>'NO_COMPANY', 'message'=>'La sesión no tiene empresa asociada.' ]; } return saas_access_status($pdo,$empresaId); } function require_saas_access(PDO $pdo): void { $access = saas_current_access($pdo); $GLOBALS['mypyme360_saas_access'] = $access; if (!empty($access['allowed'])) return; if (!headers_sent()) { header('Location: /suscripcion_v3.php?blocked=1&status='.rawurlencode((string)$access['status'])); exit; } http_response_code(402); echo 'Suscripción o período de prueba no vigente.'; exit; } ===== SUSCRIPCION ===== [ 'Suscripción activa', 'Tu Plan Inicial está activo y operativo.', 'ok' ], 'TRIAL' => [ 'Prueba gratuita activa', 'Estás utilizando el período de prueba de MyPyme360.', 'ok' ], 'PAST_DUE_GRACE' => [ 'Pago pendiente', 'No pudimos confirmar el último pago. Tu acceso continúa temporalmente.', 'warn' ], 'CANCELED_UNTIL_END' => [ 'Suscripción cancelada', 'La renovación fue cancelada, pero puedes seguir usando MyPyme360 hasta el término del período pagado.', 'warn' ], 'PAST_DUE_EXPIRED' => [ 'Pago pendiente vencido', 'El período de gracia terminó. Debes regularizar el pago para continuar utilizando MyPyme360.', 'error' ], 'CANCELED' => [ 'Suscripción cancelada', 'Tu suscripción ya no se encuentra activa.', 'error' ], 'PAUSED' => [ 'Suscripción pausada', 'Tu suscripción se encuentra temporalmente pausada.', 'error' ], 'SUBSCRIPTION_EXPIRED' => [ 'Suscripción vencida', 'No existe actualmente un período pagado vigente.', 'error' ], 'TRIAL_EXPIRED' => [ 'Prueba finalizada', 'Tu período gratuito terminó. Contrata el Plan Inicial para continuar.', 'error' ], 'NO_COMMERCIAL_ACCESS' => [ 'Sin suscripción', 'No encontramos una prueba ni una suscripción asociada a esta empresa.', 'error' ], ]; $info = $statusInfo[$status] ?? [ 'Estado de suscripción', (string)($access['message'] ?? 'Estado no identificado.'), 'warn' ]; $pricingUrl = 'https://web.mypyme360.com/precios.php'; if ($trialCode !== '') { $pricingUrl .= '?trial=' . rawurlencode($trialCode); } function fmt_saas_date($value): string { if (!$value) { return '—'; } $ts = strtotime((string)$value); if (!$ts) { return (string)$value; } return date('d-m-Y H:i', $ts); } render_header_v3( 'Suscripción', 'Plan, estado, vigencia y próximos cobros' ); ?>

Estado de la suscripción

Información comercial asociada a esta empresa.

Plan
Modalidad
Estado
Moneda

Vigencia y facturación

Fechas relacionadas con tu prueba o suscripción.

Fin de prueba
Período pagado hasta
Próximo cobro
Período de gracia hasta
Código de prueba
===== MASTER EMPRESAS ===== 'MASTER', 'TRIAL' => 'TRIAL', 'ACTIVE' => 'PAGADO / ACTIVO', 'PAST_DUE_GRACE' => 'PAGO PENDIENTE', 'PAST_DUE_EXPIRED' => 'PAGO VENCIDO', 'CANCELED_UNTIL_END' => 'CANCELADO / VIGENTE', 'CANCELED' => 'CANCELADO', 'PAUSED' => 'SUSPENDIDO', 'SUBSCRIPTION_EXPIRED' => 'VENCIDO', 'TRIAL_EXPIRED' => 'TRIAL VENCIDO', 'NO_COMMERCIAL_ACCESS' => 'SIN CONFIGURAR', 'NO_COMPANY' => 'SIN EMPRESA', ]; return $labels[$status] ?? $status; } function master_saas_badge(string $status): string { if (in_array($status, ['ACTIVE','MASTER'], true)) { return 'good'; } if ($status === 'TRIAL') { return 'info'; } if (in_array($status, ['PAST_DUE_GRACE','CANCELED_UNTIL_END'], true)) { return 'warn'; } return 'bad'; } function master_saas_vigencia(array $access): string { $value = null; if (!empty($access['current_period_end'])) { $value = $access['current_period_end']; } elseif (!empty($access['trial_end'])) { $value = $access['trial_end']; } elseif (!empty($access['grace_until'])) { $value = $access['grace_until']; } if (!$value) { return '—'; } $ts = strtotime((string)$value); return $ts ? date('d-m-Y', $ts) : (string)$value; } try { if (isset($_GET['delete'])) { $id = (int)$_GET['delete']; if ($id <= 0) { throw new Exception('Empresa inválida.'); } $pdo->prepare("UPDATE empresas SET estado = 'INACTIVA' WHERE id = ?")->execute([$id]); audit($pdo, 'DESACTIVAR_EMPRESA', 'empresas', $id); header('Location: ' . app_url('master_empresas_v3.php?ok=1')); exit; } if ($_SERVER['REQUEST_METHOD'] === 'POST') { $id = (int)($_POST['id'] ?? 0); $nombre = master_clean_text($_POST['nombre'] ?? ''); $rut = master_clean_text($_POST['rut'] ?? ''); $tipo_pyme = $_POST['tipo_pyme'] ?? 'MIXTA'; $plan = master_clean_text($_POST['plan'] ?? 'BASICO'); $estado = $_POST['estado'] ?? 'ACTIVA'; $cantidad_decimales = max(0, min(6, (int)($_POST['cantidad_decimales'] ?? 3))); $moneda_simbolo = trim((string)($_POST['moneda_simbolo'] ?? '$')); $moneda_decimales = max(0, min(6, (int)($_POST['moneda_decimales'] ?? 0))); $mostrar_codigo_item = isset($_POST['mostrar_codigo_item']) ? 1 : 0; if ($nombre === '') { throw new Exception('Debe ingresar el nombre de la empresa.'); } if (!in_array($tipo_pyme, ['INDUSTRIAL','COMERCIAL','SERVICIOS','MIXTA','DIGITAL'], true)) { throw new Exception('Tipo PYME inválido.'); } if (!in_array($estado, ['ACTIVA','INACTIVA'], true)) { throw new Exception('Estado inválido.'); } if ($plan === '') { $plan = 'BASICO'; } if ($moneda_simbolo === '' || strlen($moneda_simbolo) > 12) { throw new Exception('Símbolo monetario inválido.'); } if ($id > 0) { $pdo->prepare("UPDATE empresas SET nombre=?, rut=?, tipo_pyme=?, plan=?, estado=?, cantidad_decimales=?, moneda_simbolo=?, moneda_decimales=?, mostrar_codigo_item=? WHERE id=?") ->execute([$nombre, $rut, $tipo_pyme, $plan, $estado, $cantidad_decimales, $moneda_simbolo, $moneda_decimales, $mostrar_codigo_item, $id]); audit($pdo, 'ACTUALIZAR_EMPRESA', 'empresas', $id); header('Location: ' . app_url('master_empresas_v3.php?edit=' . $id . '&ok=1')); exit; } $pdo->prepare("INSERT INTO empresas(nombre, rut, tipo_pyme, plan, estado, cantidad_decimales, moneda_simbolo, moneda_decimales, mostrar_codigo_item) VALUES(?,?,?,?,?,?,?,?,?)") ->execute([$nombre, $rut, $tipo_pyme, $plan, $estado, $cantidad_decimales, $moneda_simbolo, $moneda_decimales, $mostrar_codigo_item]); $newId = (int)$pdo->lastInsertId(); audit($pdo, 'CREAR_EMPRESA', 'empresas', $newId); header('Location: ' . app_url('master_empresas_v3.php?edit=' . $newId . '&ok=1')); exit; } } catch (Throwable $e) { $err = $e->getMessage(); } if (isset($_GET['ok'])) { $msg = 'Cambios guardados correctamente.'; } $edit = null; if (isset($_GET['edit'])) { $st = $pdo->prepare('SELECT * FROM empresas WHERE id = ? LIMIT 1'); $st->execute([(int)$_GET['edit']]); $edit = $st->fetch(PDO::FETCH_ASSOC); } $columns = [ 'nombre' => 'Empresa', 'rut' => 'RUT', 'tipo_pyme' => 'Tipo', 'plan' => 'Plan', 'estado' => 'Estado' ]; $sortMap = [ 'id' => 'id', 'nombre' => 'nombre', 'rut' => 'rut', 'tipo_pyme' => 'tipo_pyme', 'plan' => 'plan', 'estado' => 'estado' ]; $list = list_get_params(array_keys($sortMap), 'nombre'); $q = $list['q']; $sort = $list['sort']; $dir = strtoupper($list['dir']); $where = []; $params = []; if ($q !== '') { $where[] = '(CAST(id AS CHAR) LIKE ? OR nombre LIKE ? OR rut LIKE ? OR tipo_pyme LIKE ? OR plan LIKE ? OR estado LIKE ?)'; $like = '%' . $q . '%'; array_push($params, $like, $like, $like, $like, $like, $like); } $sql = 'SELECT id, nombre, rut, tipo_pyme, plan, estado FROM empresas'; if ($where) { $sql .= ' WHERE ' . implode(' AND ', $where); } $sql .= ' ORDER BY ' . $sortMap[$sort] . ' ' . $dir . ' LIMIT 500'; $st = $pdo->prepare($sql); $st->execute($params); $rows = $st->fetchAll(PDO::FETCH_ASSOC); foreach ($rows as &$r) { /* * La plataforma Master no usa suscripción comercial. */ if (strtoupper((string)$r['plan']) === 'MASTER') { $r['saas_plan'] = 'MASTER'; $r['saas_status'] = 'MASTER'; $r['saas_label'] = 'MASTER'; $r['saas_vigencia'] = '—'; $r['saas_badge'] = 'good'; continue; } try { $access = saas_access_status( $pdo, (int)$r['id'] ); $r['saas_status'] = (string)($access['status'] ?? 'NO_COMMERCIAL_ACCESS'); $r['saas_label'] = master_saas_label($r['saas_status']); $r['saas_badge'] = master_saas_badge($r['saas_status']); $r['saas_plan'] = (string)($access['plan'] ?? $r['plan'] ?? '—'); $r['saas_vigencia'] = master_saas_vigencia($access); } catch (Throwable $e) { $r['saas_status'] = 'ERROR'; $r['saas_label'] = 'ERROR'; $r['saas_badge'] = 'bad'; $r['saas_plan'] = $r['plan'] ?? '—'; $r['saas_vigencia'] = '—'; } } unset($r); if (($_GET['export'] ?? '') === 'excel') { $exportColumns = [ 'nombre' => 'Empresa', 'rut' => 'RUT', 'tipo_pyme' => 'Tipo', 'saas_plan' => 'Plan SaaS', 'estado' => 'Estado empresa', 'saas_label' => 'Estado SaaS', 'saas_vigencia' => 'Vigencia' ]; pyme_export_excel( 'Empresas', $exportColumns, $rows ); } render_header_v3('Empresas master', 'Administración global de empresas'); ?>

Limpiar

Empresas registradas

Exportar Excel
Limpiar
Empresa RUT Tipo Plan SaaS Estado empresa Estado SaaS Vigencia Acciones
Sin registros.
ACTIVA INACTIVA Editar Desactivar