Files
PersonalAccounter/control
2025-07-20 14:15:30 +03:00

1230 lines
43 KiB
PHP
Executable File

#!/usr/bin/env php
<?php
require_once __DIR__ . '/vendor/autoload.php';
require_once __DIR__ . '/database/Migration.php';
use Medoo\Medoo;
class Control
{
private $database;
private $config;
public function __construct()
{
// Load environment variables
if (file_exists(__DIR__ . '/.env')) {
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->load();
}
// Load configuration
$this->config = require __DIR__ . '/config/app.php';
// Initialize database connection
$this->database = new Medoo($this->config['database']);
// Ensure migrations table exists
$this->ensureMigrationsTable();
}
public function run($args)
{
if (count($args) < 2) {
$this->showHelp();
return;
}
$command = $args[1];
$subCommand = $args[2] ?? null;
switch ($command) {
case 'migrate':
$this->handleMigrate($subCommand, array_slice($args, 3));
break;
case 'make':
$this->handleMake($subCommand, array_slice($args, 3));
break;
case 'user':
$this->handleUser($subCommand, array_slice($args, 3));
break;
case 'db':
$this->handleDatabase($subCommand, array_slice($args, 3));
break;
case 'faker':
$this->handleFaker($subCommand, array_slice($args, 3));
break;
case 'cache':
$this->handleCache($subCommand);
break;
case 'serve':
$this->handleServe($subCommand);
break;
case 'schedule':
$this->handleSchedule($subCommand, array_slice($args, 3));
break;
case 'help':
case '--help':
case '-h':
$this->showHelp();
break;
default:
$this->error("Unknown command: {$command}");
$this->showHelp();
}
}
private function ensureMigrationsTable()
{
try {
$this->database->query("
CREATE TABLE IF NOT EXISTS `migrations` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`migration` varchar(255) NOT NULL,
`batch` int(11) NOT NULL,
`executed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
");
} catch (Exception $e) {
$this->error("Failed to create migrations table: " . $e->getMessage());
}
}
private function handleMigrate($subCommand, $args)
{
switch ($subCommand) {
case 'run':
case null:
$this->runMigrations();
break;
case 'fresh':
$this->freshMigrations();
break;
case 'rollback':
$this->rollbackMigrations($args);
break;
case 'status':
$this->migrationStatus();
break;
case 'reset':
$this->resetMigrations();
break;
case 'docker':
$this->runDockerMigrations();
break;
default:
$this->error("Unknown migrate command: {$subCommand}");
$this->info("Available migrate commands: run, fresh, rollback, status, reset, docker");
}
}
private function handleMake($subCommand, $args)
{
switch ($subCommand) {
case 'migration':
$this->makeMigration($args);
break;
default:
$this->error("Unknown make command: {$subCommand}");
$this->info("Available make commands: migration");
}
}
private function handleUser($subCommand, $args)
{
switch ($subCommand) {
case 'create':
$this->createUser($args);
break;
case 'list':
$this->listUsers();
break;
case 'delete':
$this->deleteUser($args);
break;
case 'admin':
$this->createAdminUser();
break;
default:
$this->error("Unknown user command: {$subCommand}");
$this->info("Available user commands: create, list, delete, admin");
}
}
private function handleDatabase($subCommand, $args)
{
switch ($subCommand) {
case 'seed':
$this->seedDatabase();
break;
case 'reset':
$this->resetDatabase();
break;
case 'status':
$this->databaseStatus();
break;
default:
$this->error("Unknown database command: {$subCommand}");
$this->info("Available database commands: seed, reset, status");
}
}
private function handleFaker($subCommand, $args)
{
// Load the faker class
require_once __DIR__ . '/database/faker.php';
$faker = new DatabaseFaker($this->database);
switch ($subCommand) {
case 'generate':
$this->generateFakeData($faker, $args);
break;
case 'reset':
$faker->resetDatabase();
$this->success("Fake data cleared successfully!");
break;
case 'summary':
$faker->printSummary();
break;
case 'users':
$count = isset($args[0]) ? (int)$args[0] : 10;
$faker->generateUsers($count);
break;
case 'cards':
$count = isset($args[0]) ? (int)$args[0] : 20;
$faker->generateCreditCards($count);
break;
case 'subscriptions':
$count = isset($args[0]) ? (int)$args[0] : 50;
$faker->generateSubscriptions($count);
break;
case 'categories':
$count = isset($args[0]) ? (int)$args[0] : 10;
$faker->generateCategories($count);
break;
case 'tags':
$count = isset($args[0]) ? (int)$args[0] : 10;
$faker->generateTags($count);
break;
case 'bank-accounts':
$count = isset($args[0]) ? (int)$args[0] : 12;
$faker->generateBankAccounts($count);
break;
case 'crypto-wallets':
$count = isset($args[0]) ? (int)$args[0] : 20;
$faker->generateCryptoWallets($count);
break;
case 'expenses':
$count = isset($args[0]) ? (int)$args[0] : 50;
$faker->generateExpenses($count);
break;
case 'all':
$this->generateAllFakeData($faker, $args);
break;
default:
$this->error("Unknown faker command: {$subCommand}");
$this->info("Available faker commands: generate, reset, summary, users, cards, subscriptions, categories, tags, bank-accounts, crypto-wallets, expenses, all");
}
}
private function generateFakeData($faker, $args)
{
// Parse arguments for custom counts
$users = $this->getArgValue($args, '--users', 10);
$cards = $this->getArgValue($args, '--cards', 20);
$subscriptions = $this->getArgValue($args, '--subscriptions', 50);
$categories = $this->getArgValue($args, '--categories', 10);
$tags = $this->getArgValue($args, '--tags', 10);
$bankAccounts = $this->getArgValue($args, '--bank-accounts', 12);
$cryptoWallets = $this->getArgValue($args, '--crypto-wallets', 20);
$expenses = $this->getArgValue($args, '--expenses', 50);
$this->info("Generating fake data with custom counts...");
$this->info("Users: {$users}, Cards: {$cards}, Subscriptions: {$subscriptions}");
$this->info("Categories: {$categories}, Tags: {$tags}, Bank Accounts: {$bankAccounts}");
$this->info("Crypto Wallets: {$cryptoWallets}, Expenses: {$expenses}");
$faker->generateAll($users, $cards, $subscriptions, $categories, $tags, $bankAccounts, $cryptoWallets, $expenses);
}
private function generateAllFakeData($faker, $args)
{
$this->info("Generating comprehensive fake data with default counts...");
$faker->generateAll();
}
private function getArgValue($args, $key, $default)
{
foreach ($args as $arg) {
if (strpos($arg, $key . '=') === 0) {
return (int)substr($arg, strlen($key) + 1);
}
}
return $default;
}
private function handleCache($subCommand)
{
switch ($subCommand) {
case 'clear':
$this->clearCache();
break;
default:
$this->error("Unknown cache command: {$subCommand}");
$this->info("Available cache commands: clear");
}
}
private function handleServe($port = null)
{
$port = $port ?? '8000';
$this->info("Starting development server on http://localhost:{$port}");
$this->info("Press Ctrl+C to stop the server");
$command = "php -S localhost:{$port} -t public";
passthru($command);
}
private function handleSchedule($subCommand, $args)
{
require_once __DIR__ . '/app/Services/ScheduleService.php';
require_once __DIR__ . '/app/Services/CronScheduler.php';
$scheduleService = new ScheduleService($this->database);
$cronScheduler = new CronScheduler($this->database);
switch ($subCommand) {
case 'run':
$this->runScheduledPayments($scheduleService);
break;
case 'cron':
$this->runConsolidatedCron($cronScheduler);
break;
case 'upcoming':
$this->showUpcomingPayments($scheduleService, $args);
break;
case 'expired':
$this->handleExpiredSubscriptions($scheduleService);
break;
case 'stats':
$this->showScheduleStats($scheduleService);
break;
case 'status':
$this->showCronStatus($cronScheduler);
break;
default:
$this->error("Unknown schedule command: {$subCommand}");
$this->info("Available schedule commands: run, cron, upcoming, expired, stats, status");
$this->info("Note: 'retry' command has been removed as it's no longer needed.");
}
}
private function getMigrationFiles()
{
$migrationPath = __DIR__ . '/database/migrations';
$files = glob($migrationPath . '/*.php');
sort($files);
return $files;
}
private function getExecutedMigrations()
{
try {
return $this->database->select('migrations', 'migration');
} catch (Exception $e) {
return [];
}
}
private function runMigrations()
{
$this->info("Running database migrations...");
$migrationFiles = $this->getMigrationFiles();
$executedMigrations = $this->getExecutedMigrations();
if (empty($migrationFiles)) {
$this->warning("No migration files found.");
return;
}
$batch = $this->getNextBatchNumber();
$executed = 0;
foreach ($migrationFiles as $file) {
$migrationName = basename($file, '.php');
if (in_array($migrationName, $executedMigrations)) {
continue; // Skip already executed migrations
}
try {
require_once $file;
// Get class name from file name
$className = $this->getClassNameFromFile($migrationName);
if (!class_exists($className)) {
$this->error("Migration class {$className} not found in {$file}");
continue;
}
$migration = new $className($this->database);
$this->info("Migrating: {$migrationName}");
$migration->up();
// Record migration
$this->database->insert('migrations', [
'migration' => $migrationName,
'batch' => $batch
]);
$this->success("Migrated: {$migrationName}");
$executed++;
} catch (Exception $e) {
$this->error("Migration failed for {$migrationName}: " . $e->getMessage());
break;
}
}
if ($executed > 0) {
$this->success("Executed {$executed} migrations successfully!");
// Create admin user after migrations
$this->createAdminUser();
} else {
$this->info("Nothing to migrate.");
}
}
private function runDockerMigrations()
{
$this->info("Running database migrations for Docker environment...");
$migrationFiles = $this->getMigrationFiles();
$executedMigrations = $this->getExecutedMigrations();
if (empty($migrationFiles)) {
$this->warning("No migration files found.");
return;
}
$batch = $this->getNextBatchNumber();
$executed = 0;
foreach ($migrationFiles as $file) {
$migrationName = basename($file, '.php');
if (in_array($migrationName, $executedMigrations)) {
continue; // Skip already executed migrations
}
try {
require_once $file;
// Get class name from file name
$className = $this->getClassNameFromFile($migrationName);
if (!class_exists($className)) {
$this->error("Migration class {$className} not found in {$file}");
continue;
}
$migration = new $className($this->database);
$this->info("Migrating: {$migrationName}");
$migration->up();
// Record migration
$this->database->insert('migrations', [
'migration' => $migrationName,
'batch' => $batch
]);
$this->success("Migrated: {$migrationName}");
$executed++;
} catch (Exception $e) {
$this->error("Migration failed for {$migrationName}: " . $e->getMessage());
break;
}
}
if ($executed > 0) {
$this->success("Executed {$executed} migrations successfully!");
// Create admin user from environment variables for Docker
$this->createDockerAdminUser();
} else {
$this->info("Nothing to migrate.");
// Still try to create admin user if it doesn't exist
$this->createDockerAdminUser();
}
}
private function freshMigrations()
{
$this->info("Running fresh migrations (dropping all tables)...");
try {
// Get all migration files in reverse order for rollback
$migrationFiles = array_reverse($this->getMigrationFiles());
foreach ($migrationFiles as $file) {
$migrationName = basename($file, '.php');
try {
require_once $file;
$className = $this->getClassNameFromFile($migrationName);
if (class_exists($className)) {
$migration = new $className($this->database);
$this->info("Rolling back: {$migrationName}");
$migration->down();
}
} catch (Exception $e) {
// Continue with other migrations even if one fails
$this->warning("Warning - Failed to rollback {$migrationName}: " . $e->getMessage());
}
}
// Clear migrations table safely
try {
$this->database->delete('migrations', ['id[>]' => 0]);
} catch (Exception $e) {
$this->warning("Could not clear migrations table: " . $e->getMessage());
}
$this->success("All tables dropped successfully!");
// Run migrations again
$this->runMigrations();
} catch (Exception $e) {
$this->error("Fresh migration failed: " . $e->getMessage());
}
}
private function rollbackMigrations($args)
{
$steps = isset($args[0]) ? (int)$args[0] : 1;
$this->info("Rolling back {$steps} migration batch(es)...");
try {
// Get the last batch(es) to rollback
$batches = $this->database->select('migrations', 'batch', [
'ORDER' => ['batch' => 'DESC'],
'GROUP' => 'batch',
'LIMIT' => $steps
]);
if (empty($batches)) {
$this->info("Nothing to rollback.");
return;
}
foreach ($batches as $batch) {
$migrations = $this->database->select('migrations', '*', [
'batch' => $batch,
'ORDER' => ['id' => 'DESC']
]);
foreach ($migrations as $migrationRecord) {
$migrationName = $migrationRecord['migration'];
$file = __DIR__ . '/database/migrations/' . $migrationName . '.php';
if (file_exists($file)) {
require_once $file;
$className = $this->getClassNameFromFile($migrationName);
if (class_exists($className)) {
$migration = new $className($this->database);
$this->info("Rolling back: {$migrationName}");
$migration->down();
// Remove from migrations table
$this->database->delete('migrations', ['id' => $migrationRecord['id']]);
$this->success("Rolled back: {$migrationName}");
}
}
}
}
} catch (Exception $e) {
$this->error("Rollback failed: " . $e->getMessage());
}
}
private function migrationStatus()
{
$migrationFiles = $this->getMigrationFiles();
$executedMigrations = $this->getExecutedMigrations();
$this->info("Migration Status:");
$this->info(str_repeat("-", 60));
$this->info(sprintf("%-40s %s", "Migration", "Status"));
$this->info(str_repeat("-", 60));
foreach ($migrationFiles as $file) {
$migrationName = basename($file, '.php');
$status = in_array($migrationName, $executedMigrations) ? "✓ Executed" : "✗ Pending";
$color = in_array($migrationName, $executedMigrations) ? "\033[32m" : "\033[31m";
echo sprintf("%-40s %s%s\033[0m\n", $migrationName, $color, $status);
}
}
private function resetMigrations()
{
$this->info("Resetting all migrations...");
$this->freshMigrations();
$this->seedDatabase();
}
private function makeMigration($args)
{
if (empty($args[0])) {
$this->error("Migration name is required.");
$this->info("Usage: php control.php make migration <name>");
return;
}
$name = $args[0];
$timestamp = date('Y_m_d_His');
$fileName = sprintf("%s_%s.php", $timestamp, $name);
$className = $this->getClassNameFromFile($timestamp . '_' . $name);
$template = $this->getMigrationTemplate($className, $name);
$filePath = __DIR__ . '/database/migrations/' . $fileName;
if (file_put_contents($filePath, $template)) {
$this->success("Migration created: {$fileName}");
} else {
$this->error("Failed to create migration file.");
}
}
private function getMigrationTemplate($className, $name)
{
return "<?php
require_once __DIR__ . '/../Migration.php';
class {$className} extends Migration
{
public function getName()
{
return '" . strtolower($name) . "';
}
public function up()
{
// Add your migration logic here
// Example:
// \$this->createTable('table_name', function(\$table) {
// \$table->id();
// \$table->string('name');
// \$table->timestamps();
// });
}
public function down()
{
// Add your rollback logic here
// Example:
// \$this->dropTable('table_name');
}
}";
}
private function getClassNameFromFile($fileName)
{
// Convert snake_case to PascalCase
$parts = explode('_', $fileName);
// Remove numeric prefix (like 001, 002, etc.)
if (count($parts) > 0 && is_numeric($parts[0])) {
$parts = array_slice($parts, 1);
}
return implode('', array_map('ucfirst', $parts));
}
private function getNextBatchNumber()
{
try {
$result = $this->database->max('migrations', 'batch');
return (int)($result ?? 0) + 1;
} catch (Exception $e) {
return 1;
}
}
private function createUser($args)
{
if (count($args) < 3) {
$this->error("Usage: php control.php user create <name> <email> <password> [role]");
return;
}
$name = $args[0];
$email = $args[1];
$password = $args[2];
$role = $args[3] ?? 'admin';
if (!in_array($role, ['admin', 'superadmin'])) {
$this->error("Invalid role. Must be 'admin' or 'superadmin'");
return;
}
try {
// Check if user already exists
$existingUser = $this->database->get('users', '*', ['email' => $email]);
if ($existingUser) {
$this->error("User with email '{$email}' already exists.");
return;
}
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$this->database->insert('users', [
'name' => $name,
'email' => $email,
'password' => $hashedPassword,
'role' => $role
]);
$this->success("User '{$name}' created successfully with role '{$role}'!");
} catch (Exception $e) {
$this->error("Failed to create user: " . $e->getMessage());
}
}
private function createAdminUser()
{
try {
// Check if any admin user already exists
$existingAdmin = $this->database->get('users', '*', ['role' => 'superadmin']);
if ($existingAdmin) {
$this->info("Admin user already exists: " . $existingAdmin['email']);
return;
}
// Interactive admin user creation
$this->info("No admin user found. Let's create one:");
$name = $this->promptInput("Enter admin name", "Admin");
$email = $this->promptInput("Enter admin email");
// Validate email format
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
$this->error("Invalid email format.");
return;
}
// Check if user with this email already exists
$existingUser = $this->database->get('users', '*', ['email' => $email]);
if ($existingUser) {
$this->error("User with email '{$email}' already exists.");
return;
}
$password = $this->promptPassword("Enter admin password");
if (strlen($password) < 8) {
$this->error("Password must be at least 8 characters long.");
return;
}
$hashedPassword = password_hash($password, PASSWORD_DEFAULT);
$this->database->insert('users', [
'name' => $name,
'email' => $email,
'password' => $hashedPassword,
'role' => 'superadmin'
]);
$this->success("Admin user created successfully!");
$this->info("Email: {$email}");
} catch (Exception $e) {
$this->error("Failed to create admin user: " . $e->getMessage());
}
}
private function createDockerAdminUser()
{
try {
// Check if any admin user already exists
$existingAdmin = $this->database->get('users', '*', ['role' => 'superadmin']);
if ($existingAdmin) {
$this->info("Admin user already exists: " . $existingAdmin['email']);
return;
}
// Get admin credentials from environment variables
$adminEmail = $_ENV['ADMIN_EMAIL'] ?? null;
$adminPassword = $_ENV['ADMIN_PASSWORD'] ?? null;
$adminName = $_ENV['ADMIN_NAME'] ?? 'Admin';
if (!$adminEmail || !$adminPassword) {
$this->error("ADMIN_EMAIL and ADMIN_PASSWORD environment variables are required for Docker setup.");
$this->info("Please set these in your .env file:");
$this->info("ADMIN_EMAIL=admin@example.com");
$this->info("ADMIN_PASSWORD=your_secure_password");
return;
}
// Validate email format
if (!filter_var($adminEmail, FILTER_VALIDATE_EMAIL)) {
$this->error("Invalid email format in ADMIN_EMAIL environment variable: {$adminEmail}");
return;
}
// Check if user with this email already exists
$existingUser = $this->database->get('users', '*', ['email' => $adminEmail]);
if ($existingUser) {
$this->info("User with email '{$adminEmail}' already exists.");
return;
}
// Validate password length
if (strlen($adminPassword) < 8) {
$this->error("ADMIN_PASSWORD must be at least 8 characters long.");
return;
}
// Create admin user from environment variables
$this->info("Creating admin user from environment variables...");
$hashedPassword = password_hash($adminPassword, PASSWORD_DEFAULT);
$this->database->insert('users', [
'name' => $adminName,
'email' => $adminEmail,
'password' => $hashedPassword,
'role' => 'superadmin',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s')
]);
$this->success("Admin user created successfully!");
$this->info("Email: {$adminEmail}");
$this->info("Role: superadmin");
} catch (Exception $e) {
$this->error("Failed to create admin user: " . $e->getMessage());
}
}
private function listUsers()
{
try {
$users = $this->database->select('users', ['id', 'name', 'email', 'role', 'created_at']);
if (empty($users)) {
$this->info("No users found.");
return;
}
$this->info("Users List:");
$this->info(str_repeat("-", 80));
$this->info(sprintf("%-5s %-20s %-30s %-12s %-20s", "ID", "Name", "Email", "Role", "Created At"));
$this->info(str_repeat("-", 80));
foreach ($users as $user) {
$this->info(sprintf(
"%-5s %-20s %-30s %-12s %-20s",
$user['id'],
substr($user['name'], 0, 20),
substr($user['email'], 0, 30),
$user['role'],
$user['created_at']
));
}
} catch (Exception $e) {
$this->error("Failed to list users: " . $e->getMessage());
}
}
private function deleteUser($args)
{
if (count($args) < 1) {
$this->error("Usage: php control.php user delete <email>");
return;
}
$email = $args[0];
try {
$user = $this->database->get('users', '*', ['email' => $email]);
if (!$user) {
$this->error("User with email '{$email}' not found.");
return;
}
$this->database->delete('users', ['email' => $email]);
$this->success("User '{$email}' deleted successfully!");
} catch (Exception $e) {
$this->error("Failed to delete user: " . $e->getMessage());
}
}
private function seedDatabase()
{
$this->info("Seeding database with sample data...");
try {
// Create sample users
$users = [
['name' => 'John Doe', 'email' => 'john@example.com', 'role' => 'admin'],
['name' => 'Jane Smith', 'email' => 'jane@example.com', 'role' => 'admin']
];
foreach ($users as $userData) {
$existingUser = $this->database->get('users', '*', ['email' => $userData['email']]);
if (!$existingUser) {
$this->database->insert('users', [
'name' => $userData['name'],
'email' => $userData['email'],
'password' => password_hash('password123', PASSWORD_DEFAULT),
'role' => $userData['role']
]);
}
}
$this->success("Database seeded successfully!");
} catch (Exception $e) {
$this->error("Failed to seed database: " . $e->getMessage());
}
}
private function resetDatabase()
{
$this->info("Resetting database...");
$this->freshMigrations();
$this->seedDatabase();
}
private function databaseStatus()
{
try {
$tables = ['users', 'credit_cards', 'subscriptions', 'transactions'];
$this->info("Database Status:");
$this->info(str_repeat("-", 40));
foreach ($tables as $table) {
try {
$count = $this->database->count($table);
$this->info(sprintf("%-20s: %d records", ucfirst($table), $count));
} catch (Exception $e) {
$this->info(sprintf("%-20s: Table not found", ucfirst($table)));
}
}
} catch (Exception $e) {
$this->error("Failed to get database status: " . $e->getMessage());
}
}
private function clearCache()
{
$this->info("Clearing application cache...");
// Clear session files
$sessionPath = __DIR__ . '/sessions';
if (is_dir($sessionPath)) {
$files = glob($sessionPath . '/*');
foreach ($files as $file) {
if (is_file($file)) {
unlink($file);
}
}
}
// Clear log files (optional)
$logPath = __DIR__ . '/logs';
if (is_dir($logPath)) {
$files = glob($logPath . '/*.log');
foreach ($files as $file) {
if (is_file($file)) {
file_put_contents($file, '');
}
}
}
$this->success("Cache cleared successfully!");
}
private function runScheduledPayments($scheduleService)
{
$this->info("Running scheduled payments...");
try {
$result = $scheduleService->processDuePayments();
$this->info("Payment processing completed:");
$this->info("- Total due: " . $result['total_due']);
$this->info("- Processed: " . $result['processed']);
$this->info("- Failed: " . $result['failed']);
if ($result['processed'] > 0) {
$this->success("Successfully processed {$result['processed']} payments!");
}
if ($result['failed'] > 0) {
$this->warning("{$result['failed']} payments failed.");
}
if (isset($result['error'])) {
$this->error("Error: " . $result['error']);
}
} catch (Exception $e) {
$this->error("Failed to run scheduled payments: " . $e->getMessage());
}
}
private function showUpcomingPayments($scheduleService, $args)
{
$days = isset($args[0]) ? (int)$args[0] : 7;
$this->info("Showing upcoming payments for next {$days} days...");
try {
$result = $scheduleService->processUpcomingPayments($days);
$this->info("Upcoming payments: " . $result['upcoming_count']);
$this->info("Date range: " . $result['date_range']['from'] . " to " . $result['date_range']['to']);
if (!empty($result['subscriptions'])) {
$this->info(str_repeat("-", 80));
$this->info(sprintf("%-5s %-30s %-15s %-15s %-15s", "ID", "Name", "Amount", "Next Payment", "Billing Cycle"));
$this->info(str_repeat("-", 80));
foreach ($result['subscriptions'] as $subscription) {
$this->info(sprintf(
"%-5s %-30s %-15s %-15s %-15s",
$subscription['id'],
substr($subscription['name'], 0, 30),
$subscription['amount'] . ' ' . $subscription['currency'],
$subscription['next_payment_date'],
$subscription['billing_cycle']
));
}
}
} catch (Exception $e) {
$this->error("Failed to get upcoming payments: " . $e->getMessage());
}
}
private function handleExpiredSubscriptions($scheduleService)
{
$this->info("Handling expired subscriptions...");
try {
$result = $scheduleService->handleExpiredSubscriptions();
$this->info("Expired subscriptions handled: " . $result['expired_count']);
if ($result['expired_count'] > 0) {
$this->success("Marked {$result['expired_count']} subscriptions as expired.");
} else {
$this->info("No subscriptions to expire.");
}
if (isset($result['error'])) {
$this->error("Error: " . $result['error']);
}
} catch (Exception $e) {
$this->error("Failed to handle expired subscriptions: " . $e->getMessage());
}
}
private function runConsolidatedCron($cronScheduler)
{
$this->info("Running consolidated cron scheduler...");
try {
$cronScheduler->run();
$this->success("Cron scheduler completed successfully!");
} catch (Exception $e) {
$this->error("Failed to run cron scheduler: " . $e->getMessage());
}
}
private function showCronStatus($cronScheduler)
{
$this->info("Cron Scheduler Status:");
$this->info(str_repeat("-", 60));
try {
$upcomingTasks = $cronScheduler->getUpcomingTasks();
foreach ($upcomingTasks as $task) {
$this->info(sprintf("%-35s: %s (%s)",
$task['name'],
$task['next_run'],
$task['frequency']
));
}
$this->info(str_repeat("-", 60));
$this->info("Current time: " . date('Y-m-d H:i:s'));
} catch (Exception $e) {
$this->error("Failed to get cron status: " . $e->getMessage());
}
}
private function showScheduleStats($scheduleService)
{
$this->info("Schedule Statistics:");
try {
$stats = $scheduleService->getScheduleStats();
$this->info(str_repeat("-", 40));
$this->info(sprintf("%-25s: %d", "Due today", $stats['due_today']));
$this->info(sprintf("%-25s: %d", "Due this week", $stats['due_this_week']));
$this->info(sprintf("%-25s: %d", "Due this month", $stats['due_this_month']));
$this->info(sprintf("%-25s: %d", "Overdue", $stats['overdue']));
$this->info(sprintf("%-25s: %d", "Active recurring", $stats['active_recurring']));
$this->info(str_repeat("-", 40));
} catch (Exception $e) {
$this->error("Failed to get schedule stats: " . $e->getMessage());
}
}
private function showHelp()
{
$this->info("Accounting Panel Console Tool");
$this->info("Usage: php control.php <command> [options]");
$this->info("");
$this->info("Available commands:");
$this->info("");
$this->info("Migration commands:");
$this->info(" migrate run Run pending migrations");
$this->info(" migrate fresh Drop all tables and run migrations");
$this->info(" migrate rollback [steps] Rollback migrations (default: 1 batch)");
$this->info(" migrate status Show migration status");
$this->info(" migrate reset Reset database (fresh + seed)");
$this->info(" migrate docker Run migrations for Docker environment");
$this->info("");
$this->info("Make commands:");
$this->info(" make migration <name> Create a new migration file");
$this->info("");
$this->info("User management:");
$this->info(" user create <name> <email> <password> [role] Create a new user");
$this->info(" user list List all users");
$this->info(" user delete <email> Delete a user");
$this->info(" user admin Create admin user from config");
$this->info("");
$this->info("Database commands:");
$this->info(" db seed Seed database with sample data");
$this->info(" db reset Reset database (fresh + seed)");
$this->info(" db status Show database status");
$this->info("");
$this->info("Faker commands:");
$this->info(" faker all Generate all fake data with default counts");
$this->info(" faker generate [options] Generate fake data with custom counts");
$this->info(" Options: --users=N --cards=N --subscriptions=N --categories=N");
$this->info(" --tags=N --bank-accounts=N --crypto-wallets=N --expenses=N");
$this->info(" faker users [count] Generate fake users");
$this->info(" faker cards [count] Generate fake credit cards");
$this->info(" faker subscriptions [count] Generate fake subscriptions");
$this->info(" faker categories [count] Generate fake categories");
$this->info(" faker tags [count] Generate fake tags");
$this->info(" faker bank-accounts [count] Generate fake bank accounts");
$this->info(" faker crypto-wallets [count] Generate fake crypto wallets");
$this->info(" faker expenses [count] Generate fake expenses");
$this->info(" faker reset Clear all fake data");
$this->info(" faker summary Show current data summary");
$this->info("");
$this->info("Schedule commands:");
$this->info(" schedule cron Run consolidated cron scheduler (every minute)");
$this->info(" schedule run Process all due payments (legacy command)");
$this->info(" schedule upcoming [days] Show upcoming payments (default: 7 days)");
$this->info(" schedule expired Handle expired subscriptions (legacy command)");
$this->info(" schedule stats Show schedule statistics");
$this->info(" schedule status Show cron scheduler status and upcoming tasks");
$this->info("");
$this->info("Other commands:");
$this->info(" cache clear Clear application cache");
$this->info(" serve [port] Start development server (default: 8000)");
$this->info(" help Show this help message");
}
private function success($message)
{
echo "\033[32m✓ {$message}\033[0m\n";
}
private function error($message)
{
echo "\033[31m✗ {$message}\033[0m\n";
}
private function warning($message)
{
echo "\033[33m⚠ {$message}\033[0m\n";
}
private function info($message)
{
echo "{$message}\n";
}
private function promptInput($prompt, $default = null)
{
$defaultText = $default ? " [{$default}]" : "";
echo "{$prompt}{$defaultText}: ";
$input = trim(fgets(STDIN));
if (empty($input) && $default !== null) {
return $default;
}
return $input;
}
private function promptPassword($prompt)
{
echo "{$prompt}: ";
// Turn off echoing for password input
system('stty -echo');
$password = trim(fgets(STDIN));
system('stty echo');
echo "\n"; // Add newline after password input
return $password;
}
}
// Run the console application
$control = new Control();
$control->run($argv);