Initial commit: Sistema de comisiones y gastos personales

This commit is contained in:
2026-04-19 09:59:57 -06:00
commit dc964d6bce
103 changed files with 15859 additions and 0 deletions

View File

@@ -0,0 +1,211 @@
<?php
namespace App\Services;
use App\Models\DailySale;
use App\Models\Expense;
use App\Models\Month;
use App\Models\User;
use Illuminate\Support\Facades\DB;
class CommissionCalculator
{
/**
* Calcular comisión para un usuario en un mes específico
*/
public static function calculateForMonth(User $user, Month $month): array
{
// Verificar que el mes pertenece al usuario
if ($month->user_id !== $user->id) {
throw new \InvalidArgumentException('El mes no pertenece al usuario');
}
// Total de ventas del usuario
$totalUserSales = $month->dailySales()->sum('user_sales');
// Total de ventas del sistema
$totalSystemSales = $month->dailySales()->sum('system_sales');
// Total de gastos
$totalExpenses = $month->expenses()->sum('amount');
// Salario base
$monthlySalary = $user->monthly_salary;
// Porcentaje de comisión
$commissionPercentage = $user->commission_percentage;
// Calcular comisión basada en ventas del sistema (ventas consolidadas)
$commission = ($totalSystemSales * $commissionPercentage) / 100;
// Calcular percepción total (salario + comisión - gastos)
$totalEarning = $monthlySalary + $commission - $totalExpenses;
return [
'user_id' => $user->id,
'month_id' => $month->id,
'month_name' => $month->name . ' ' . $month->year,
'total_user_sales' => round($totalUserSales, 2),
'total_system_sales' => round($totalSystemSales, 2),
'total_expenses' => round($totalExpenses, 2),
'monthly_salary' => round($monthlySalary, 2),
'commission_percentage' => round($commissionPercentage, 2),
'commission_amount' => round($commission, 2),
'total_earning' => round($totalEarning, 2),
'has_difference' => ($totalUserSales !== $totalSystemSales),
'sales_difference' => round($totalUserSales - $totalSystemSales, 2),
];
}
/**
* Calcular quincena (primera o segunda)
*
* QUINCENA 1 (ANTICIPO): mitad salary + comisiones del MES completo
* QUINCENA 2 (LIQUIDACIÓN): mitad salary - gastos de la segunda quincena
*/
public static function calculateBiweekly(User $user, Month $month, int $biweekly): array
{
if ($month->user_id !== $user->id) {
throw new \InvalidArgumentException('El mes no pertenece al usuario');
}
$monthlySalary = $user->monthly_salary;
$biweeklySalary = $monthlySalary / 2; // Mitad del sueldo
$commissionPercentage = $user->commission_percentage;
// Get month number
$monthNumber = self::getMonthNumber($month->name);
$year = $month->year;
$lastDay = self::getLastDayOfMonth($month->name, $year);
if ($biweekly === 1) {
// =====================
// QUINCENA 1 - ANTICIPO
// =====================
// Anticipo = mitad del sueldo + comisiones del MES completo
$totalSystemSales = $month->dailySales()->sum('system_sales');
$commission = ($totalSystemSales * $commissionPercentage) / 100;
// Gastos: q1 completo + mensual/2
$expensesQ1Amount = $month->expenses()
->where(function($q) {
$q->where('expense_type', 'q1')
->orWhere('expense_type', 'mensual');
})
->get()
->sum(function($e) {
return $e->expense_type === 'mensual' ? $e->amount / 2 : $e->amount;
});
$totalEarning = $biweeklySalary + $commission - $expensesQ1Amount;
return [
'user_id' => $user->id,
'month_id' => $month->id,
'month_name' => $month->name . ' ' . $year,
'biweekly' => $biweekly,
'period' => '1ra Quincena (1-15) - ANTICIPO',
'description' => 'Mitad del sueldo + comisiones del mes completo',
'biweekly_salary' => round($biweeklySalary, 2),
'total_system_sales' => round($totalSystemSales, 2),
'commission_percentage' => round($commissionPercentage, 2),
'commission_amount' => round($commission, 2),
'total_expenses_month' => 0,
'expenses_q1' => round($expensesQ1Amount, 2),
'expenses_q2' => 0,
'total_earning' => round($totalEarning, 2),
'type' => 'anticipo',
];
} else {
// =====================
// QUINCENA 2 - LIQUIDACIÓN
// =====================
// Liquidación = mitad del sueldo - gastos de Q2
// Gastos: q2 completo + mensual/2
$expensesQ2Amount = $month->expenses()
->where(function($q) {
$q->where('expense_type', 'q2')
->orWhere('expense_type', 'mensual');
})
->get()
->sum(function($e) {
return $e->expense_type === 'mensual' ? $e->amount / 2 : $e->amount;
});
// Total a pagar en liquidacion
$totalEarning = $biweeklySalary - $expensesQ2Amount;
return [
'user_id' => $user->id,
'month_id' => $month->id,
'month_name' => $month->name . ' ' . $year,
'biweekly' => $biweekly,
'period' => "2da Quincena (16-$lastDay) - LIQUIDACIÓN",
'description' => 'Mitad del sueldo - mitad de gastos del mes',
'biweekly_salary' => round($biweeklySalary, 2),
'total_system_sales' => 0,
'expenses_q2' => round($expensesQ2Amount, 2),
'total_earning' => round($totalEarning, 2),
'type' => 'liquidacion',
];
}
}
/**
* Resumen anual del usuario
*/
public static function calculateYearly(User $user, int $year): array
{
$months = $user->months()->where('year', $year)->get();
$totalUserSales = 0;
$totalSystemSales = 0;
$totalExpenses = 0;
$totalSalary = 0;
$totalCommission = 0;
foreach ($months as $month) {
$totalUserSales += $month->dailySales()->sum('user_sales');
$totalSystemSales += $month->dailySales()->sum('system_sales');
$totalExpenses += $month->expenses()->sum('amount');
$totalSalary += $user->monthly_salary;
$totalCommission += ($month->dailySales()->sum('system_sales') * $user->commission_percentage) / 100;
}
return [
'user_id' => $user->id,
'year' => $year,
'months_count' => $months->count(),
'total_user_sales' => round($totalUserSales, 2),
'total_system_sales' => round($totalSystemSales, 2),
'total_expenses' => round($totalExpenses, 2),
'total_salary' => round($totalSalary, 2),
'total_commission' => round($totalCommission, 2),
'total_earning' => round($totalSalary + $totalCommission - $totalExpenses, 2),
];
}
/**
* Obtener número del mes por nombre
*/
private static function getMonthNumber(string $monthName): int
{
$months = [
'Enero' => 1, 'Febrero' => 2, 'Marzo' => 3, 'Abril' => 4,
'Mayo' => 5, 'Junio' => 6, 'Julio' => 7, 'Agosto' => 8,
'Septiembre' => 9, 'Octubre' => 10, 'Noviembre' => 11, 'Diciembre' => 12
];
return $months[$monthName] ?? 1;
}
/**
* Obtener último día del mes
*/
private static function getLastDayOfMonth(string $monthName, int $year): int
{
$monthNumber = self::getMonthNumber($monthName);
return (int) date('t', mktime(0, 0, 0, $monthNumber, 1, $year));
}
}

View File

@@ -0,0 +1,300 @@
<?php
namespace App\Services;
use App\Models\TelegramAccount;
use App\Models\User;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class TelegramBotService
{
private ?string $botToken;
private ?string $webhookUrl;
public function __construct()
{
$this->botToken = config('services.telegram.bot_token', env('TELEGRAM_BOT_TOKEN'));
$appUrl = config('app.url', env('APP_URL', 'http://nomina-pegaso.casa'));
$this->webhookUrl = rtrim($appUrl, '/') . '/telegram/webhook';
}
/**
* Procesar actualización recibida del webhook
*/
public function handleUpdate(array $update): array
{
if (!isset($update['message'])) {
return ['ok' => false, 'error' => 'No message found'];
}
$message = $update['message'];
$chatId = $message['chat']['id'];
$text = $message['text'] ?? '';
$from = $message['from'] ?? [];
Log::info('Telegram update received', [
'chat_id' => $chatId,
'text' => $text,
'from' => $from
]);
// Verificar si el usuario está verificado
$telegramAccount = TelegramAccount::where('chat_id', $chatId)->first();
if (!$telegramAccount || !$telegramAccount->is_verified) {
return $this->handleUnverifiedUser($chatId, $text);
}
// Procesar comandos del usuario verificado
return $this->handleCommand($telegramAccount->user, $text, $chatId);
}
/**
* Manejar usuario no verificado
*/
private function handleUnverifiedUser(string $chatId, string $text): array
{
// Si es un código de verificación
if (strlen($text) === 6 && is_numeric($text)) {
$telegramAccount = TelegramAccount::where('chat_id', $chatId)
->where('verification_code', $text)
->first();
if ($telegramAccount) {
$telegramAccount->update([
'is_verified' => true,
'verification_code' => null
]);
$user = $telegramAccount->user;
$this->sendMessage($chatId, "¡Verificación exitosa! Tu cuenta de Telegram está vinculada a {$user->name}. Ahora recibirás notificaciones de tus comisiones.");
return ['ok' => true, 'verified' => true];
} else {
$this->sendMessage($chatId, "Código de verificación inválido. Por favor intenta con el código correcto.");
return ['ok' => true, 'verified' => false];
}
}
// Mensaje de bienvenida para usuarios no verificados
$this->sendMessage($chatId, "¡Hola! Para usar este bot necesitas verificar tu cuenta.\n\nPor favor ingresa el código de verificación de 6 dígitos que encontrarás en la sección de Telegram de tu panel de usuario.");
return ['ok' => true, 'verified' => false];
}
/**
* Manejar comandos de usuario verificado
*/
private function handleCommand(User $user, string $text, string $chatId): array
{
$command = strtolower(trim($text));
$commandParts = explode(' ', $command);
$mainCommand = $commandParts[0] ?? '';
switch ($mainCommand) {
case '/start':
$this->sendMessage($chatId, "¡Hola {$user->name}! Usa /help para ver los comandos disponibles.");
break;
case '/help':
$this->sendHelp($chatId);
break;
case '/mes':
$this->showCurrentMonth($user, $chatId);
break;
case '/ventas':
$this->showSales($user, $chatId);
break;
case '/gastos':
$this->showExpenses($user, $chatId);
break;
case '/resumen':
$this->showSummary($user, $chatId);
break;
default:
$this->sendMessage($chatId, "Comando no reconocido. Usa /help para ver los comandos disponibles.");
}
return ['ok' => true];
}
/**
* Enviar mensaje
*/
public function sendMessage(string $chatId, string $text): array
{
if (!$this->botToken) {
Log::warning('Telegram bot token not configured');
return ['ok' => false, 'error' => 'Bot token not configured'];
}
try {
$response = Http::post("https://api.telegram.org/bot{$this->botToken}/sendMessage", [
'chat_id' => $chatId,
'text' => $text,
'parse_mode' => 'Markdown'
]);
return $response->json();
} catch (\Exception $e) {
Log::error('Telegram send message error', ['error' => $e->getMessage()]);
return ['ok' => false, 'error' => $e->getMessage()];
}
}
/**
* Enviar mensaje de ayuda
*/
private function sendHelp(string $chatId): void
{
$text = "📋 *Comandos disponibles:*\n\n" .
"• /start - Iniciar bot\n" .
"• /help - Mostrar ayuda\n" .
"• /mes - Ver mes actual\n" .
"• /ventas - Ver ventas del mes\n" .
"• /gastos - Ver gastos del mes\n" .
"• /resumen - Resumen de comisiones\n";
$this->sendMessage($chatId, $text);
}
/**
* Mostrar mes actual
*/
private function showCurrentMonth(User $user, string $chatId): void
{
$month = $user->getCurrentMonth();
if (!$month) {
$this->sendMessage($chatId, "No tienes ningún mes abierto actualmente.");
return;
}
$statusText = match($month->status) {
'open' => '🟢 Abierto',
'closed' => '🟡 Cerrado',
'paid' => '✅ Pagado',
default => 'Desconocido'
};
$text = "📅 *Mes Actual*\n\n" .
"• *Nombre:* {$month->name} {$month->year}\n" .
"• *Estado:* {$statusText}";
$this->sendMessage($chatId, $text);
}
/**
* Mostrar ventas del mes
*/
private function showSales(User $user, string $chatId): void
{
$month = $user->getCurrentMonth();
if (!$month) {
$this->sendMessage($chatId, "No tienes ningún mes abierto actualmente.");
return;
}
$totalUserSales = $month->dailySales()->sum('user_sales');
$totalSystemSales = $month->dailySales()->sum('system_sales');
$diff = $totalUserSales - $totalSystemSales;
$text = "💰 *Ventas del Mes*\n\n" .
"• *Usuario:* $" . number_format($totalUserSales, 2) . "\n" .
"• *Sistema:* $" . number_format($totalSystemSales, 2) . "\n" .
"• *Diferencia:* $" . number_format($diff, 2);
$this->sendMessage($chatId, $text);
}
/**
* Mostrar gastos del mes
*/
private function showExpenses(User $user, string $chatId): void
{
$month = $user->getCurrentMonth();
if (!$month) {
$this->sendMessage($chatId, "No tienes ningún mes abierto actualmente.");
return;
}
$totalExpenses = $month->expenses()->sum('amount');
$expensesList = $month->expenses()->latest()->limit(5)->get();
$text = "📝 *Gastos del Mes*\n\n" .
"• *Total:* $" . number_format($totalExpenses, 2) . "\n\n";
if ($expensesList->count() > 0) {
$text .= "Últimos gastos:\n";
foreach ($expensesList as $expense) {
$text .= "{$expense->description}: \$" . number_format($expense->amount, 2) . "\n";
}
}
$this->sendMessage($chatId, $text);
}
/**
* Mostrar resumen de comisiones
*/
private function showSummary(User $user, string $chatId): void
{
$month = $user->getCurrentMonth();
if (!$month) {
$this->sendMessage($chatId, "No tienes ningún mes abierto actualmente.");
return;
}
$data = CommissionCalculator::calculateForMonth($user, $month);
$text = "💵 *Resumen de Comisiones*\n\n" .
"• *Mes:* {$data['month_name']}\n" .
"• *Ventas Sistema:* \$" . number_format($data['total_system_sales'], 2) . "\n" .
"• *Comisión ({$data['commission_percentage']}%):* \$" . number_format($data['commission_amount'], 2) . "\n" .
"• *Salario:* \$" . number_format($data['monthly_salary'], 2) . "\n" .
"• *Gastos:* \$" . number_format($data['total_expenses'], 2) . "\n" .
"• *Total a Recibir:* \$" . number_format($data['total_earning'], 2);
$this->sendMessage($chatId, $text);
}
/**
* Generar código de verificación
*/
public static function generateVerificationCode(): string
{
return str_pad((string) random_int(0, 999999), 6, '0', STR_PAD_LEFT);
}
/**
* Configurar webhook
*/
public function setWebhook(): bool
{
if (!$this->botToken || !$this->webhookUrl) {
Log::warning('Cannot set webhook: missing configuration');
return false;
}
try {
$response = Http::post("https://api.telegram.org/bot{$this->botToken}/setWebhook", [
'url' => $this->webhookUrl
]);
return $response->json('ok', false);
} catch (\Exception $e) {
Log::error('Telegram set webhook error', ['error' => $e->getMessage()]);
return false;
}
}
}