route('agents.dashboard'); } return view('agents.login'); } public function login(Request $request): RedirectResponse { $credentials = $request->validate([ 'email' => ['required', 'email'], 'password' => ['required', 'string', 'min:8'], ]); try { $data = $this->cloud->login($credentials['email'], $credentials['password']); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withInput($request->only('email'))->withErrors(['email' => $exception->getMessage()]); } } public function quickLogin(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login'); } public function quickLoginEliezer(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login_eliezer'); } private function performQuickLogin(Request $request, string $configKey): RedirectResponse { abort_unless(app()->environment('local') && config("comersoft.{$configKey}.enabled"), 404); $email = (string) config("comersoft.{$configKey}.email"); $password = (string) config("comersoft.{$configKey}.password"); if ($email === '' || $password === '') { return back()->withErrors(['email' => 'El inicio rápido no está configurado.']); } try { $data = $this->cloud->login($email, $password); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withErrors(['email' => $exception->getMessage()]); } } public function logout(Request $request): RedirectResponse { $request->session()->forget(['agent.access_token', 'agent.user']); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect()->route('agents.login'); } public function dashboard(): View|RedirectResponse { try { $user = session('agent.user', []); $data = $this->cloud->dashboard($user['resellerId'] ?? null); $allLicenses = $data['licenses'] ?? []; $allSales = $data['sales'] ?? []; $allTenants = $data['tenants'] ?? []; $salesByLicense = collect($allSales) ->filter(fn (array $sale) => data_get($sale, 'licenseId') || data_get($sale, 'license.id')) ->keyBy(fn (array $sale) => data_get($sale, 'licenseId') ?: data_get($sale, 'license.id')); $licensesByBusiness = collect($allLicenses) ->filter(fn (array $license) => data_get($license, 'businessId') || data_get($license, 'business.email')) ->groupBy(fn (array $license) => data_get($license, 'businessId') ?: strtolower((string) data_get($license, 'business.email'))); $managedCommerces = collect($allTenants) ->map(function (array $tenant) use ($salesByLicense, $licensesByBusiness) { $tenantLicenses = collect($tenant['licenses'] ?? []); $license = $tenantLicenses->sortByDesc('issuedAt')->first(); if (! $license) { $license = $licensesByBusiness ->get($tenant['id'] ?? null, collect()) ->sortByDesc('issuedAt') ->first(); } if (! $license && ! empty($tenant['email'])) { $license = $licensesByBusiness ->get(strtolower((string) $tenant['email']), collect()) ->sortByDesc('issuedAt') ->first(); } $sale = $license ? $salesByLicense->get($license['id'] ?? null) : null; $licenseType = (int) data_get($license, 'type', 0); return [ 'tenant' => $tenant, 'license' => $license, 'sale' => $sale, 'customerEmail' => data_get($sale, 'customerEmail') ?: data_get($license, 'ownerEmail') ?: data_get($license, 'business.email') ?: ($tenant['email'] ?? null), 'licenseTypeLabel' => match ($licenseType) { 1 => 'COMERSOFT Sistema Completo', 2 => 'Licencia de prueba', 3 => 'COMERSOFT Versión con Sync', 4 => 'COMERSOFT Versión con IA', default => 'Licencia histórica', }, 'licenseOwner' => data_get($license, 'reseller.name') ?: data_get($license, 'owner.name') ?: 'COMERSOFT raíz', 'licenseOwnerEmail' => data_get($license, 'reseller.email') ?: data_get($license, 'owner.email') ?: 'admin@comersoft.com', ]; }) ->values() ->all(); return view('agents.dashboard', [ 'user' => $user, 'summary' => $data['summary'] ?? [], 'licenses' => array_slice($allLicenses, 0, 8), 'sales' => array_slice($allSales, 0, 8), 'tenants' => array_slice($allTenants, 0, 8), 'managedCommerces' => $managedCommerces, 'licenseCount' => count($allLicenses), 'salesCount' => count($allSales), 'tenantCount' => count($allTenants), 'pendingSales' => count(array_filter($allSales, fn ($sale) => ($sale['status'] ?? null) === 'PENDING')), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenses(): View|RedirectResponse { try { $licenses = $this->cloud->licenses(); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); $available = $items->firstWhere('status', 'AVAILABLE'); return [ 'owner' => $first['owner'] ?? ['id' => null, 'name' => 'COMERSOFT raíz', 'type' => 'COMERSOFT_ROOT'], 'reseller' => $first['reseller'] ?? null, 'total' => $items->count(), 'available' => $items->where('status', 'AVAILABLE')->count(), 'active' => $items->where('status', 'ACTIVE')->count(), 'sold' => $items->filter(fn (array $license) => ! empty($license['sale']))->count(), 'sampleAvailableId' => $available['id'] ?? null, 'sampleLicenseId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.index', [ 'licenseGroups' => $licenseGroups, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function subscriptions(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La pantalla conserva el plan aunque momentáneamente no haya cotización. } return view('agents.subscriptions.index', [ 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicensesForm(): View|RedirectResponse { try { $user = session('agent.user', []); $availableInventory = null; if (($user['role'] ?? null) === 'RESELLER') { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); } return view('agents.licenses.generate', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'availableInventory' => $availableInventory, 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function provisionTenantForm(Request $request): View|RedirectResponse { try { return view('agents.licenses.provision', [ 'selectedLicenseId' => $request->query('licenseId'), 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); $licenses = array_values(array_filter( $this->cloud->licenses(), fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' )); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); return [ 'owner' => $first['owner'] ?? ['name' => 'COMERSOFT raíz'], 'available' => $items->count(), 'sampleAvailableId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.provision', [ 'licenseGroups' => $licenseGroups, 'selectedLicenseId' => $request->query('licenseId'), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function searchLicenses(Request $request) { $query = trim((string) $request->query('q', '')); if (mb_strlen($query) < 2 || mb_strlen($query) > 180) { return response()->json(['licenses' => []]); } try { return response()->json(['licenses' => $this->cloud->searchLicenses($query)]); } catch (AgentCloudException $exception) { $status = $exception->getCode() >= 400 ? $exception->getCode() : 502; return response()->json(['message' => $exception->getMessage()], $status); } } public function licenseLookup(Request $request): View|RedirectResponse { $query = trim((string) $request->query('q', '')); $licenses = []; $lookupError = null; if (mb_strlen($query) >= 2) { try { $licenses = $this->cloud->searchLicenses($query, true); } catch (AgentCloudException $exception) { $lookupError = $exception->getMessage(); } } return view('agents.licenses.lookup', [ 'query' => $query, 'licenses' => $licenses, 'lookupError' => $lookupError, 'user' => session('agent.user', []), ]); } public function assignLicenseForm(string $licenseId): View|RedirectResponse { $this->ensureLicenseManager(); try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['license' => 'No se encontró la licencia seleccionada.']); } return view('agents.licenses.assign', [ 'license' => $license, 'resellers' => $this->cloud->resellers(), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function registerSaleForm(string $licenseId): View|RedirectResponse { try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['sale' => 'No se encontró la licencia seleccionada.']); } $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Se puede abrir la acción aunque temporalmente no haya cotización. } return view('agents.licenses.sale', [ 'license' => $license, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicenses(Request $request): RedirectResponse { $payload = $request->validate([ 'quantity' => ['required', 'integer', 'in:1'], 'activationLimit' => ['nullable', 'integer', 'in:10'], 'businessName' => ['required', 'string', 'min:2', 'max:120'], 'ownerEmail' => ['required', 'email'], 'password' => ['required', 'string', 'min:8', 'max:200'], 'resellerId' => ['nullable', 'string'], ]); // Una emisión representa una sola suscripción; el plazo comienza al activar. $payload['quantity'] = 1; $payload['activationLimit'] = 10; // El inventario siempre se determina por la cuenta autenticada. No se // permite elegir otro propietario desde el formulario web. unset($payload['resellerId']); if (session('agent.user.role') === 'RESELLER') { try { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); if ($availableInventory === 0) { return back()->withInput()->withErrors(['license' => 'No tenés licencias disponibles en tu inventario para generar otra.']); } if ($availableInventory < (int) $payload['quantity']) { return back()->withInput()->withErrors(['license' => "Solo tenés {$availableInventory} licencia(s) disponible(s) en tu inventario."]); } } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } try { $data = $this->cloud->generateLicenses($payload); $generated = $data['licenses'] ?? []; $keys = array_values(array_filter(array_column($generated, 'licenseKey'))); $licenseDetails = collect($generated)->map(function (array $license): array { return [ 'licenseKey' => $license['licenseKey'] ?? null, 'businessName' => data_get($license, 'business.name'), 'ownerEmail' => $license['ownerEmail'] ?? null, 'activationLimit' => $license['activationLimit'] ?? 1, 'expiresAt' => $license['expiresAt'] ?? null, ]; })->values()->all(); return redirect()->route('agents.licenses')->with([ 'success' => count($generated) . ' licencia(s) generada(s).', 'license_keys' => $keys, 'license_details' => $licenseDetails, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['license' => $exception->getMessage()]); } } public function trialLicenseForm(): View|RedirectResponse { try { return view('agents.licenses.trial', [ 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createTrialLicense(Request $request): RedirectResponse { $payload = $request->validate([ 'ownerEmail' => ['required', 'email'], 'days' => ['required', 'integer', 'min:1', 'max:30'], 'profile' => ['required', 'in:full,custom'], 'modules' => ['nullable', 'string', 'max:2000'], 'referralResellerId' => ['nullable', 'string', 'max:80'], ]); $payload['modules'] = $payload['profile'] === 'custom' ? array_values(array_filter(array_unique(array_map('trim', explode(',', (string) ($payload['modules'] ?? '')))))) : null; unset($payload['profile']); if (session('agent.user.role') === 'RESELLER') { unset($payload['referralResellerId']); } try { $data = $this->cloud->createTrial($payload); $license = $data['license'] ?? []; return redirect()->route('agents.licenses')->with([ 'success' => 'Licencia de prueba emitida. No consume inventario Tipo 1.', 'license_keys' => array_values(array_filter([$license['licenseKey'] ?? null])), 'trial_details' => [ 'licenseKey' => $license['licenseKey'] ?? null, 'ownerEmail' => $license['ownerEmail'] ?? $payload['ownerEmail'], 'trialDays' => $license['trialDays'] ?? $payload['days'], 'expiresAt' => $license['expiresAt'] ?? null, ], ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['trial' => $exception->getMessage()]); } } public function assignLicense(string $licenseId, Request $request): RedirectResponse { $this->ensureSuperAdmin(); $payload = $request->validate(['resellerId' => ['required', 'string']]); try { $this->cloud->assignLicense($licenseId, $payload['resellerId']); return redirect()->route('agents.licenses')->with('success', 'Licencia asignada al agente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['license' => $exception->getMessage()]); } } public function provisionTenant(Request $request): RedirectResponse { $payload = $request->validate([ 'licenseId' => ['nullable', 'string', 'min:8', 'max:80'], 'licenseKey' => ['nullable', 'string', 'min:12', 'max:2048'], 'email' => ['required', 'email'], 'name' => ['required', 'string', 'min:2', 'max:120'], 'resellerId' => ['nullable', 'string', 'max:80'], ]); if (empty($payload['licenseId']) && empty($payload['licenseKey'])) { return back()->withInput()->withErrors(['tenant' => 'Seleccioná una licencia disponible.']); } if (session('agent.user.role') === 'RESELLER') { unset($payload['resellerId']); } try { $data = $this->cloud->provisionTenant($payload); return redirect()->route('agents.commerce', $data['tenant']['id'] ?? '')->with('success', 'Comercio vinculado correctamente.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['tenant' => $exception->getMessage()]); } } public function registerSale(string $licenseId, Request $request): RedirectResponse { $payload = $request->validate([ 'saleAmount' => ['nullable', 'numeric', 'min:0'], 'customerEmail' => ['nullable', 'email'], 'currency' => ['nullable', 'string', 'max:8'], 'notes' => ['nullable', 'string', 'max:1000'], ]); try { $data = $this->cloud->convertLicenseToMax($licenseId, $payload); $businessId = data_get($data, 'license.business.id') ?: data_get($data, 'license.businessId'); if ($businessId) { return redirect()->route('agents.commerce', $businessId)->with('success', 'Licencia convertida a COMERSOFT Sistema Completo. El correo titular recibira la nueva clave.'); } return redirect()->route('agents.dashboard')->with('success', 'Licencia convertida a COMERSOFT Sistema Completo.'); return redirect()->route('agents.sales')->with('success', 'Venta registrada como pendiente de liquidación.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['sale' => $exception->getMessage()]); } } public function sales(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Las ventas historicas siguen visibles aunque falle la cotizacion en vivo. } return view('agents.sales.index', [ 'sales' => $this->cloud->sales(), 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenseRequests(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La bandeja conserva su operacion aunque falle la cotizacion. } $accessRequests = $this->cloud->accessRequests('PENDING'); $freeRequests = []; if (in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true)) { $freeRequests = $this->cloud->publicLicenseRequests('PENDING'); } return view('agents.license-requests', [ 'requests' => $accessRequests, 'freeRequests' => $freeRequests, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function approveAccessRequest(string $requestId): RedirectResponse { try { $this->cloud->approveAccessRequest($requestId, session('agent.user.resellerId')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud aprobada y licencia comercial vinculada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function rejectAccessRequest(string $requestId, Request $request): RedirectResponse { try { $this->cloud->rejectAccessRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function approvePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureLicenseManager(); try { $data = $this->cloud->approvePublicLicenseRequest($requestId); $licenseKey = data_get($data, 'license.licenseKey'); return redirect()->route('agents.license.requests')->with([ 'success' => 'Cuenta confirmada y Licencia histórica emitida.', 'license_keys' => $licenseKey ? [$licenseKey] : [], ]); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function rejectPublicLicenseRequest(string $requestId, Request $request): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->rejectPublicLicenseRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud de Licencia histórica rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function deletePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deletePublicLicenseRequest($requestId); return redirect()->route('agents.license.requests')->with('success', 'Solicitud y cuenta eliminadas correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function saleAction(string $saleId): View|RedirectResponse { try { $sale = collect($this->cloud->sales())->firstWhere('id', $saleId); if (! $sale) { return redirect()->route('agents.sales')->withErrors(['sale' => 'No se encontró la venta seleccionada.']); } return view('agents.sales.action', [ 'sale' => $sale, 'saleStatus' => $sale['status'] ?? 'PENDING', 'canApprove' => in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function updateSaleStatus(string $saleId, Request $request): RedirectResponse { $this->ensureLicenseManager(); $payload = $request->validate(['status' => ['required', 'in:PENDING,PAID,CANCELLED']]); try { $this->cloud->updateSaleStatus($saleId, $payload['status']); return redirect()->route('agents.sales')->with('success', 'Estado de la venta actualizado.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function deleteSale(string $saleId): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->deleteSale($saleId); return redirect()->route('agents.sales')->with('success', 'Venta eliminada correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function commerce(string $id): View|RedirectResponse { try { return view('agents.commerce.show', ['commerce' => $this->cloud->tenant($id)]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function deleteCommerce(string $id): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deleteTenant($id); return redirect()->route('agents.dashboard')->with('success', 'Comercio eliminado correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['commerce' => $exception->getMessage()]); } } public function agents(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.index', ['resellers' => $this->visibleManagedResellers($user), 'user' => $user, 'hierarchyAdmin' => $this->isHierarchyAdmin()]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentsMap(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); $allResellers = $this->cloud->resellers(); $resellers = $this->visibleManagedResellers($user); if (! empty($user['resellerId'])) { $manager = collect($allResellers)->firstWhere('id', $user['resellerId']); if ($manager) { $hierarchy = collect([$manager]); $ancestor = $manager; while (! empty($ancestor['parentId'])) { $ancestor = collect($allResellers)->firstWhere('id', $ancestor['parentId']); if (! $ancestor) break; $hierarchy->prepend($ancestor); } $resellers = $hierarchy->merge($resellers)->unique('id')->values()->all(); } } $nodes = collect($resellers)->map(function (array $reseller) use ($resellers) { $coordinates = $this->resellerCoordinates($reseller, $resellers); $level = (int) ($reseller['level'] ?? 3); return [ 'id' => $reseller['id'], 'parentId' => $reseller['parentId'] ?? null, 'name' => $reseller['name'], 'email' => $reseller['email'], 'level' => $level, 'levelLabel' => [1 => 'Nacional', 2 => 'Provincial / Estatal', 3 => 'Ciudad / Zona'][$level] ?? 'Socio', 'country' => $reseller['country'] ?? 'AR', 'province' => $reseller['province'] ?? null, 'city' => $reseller['city'] ?? null, 'zone' => $reseller['zone'] ?? null, 'status' => $reseller['status'] ?? 'ACTIVE', 'commissionPercent' => $reseller['commissionPercent'] ?? 70, 'counts' => $reseller['counts'] ?? [], 'latitude' => $coordinates['latitude'], 'longitude' => $coordinates['longitude'], 'location' => collect([$reseller['province'] ?? null, $reseller['city'] ?? null, $reseller['zone'] ?? null])->filter()->implode(' · ') ?: (($reseller['country'] ?? 'AR') === 'AR' ? 'Argentina' : ($reseller['country'] ?? '')), ]; })->values()->all(); return view('agents.agents.map', [ 'nodes' => $nodes, 'managerName' => 'COMERSOFT Global', 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentDetail(string $id): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); if (($user['resellerId'] ?? null) === $id) { return redirect()->route('agents.agents'); } $resellers = $this->cloud->resellers(); $reseller = collect($resellers)->firstWhere('id', $id); if (! $reseller) { abort(404, 'Socio no encontrado.'); } $summary = $this->cloud->resellerSummary($id); return view('agents.agents.show', [ 'reseller' => $reseller, 'children' => collect($resellers)->where('parentId', $id)->values()->all(), 'summary' => $summary, 'users' => $summary['users'] ?? [], 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgentForm(Request $request): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.create', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : $this->visibleManagedResellers($user), 'selectedParentId' => $request->query('parentId') ?: data_get($user, 'resellerId'), 'hierarchyAdmin' => $this->isHierarchyAdmin(), 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgent(Request $request): RedirectResponse { $this->ensureHierarchyAdmin(); $payload = $request->validate([ 'name' => ['required', 'string', 'min:2', 'max:120'], 'email' => ['required', 'email'], 'companyName' => ['nullable', 'string', 'max:160'], 'phone' => ['nullable', 'string', 'max:40'], 'level' => ['required', 'integer', 'in:1,2,3'], 'country' => ['required', 'string', 'size:2'], 'province' => ['nullable', 'string', 'max:120'], 'city' => ['nullable', 'string', 'max:120'], 'zone' => ['nullable', 'string', 'max:120'], 'parentId' => ['nullable', 'string', 'max:80'], 'password' => ['nullable', 'string', 'min:12', 'max:200'], ]); if (session('agent.user.role') === 'RESELLER') { $payload['parentId'] = session('agent.user.resellerId'); $payload['level'] = max(2, (int) data_get(session('agent.user'), 'reseller.level', 1) + 1); } $payload['commissionPercent'] = 70; try { $data = $this->cloud->createReseller($payload); return redirect()->route('agents.agents')->with([ 'success' => 'Agente creado correctamente con 1.000 licencias disponibles.', 'temporary_password' => $data['temporaryPassword'] ?? null, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['agent' => $exception->getMessage()]); } } private function resellerCoordinates(array $reseller, array $resellers = []): array { $normalize = static function (?string $value): string { return strtr(mb_strtolower(trim((string) $value)), [ 'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u', 'ü' => 'u', 'ñ' => 'n', ]); }; $locations = [ 'buenos aires' => [-36.67, -60.56], 'caba' => [-34.60, -58.38], 'ciudad autonoma de buenos aires' => [-34.60, -58.38], 'cordoba' => [-31.42, -64.19], 'santa fe' => [-31.63, -60.70], 'mendoza' => [-32.89, -68.84], 'tucuman' => [-26.82, -65.22], 'entre rios' => [-31.74, -60.51], 'salta' => [-24.78, -65.41], 'misiones' => [-27.36, -55.90], 'chaco' => [-27.45, -58.99], 'corrientes' => [-27.47, -58.83], 'jujuy' => [-24.18, -65.30], 'la pampa' => [-36.62, -64.29], 'neuquen' => [-38.95, -68.06], 'rio negro' => [-40.81, -63.00], 'chubut' => [-43.30, -65.10], 'santa cruz' => [-50.00, -68.00], 'tierra del fuego' => [-54.80, -68.30], 'san juan' => [-31.54, -68.52], 'san luis' => [-33.30, -66.34], 'la rioja' => [-29.41, -66.86], 'catamarca' => [-28.47, -65.78], 'formosa' => [-26.18, -58.18], 'santiago del estero' => [-27.78, -64.26], ]; $city = $normalize($reseller['city'] ?? null); $province = $normalize($reseller['province'] ?? null); if ($city && isset($locations[$city])) return ['latitude' => $locations[$city][0], 'longitude' => $locations[$city][1]]; if ($province && isset($locations[$province])) return ['latitude' => $locations[$province][0], 'longitude' => $locations[$province][1]]; if (! empty($reseller['parentId'])) { $parent = collect($resellers)->firstWhere('id', $reseller['parentId']); if ($parent) return $this->resellerCoordinates($parent, $resellers); } return ($reseller['country'] ?? 'AR') === 'AR' ? ['latitude' => -34.6037, 'longitude' => -58.3816] : ['latitude' => 0, 'longitude' => 0]; } private function visibleManagedResellers(array $user): array { $currentResellerId = $user['resellerId'] ?? null; return collect($this->cloud->resellers()) ->reject(fn (array $reseller) => $currentResellerId && ($reseller['id'] ?? null) === $currentResellerId) ->values() ->all(); } private function ensureSuperAdmin(): void { abort_unless(session('agent.user.role') === 'SUPERADMIN', 403); } private function ensureLicenseManager(): void { abort_unless(in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 403); } private function isHierarchyAdmin(): bool { return session('agent.user.role') === 'SUPERADMIN' || (session('agent.user.role') === 'RESELLER' && (int) data_get(session('agent.user'), 'reseller.level', 0) === 1); } private function ensureHierarchyAdmin(): void { abort_unless($this->isHierarchyAdmin(), 403); } private function handleCloudError(AgentCloudException $exception): RedirectResponse { if ($exception->getCode() === 401) { session()->forget(['agent.access_token', 'agent.user']); return redirect()->route('agents.login')->withErrors(['email' => $exception->getMessage()]); } abort(502, $exception->getMessage()); } } route('agents.dashboard'); } return view('agents.login'); } public function login(Request $request): RedirectResponse { $credentials = $request->validate([ 'email' => ['required', 'email'], 'password' => ['required', 'string', 'min:8'], ]); try { $data = $this->cloud->login($credentials['email'], $credentials['password']); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withInput($request->only('email'))->withErrors(['email' => $exception->getMessage()]); } } public function quickLogin(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login'); } public function quickLoginEliezer(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login_eliezer'); } private function performQuickLogin(Request $request, string $configKey): RedirectResponse { abort_unless(app()->environment('local') && config("comersoft.{$configKey}.enabled"), 404); $email = (string) config("comersoft.{$configKey}.email"); $password = (string) config("comersoft.{$configKey}.password"); if ($email === '' || $password === '') { return back()->withErrors(['email' => 'El inicio rápido no está configurado.']); } try { $data = $this->cloud->login($email, $password); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withErrors(['email' => $exception->getMessage()]); } } public function logout(Request $request): RedirectResponse { $request->session()->forget(['agent.access_token', 'agent.user']); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect()->route('agents.login'); } public function dashboard(): View|RedirectResponse { try { $user = session('agent.user', []); $data = $this->cloud->dashboard($user['resellerId'] ?? null); $allLicenses = $data['licenses'] ?? []; $allSales = $data['sales'] ?? []; $allTenants = $data['tenants'] ?? []; $salesByLicense = collect($allSales) ->filter(fn (array $sale) => data_get($sale, 'licenseId') || data_get($sale, 'license.id')) ->keyBy(fn (array $sale) => data_get($sale, 'licenseId') ?: data_get($sale, 'license.id')); $licensesByBusiness = collect($allLicenses) ->filter(fn (array $license) => data_get($license, 'businessId') || data_get($license, 'business.email')) ->groupBy(fn (array $license) => data_get($license, 'businessId') ?: strtolower((string) data_get($license, 'business.email'))); $managedCommerces = collect($allTenants) ->map(function (array $tenant) use ($salesByLicense, $licensesByBusiness) { $tenantLicenses = collect($tenant['licenses'] ?? []); $license = $tenantLicenses->sortByDesc('issuedAt')->first(); if (! $license) { $license = $licensesByBusiness ->get($tenant['id'] ?? null, collect()) ->sortByDesc('issuedAt') ->first(); } if (! $license && ! empty($tenant['email'])) { $license = $licensesByBusiness ->get(strtolower((string) $tenant['email']), collect()) ->sortByDesc('issuedAt') ->first(); } $sale = $license ? $salesByLicense->get($license['id'] ?? null) : null; $licenseType = (int) data_get($license, 'type', 0); return [ 'tenant' => $tenant, 'license' => $license, 'sale' => $sale, 'customerEmail' => data_get($sale, 'customerEmail') ?: data_get($license, 'ownerEmail') ?: data_get($license, 'business.email') ?: ($tenant['email'] ?? null), 'licenseTypeLabel' => match ($licenseType) { 1 => 'COMERSOFT Sistema Completo', 2 => 'Licencia de prueba', 3 => 'COMERSOFT Versión con Sync', 4 => 'COMERSOFT Versión con IA', default => 'Licencia histórica', }, 'licenseOwner' => data_get($license, 'reseller.name') ?: data_get($license, 'owner.name') ?: 'COMERSOFT raíz', 'licenseOwnerEmail' => data_get($license, 'reseller.email') ?: data_get($license, 'owner.email') ?: 'admin@comersoft.com', ]; }) ->values() ->all(); return view('agents.dashboard', [ 'user' => $user, 'summary' => $data['summary'] ?? [], 'licenses' => array_slice($allLicenses, 0, 8), 'sales' => array_slice($allSales, 0, 8), 'tenants' => array_slice($allTenants, 0, 8), 'managedCommerces' => $managedCommerces, 'licenseCount' => count($allLicenses), 'salesCount' => count($allSales), 'tenantCount' => count($allTenants), 'pendingSales' => count(array_filter($allSales, fn ($sale) => ($sale['status'] ?? null) === 'PENDING')), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenses(): View|RedirectResponse { try { $licenses = $this->cloud->licenses(); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); $available = $items->firstWhere('status', 'AVAILABLE'); return [ 'owner' => $first['owner'] ?? ['id' => null, 'name' => 'COMERSOFT raíz', 'type' => 'COMERSOFT_ROOT'], 'reseller' => $first['reseller'] ?? null, 'total' => $items->count(), 'available' => $items->where('status', 'AVAILABLE')->count(), 'active' => $items->where('status', 'ACTIVE')->count(), 'sold' => $items->filter(fn (array $license) => ! empty($license['sale']))->count(), 'sampleAvailableId' => $available['id'] ?? null, 'sampleLicenseId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.index', [ 'licenseGroups' => $licenseGroups, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function subscriptions(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La pantalla conserva el plan aunque momentáneamente no haya cotización. } return view('agents.subscriptions.index', [ 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicensesForm(): View|RedirectResponse { try { $user = session('agent.user', []); $availableInventory = null; if (($user['role'] ?? null) === 'RESELLER') { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); } return view('agents.licenses.generate', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'availableInventory' => $availableInventory, 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function provisionTenantForm(Request $request): View|RedirectResponse { try { return view('agents.licenses.provision', [ 'selectedLicenseId' => $request->query('licenseId'), 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); $licenses = array_values(array_filter( $this->cloud->licenses(), fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' )); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); return [ 'owner' => $first['owner'] ?? ['name' => 'COMERSOFT raíz'], 'available' => $items->count(), 'sampleAvailableId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.provision', [ 'licenseGroups' => $licenseGroups, 'selectedLicenseId' => $request->query('licenseId'), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function searchLicenses(Request $request) { $query = trim((string) $request->query('q', '')); if (mb_strlen($query) < 2 || mb_strlen($query) > 180) { return response()->json(['licenses' => []]); } try { return response()->json(['licenses' => $this->cloud->searchLicenses($query)]); } catch (AgentCloudException $exception) { $status = $exception->getCode() >= 400 ? $exception->getCode() : 502; return response()->json(['message' => $exception->getMessage()], $status); } } public function licenseLookup(Request $request): View|RedirectResponse { $query = trim((string) $request->query('q', '')); $licenses = []; $lookupError = null; if (mb_strlen($query) >= 2) { try { $licenses = $this->cloud->searchLicenses($query, true); } catch (AgentCloudException $exception) { $lookupError = $exception->getMessage(); } } return view('agents.licenses.lookup', [ 'query' => $query, 'licenses' => $licenses, 'lookupError' => $lookupError, 'user' => session('agent.user', []), ]); } public function assignLicenseForm(string $licenseId): View|RedirectResponse { $this->ensureLicenseManager(); try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['license' => 'No se encontró la licencia seleccionada.']); } return view('agents.licenses.assign', [ 'license' => $license, 'resellers' => $this->cloud->resellers(), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function registerSaleForm(string $licenseId): View|RedirectResponse { try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['sale' => 'No se encontró la licencia seleccionada.']); } $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Se puede abrir la acción aunque temporalmente no haya cotización. } return view('agents.licenses.sale', [ 'license' => $license, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicenses(Request $request): RedirectResponse { $payload = $request->validate([ 'quantity' => ['required', 'integer', 'in:1'], 'activationLimit' => ['nullable', 'integer', 'in:10'], 'businessName' => ['required', 'string', 'min:2', 'max:120'], 'ownerEmail' => ['required', 'email'], 'password' => ['required', 'string', 'min:8', 'max:200'], 'resellerId' => ['nullable', 'string'], ]); // Una emisión representa una sola suscripción; el plazo comienza al activar. $payload['quantity'] = 1; $payload['activationLimit'] = 10; // El inventario siempre se determina por la cuenta autenticada. No se // permite elegir otro propietario desde el formulario web. unset($payload['resellerId']); if (session('agent.user.role') === 'RESELLER') { try { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); if ($availableInventory === 0) { return back()->withInput()->withErrors(['license' => 'No tenés licencias disponibles en tu inventario para generar otra.']); } if ($availableInventory < (int) $payload['quantity']) { return back()->withInput()->withErrors(['license' => "Solo tenés {$availableInventory} licencia(s) disponible(s) en tu inventario."]); } } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } try { $data = $this->cloud->generateLicenses($payload); $generated = $data['licenses'] ?? []; $keys = array_values(array_filter(array_column($generated, 'licenseKey'))); $licenseDetails = collect($generated)->map(function (array $license): array { return [ 'licenseKey' => $license['licenseKey'] ?? null, 'businessName' => data_get($license, 'business.name'), 'ownerEmail' => $license['ownerEmail'] ?? null, 'activationLimit' => $license['activationLimit'] ?? 1, 'expiresAt' => $license['expiresAt'] ?? null, ]; })->values()->all(); return redirect()->route('agents.licenses')->with([ 'success' => count($generated) . ' licencia(s) generada(s).', 'license_keys' => $keys, 'license_details' => $licenseDetails, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['license' => $exception->getMessage()]); } } public function trialLicenseForm(): View|RedirectResponse { try { return view('agents.licenses.trial', [ 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createTrialLicense(Request $request): RedirectResponse { $payload = $request->validate([ 'ownerEmail' => ['required', 'email'], 'days' => ['required', 'integer', 'min:1', 'max:30'], 'profile' => ['required', 'in:full,custom'], 'modules' => ['nullable', 'string', 'max:2000'], 'referralResellerId' => ['nullable', 'string', 'max:80'], ]); $payload['modules'] = $payload['profile'] === 'custom' ? array_values(array_filter(array_unique(array_map('trim', explode(',', (string) ($payload['modules'] ?? '')))))) : null; unset($payload['profile']); if (session('agent.user.role') === 'RESELLER') { unset($payload['referralResellerId']); } try { $data = $this->cloud->createTrial($payload); $license = $data['license'] ?? []; return redirect()->route('agents.licenses')->with([ 'success' => 'Licencia de prueba emitida. No consume inventario Tipo 1.', 'license_keys' => array_values(array_filter([$license['licenseKey'] ?? null])), 'trial_details' => [ 'licenseKey' => $license['licenseKey'] ?? null, 'ownerEmail' => $license['ownerEmail'] ?? $payload['ownerEmail'], 'trialDays' => $license['trialDays'] ?? $payload['days'], 'expiresAt' => $license['expiresAt'] ?? null, ], ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['trial' => $exception->getMessage()]); } } public function assignLicense(string $licenseId, Request $request): RedirectResponse { $this->ensureSuperAdmin(); $payload = $request->validate(['resellerId' => ['required', 'string']]); try { $this->cloud->assignLicense($licenseId, $payload['resellerId']); return redirect()->route('agents.licenses')->with('success', 'Licencia asignada al agente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['license' => $exception->getMessage()]); } } public function provisionTenant(Request $request): RedirectResponse { $payload = $request->validate([ 'licenseId' => ['nullable', 'string', 'min:8', 'max:80'], 'licenseKey' => ['nullable', 'string', 'min:12', 'max:2048'], 'email' => ['required', 'email'], 'name' => ['required', 'string', 'min:2', 'max:120'], 'resellerId' => ['nullable', 'string', 'max:80'], ]); if (empty($payload['licenseId']) && empty($payload['licenseKey'])) { return back()->withInput()->withErrors(['tenant' => 'Seleccioná una licencia disponible.']); } if (session('agent.user.role') === 'RESELLER') { unset($payload['resellerId']); } try { $data = $this->cloud->provisionTenant($payload); return redirect()->route('agents.commerce', $data['tenant']['id'] ?? '')->with('success', 'Comercio vinculado correctamente.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['tenant' => $exception->getMessage()]); } } public function registerSale(string $licenseId, Request $request): RedirectResponse { $payload = $request->validate([ 'saleAmount' => ['nullable', 'numeric', 'min:0'], 'customerEmail' => ['nullable', 'email'], 'currency' => ['nullable', 'string', 'max:8'], 'notes' => ['nullable', 'string', 'max:1000'], ]); try { $data = $this->cloud->convertLicenseToMax($licenseId, $payload); $businessId = data_get($data, 'license.business.id') ?: data_get($data, 'license.businessId'); if ($businessId) { return redirect()->route('agents.commerce', $businessId)->with('success', 'Licencia convertida a COMERSOFT Sistema Completo. El correo titular recibira la nueva clave.'); } return redirect()->route('agents.dashboard')->with('success', 'Licencia convertida a COMERSOFT Sistema Completo.'); return redirect()->route('agents.sales')->with('success', 'Venta registrada como pendiente de liquidación.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['sale' => $exception->getMessage()]); } } public function sales(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Las ventas historicas siguen visibles aunque falle la cotizacion en vivo. } return view('agents.sales.index', [ 'sales' => $this->cloud->sales(), 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenseRequests(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La bandeja conserva su operacion aunque falle la cotizacion. } $accessRequests = $this->cloud->accessRequests('PENDING'); $freeRequests = []; if (in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true)) { $freeRequests = $this->cloud->publicLicenseRequests('PENDING'); } return view('agents.license-requests', [ 'requests' => $accessRequests, 'freeRequests' => $freeRequests, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function approveAccessRequest(string $requestId): RedirectResponse { try { $this->cloud->approveAccessRequest($requestId, session('agent.user.resellerId')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud aprobada y licencia comercial vinculada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function rejectAccessRequest(string $requestId, Request $request): RedirectResponse { try { $this->cloud->rejectAccessRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function approvePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureLicenseManager(); try { $data = $this->cloud->approvePublicLicenseRequest($requestId); $licenseKey = data_get($data, 'license.licenseKey'); return redirect()->route('agents.license.requests')->with([ 'success' => 'Cuenta confirmada y Licencia histórica emitida.', 'license_keys' => $licenseKey ? [$licenseKey] : [], ]); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function rejectPublicLicenseRequest(string $requestId, Request $request): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->rejectPublicLicenseRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud de Licencia histórica rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function deletePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deletePublicLicenseRequest($requestId); return redirect()->route('agents.license.requests')->with('success', 'Solicitud y cuenta eliminadas correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function saleAction(string $saleId): View|RedirectResponse { try { $sale = collect($this->cloud->sales())->firstWhere('id', $saleId); if (! $sale) { return redirect()->route('agents.sales')->withErrors(['sale' => 'No se encontró la venta seleccionada.']); } return view('agents.sales.action', [ 'sale' => $sale, 'saleStatus' => $sale['status'] ?? 'PENDING', 'canApprove' => in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function updateSaleStatus(string $saleId, Request $request): RedirectResponse { $this->ensureLicenseManager(); $payload = $request->validate(['status' => ['required', 'in:PENDING,PAID,CANCELLED']]); try { $this->cloud->updateSaleStatus($saleId, $payload['status']); return redirect()->route('agents.sales')->with('success', 'Estado de la venta actualizado.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function deleteSale(string $saleId): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->deleteSale($saleId); return redirect()->route('agents.sales')->with('success', 'Venta eliminada correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function commerce(string $id): View|RedirectResponse { try { return view('agents.commerce.show', ['commerce' => $this->cloud->tenant($id)]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function deleteCommerce(string $id): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deleteTenant($id); return redirect()->route('agents.dashboard')->with('success', 'Comercio eliminado correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['commerce' => $exception->getMessage()]); } } public function agents(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.index', ['resellers' => $this->visibleManagedResellers($user), 'user' => $user, 'hierarchyAdmin' => $this->isHierarchyAdmin()]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentsMap(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); $allResellers = $this->cloud->resellers(); $resellers = $this->visibleManagedResellers($user); if (! empty($user['resellerId'])) { $manager = collect($allResellers)->firstWhere('id', $user['resellerId']); if ($manager) { $hierarchy = collect([$manager]); $ancestor = $manager; while (! empty($ancestor['parentId'])) { $ancestor = collect($allResellers)->firstWhere('id', $ancestor['parentId']); if (! $ancestor) break; $hierarchy->prepend($ancestor); } $resellers = $hierarchy->merge($resellers)->unique('id')->values()->all(); } } $nodes = collect($resellers)->map(function (array $reseller) use ($resellers) { $coordinates = $this->resellerCoordinates($reseller, $resellers); $level = (int) ($reseller['level'] ?? 3); return [ 'id' => $reseller['id'], 'parentId' => $reseller['parentId'] ?? null, 'name' => $reseller['name'], 'email' => $reseller['email'], 'level' => $level, 'levelLabel' => [1 => 'Nacional', 2 => 'Provincial / Estatal', 3 => 'Ciudad / Zona'][$level] ?? 'Socio', 'country' => $reseller['country'] ?? 'AR', 'province' => $reseller['province'] ?? null, 'city' => $reseller['city'] ?? null, 'zone' => $reseller['zone'] ?? null, 'status' => $reseller['status'] ?? 'ACTIVE', 'commissionPercent' => $reseller['commissionPercent'] ?? 70, 'counts' => $reseller['counts'] ?? [], 'latitude' => $coordinates['latitude'], 'longitude' => $coordinates['longitude'], 'location' => collect([$reseller['province'] ?? null, $reseller['city'] ?? null, $reseller['zone'] ?? null])->filter()->implode(' · ') ?: (($reseller['country'] ?? 'AR') === 'AR' ? 'Argentina' : ($reseller['country'] ?? '')), ]; })->values()->all(); return view('agents.agents.map', [ 'nodes' => $nodes, 'managerName' => 'COMERSOFT Global', 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentDetail(string $id): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); if (($user['resellerId'] ?? null) === $id) { return redirect()->route('agents.agents'); } $resellers = $this->cloud->resellers(); $reseller = collect($resellers)->firstWhere('id', $id); if (! $reseller) { abort(404, 'Socio no encontrado.'); } $summary = $this->cloud->resellerSummary($id); return view('agents.agents.show', [ 'reseller' => $reseller, 'children' => collect($resellers)->where('parentId', $id)->values()->all(), 'summary' => $summary, 'users' => $summary['users'] ?? [], 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgentForm(Request $request): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.create', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : $this->visibleManagedResellers($user), 'selectedParentId' => $request->query('parentId') ?: data_get($user, 'resellerId'), 'hierarchyAdmin' => $this->isHierarchyAdmin(), 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgent(Request $request): RedirectResponse { $this->ensureHierarchyAdmin(); $payload = $request->validate([ 'name' => ['required', 'string', 'min:2', 'max:120'], 'email' => ['required', 'email'], 'companyName' => ['nullable', 'string', 'max:160'], 'phone' => ['nullable', 'string', 'max:40'], 'level' => ['required', 'integer', 'in:1,2,3'], 'country' => ['required', 'string', 'size:2'], 'province' => ['nullable', 'string', 'max:120'], 'city' => ['nullable', 'string', 'max:120'], 'zone' => ['nullable', 'string', 'max:120'], 'parentId' => ['nullable', 'string', 'max:80'], 'password' => ['nullable', 'string', 'min:12', 'max:200'], ]); if (session('agent.user.role') === 'RESELLER') { $payload['parentId'] = session('agent.user.resellerId'); $payload['level'] = max(2, (int) data_get(session('agent.user'), 'reseller.level', 1) + 1); } $payload['commissionPercent'] = 70; try { $data = $this->cloud->createReseller($payload); return redirect()->route('agents.agents')->with([ 'success' => 'Agente creado correctamente con 1.000 licencias disponibles.', 'temporary_password' => $data['temporaryPassword'] ?? null, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['agent' => $exception->getMessage()]); } } private function resellerCoordinates(array $reseller, array $resellers = []): array { $normalize = static function (?string $value): string { return strtr(mb_strtolower(trim((string) $value)), [ 'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u', 'ü' => 'u', 'ñ' => 'n', ]); }; $locations = [ 'buenos aires' => [-36.67, -60.56], 'caba' => [-34.60, -58.38], 'ciudad autonoma de buenos aires' => [-34.60, -58.38], 'cordoba' => [-31.42, -64.19], 'santa fe' => [-31.63, -60.70], 'mendoza' => [-32.89, -68.84], 'tucuman' => [-26.82, -65.22], 'entre rios' => [-31.74, -60.51], 'salta' => [-24.78, -65.41], 'misiones' => [-27.36, -55.90], 'chaco' => [-27.45, -58.99], 'corrientes' => [-27.47, -58.83], 'jujuy' => [-24.18, -65.30], 'la pampa' => [-36.62, -64.29], 'neuquen' => [-38.95, -68.06], 'rio negro' => [-40.81, -63.00], 'chubut' => [-43.30, -65.10], 'santa cruz' => [-50.00, -68.00], 'tierra del fuego' => [-54.80, -68.30], 'san juan' => [-31.54, -68.52], 'san luis' => [-33.30, -66.34], 'la rioja' => [-29.41, -66.86], 'catamarca' => [-28.47, -65.78], 'formosa' => [-26.18, -58.18], 'santiago del estero' => [-27.78, -64.26], ]; $city = $normalize($reseller['city'] ?? null); $province = $normalize($reseller['province'] ?? null); if ($city && isset($locations[$city])) return ['latitude' => $locations[$city][0], 'longitude' => $locations[$city][1]]; if ($province && isset($locations[$province])) return ['latitude' => $locations[$province][0], 'longitude' => $locations[$province][1]]; if (! empty($reseller['parentId'])) { $parent = collect($resellers)->firstWhere('id', $reseller['parentId']); if ($parent) return $this->resellerCoordinates($parent, $resellers); } return ($reseller['country'] ?? 'AR') === 'AR' ? ['latitude' => -34.6037, 'longitude' => -58.3816] : ['latitude' => 0, 'longitude' => 0]; } private function visibleManagedResellers(array $user): array { $currentResellerId = $user['resellerId'] ?? null; return collect($this->cloud->resellers()) ->reject(fn (array $reseller) => $currentResellerId && ($reseller['id'] ?? null) === $currentResellerId) ->values() ->all(); } private function ensureSuperAdmin(): void { abort_unless(session('agent.user.role') === 'SUPERADMIN', 403); } private function ensureLicenseManager(): void { abort_unless(in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 403); } private function isHierarchyAdmin(): bool { return session('agent.user.role') === 'SUPERADMIN' || (session('agent.user.role') === 'RESELLER' && (int) data_get(session('agent.user'), 'reseller.level', 0) === 1); } private function ensureHierarchyAdmin(): void { abort_unless($this->isHierarchyAdmin(), 403); } private function handleCloudError(AgentCloudException $exception): RedirectResponse { if ($exception->getCode() === 401) { session()->forget(['agent.access_token', 'agent.user']); return redirect()->route('agents.login')->withErrors(['email' => $exception->getMessage()]); } abort(502, $exception->getMessage()); } } route('agents.dashboard'); } return view('agents.login'); } public function login(Request $request): RedirectResponse { $credentials = $request->validate([ 'email' => ['required', 'email'], 'password' => ['required', 'string', 'min:8'], ]); try { $data = $this->cloud->login($credentials['email'], $credentials['password']); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withInput($request->only('email'))->withErrors(['email' => $exception->getMessage()]); } } public function quickLogin(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login'); } public function quickLoginEliezer(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login_eliezer'); } private function performQuickLogin(Request $request, string $configKey): RedirectResponse { abort_unless(app()->environment('local') && config("comersoft.{$configKey}.enabled"), 404); $email = (string) config("comersoft.{$configKey}.email"); $password = (string) config("comersoft.{$configKey}.password"); if ($email === '' || $password === '') { return back()->withErrors(['email' => 'El inicio rápido no está configurado.']); } try { $data = $this->cloud->login($email, $password); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withErrors(['email' => $exception->getMessage()]); } } public function logout(Request $request): RedirectResponse { $request->session()->forget(['agent.access_token', 'agent.user']); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect()->route('agents.login'); } public function dashboard(): View|RedirectResponse { try { $user = session('agent.user', []); $data = $this->cloud->dashboard($user['resellerId'] ?? null); $allLicenses = $data['licenses'] ?? []; $allSales = $data['sales'] ?? []; $allTenants = $data['tenants'] ?? []; $salesByLicense = collect($allSales) ->filter(fn (array $sale) => data_get($sale, 'licenseId') || data_get($sale, 'license.id')) ->keyBy(fn (array $sale) => data_get($sale, 'licenseId') ?: data_get($sale, 'license.id')); $licensesByBusiness = collect($allLicenses) ->filter(fn (array $license) => data_get($license, 'businessId') || data_get($license, 'business.email')) ->groupBy(fn (array $license) => data_get($license, 'businessId') ?: strtolower((string) data_get($license, 'business.email'))); $managedCommerces = collect($allTenants) ->map(function (array $tenant) use ($salesByLicense, $licensesByBusiness) { $tenantLicenses = collect($tenant['licenses'] ?? []); $license = $tenantLicenses->sortByDesc('issuedAt')->first(); if (! $license) { $license = $licensesByBusiness ->get($tenant['id'] ?? null, collect()) ->sortByDesc('issuedAt') ->first(); } if (! $license && ! empty($tenant['email'])) { $license = $licensesByBusiness ->get(strtolower((string) $tenant['email']), collect()) ->sortByDesc('issuedAt') ->first(); } $sale = $license ? $salesByLicense->get($license['id'] ?? null) : null; $licenseType = (int) data_get($license, 'type', 0); return [ 'tenant' => $tenant, 'license' => $license, 'sale' => $sale, 'customerEmail' => data_get($sale, 'customerEmail') ?: data_get($license, 'ownerEmail') ?: data_get($license, 'business.email') ?: ($tenant['email'] ?? null), 'licenseTypeLabel' => match ($licenseType) { 1 => 'COMERSOFT Sistema Completo', 2 => 'Licencia de prueba', 3 => 'COMERSOFT Versión con Sync', 4 => 'COMERSOFT Versión con IA', default => 'Licencia histórica', }, 'licenseOwner' => data_get($license, 'reseller.name') ?: data_get($license, 'owner.name') ?: 'COMERSOFT raíz', 'licenseOwnerEmail' => data_get($license, 'reseller.email') ?: data_get($license, 'owner.email') ?: 'admin@comersoft.com', ]; }) ->values() ->all(); return view('agents.dashboard', [ 'user' => $user, 'summary' => $data['summary'] ?? [], 'licenses' => array_slice($allLicenses, 0, 8), 'sales' => array_slice($allSales, 0, 8), 'tenants' => array_slice($allTenants, 0, 8), 'managedCommerces' => $managedCommerces, 'licenseCount' => count($allLicenses), 'salesCount' => count($allSales), 'tenantCount' => count($allTenants), 'pendingSales' => count(array_filter($allSales, fn ($sale) => ($sale['status'] ?? null) === 'PENDING')), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenses(): View|RedirectResponse { try { $licenses = $this->cloud->licenses(); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); $available = $items->firstWhere('status', 'AVAILABLE'); return [ 'owner' => $first['owner'] ?? ['id' => null, 'name' => 'COMERSOFT raíz', 'type' => 'COMERSOFT_ROOT'], 'reseller' => $first['reseller'] ?? null, 'total' => $items->count(), 'available' => $items->where('status', 'AVAILABLE')->count(), 'active' => $items->where('status', 'ACTIVE')->count(), 'sold' => $items->filter(fn (array $license) => ! empty($license['sale']))->count(), 'sampleAvailableId' => $available['id'] ?? null, 'sampleLicenseId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.index', [ 'licenseGroups' => $licenseGroups, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function subscriptions(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La pantalla conserva el plan aunque momentáneamente no haya cotización. } return view('agents.subscriptions.index', [ 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicensesForm(): View|RedirectResponse { try { $user = session('agent.user', []); $availableInventory = null; if (($user['role'] ?? null) === 'RESELLER') { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); } return view('agents.licenses.generate', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'availableInventory' => $availableInventory, 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function provisionTenantForm(Request $request): View|RedirectResponse { try { return view('agents.licenses.provision', [ 'selectedLicenseId' => $request->query('licenseId'), 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); $licenses = array_values(array_filter( $this->cloud->licenses(), fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' )); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); return [ 'owner' => $first['owner'] ?? ['name' => 'COMERSOFT raíz'], 'available' => $items->count(), 'sampleAvailableId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.provision', [ 'licenseGroups' => $licenseGroups, 'selectedLicenseId' => $request->query('licenseId'), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function searchLicenses(Request $request) { $query = trim((string) $request->query('q', '')); if (mb_strlen($query) < 2 || mb_strlen($query) > 180) { return response()->json(['licenses' => []]); } try { return response()->json(['licenses' => $this->cloud->searchLicenses($query)]); } catch (AgentCloudException $exception) { $status = $exception->getCode() >= 400 ? $exception->getCode() : 502; return response()->json(['message' => $exception->getMessage()], $status); } } public function licenseLookup(Request $request): View|RedirectResponse { $query = trim((string) $request->query('q', '')); $licenses = []; $lookupError = null; if (mb_strlen($query) >= 2) { try { $licenses = $this->cloud->searchLicenses($query, true); } catch (AgentCloudException $exception) { $lookupError = $exception->getMessage(); } } return view('agents.licenses.lookup', [ 'query' => $query, 'licenses' => $licenses, 'lookupError' => $lookupError, 'user' => session('agent.user', []), ]); } public function assignLicenseForm(string $licenseId): View|RedirectResponse { $this->ensureLicenseManager(); try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['license' => 'No se encontró la licencia seleccionada.']); } return view('agents.licenses.assign', [ 'license' => $license, 'resellers' => $this->cloud->resellers(), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function registerSaleForm(string $licenseId): View|RedirectResponse { try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['sale' => 'No se encontró la licencia seleccionada.']); } $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Se puede abrir la acción aunque temporalmente no haya cotización. } return view('agents.licenses.sale', [ 'license' => $license, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicenses(Request $request): RedirectResponse { $payload = $request->validate([ 'quantity' => ['required', 'integer', 'in:1'], 'activationLimit' => ['nullable', 'integer', 'in:10'], 'businessName' => ['required', 'string', 'min:2', 'max:120'], 'ownerEmail' => ['required', 'email'], 'password' => ['required', 'string', 'min:8', 'max:200'], 'resellerId' => ['nullable', 'string'], ]); // Una emisión representa una sola suscripción; el plazo comienza al activar. $payload['quantity'] = 1; $payload['activationLimit'] = 10; // El inventario siempre se determina por la cuenta autenticada. No se // permite elegir otro propietario desde el formulario web. unset($payload['resellerId']); if (session('agent.user.role') === 'RESELLER') { try { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); if ($availableInventory === 0) { return back()->withInput()->withErrors(['license' => 'No tenés licencias disponibles en tu inventario para generar otra.']); } if ($availableInventory < (int) $payload['quantity']) { return back()->withInput()->withErrors(['license' => "Solo tenés {$availableInventory} licencia(s) disponible(s) en tu inventario."]); } } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } try { $data = $this->cloud->generateLicenses($payload); $generated = $data['licenses'] ?? []; $keys = array_values(array_filter(array_column($generated, 'licenseKey'))); $licenseDetails = collect($generated)->map(function (array $license): array { return [ 'licenseKey' => $license['licenseKey'] ?? null, 'businessName' => data_get($license, 'business.name'), 'ownerEmail' => $license['ownerEmail'] ?? null, 'activationLimit' => $license['activationLimit'] ?? 1, 'expiresAt' => $license['expiresAt'] ?? null, ]; })->values()->all(); return redirect()->route('agents.licenses')->with([ 'success' => count($generated) . ' licencia(s) generada(s).', 'license_keys' => $keys, 'license_details' => $licenseDetails, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['license' => $exception->getMessage()]); } } public function trialLicenseForm(): View|RedirectResponse { try { return view('agents.licenses.trial', [ 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createTrialLicense(Request $request): RedirectResponse { $payload = $request->validate([ 'ownerEmail' => ['required', 'email'], 'days' => ['required', 'integer', 'min:1', 'max:30'], 'profile' => ['required', 'in:full,custom'], 'modules' => ['nullable', 'string', 'max:2000'], 'referralResellerId' => ['nullable', 'string', 'max:80'], ]); $payload['modules'] = $payload['profile'] === 'custom' ? array_values(array_filter(array_unique(array_map('trim', explode(',', (string) ($payload['modules'] ?? '')))))) : null; unset($payload['profile']); if (session('agent.user.role') === 'RESELLER') { unset($payload['referralResellerId']); } try { $data = $this->cloud->createTrial($payload); $license = $data['license'] ?? []; return redirect()->route('agents.licenses')->with([ 'success' => 'Licencia de prueba emitida. No consume inventario Tipo 1.', 'license_keys' => array_values(array_filter([$license['licenseKey'] ?? null])), 'trial_details' => [ 'licenseKey' => $license['licenseKey'] ?? null, 'ownerEmail' => $license['ownerEmail'] ?? $payload['ownerEmail'], 'trialDays' => $license['trialDays'] ?? $payload['days'], 'expiresAt' => $license['expiresAt'] ?? null, ], ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['trial' => $exception->getMessage()]); } } public function assignLicense(string $licenseId, Request $request): RedirectResponse { $this->ensureSuperAdmin(); $payload = $request->validate(['resellerId' => ['required', 'string']]); try { $this->cloud->assignLicense($licenseId, $payload['resellerId']); return redirect()->route('agents.licenses')->with('success', 'Licencia asignada al agente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['license' => $exception->getMessage()]); } } public function provisionTenant(Request $request): RedirectResponse { $payload = $request->validate([ 'licenseId' => ['nullable', 'string', 'min:8', 'max:80'], 'licenseKey' => ['nullable', 'string', 'min:12', 'max:2048'], 'email' => ['required', 'email'], 'name' => ['required', 'string', 'min:2', 'max:120'], 'resellerId' => ['nullable', 'string', 'max:80'], ]); if (empty($payload['licenseId']) && empty($payload['licenseKey'])) { return back()->withInput()->withErrors(['tenant' => 'Seleccioná una licencia disponible.']); } if (session('agent.user.role') === 'RESELLER') { unset($payload['resellerId']); } try { $data = $this->cloud->provisionTenant($payload); return redirect()->route('agents.commerce', $data['tenant']['id'] ?? '')->with('success', 'Comercio vinculado correctamente.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['tenant' => $exception->getMessage()]); } } public function registerSale(string $licenseId, Request $request): RedirectResponse { $payload = $request->validate([ 'saleAmount' => ['nullable', 'numeric', 'min:0'], 'customerEmail' => ['nullable', 'email'], 'currency' => ['nullable', 'string', 'max:8'], 'notes' => ['nullable', 'string', 'max:1000'], ]); try { $data = $this->cloud->convertLicenseToMax($licenseId, $payload); $businessId = data_get($data, 'license.business.id') ?: data_get($data, 'license.businessId'); if ($businessId) { return redirect()->route('agents.commerce', $businessId)->with('success', 'Licencia convertida a COMERSOFT Sistema Completo. El correo titular recibira la nueva clave.'); } return redirect()->route('agents.dashboard')->with('success', 'Licencia convertida a COMERSOFT Sistema Completo.'); return redirect()->route('agents.sales')->with('success', 'Venta registrada como pendiente de liquidación.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['sale' => $exception->getMessage()]); } } public function sales(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Las ventas historicas siguen visibles aunque falle la cotizacion en vivo. } return view('agents.sales.index', [ 'sales' => $this->cloud->sales(), 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenseRequests(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La bandeja conserva su operacion aunque falle la cotizacion. } $accessRequests = $this->cloud->accessRequests('PENDING'); $freeRequests = []; if (in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true)) { $freeRequests = $this->cloud->publicLicenseRequests('PENDING'); } return view('agents.license-requests', [ 'requests' => $accessRequests, 'freeRequests' => $freeRequests, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function approveAccessRequest(string $requestId): RedirectResponse { try { $this->cloud->approveAccessRequest($requestId, session('agent.user.resellerId')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud aprobada y licencia comercial vinculada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function rejectAccessRequest(string $requestId, Request $request): RedirectResponse { try { $this->cloud->rejectAccessRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function approvePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureLicenseManager(); try { $data = $this->cloud->approvePublicLicenseRequest($requestId); $licenseKey = data_get($data, 'license.licenseKey'); return redirect()->route('agents.license.requests')->with([ 'success' => 'Cuenta confirmada y Licencia histórica emitida.', 'license_keys' => $licenseKey ? [$licenseKey] : [], ]); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function rejectPublicLicenseRequest(string $requestId, Request $request): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->rejectPublicLicenseRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud de Licencia histórica rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function deletePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deletePublicLicenseRequest($requestId); return redirect()->route('agents.license.requests')->with('success', 'Solicitud y cuenta eliminadas correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function saleAction(string $saleId): View|RedirectResponse { try { $sale = collect($this->cloud->sales())->firstWhere('id', $saleId); if (! $sale) { return redirect()->route('agents.sales')->withErrors(['sale' => 'No se encontró la venta seleccionada.']); } return view('agents.sales.action', [ 'sale' => $sale, 'saleStatus' => $sale['status'] ?? 'PENDING', 'canApprove' => in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function updateSaleStatus(string $saleId, Request $request): RedirectResponse { $this->ensureLicenseManager(); $payload = $request->validate(['status' => ['required', 'in:PENDING,PAID,CANCELLED']]); try { $this->cloud->updateSaleStatus($saleId, $payload['status']); return redirect()->route('agents.sales')->with('success', 'Estado de la venta actualizado.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function deleteSale(string $saleId): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->deleteSale($saleId); return redirect()->route('agents.sales')->with('success', 'Venta eliminada correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function commerce(string $id): View|RedirectResponse { try { return view('agents.commerce.show', ['commerce' => $this->cloud->tenant($id)]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function deleteCommerce(string $id): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deleteTenant($id); return redirect()->route('agents.dashboard')->with('success', 'Comercio eliminado correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['commerce' => $exception->getMessage()]); } } public function agents(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.index', ['resellers' => $this->visibleManagedResellers($user), 'user' => $user, 'hierarchyAdmin' => $this->isHierarchyAdmin()]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentsMap(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); $allResellers = $this->cloud->resellers(); $resellers = $this->visibleManagedResellers($user); if (! empty($user['resellerId'])) { $manager = collect($allResellers)->firstWhere('id', $user['resellerId']); if ($manager) { $hierarchy = collect([$manager]); $ancestor = $manager; while (! empty($ancestor['parentId'])) { $ancestor = collect($allResellers)->firstWhere('id', $ancestor['parentId']); if (! $ancestor) break; $hierarchy->prepend($ancestor); } $resellers = $hierarchy->merge($resellers)->unique('id')->values()->all(); } } $nodes = collect($resellers)->map(function (array $reseller) use ($resellers) { $coordinates = $this->resellerCoordinates($reseller, $resellers); $level = (int) ($reseller['level'] ?? 3); return [ 'id' => $reseller['id'], 'parentId' => $reseller['parentId'] ?? null, 'name' => $reseller['name'], 'email' => $reseller['email'], 'level' => $level, 'levelLabel' => [1 => 'Nacional', 2 => 'Provincial / Estatal', 3 => 'Ciudad / Zona'][$level] ?? 'Socio', 'country' => $reseller['country'] ?? 'AR', 'province' => $reseller['province'] ?? null, 'city' => $reseller['city'] ?? null, 'zone' => $reseller['zone'] ?? null, 'status' => $reseller['status'] ?? 'ACTIVE', 'commissionPercent' => $reseller['commissionPercent'] ?? 70, 'counts' => $reseller['counts'] ?? [], 'latitude' => $coordinates['latitude'], 'longitude' => $coordinates['longitude'], 'location' => collect([$reseller['province'] ?? null, $reseller['city'] ?? null, $reseller['zone'] ?? null])->filter()->implode(' · ') ?: (($reseller['country'] ?? 'AR') === 'AR' ? 'Argentina' : ($reseller['country'] ?? '')), ]; })->values()->all(); return view('agents.agents.map', [ 'nodes' => $nodes, 'managerName' => 'COMERSOFT Global', 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentDetail(string $id): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); if (($user['resellerId'] ?? null) === $id) { return redirect()->route('agents.agents'); } $resellers = $this->cloud->resellers(); $reseller = collect($resellers)->firstWhere('id', $id); if (! $reseller) { abort(404, 'Socio no encontrado.'); } $summary = $this->cloud->resellerSummary($id); return view('agents.agents.show', [ 'reseller' => $reseller, 'children' => collect($resellers)->where('parentId', $id)->values()->all(), 'summary' => $summary, 'users' => $summary['users'] ?? [], 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgentForm(Request $request): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.create', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : $this->visibleManagedResellers($user), 'selectedParentId' => $request->query('parentId') ?: data_get($user, 'resellerId'), 'hierarchyAdmin' => $this->isHierarchyAdmin(), 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgent(Request $request): RedirectResponse { $this->ensureHierarchyAdmin(); $payload = $request->validate([ 'name' => ['required', 'string', 'min:2', 'max:120'], 'email' => ['required', 'email'], 'companyName' => ['nullable', 'string', 'max:160'], 'phone' => ['nullable', 'string', 'max:40'], 'level' => ['required', 'integer', 'in:1,2,3'], 'country' => ['required', 'string', 'size:2'], 'province' => ['nullable', 'string', 'max:120'], 'city' => ['nullable', 'string', 'max:120'], 'zone' => ['nullable', 'string', 'max:120'], 'parentId' => ['nullable', 'string', 'max:80'], 'password' => ['nullable', 'string', 'min:12', 'max:200'], ]); if (session('agent.user.role') === 'RESELLER') { $payload['parentId'] = session('agent.user.resellerId'); $payload['level'] = max(2, (int) data_get(session('agent.user'), 'reseller.level', 1) + 1); } $payload['commissionPercent'] = 70; try { $data = $this->cloud->createReseller($payload); return redirect()->route('agents.agents')->with([ 'success' => 'Agente creado correctamente con 1.000 licencias disponibles.', 'temporary_password' => $data['temporaryPassword'] ?? null, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['agent' => $exception->getMessage()]); } } private function resellerCoordinates(array $reseller, array $resellers = []): array { $normalize = static function (?string $value): string { return strtr(mb_strtolower(trim((string) $value)), [ 'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u', 'ü' => 'u', 'ñ' => 'n', ]); }; $locations = [ 'buenos aires' => [-36.67, -60.56], 'caba' => [-34.60, -58.38], 'ciudad autonoma de buenos aires' => [-34.60, -58.38], 'cordoba' => [-31.42, -64.19], 'santa fe' => [-31.63, -60.70], 'mendoza' => [-32.89, -68.84], 'tucuman' => [-26.82, -65.22], 'entre rios' => [-31.74, -60.51], 'salta' => [-24.78, -65.41], 'misiones' => [-27.36, -55.90], 'chaco' => [-27.45, -58.99], 'corrientes' => [-27.47, -58.83], 'jujuy' => [-24.18, -65.30], 'la pampa' => [-36.62, -64.29], 'neuquen' => [-38.95, -68.06], 'rio negro' => [-40.81, -63.00], 'chubut' => [-43.30, -65.10], 'santa cruz' => [-50.00, -68.00], 'tierra del fuego' => [-54.80, -68.30], 'san juan' => [-31.54, -68.52], 'san luis' => [-33.30, -66.34], 'la rioja' => [-29.41, -66.86], 'catamarca' => [-28.47, -65.78], 'formosa' => [-26.18, -58.18], 'santiago del estero' => [-27.78, -64.26], ]; $city = $normalize($reseller['city'] ?? null); $province = $normalize($reseller['province'] ?? null); if ($city && isset($locations[$city])) return ['latitude' => $locations[$city][0], 'longitude' => $locations[$city][1]]; if ($province && isset($locations[$province])) return ['latitude' => $locations[$province][0], 'longitude' => $locations[$province][1]]; if (! empty($reseller['parentId'])) { $parent = collect($resellers)->firstWhere('id', $reseller['parentId']); if ($parent) return $this->resellerCoordinates($parent, $resellers); } return ($reseller['country'] ?? 'AR') === 'AR' ? ['latitude' => -34.6037, 'longitude' => -58.3816] : ['latitude' => 0, 'longitude' => 0]; } private function visibleManagedResellers(array $user): array { $currentResellerId = $user['resellerId'] ?? null; return collect($this->cloud->resellers()) ->reject(fn (array $reseller) => $currentResellerId && ($reseller['id'] ?? null) === $currentResellerId) ->values() ->all(); } private function ensureSuperAdmin(): void { abort_unless(session('agent.user.role') === 'SUPERADMIN', 403); } private function ensureLicenseManager(): void { abort_unless(in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 403); } private function isHierarchyAdmin(): bool { return session('agent.user.role') === 'SUPERADMIN' || (session('agent.user.role') === 'RESELLER' && (int) data_get(session('agent.user'), 'reseller.level', 0) === 1); } private function ensureHierarchyAdmin(): void { abort_unless($this->isHierarchyAdmin(), 403); } private function handleCloudError(AgentCloudException $exception): RedirectResponse { if ($exception->getCode() === 401) { session()->forget(['agent.access_token', 'agent.user']); return redirect()->route('agents.login')->withErrors(['email' => $exception->getMessage()]); } abort(502, $exception->getMessage()); } } route('agents.dashboard'); } return view('agents.login'); } public function login(Request $request): RedirectResponse { $credentials = $request->validate([ 'email' => ['required', 'email'], 'password' => ['required', 'string', 'min:8'], ]); try { $data = $this->cloud->login($credentials['email'], $credentials['password']); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withInput($request->only('email'))->withErrors(['email' => $exception->getMessage()]); } } public function quickLogin(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login'); } public function quickLoginEliezer(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login_eliezer'); } private function performQuickLogin(Request $request, string $configKey): RedirectResponse { abort_unless(app()->environment('local') && config("comersoft.{$configKey}.enabled"), 404); $email = (string) config("comersoft.{$configKey}.email"); $password = (string) config("comersoft.{$configKey}.password"); if ($email === '' || $password === '') { return back()->withErrors(['email' => 'El inicio rápido no está configurado.']); } try { $data = $this->cloud->login($email, $password); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withErrors(['email' => $exception->getMessage()]); } } public function logout(Request $request): RedirectResponse { $request->session()->forget(['agent.access_token', 'agent.user']); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect()->route('agents.login'); } public function dashboard(): View|RedirectResponse { try { $user = session('agent.user', []); $data = $this->cloud->dashboard($user['resellerId'] ?? null); $allLicenses = $data['licenses'] ?? []; $allSales = $data['sales'] ?? []; $allTenants = $data['tenants'] ?? []; $salesByLicense = collect($allSales) ->filter(fn (array $sale) => data_get($sale, 'licenseId') || data_get($sale, 'license.id')) ->keyBy(fn (array $sale) => data_get($sale, 'licenseId') ?: data_get($sale, 'license.id')); $licensesByBusiness = collect($allLicenses) ->filter(fn (array $license) => data_get($license, 'businessId') || data_get($license, 'business.email')) ->groupBy(fn (array $license) => data_get($license, 'businessId') ?: strtolower((string) data_get($license, 'business.email'))); $managedCommerces = collect($allTenants) ->map(function (array $tenant) use ($salesByLicense, $licensesByBusiness) { $tenantLicenses = collect($tenant['licenses'] ?? []); $license = $tenantLicenses->sortByDesc('issuedAt')->first(); if (! $license) { $license = $licensesByBusiness ->get($tenant['id'] ?? null, collect()) ->sortByDesc('issuedAt') ->first(); } if (! $license && ! empty($tenant['email'])) { $license = $licensesByBusiness ->get(strtolower((string) $tenant['email']), collect()) ->sortByDesc('issuedAt') ->first(); } $sale = $license ? $salesByLicense->get($license['id'] ?? null) : null; $licenseType = (int) data_get($license, 'type', 0); return [ 'tenant' => $tenant, 'license' => $license, 'sale' => $sale, 'customerEmail' => data_get($sale, 'customerEmail') ?: data_get($license, 'ownerEmail') ?: data_get($license, 'business.email') ?: ($tenant['email'] ?? null), 'licenseTypeLabel' => match ($licenseType) { 1 => 'COMERSOFT Sistema Completo', 2 => 'Licencia de prueba', 3 => 'COMERSOFT Versión con Sync', 4 => 'COMERSOFT Versión con IA', default => 'Licencia histórica', }, 'licenseOwner' => data_get($license, 'reseller.name') ?: data_get($license, 'owner.name') ?: 'COMERSOFT raíz', 'licenseOwnerEmail' => data_get($license, 'reseller.email') ?: data_get($license, 'owner.email') ?: 'admin@comersoft.com', ]; }) ->values() ->all(); return view('agents.dashboard', [ 'user' => $user, 'summary' => $data['summary'] ?? [], 'licenses' => array_slice($allLicenses, 0, 8), 'sales' => array_slice($allSales, 0, 8), 'tenants' => array_slice($allTenants, 0, 8), 'managedCommerces' => $managedCommerces, 'licenseCount' => count($allLicenses), 'salesCount' => count($allSales), 'tenantCount' => count($allTenants), 'pendingSales' => count(array_filter($allSales, fn ($sale) => ($sale['status'] ?? null) === 'PENDING')), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenses(): View|RedirectResponse { try { $licenses = $this->cloud->licenses(); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); $available = $items->firstWhere('status', 'AVAILABLE'); return [ 'owner' => $first['owner'] ?? ['id' => null, 'name' => 'COMERSOFT raíz', 'type' => 'COMERSOFT_ROOT'], 'reseller' => $first['reseller'] ?? null, 'total' => $items->count(), 'available' => $items->where('status', 'AVAILABLE')->count(), 'active' => $items->where('status', 'ACTIVE')->count(), 'sold' => $items->filter(fn (array $license) => ! empty($license['sale']))->count(), 'sampleAvailableId' => $available['id'] ?? null, 'sampleLicenseId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.index', [ 'licenseGroups' => $licenseGroups, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function subscriptions(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La pantalla conserva el plan aunque momentáneamente no haya cotización. } return view('agents.subscriptions.index', [ 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicensesForm(): View|RedirectResponse { try { $user = session('agent.user', []); $availableInventory = null; if (($user['role'] ?? null) === 'RESELLER') { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); } return view('agents.licenses.generate', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'availableInventory' => $availableInventory, 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function provisionTenantForm(Request $request): View|RedirectResponse { try { return view('agents.licenses.provision', [ 'selectedLicenseId' => $request->query('licenseId'), 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); $licenses = array_values(array_filter( $this->cloud->licenses(), fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' )); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); return [ 'owner' => $first['owner'] ?? ['name' => 'COMERSOFT raíz'], 'available' => $items->count(), 'sampleAvailableId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.provision', [ 'licenseGroups' => $licenseGroups, 'selectedLicenseId' => $request->query('licenseId'), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function searchLicenses(Request $request) { $query = trim((string) $request->query('q', '')); if (mb_strlen($query) < 2 || mb_strlen($query) > 180) { return response()->json(['licenses' => []]); } try { return response()->json(['licenses' => $this->cloud->searchLicenses($query)]); } catch (AgentCloudException $exception) { $status = $exception->getCode() >= 400 ? $exception->getCode() : 502; return response()->json(['message' => $exception->getMessage()], $status); } } public function licenseLookup(Request $request): View|RedirectResponse { $query = trim((string) $request->query('q', '')); $licenses = []; $lookupError = null; if (mb_strlen($query) >= 2) { try { $licenses = $this->cloud->searchLicenses($query, true); } catch (AgentCloudException $exception) { $lookupError = $exception->getMessage(); } } return view('agents.licenses.lookup', [ 'query' => $query, 'licenses' => $licenses, 'lookupError' => $lookupError, 'user' => session('agent.user', []), ]); } public function assignLicenseForm(string $licenseId): View|RedirectResponse { $this->ensureLicenseManager(); try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['license' => 'No se encontró la licencia seleccionada.']); } return view('agents.licenses.assign', [ 'license' => $license, 'resellers' => $this->cloud->resellers(), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function registerSaleForm(string $licenseId): View|RedirectResponse { try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['sale' => 'No se encontró la licencia seleccionada.']); } $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Se puede abrir la acción aunque temporalmente no haya cotización. } return view('agents.licenses.sale', [ 'license' => $license, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicenses(Request $request): RedirectResponse { $payload = $request->validate([ 'quantity' => ['required', 'integer', 'in:1'], 'activationLimit' => ['nullable', 'integer', 'in:10'], 'businessName' => ['required', 'string', 'min:2', 'max:120'], 'ownerEmail' => ['required', 'email'], 'password' => ['required', 'string', 'min:8', 'max:200'], 'resellerId' => ['nullable', 'string'], ]); // Una emisión representa una sola suscripción; el plazo comienza al activar. $payload['quantity'] = 1; $payload['activationLimit'] = 10; // El inventario siempre se determina por la cuenta autenticada. No se // permite elegir otro propietario desde el formulario web. unset($payload['resellerId']); if (session('agent.user.role') === 'RESELLER') { try { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); if ($availableInventory === 0) { return back()->withInput()->withErrors(['license' => 'No tenés licencias disponibles en tu inventario para generar otra.']); } if ($availableInventory < (int) $payload['quantity']) { return back()->withInput()->withErrors(['license' => "Solo tenés {$availableInventory} licencia(s) disponible(s) en tu inventario."]); } } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } try { $data = $this->cloud->generateLicenses($payload); $generated = $data['licenses'] ?? []; $keys = array_values(array_filter(array_column($generated, 'licenseKey'))); $licenseDetails = collect($generated)->map(function (array $license): array { return [ 'licenseKey' => $license['licenseKey'] ?? null, 'businessName' => data_get($license, 'business.name'), 'ownerEmail' => $license['ownerEmail'] ?? null, 'activationLimit' => $license['activationLimit'] ?? 1, 'expiresAt' => $license['expiresAt'] ?? null, ]; })->values()->all(); return redirect()->route('agents.licenses')->with([ 'success' => count($generated) . ' licencia(s) generada(s).', 'license_keys' => $keys, 'license_details' => $licenseDetails, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['license' => $exception->getMessage()]); } } public function trialLicenseForm(): View|RedirectResponse { try { return view('agents.licenses.trial', [ 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createTrialLicense(Request $request): RedirectResponse { $payload = $request->validate([ 'ownerEmail' => ['required', 'email'], 'days' => ['required', 'integer', 'min:1', 'max:30'], 'profile' => ['required', 'in:full,custom'], 'modules' => ['nullable', 'string', 'max:2000'], 'referralResellerId' => ['nullable', 'string', 'max:80'], ]); $payload['modules'] = $payload['profile'] === 'custom' ? array_values(array_filter(array_unique(array_map('trim', explode(',', (string) ($payload['modules'] ?? '')))))) : null; unset($payload['profile']); if (session('agent.user.role') === 'RESELLER') { unset($payload['referralResellerId']); } try { $data = $this->cloud->createTrial($payload); $license = $data['license'] ?? []; return redirect()->route('agents.licenses')->with([ 'success' => 'Licencia de prueba emitida. No consume inventario Tipo 1.', 'license_keys' => array_values(array_filter([$license['licenseKey'] ?? null])), 'trial_details' => [ 'licenseKey' => $license['licenseKey'] ?? null, 'ownerEmail' => $license['ownerEmail'] ?? $payload['ownerEmail'], 'trialDays' => $license['trialDays'] ?? $payload['days'], 'expiresAt' => $license['expiresAt'] ?? null, ], ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['trial' => $exception->getMessage()]); } } public function assignLicense(string $licenseId, Request $request): RedirectResponse { $this->ensureSuperAdmin(); $payload = $request->validate(['resellerId' => ['required', 'string']]); try { $this->cloud->assignLicense($licenseId, $payload['resellerId']); return redirect()->route('agents.licenses')->with('success', 'Licencia asignada al agente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['license' => $exception->getMessage()]); } } public function provisionTenant(Request $request): RedirectResponse { $payload = $request->validate([ 'licenseId' => ['nullable', 'string', 'min:8', 'max:80'], 'licenseKey' => ['nullable', 'string', 'min:12', 'max:2048'], 'email' => ['required', 'email'], 'name' => ['required', 'string', 'min:2', 'max:120'], 'resellerId' => ['nullable', 'string', 'max:80'], ]); if (empty($payload['licenseId']) && empty($payload['licenseKey'])) { return back()->withInput()->withErrors(['tenant' => 'Seleccioná una licencia disponible.']); } if (session('agent.user.role') === 'RESELLER') { unset($payload['resellerId']); } try { $data = $this->cloud->provisionTenant($payload); return redirect()->route('agents.commerce', $data['tenant']['id'] ?? '')->with('success', 'Comercio vinculado correctamente.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['tenant' => $exception->getMessage()]); } } public function registerSale(string $licenseId, Request $request): RedirectResponse { $payload = $request->validate([ 'saleAmount' => ['nullable', 'numeric', 'min:0'], 'customerEmail' => ['nullable', 'email'], 'currency' => ['nullable', 'string', 'max:8'], 'notes' => ['nullable', 'string', 'max:1000'], ]); try { $data = $this->cloud->convertLicenseToMax($licenseId, $payload); $businessId = data_get($data, 'license.business.id') ?: data_get($data, 'license.businessId'); if ($businessId) { return redirect()->route('agents.commerce', $businessId)->with('success', 'Licencia convertida a COMERSOFT Sistema Completo. El correo titular recibira la nueva clave.'); } return redirect()->route('agents.dashboard')->with('success', 'Licencia convertida a COMERSOFT Sistema Completo.'); return redirect()->route('agents.sales')->with('success', 'Venta registrada como pendiente de liquidación.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['sale' => $exception->getMessage()]); } } public function sales(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Las ventas historicas siguen visibles aunque falle la cotizacion en vivo. } return view('agents.sales.index', [ 'sales' => $this->cloud->sales(), 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenseRequests(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La bandeja conserva su operacion aunque falle la cotizacion. } $accessRequests = $this->cloud->accessRequests('PENDING'); $freeRequests = []; if (in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true)) { $freeRequests = $this->cloud->publicLicenseRequests('PENDING'); } return view('agents.license-requests', [ 'requests' => $accessRequests, 'freeRequests' => $freeRequests, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function approveAccessRequest(string $requestId): RedirectResponse { try { $this->cloud->approveAccessRequest($requestId, session('agent.user.resellerId')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud aprobada y licencia comercial vinculada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function rejectAccessRequest(string $requestId, Request $request): RedirectResponse { try { $this->cloud->rejectAccessRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function approvePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureLicenseManager(); try { $data = $this->cloud->approvePublicLicenseRequest($requestId); $licenseKey = data_get($data, 'license.licenseKey'); return redirect()->route('agents.license.requests')->with([ 'success' => 'Cuenta confirmada y Licencia histórica emitida.', 'license_keys' => $licenseKey ? [$licenseKey] : [], ]); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function rejectPublicLicenseRequest(string $requestId, Request $request): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->rejectPublicLicenseRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud de Licencia histórica rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function deletePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deletePublicLicenseRequest($requestId); return redirect()->route('agents.license.requests')->with('success', 'Solicitud y cuenta eliminadas correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function saleAction(string $saleId): View|RedirectResponse { try { $sale = collect($this->cloud->sales())->firstWhere('id', $saleId); if (! $sale) { return redirect()->route('agents.sales')->withErrors(['sale' => 'No se encontró la venta seleccionada.']); } return view('agents.sales.action', [ 'sale' => $sale, 'saleStatus' => $sale['status'] ?? 'PENDING', 'canApprove' => in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function updateSaleStatus(string $saleId, Request $request): RedirectResponse { $this->ensureLicenseManager(); $payload = $request->validate(['status' => ['required', 'in:PENDING,PAID,CANCELLED']]); try { $this->cloud->updateSaleStatus($saleId, $payload['status']); return redirect()->route('agents.sales')->with('success', 'Estado de la venta actualizado.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function deleteSale(string $saleId): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->deleteSale($saleId); return redirect()->route('agents.sales')->with('success', 'Venta eliminada correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function commerce(string $id): View|RedirectResponse { try { return view('agents.commerce.show', ['commerce' => $this->cloud->tenant($id)]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function deleteCommerce(string $id): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deleteTenant($id); return redirect()->route('agents.dashboard')->with('success', 'Comercio eliminado correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['commerce' => $exception->getMessage()]); } } public function agents(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.index', ['resellers' => $this->visibleManagedResellers($user), 'user' => $user, 'hierarchyAdmin' => $this->isHierarchyAdmin()]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentsMap(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); $allResellers = $this->cloud->resellers(); $resellers = $this->visibleManagedResellers($user); if (! empty($user['resellerId'])) { $manager = collect($allResellers)->firstWhere('id', $user['resellerId']); if ($manager) { $hierarchy = collect([$manager]); $ancestor = $manager; while (! empty($ancestor['parentId'])) { $ancestor = collect($allResellers)->firstWhere('id', $ancestor['parentId']); if (! $ancestor) break; $hierarchy->prepend($ancestor); } $resellers = $hierarchy->merge($resellers)->unique('id')->values()->all(); } } $nodes = collect($resellers)->map(function (array $reseller) use ($resellers) { $coordinates = $this->resellerCoordinates($reseller, $resellers); $level = (int) ($reseller['level'] ?? 3); return [ 'id' => $reseller['id'], 'parentId' => $reseller['parentId'] ?? null, 'name' => $reseller['name'], 'email' => $reseller['email'], 'level' => $level, 'levelLabel' => [1 => 'Nacional', 2 => 'Provincial / Estatal', 3 => 'Ciudad / Zona'][$level] ?? 'Socio', 'country' => $reseller['country'] ?? 'AR', 'province' => $reseller['province'] ?? null, 'city' => $reseller['city'] ?? null, 'zone' => $reseller['zone'] ?? null, 'status' => $reseller['status'] ?? 'ACTIVE', 'commissionPercent' => $reseller['commissionPercent'] ?? 70, 'counts' => $reseller['counts'] ?? [], 'latitude' => $coordinates['latitude'], 'longitude' => $coordinates['longitude'], 'location' => collect([$reseller['province'] ?? null, $reseller['city'] ?? null, $reseller['zone'] ?? null])->filter()->implode(' · ') ?: (($reseller['country'] ?? 'AR') === 'AR' ? 'Argentina' : ($reseller['country'] ?? '')), ]; })->values()->all(); return view('agents.agents.map', [ 'nodes' => $nodes, 'managerName' => 'COMERSOFT Global', 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentDetail(string $id): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); if (($user['resellerId'] ?? null) === $id) { return redirect()->route('agents.agents'); } $resellers = $this->cloud->resellers(); $reseller = collect($resellers)->firstWhere('id', $id); if (! $reseller) { abort(404, 'Socio no encontrado.'); } $summary = $this->cloud->resellerSummary($id); return view('agents.agents.show', [ 'reseller' => $reseller, 'children' => collect($resellers)->where('parentId', $id)->values()->all(), 'summary' => $summary, 'users' => $summary['users'] ?? [], 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgentForm(Request $request): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.create', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : $this->visibleManagedResellers($user), 'selectedParentId' => $request->query('parentId') ?: data_get($user, 'resellerId'), 'hierarchyAdmin' => $this->isHierarchyAdmin(), 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgent(Request $request): RedirectResponse { $this->ensureHierarchyAdmin(); $payload = $request->validate([ 'name' => ['required', 'string', 'min:2', 'max:120'], 'email' => ['required', 'email'], 'companyName' => ['nullable', 'string', 'max:160'], 'phone' => ['nullable', 'string', 'max:40'], 'level' => ['required', 'integer', 'in:1,2,3'], 'country' => ['required', 'string', 'size:2'], 'province' => ['nullable', 'string', 'max:120'], 'city' => ['nullable', 'string', 'max:120'], 'zone' => ['nullable', 'string', 'max:120'], 'parentId' => ['nullable', 'string', 'max:80'], 'password' => ['nullable', 'string', 'min:12', 'max:200'], ]); if (session('agent.user.role') === 'RESELLER') { $payload['parentId'] = session('agent.user.resellerId'); $payload['level'] = max(2, (int) data_get(session('agent.user'), 'reseller.level', 1) + 1); } $payload['commissionPercent'] = 70; try { $data = $this->cloud->createReseller($payload); return redirect()->route('agents.agents')->with([ 'success' => 'Agente creado correctamente con 1.000 licencias disponibles.', 'temporary_password' => $data['temporaryPassword'] ?? null, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['agent' => $exception->getMessage()]); } } private function resellerCoordinates(array $reseller, array $resellers = []): array { $normalize = static function (?string $value): string { return strtr(mb_strtolower(trim((string) $value)), [ 'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u', 'ü' => 'u', 'ñ' => 'n', ]); }; $locations = [ 'buenos aires' => [-36.67, -60.56], 'caba' => [-34.60, -58.38], 'ciudad autonoma de buenos aires' => [-34.60, -58.38], 'cordoba' => [-31.42, -64.19], 'santa fe' => [-31.63, -60.70], 'mendoza' => [-32.89, -68.84], 'tucuman' => [-26.82, -65.22], 'entre rios' => [-31.74, -60.51], 'salta' => [-24.78, -65.41], 'misiones' => [-27.36, -55.90], 'chaco' => [-27.45, -58.99], 'corrientes' => [-27.47, -58.83], 'jujuy' => [-24.18, -65.30], 'la pampa' => [-36.62, -64.29], 'neuquen' => [-38.95, -68.06], 'rio negro' => [-40.81, -63.00], 'chubut' => [-43.30, -65.10], 'santa cruz' => [-50.00, -68.00], 'tierra del fuego' => [-54.80, -68.30], 'san juan' => [-31.54, -68.52], 'san luis' => [-33.30, -66.34], 'la rioja' => [-29.41, -66.86], 'catamarca' => [-28.47, -65.78], 'formosa' => [-26.18, -58.18], 'santiago del estero' => [-27.78, -64.26], ]; $city = $normalize($reseller['city'] ?? null); $province = $normalize($reseller['province'] ?? null); if ($city && isset($locations[$city])) return ['latitude' => $locations[$city][0], 'longitude' => $locations[$city][1]]; if ($province && isset($locations[$province])) return ['latitude' => $locations[$province][0], 'longitude' => $locations[$province][1]]; if (! empty($reseller['parentId'])) { $parent = collect($resellers)->firstWhere('id', $reseller['parentId']); if ($parent) return $this->resellerCoordinates($parent, $resellers); } return ($reseller['country'] ?? 'AR') === 'AR' ? ['latitude' => -34.6037, 'longitude' => -58.3816] : ['latitude' => 0, 'longitude' => 0]; } private function visibleManagedResellers(array $user): array { $currentResellerId = $user['resellerId'] ?? null; return collect($this->cloud->resellers()) ->reject(fn (array $reseller) => $currentResellerId && ($reseller['id'] ?? null) === $currentResellerId) ->values() ->all(); } private function ensureSuperAdmin(): void { abort_unless(session('agent.user.role') === 'SUPERADMIN', 403); } private function ensureLicenseManager(): void { abort_unless(in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 403); } private function isHierarchyAdmin(): bool { return session('agent.user.role') === 'SUPERADMIN' || (session('agent.user.role') === 'RESELLER' && (int) data_get(session('agent.user'), 'reseller.level', 0) === 1); } private function ensureHierarchyAdmin(): void { abort_unless($this->isHierarchyAdmin(), 403); } private function handleCloudError(AgentCloudException $exception): RedirectResponse { if ($exception->getCode() === 401) { session()->forget(['agent.access_token', 'agent.user']); return redirect()->route('agents.login')->withErrors(['email' => $exception->getMessage()]); } abort(502, $exception->getMessage()); } } route('agents.dashboard'); } return view('agents.login'); } public function login(Request $request): RedirectResponse { $credentials = $request->validate([ 'email' => ['required', 'email'], 'password' => ['required', 'string', 'min:8'], ]); try { $data = $this->cloud->login($credentials['email'], $credentials['password']); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withInput($request->only('email'))->withErrors(['email' => $exception->getMessage()]); } } public function quickLogin(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login'); } public function quickLoginEliezer(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login_eliezer'); } private function performQuickLogin(Request $request, string $configKey): RedirectResponse { abort_unless(app()->environment('local') && config("comersoft.{$configKey}.enabled"), 404); $email = (string) config("comersoft.{$configKey}.email"); $password = (string) config("comersoft.{$configKey}.password"); if ($email === '' || $password === '') { return back()->withErrors(['email' => 'El inicio rápido no está configurado.']); } try { $data = $this->cloud->login($email, $password); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withErrors(['email' => $exception->getMessage()]); } } public function logout(Request $request): RedirectResponse { $request->session()->forget(['agent.access_token', 'agent.user']); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect()->route('agents.login'); } public function dashboard(): View|RedirectResponse { try { $user = session('agent.user', []); $data = $this->cloud->dashboard($user['resellerId'] ?? null); $allLicenses = $data['licenses'] ?? []; $allSales = $data['sales'] ?? []; $allTenants = $data['tenants'] ?? []; $salesByLicense = collect($allSales) ->filter(fn (array $sale) => data_get($sale, 'licenseId') || data_get($sale, 'license.id')) ->keyBy(fn (array $sale) => data_get($sale, 'licenseId') ?: data_get($sale, 'license.id')); $licensesByBusiness = collect($allLicenses) ->filter(fn (array $license) => data_get($license, 'businessId') || data_get($license, 'business.email')) ->groupBy(fn (array $license) => data_get($license, 'businessId') ?: strtolower((string) data_get($license, 'business.email'))); $managedCommerces = collect($allTenants) ->map(function (array $tenant) use ($salesByLicense, $licensesByBusiness) { $tenantLicenses = collect($tenant['licenses'] ?? []); $license = $tenantLicenses->sortByDesc('issuedAt')->first(); if (! $license) { $license = $licensesByBusiness ->get($tenant['id'] ?? null, collect()) ->sortByDesc('issuedAt') ->first(); } if (! $license && ! empty($tenant['email'])) { $license = $licensesByBusiness ->get(strtolower((string) $tenant['email']), collect()) ->sortByDesc('issuedAt') ->first(); } $sale = $license ? $salesByLicense->get($license['id'] ?? null) : null; $licenseType = (int) data_get($license, 'type', 0); return [ 'tenant' => $tenant, 'license' => $license, 'sale' => $sale, 'customerEmail' => data_get($sale, 'customerEmail') ?: data_get($license, 'ownerEmail') ?: data_get($license, 'business.email') ?: ($tenant['email'] ?? null), 'licenseTypeLabel' => match ($licenseType) { 1 => 'COMERSOFT Sistema Completo', 2 => 'Licencia de prueba', 3 => 'COMERSOFT Versión con Sync', 4 => 'COMERSOFT Versión con IA', default => 'Licencia histórica', }, 'licenseOwner' => data_get($license, 'reseller.name') ?: data_get($license, 'owner.name') ?: 'COMERSOFT raíz', 'licenseOwnerEmail' => data_get($license, 'reseller.email') ?: data_get($license, 'owner.email') ?: 'admin@comersoft.com', ]; }) ->values() ->all(); return view('agents.dashboard', [ 'user' => $user, 'summary' => $data['summary'] ?? [], 'licenses' => array_slice($allLicenses, 0, 8), 'sales' => array_slice($allSales, 0, 8), 'tenants' => array_slice($allTenants, 0, 8), 'managedCommerces' => $managedCommerces, 'licenseCount' => count($allLicenses), 'salesCount' => count($allSales), 'tenantCount' => count($allTenants), 'pendingSales' => count(array_filter($allSales, fn ($sale) => ($sale['status'] ?? null) === 'PENDING')), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenses(): View|RedirectResponse { try { $licenses = $this->cloud->licenses(); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); $available = $items->firstWhere('status', 'AVAILABLE'); return [ 'owner' => $first['owner'] ?? ['id' => null, 'name' => 'COMERSOFT raíz', 'type' => 'COMERSOFT_ROOT'], 'reseller' => $first['reseller'] ?? null, 'total' => $items->count(), 'available' => $items->where('status', 'AVAILABLE')->count(), 'active' => $items->where('status', 'ACTIVE')->count(), 'sold' => $items->filter(fn (array $license) => ! empty($license['sale']))->count(), 'sampleAvailableId' => $available['id'] ?? null, 'sampleLicenseId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.index', [ 'licenseGroups' => $licenseGroups, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function subscriptions(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La pantalla conserva el plan aunque momentáneamente no haya cotización. } return view('agents.subscriptions.index', [ 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicensesForm(): View|RedirectResponse { try { $user = session('agent.user', []); $availableInventory = null; if (($user['role'] ?? null) === 'RESELLER') { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); } return view('agents.licenses.generate', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'availableInventory' => $availableInventory, 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function provisionTenantForm(Request $request): View|RedirectResponse { try { return view('agents.licenses.provision', [ 'selectedLicenseId' => $request->query('licenseId'), 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); $licenses = array_values(array_filter( $this->cloud->licenses(), fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' )); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); return [ 'owner' => $first['owner'] ?? ['name' => 'COMERSOFT raíz'], 'available' => $items->count(), 'sampleAvailableId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.provision', [ 'licenseGroups' => $licenseGroups, 'selectedLicenseId' => $request->query('licenseId'), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function searchLicenses(Request $request) { $query = trim((string) $request->query('q', '')); if (mb_strlen($query) < 2 || mb_strlen($query) > 180) { return response()->json(['licenses' => []]); } try { return response()->json(['licenses' => $this->cloud->searchLicenses($query)]); } catch (AgentCloudException $exception) { $status = $exception->getCode() >= 400 ? $exception->getCode() : 502; return response()->json(['message' => $exception->getMessage()], $status); } } public function licenseLookup(Request $request): View|RedirectResponse { $query = trim((string) $request->query('q', '')); $licenses = []; $lookupError = null; if (mb_strlen($query) >= 2) { try { $licenses = $this->cloud->searchLicenses($query, true); } catch (AgentCloudException $exception) { $lookupError = $exception->getMessage(); } } return view('agents.licenses.lookup', [ 'query' => $query, 'licenses' => $licenses, 'lookupError' => $lookupError, 'user' => session('agent.user', []), ]); } public function assignLicenseForm(string $licenseId): View|RedirectResponse { $this->ensureLicenseManager(); try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['license' => 'No se encontró la licencia seleccionada.']); } return view('agents.licenses.assign', [ 'license' => $license, 'resellers' => $this->cloud->resellers(), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function registerSaleForm(string $licenseId): View|RedirectResponse { try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['sale' => 'No se encontró la licencia seleccionada.']); } $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Se puede abrir la acción aunque temporalmente no haya cotización. } return view('agents.licenses.sale', [ 'license' => $license, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicenses(Request $request): RedirectResponse { $payload = $request->validate([ 'quantity' => ['required', 'integer', 'in:1'], 'activationLimit' => ['nullable', 'integer', 'in:10'], 'businessName' => ['required', 'string', 'min:2', 'max:120'], 'ownerEmail' => ['required', 'email'], 'password' => ['required', 'string', 'min:8', 'max:200'], 'resellerId' => ['nullable', 'string'], ]); // Una emisión representa una sola suscripción; el plazo comienza al activar. $payload['quantity'] = 1; $payload['activationLimit'] = 10; // El inventario siempre se determina por la cuenta autenticada. No se // permite elegir otro propietario desde el formulario web. unset($payload['resellerId']); if (session('agent.user.role') === 'RESELLER') { try { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); if ($availableInventory === 0) { return back()->withInput()->withErrors(['license' => 'No tenés licencias disponibles en tu inventario para generar otra.']); } if ($availableInventory < (int) $payload['quantity']) { return back()->withInput()->withErrors(['license' => "Solo tenés {$availableInventory} licencia(s) disponible(s) en tu inventario."]); } } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } try { $data = $this->cloud->generateLicenses($payload); $generated = $data['licenses'] ?? []; $keys = array_values(array_filter(array_column($generated, 'licenseKey'))); $licenseDetails = collect($generated)->map(function (array $license): array { return [ 'licenseKey' => $license['licenseKey'] ?? null, 'businessName' => data_get($license, 'business.name'), 'ownerEmail' => $license['ownerEmail'] ?? null, 'activationLimit' => $license['activationLimit'] ?? 1, 'expiresAt' => $license['expiresAt'] ?? null, ]; })->values()->all(); return redirect()->route('agents.licenses')->with([ 'success' => count($generated) . ' licencia(s) generada(s).', 'license_keys' => $keys, 'license_details' => $licenseDetails, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['license' => $exception->getMessage()]); } } public function trialLicenseForm(): View|RedirectResponse { try { return view('agents.licenses.trial', [ 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createTrialLicense(Request $request): RedirectResponse { $payload = $request->validate([ 'ownerEmail' => ['required', 'email'], 'days' => ['required', 'integer', 'min:1', 'max:30'], 'profile' => ['required', 'in:full,custom'], 'modules' => ['nullable', 'string', 'max:2000'], 'referralResellerId' => ['nullable', 'string', 'max:80'], ]); $payload['modules'] = $payload['profile'] === 'custom' ? array_values(array_filter(array_unique(array_map('trim', explode(',', (string) ($payload['modules'] ?? '')))))) : null; unset($payload['profile']); if (session('agent.user.role') === 'RESELLER') { unset($payload['referralResellerId']); } try { $data = $this->cloud->createTrial($payload); $license = $data['license'] ?? []; return redirect()->route('agents.licenses')->with([ 'success' => 'Licencia de prueba emitida. No consume inventario Tipo 1.', 'license_keys' => array_values(array_filter([$license['licenseKey'] ?? null])), 'trial_details' => [ 'licenseKey' => $license['licenseKey'] ?? null, 'ownerEmail' => $license['ownerEmail'] ?? $payload['ownerEmail'], 'trialDays' => $license['trialDays'] ?? $payload['days'], 'expiresAt' => $license['expiresAt'] ?? null, ], ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['trial' => $exception->getMessage()]); } } public function assignLicense(string $licenseId, Request $request): RedirectResponse { $this->ensureSuperAdmin(); $payload = $request->validate(['resellerId' => ['required', 'string']]); try { $this->cloud->assignLicense($licenseId, $payload['resellerId']); return redirect()->route('agents.licenses')->with('success', 'Licencia asignada al agente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['license' => $exception->getMessage()]); } } public function provisionTenant(Request $request): RedirectResponse { $payload = $request->validate([ 'licenseId' => ['nullable', 'string', 'min:8', 'max:80'], 'licenseKey' => ['nullable', 'string', 'min:12', 'max:2048'], 'email' => ['required', 'email'], 'name' => ['required', 'string', 'min:2', 'max:120'], 'resellerId' => ['nullable', 'string', 'max:80'], ]); if (empty($payload['licenseId']) && empty($payload['licenseKey'])) { return back()->withInput()->withErrors(['tenant' => 'Seleccioná una licencia disponible.']); } if (session('agent.user.role') === 'RESELLER') { unset($payload['resellerId']); } try { $data = $this->cloud->provisionTenant($payload); return redirect()->route('agents.commerce', $data['tenant']['id'] ?? '')->with('success', 'Comercio vinculado correctamente.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['tenant' => $exception->getMessage()]); } } public function registerSale(string $licenseId, Request $request): RedirectResponse { $payload = $request->validate([ 'saleAmount' => ['nullable', 'numeric', 'min:0'], 'customerEmail' => ['nullable', 'email'], 'currency' => ['nullable', 'string', 'max:8'], 'notes' => ['nullable', 'string', 'max:1000'], ]); try { $data = $this->cloud->convertLicenseToMax($licenseId, $payload); $businessId = data_get($data, 'license.business.id') ?: data_get($data, 'license.businessId'); if ($businessId) { return redirect()->route('agents.commerce', $businessId)->with('success', 'Licencia convertida a COMERSOFT Sistema Completo. El correo titular recibira la nueva clave.'); } return redirect()->route('agents.dashboard')->with('success', 'Licencia convertida a COMERSOFT Sistema Completo.'); return redirect()->route('agents.sales')->with('success', 'Venta registrada como pendiente de liquidación.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['sale' => $exception->getMessage()]); } } public function sales(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Las ventas historicas siguen visibles aunque falle la cotizacion en vivo. } return view('agents.sales.index', [ 'sales' => $this->cloud->sales(), 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenseRequests(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La bandeja conserva su operacion aunque falle la cotizacion. } $accessRequests = $this->cloud->accessRequests('PENDING'); $freeRequests = []; if (in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true)) { $freeRequests = $this->cloud->publicLicenseRequests('PENDING'); } return view('agents.license-requests', [ 'requests' => $accessRequests, 'freeRequests' => $freeRequests, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function approveAccessRequest(string $requestId): RedirectResponse { try { $this->cloud->approveAccessRequest($requestId, session('agent.user.resellerId')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud aprobada y licencia comercial vinculada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function rejectAccessRequest(string $requestId, Request $request): RedirectResponse { try { $this->cloud->rejectAccessRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function approvePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureLicenseManager(); try { $data = $this->cloud->approvePublicLicenseRequest($requestId); $licenseKey = data_get($data, 'license.licenseKey'); return redirect()->route('agents.license.requests')->with([ 'success' => 'Cuenta confirmada y Licencia histórica emitida.', 'license_keys' => $licenseKey ? [$licenseKey] : [], ]); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function rejectPublicLicenseRequest(string $requestId, Request $request): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->rejectPublicLicenseRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud de Licencia histórica rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function deletePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deletePublicLicenseRequest($requestId); return redirect()->route('agents.license.requests')->with('success', 'Solicitud y cuenta eliminadas correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function saleAction(string $saleId): View|RedirectResponse { try { $sale = collect($this->cloud->sales())->firstWhere('id', $saleId); if (! $sale) { return redirect()->route('agents.sales')->withErrors(['sale' => 'No se encontró la venta seleccionada.']); } return view('agents.sales.action', [ 'sale' => $sale, 'saleStatus' => $sale['status'] ?? 'PENDING', 'canApprove' => in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function updateSaleStatus(string $saleId, Request $request): RedirectResponse { $this->ensureLicenseManager(); $payload = $request->validate(['status' => ['required', 'in:PENDING,PAID,CANCELLED']]); try { $this->cloud->updateSaleStatus($saleId, $payload['status']); return redirect()->route('agents.sales')->with('success', 'Estado de la venta actualizado.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function deleteSale(string $saleId): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->deleteSale($saleId); return redirect()->route('agents.sales')->with('success', 'Venta eliminada correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function commerce(string $id): View|RedirectResponse { try { return view('agents.commerce.show', ['commerce' => $this->cloud->tenant($id)]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function deleteCommerce(string $id): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deleteTenant($id); return redirect()->route('agents.dashboard')->with('success', 'Comercio eliminado correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['commerce' => $exception->getMessage()]); } } public function agents(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.index', ['resellers' => $this->visibleManagedResellers($user), 'user' => $user, 'hierarchyAdmin' => $this->isHierarchyAdmin()]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentsMap(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); $allResellers = $this->cloud->resellers(); $resellers = $this->visibleManagedResellers($user); if (! empty($user['resellerId'])) { $manager = collect($allResellers)->firstWhere('id', $user['resellerId']); if ($manager) { $hierarchy = collect([$manager]); $ancestor = $manager; while (! empty($ancestor['parentId'])) { $ancestor = collect($allResellers)->firstWhere('id', $ancestor['parentId']); if (! $ancestor) break; $hierarchy->prepend($ancestor); } $resellers = $hierarchy->merge($resellers)->unique('id')->values()->all(); } } $nodes = collect($resellers)->map(function (array $reseller) use ($resellers) { $coordinates = $this->resellerCoordinates($reseller, $resellers); $level = (int) ($reseller['level'] ?? 3); return [ 'id' => $reseller['id'], 'parentId' => $reseller['parentId'] ?? null, 'name' => $reseller['name'], 'email' => $reseller['email'], 'level' => $level, 'levelLabel' => [1 => 'Nacional', 2 => 'Provincial / Estatal', 3 => 'Ciudad / Zona'][$level] ?? 'Socio', 'country' => $reseller['country'] ?? 'AR', 'province' => $reseller['province'] ?? null, 'city' => $reseller['city'] ?? null, 'zone' => $reseller['zone'] ?? null, 'status' => $reseller['status'] ?? 'ACTIVE', 'commissionPercent' => $reseller['commissionPercent'] ?? 70, 'counts' => $reseller['counts'] ?? [], 'latitude' => $coordinates['latitude'], 'longitude' => $coordinates['longitude'], 'location' => collect([$reseller['province'] ?? null, $reseller['city'] ?? null, $reseller['zone'] ?? null])->filter()->implode(' · ') ?: (($reseller['country'] ?? 'AR') === 'AR' ? 'Argentina' : ($reseller['country'] ?? '')), ]; })->values()->all(); return view('agents.agents.map', [ 'nodes' => $nodes, 'managerName' => 'COMERSOFT Global', 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentDetail(string $id): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); if (($user['resellerId'] ?? null) === $id) { return redirect()->route('agents.agents'); } $resellers = $this->cloud->resellers(); $reseller = collect($resellers)->firstWhere('id', $id); if (! $reseller) { abort(404, 'Socio no encontrado.'); } $summary = $this->cloud->resellerSummary($id); return view('agents.agents.show', [ 'reseller' => $reseller, 'children' => collect($resellers)->where('parentId', $id)->values()->all(), 'summary' => $summary, 'users' => $summary['users'] ?? [], 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgentForm(Request $request): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.create', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : $this->visibleManagedResellers($user), 'selectedParentId' => $request->query('parentId') ?: data_get($user, 'resellerId'), 'hierarchyAdmin' => $this->isHierarchyAdmin(), 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgent(Request $request): RedirectResponse { $this->ensureHierarchyAdmin(); $payload = $request->validate([ 'name' => ['required', 'string', 'min:2', 'max:120'], 'email' => ['required', 'email'], 'companyName' => ['nullable', 'string', 'max:160'], 'phone' => ['nullable', 'string', 'max:40'], 'level' => ['required', 'integer', 'in:1,2,3'], 'country' => ['required', 'string', 'size:2'], 'province' => ['nullable', 'string', 'max:120'], 'city' => ['nullable', 'string', 'max:120'], 'zone' => ['nullable', 'string', 'max:120'], 'parentId' => ['nullable', 'string', 'max:80'], 'password' => ['nullable', 'string', 'min:12', 'max:200'], ]); if (session('agent.user.role') === 'RESELLER') { $payload['parentId'] = session('agent.user.resellerId'); $payload['level'] = max(2, (int) data_get(session('agent.user'), 'reseller.level', 1) + 1); } $payload['commissionPercent'] = 70; try { $data = $this->cloud->createReseller($payload); return redirect()->route('agents.agents')->with([ 'success' => 'Agente creado correctamente con 1.000 licencias disponibles.', 'temporary_password' => $data['temporaryPassword'] ?? null, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['agent' => $exception->getMessage()]); } } private function resellerCoordinates(array $reseller, array $resellers = []): array { $normalize = static function (?string $value): string { return strtr(mb_strtolower(trim((string) $value)), [ 'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u', 'ü' => 'u', 'ñ' => 'n', ]); }; $locations = [ 'buenos aires' => [-36.67, -60.56], 'caba' => [-34.60, -58.38], 'ciudad autonoma de buenos aires' => [-34.60, -58.38], 'cordoba' => [-31.42, -64.19], 'santa fe' => [-31.63, -60.70], 'mendoza' => [-32.89, -68.84], 'tucuman' => [-26.82, -65.22], 'entre rios' => [-31.74, -60.51], 'salta' => [-24.78, -65.41], 'misiones' => [-27.36, -55.90], 'chaco' => [-27.45, -58.99], 'corrientes' => [-27.47, -58.83], 'jujuy' => [-24.18, -65.30], 'la pampa' => [-36.62, -64.29], 'neuquen' => [-38.95, -68.06], 'rio negro' => [-40.81, -63.00], 'chubut' => [-43.30, -65.10], 'santa cruz' => [-50.00, -68.00], 'tierra del fuego' => [-54.80, -68.30], 'san juan' => [-31.54, -68.52], 'san luis' => [-33.30, -66.34], 'la rioja' => [-29.41, -66.86], 'catamarca' => [-28.47, -65.78], 'formosa' => [-26.18, -58.18], 'santiago del estero' => [-27.78, -64.26], ]; $city = $normalize($reseller['city'] ?? null); $province = $normalize($reseller['province'] ?? null); if ($city && isset($locations[$city])) return ['latitude' => $locations[$city][0], 'longitude' => $locations[$city][1]]; if ($province && isset($locations[$province])) return ['latitude' => $locations[$province][0], 'longitude' => $locations[$province][1]]; if (! empty($reseller['parentId'])) { $parent = collect($resellers)->firstWhere('id', $reseller['parentId']); if ($parent) return $this->resellerCoordinates($parent, $resellers); } return ($reseller['country'] ?? 'AR') === 'AR' ? ['latitude' => -34.6037, 'longitude' => -58.3816] : ['latitude' => 0, 'longitude' => 0]; } private function visibleManagedResellers(array $user): array { $currentResellerId = $user['resellerId'] ?? null; return collect($this->cloud->resellers()) ->reject(fn (array $reseller) => $currentResellerId && ($reseller['id'] ?? null) === $currentResellerId) ->values() ->all(); } private function ensureSuperAdmin(): void { abort_unless(session('agent.user.role') === 'SUPERADMIN', 403); } private function ensureLicenseManager(): void { abort_unless(in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 403); } private function isHierarchyAdmin(): bool { return session('agent.user.role') === 'SUPERADMIN' || (session('agent.user.role') === 'RESELLER' && (int) data_get(session('agent.user'), 'reseller.level', 0) === 1); } private function ensureHierarchyAdmin(): void { abort_unless($this->isHierarchyAdmin(), 403); } private function handleCloudError(AgentCloudException $exception): RedirectResponse { if ($exception->getCode() === 401) { session()->forget(['agent.access_token', 'agent.user']); return redirect()->route('agents.login')->withErrors(['email' => $exception->getMessage()]); } abort(502, $exception->getMessage()); } } route('agents.dashboard'); } return view('agents.login'); } public function login(Request $request): RedirectResponse { $credentials = $request->validate([ 'email' => ['required', 'email'], 'password' => ['required', 'string', 'min:8'], ]); try { $data = $this->cloud->login($credentials['email'], $credentials['password']); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withInput($request->only('email'))->withErrors(['email' => $exception->getMessage()]); } } public function quickLogin(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login'); } public function quickLoginEliezer(Request $request): RedirectResponse { return $this->performQuickLogin($request, 'quick_login_eliezer'); } private function performQuickLogin(Request $request, string $configKey): RedirectResponse { abort_unless(app()->environment('local') && config("comersoft.{$configKey}.enabled"), 404); $email = (string) config("comersoft.{$configKey}.email"); $password = (string) config("comersoft.{$configKey}.password"); if ($email === '' || $password === '') { return back()->withErrors(['email' => 'El inicio rápido no está configurado.']); } try { $data = $this->cloud->login($email, $password); $request->session()->regenerate(); $request->session()->put([ 'agent.access_token' => $data['accessToken'] ?? null, 'agent.user' => $data['user'] ?? [], ]); return redirect()->route('agents.dashboard'); } catch (AgentCloudException $exception) { return back()->withErrors(['email' => $exception->getMessage()]); } } public function logout(Request $request): RedirectResponse { $request->session()->forget(['agent.access_token', 'agent.user']); $request->session()->invalidate(); $request->session()->regenerateToken(); return redirect()->route('agents.login'); } public function dashboard(): View|RedirectResponse { try { $user = session('agent.user', []); $data = $this->cloud->dashboard($user['resellerId'] ?? null); $allLicenses = $data['licenses'] ?? []; $allSales = $data['sales'] ?? []; $allTenants = $data['tenants'] ?? []; $salesByLicense = collect($allSales) ->filter(fn (array $sale) => data_get($sale, 'licenseId') || data_get($sale, 'license.id')) ->keyBy(fn (array $sale) => data_get($sale, 'licenseId') ?: data_get($sale, 'license.id')); $licensesByBusiness = collect($allLicenses) ->filter(fn (array $license) => data_get($license, 'businessId') || data_get($license, 'business.email')) ->groupBy(fn (array $license) => data_get($license, 'businessId') ?: strtolower((string) data_get($license, 'business.email'))); $managedCommerces = collect($allTenants) ->map(function (array $tenant) use ($salesByLicense, $licensesByBusiness) { $tenantLicenses = collect($tenant['licenses'] ?? []); $license = $tenantLicenses->sortByDesc('issuedAt')->first(); if (! $license) { $license = $licensesByBusiness ->get($tenant['id'] ?? null, collect()) ->sortByDesc('issuedAt') ->first(); } if (! $license && ! empty($tenant['email'])) { $license = $licensesByBusiness ->get(strtolower((string) $tenant['email']), collect()) ->sortByDesc('issuedAt') ->first(); } $sale = $license ? $salesByLicense->get($license['id'] ?? null) : null; $licenseType = (int) data_get($license, 'type', 0); return [ 'tenant' => $tenant, 'license' => $license, 'sale' => $sale, 'customerEmail' => data_get($sale, 'customerEmail') ?: data_get($license, 'ownerEmail') ?: data_get($license, 'business.email') ?: ($tenant['email'] ?? null), 'licenseTypeLabel' => match ($licenseType) { 1 => 'COMERSOFT Sistema Completo', 2 => 'Licencia de prueba', 3 => 'COMERSOFT Versión con Sync', 4 => 'COMERSOFT Versión con IA', default => 'Licencia histórica', }, 'licenseOwner' => data_get($license, 'reseller.name') ?: data_get($license, 'owner.name') ?: 'COMERSOFT raíz', 'licenseOwnerEmail' => data_get($license, 'reseller.email') ?: data_get($license, 'owner.email') ?: 'admin@comersoft.com', ]; }) ->values() ->all(); return view('agents.dashboard', [ 'user' => $user, 'summary' => $data['summary'] ?? [], 'licenses' => array_slice($allLicenses, 0, 8), 'sales' => array_slice($allSales, 0, 8), 'tenants' => array_slice($allTenants, 0, 8), 'managedCommerces' => $managedCommerces, 'licenseCount' => count($allLicenses), 'salesCount' => count($allSales), 'tenantCount' => count($allTenants), 'pendingSales' => count(array_filter($allSales, fn ($sale) => ($sale['status'] ?? null) === 'PENDING')), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenses(): View|RedirectResponse { try { $licenses = $this->cloud->licenses(); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); $available = $items->firstWhere('status', 'AVAILABLE'); return [ 'owner' => $first['owner'] ?? ['id' => null, 'name' => 'COMERSOFT raíz', 'type' => 'COMERSOFT_ROOT'], 'reseller' => $first['reseller'] ?? null, 'total' => $items->count(), 'available' => $items->where('status', 'AVAILABLE')->count(), 'active' => $items->where('status', 'ACTIVE')->count(), 'sold' => $items->filter(fn (array $license) => ! empty($license['sale']))->count(), 'sampleAvailableId' => $available['id'] ?? null, 'sampleLicenseId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.index', [ 'licenseGroups' => $licenseGroups, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function subscriptions(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La pantalla conserva el plan aunque momentáneamente no haya cotización. } return view('agents.subscriptions.index', [ 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicensesForm(): View|RedirectResponse { try { $user = session('agent.user', []); $availableInventory = null; if (($user['role'] ?? null) === 'RESELLER') { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); } return view('agents.licenses.generate', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'availableInventory' => $availableInventory, 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function provisionTenantForm(Request $request): View|RedirectResponse { try { return view('agents.licenses.provision', [ 'selectedLicenseId' => $request->query('licenseId'), 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); $licenses = array_values(array_filter( $this->cloud->licenses(), fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' )); $licenseGroups = collect($licenses) ->groupBy(fn (array $license) => data_get($license, 'owner.id') ?: 'COMERSOFT_ROOT') ->map(function ($items) { $first = $items->first(); return [ 'owner' => $first['owner'] ?? ['name' => 'COMERSOFT raíz'], 'available' => $items->count(), 'sampleAvailableId' => $first['id'] ?? null, ]; }) ->values() ->all(); return view('agents.licenses.provision', [ 'licenseGroups' => $licenseGroups, 'selectedLicenseId' => $request->query('licenseId'), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function searchLicenses(Request $request) { $query = trim((string) $request->query('q', '')); if (mb_strlen($query) < 2 || mb_strlen($query) > 180) { return response()->json(['licenses' => []]); } try { return response()->json(['licenses' => $this->cloud->searchLicenses($query)]); } catch (AgentCloudException $exception) { $status = $exception->getCode() >= 400 ? $exception->getCode() : 502; return response()->json(['message' => $exception->getMessage()], $status); } } public function licenseLookup(Request $request): View|RedirectResponse { $query = trim((string) $request->query('q', '')); $licenses = []; $lookupError = null; if (mb_strlen($query) >= 2) { try { $licenses = $this->cloud->searchLicenses($query, true); } catch (AgentCloudException $exception) { $lookupError = $exception->getMessage(); } } return view('agents.licenses.lookup', [ 'query' => $query, 'licenses' => $licenses, 'lookupError' => $lookupError, 'user' => session('agent.user', []), ]); } public function assignLicenseForm(string $licenseId): View|RedirectResponse { $this->ensureLicenseManager(); try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['license' => 'No se encontró la licencia seleccionada.']); } return view('agents.licenses.assign', [ 'license' => $license, 'resellers' => $this->cloud->resellers(), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function registerSaleForm(string $licenseId): View|RedirectResponse { try { $license = collect($this->cloud->licenses())->firstWhere('id', $licenseId); if (! $license) { return redirect()->route('agents.licenses')->withErrors(['sale' => 'No se encontró la licencia seleccionada.']); } $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Se puede abrir la acción aunque temporalmente no haya cotización. } return view('agents.licenses.sale', [ 'license' => $license, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function generateLicenses(Request $request): RedirectResponse { $payload = $request->validate([ 'quantity' => ['required', 'integer', 'in:1'], 'activationLimit' => ['nullable', 'integer', 'in:10'], 'businessName' => ['required', 'string', 'min:2', 'max:120'], 'ownerEmail' => ['required', 'email'], 'password' => ['required', 'string', 'min:8', 'max:200'], 'resellerId' => ['nullable', 'string'], ]); // Una emisión representa una sola suscripción; el plazo comienza al activar. $payload['quantity'] = 1; $payload['activationLimit'] = 10; // El inventario siempre se determina por la cuenta autenticada. No se // permite elegir otro propietario desde el formulario web. unset($payload['resellerId']); if (session('agent.user.role') === 'RESELLER') { try { $availableInventory = collect($this->cloud->licenses('own')) ->filter(fn (array $license) => ($license['status'] ?? null) === 'AVAILABLE' && empty($license['businessId']) && empty($license['ownerEmail'])) ->count(); if ($availableInventory === 0) { return back()->withInput()->withErrors(['license' => 'No tenés licencias disponibles en tu inventario para generar otra.']); } if ($availableInventory < (int) $payload['quantity']) { return back()->withInput()->withErrors(['license' => "Solo tenés {$availableInventory} licencia(s) disponible(s) en tu inventario."]); } } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } try { $data = $this->cloud->generateLicenses($payload); $generated = $data['licenses'] ?? []; $keys = array_values(array_filter(array_column($generated, 'licenseKey'))); $licenseDetails = collect($generated)->map(function (array $license): array { return [ 'licenseKey' => $license['licenseKey'] ?? null, 'businessName' => data_get($license, 'business.name'), 'ownerEmail' => $license['ownerEmail'] ?? null, 'activationLimit' => $license['activationLimit'] ?? 1, 'expiresAt' => $license['expiresAt'] ?? null, ]; })->values()->all(); return redirect()->route('agents.licenses')->with([ 'success' => count($generated) . ' licencia(s) generada(s).', 'license_keys' => $keys, 'license_details' => $licenseDetails, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['license' => $exception->getMessage()]); } } public function trialLicenseForm(): View|RedirectResponse { try { return view('agents.licenses.trial', [ 'resellers' => session('agent.user.role') === 'SUPERADMIN' ? $this->cloud->resellers() : [], 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createTrialLicense(Request $request): RedirectResponse { $payload = $request->validate([ 'ownerEmail' => ['required', 'email'], 'days' => ['required', 'integer', 'min:1', 'max:30'], 'profile' => ['required', 'in:full,custom'], 'modules' => ['nullable', 'string', 'max:2000'], 'referralResellerId' => ['nullable', 'string', 'max:80'], ]); $payload['modules'] = $payload['profile'] === 'custom' ? array_values(array_filter(array_unique(array_map('trim', explode(',', (string) ($payload['modules'] ?? '')))))) : null; unset($payload['profile']); if (session('agent.user.role') === 'RESELLER') { unset($payload['referralResellerId']); } try { $data = $this->cloud->createTrial($payload); $license = $data['license'] ?? []; return redirect()->route('agents.licenses')->with([ 'success' => 'Licencia de prueba emitida. No consume inventario Tipo 1.', 'license_keys' => array_values(array_filter([$license['licenseKey'] ?? null])), 'trial_details' => [ 'licenseKey' => $license['licenseKey'] ?? null, 'ownerEmail' => $license['ownerEmail'] ?? $payload['ownerEmail'], 'trialDays' => $license['trialDays'] ?? $payload['days'], 'expiresAt' => $license['expiresAt'] ?? null, ], ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['trial' => $exception->getMessage()]); } } public function assignLicense(string $licenseId, Request $request): RedirectResponse { $this->ensureSuperAdmin(); $payload = $request->validate(['resellerId' => ['required', 'string']]); try { $this->cloud->assignLicense($licenseId, $payload['resellerId']); return redirect()->route('agents.licenses')->with('success', 'Licencia asignada al agente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['license' => $exception->getMessage()]); } } public function provisionTenant(Request $request): RedirectResponse { $payload = $request->validate([ 'licenseId' => ['nullable', 'string', 'min:8', 'max:80'], 'licenseKey' => ['nullable', 'string', 'min:12', 'max:2048'], 'email' => ['required', 'email'], 'name' => ['required', 'string', 'min:2', 'max:120'], 'resellerId' => ['nullable', 'string', 'max:80'], ]); if (empty($payload['licenseId']) && empty($payload['licenseKey'])) { return back()->withInput()->withErrors(['tenant' => 'Seleccioná una licencia disponible.']); } if (session('agent.user.role') === 'RESELLER') { unset($payload['resellerId']); } try { $data = $this->cloud->provisionTenant($payload); return redirect()->route('agents.commerce', $data['tenant']['id'] ?? '')->with('success', 'Comercio vinculado correctamente.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['tenant' => $exception->getMessage()]); } } public function registerSale(string $licenseId, Request $request): RedirectResponse { $payload = $request->validate([ 'saleAmount' => ['nullable', 'numeric', 'min:0'], 'customerEmail' => ['nullable', 'email'], 'currency' => ['nullable', 'string', 'max:8'], 'notes' => ['nullable', 'string', 'max:1000'], ]); try { $data = $this->cloud->convertLicenseToMax($licenseId, $payload); $businessId = data_get($data, 'license.business.id') ?: data_get($data, 'license.businessId'); if ($businessId) { return redirect()->route('agents.commerce', $businessId)->with('success', 'Licencia convertida a COMERSOFT Sistema Completo. El correo titular recibira la nueva clave.'); } return redirect()->route('agents.dashboard')->with('success', 'Licencia convertida a COMERSOFT Sistema Completo.'); return redirect()->route('agents.sales')->with('success', 'Venta registrada como pendiente de liquidación.'); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['sale' => $exception->getMessage()]); } } public function sales(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // Las ventas historicas siguen visibles aunque falle la cotizacion en vivo. } return view('agents.sales.index', [ 'sales' => $this->cloud->sales(), 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function licenseRequests(): View|RedirectResponse { try { $pricing = []; try { $pricing = $this->cloud->pricing(); } catch (AgentCloudException) { // La bandeja conserva su operacion aunque falle la cotizacion. } $accessRequests = $this->cloud->accessRequests('PENDING'); $freeRequests = []; if (in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true)) { $freeRequests = $this->cloud->publicLicenseRequests('PENDING'); } return view('agents.license-requests', [ 'requests' => $accessRequests, 'freeRequests' => $freeRequests, 'pricing' => $pricing, 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function approveAccessRequest(string $requestId): RedirectResponse { try { $this->cloud->approveAccessRequest($requestId, session('agent.user.resellerId')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud aprobada y licencia comercial vinculada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function rejectAccessRequest(string $requestId, Request $request): RedirectResponse { try { $this->cloud->rejectAccessRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['request' => $exception->getMessage()]); } } public function approvePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureLicenseManager(); try { $data = $this->cloud->approvePublicLicenseRequest($requestId); $licenseKey = data_get($data, 'license.licenseKey'); return redirect()->route('agents.license.requests')->with([ 'success' => 'Cuenta confirmada y Licencia histórica emitida.', 'license_keys' => $licenseKey ? [$licenseKey] : [], ]); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function rejectPublicLicenseRequest(string $requestId, Request $request): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->rejectPublicLicenseRequest($requestId, $request->input('message')); return redirect()->route('agents.license.requests')->with('success', 'Solicitud de Licencia histórica rechazada.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function deletePublicLicenseRequest(string $requestId): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deletePublicLicenseRequest($requestId); return redirect()->route('agents.license.requests')->with('success', 'Solicitud y cuenta eliminadas correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['free_request' => $exception->getMessage()]); } } public function saleAction(string $saleId): View|RedirectResponse { try { $sale = collect($this->cloud->sales())->firstWhere('id', $saleId); if (! $sale) { return redirect()->route('agents.sales')->withErrors(['sale' => 'No se encontró la venta seleccionada.']); } return view('agents.sales.action', [ 'sale' => $sale, 'saleStatus' => $sale['status'] ?? 'PENDING', 'canApprove' => in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 'user' => session('agent.user', []), ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function updateSaleStatus(string $saleId, Request $request): RedirectResponse { $this->ensureLicenseManager(); $payload = $request->validate(['status' => ['required', 'in:PENDING,PAID,CANCELLED']]); try { $this->cloud->updateSaleStatus($saleId, $payload['status']); return redirect()->route('agents.sales')->with('success', 'Estado de la venta actualizado.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function deleteSale(string $saleId): RedirectResponse { $this->ensureLicenseManager(); try { $this->cloud->deleteSale($saleId); return redirect()->route('agents.sales')->with('success', 'Venta eliminada correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['sale' => $exception->getMessage()]); } } public function commerce(string $id): View|RedirectResponse { try { return view('agents.commerce.show', ['commerce' => $this->cloud->tenant($id)]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function deleteCommerce(string $id): RedirectResponse { $this->ensureSuperAdmin(); try { $this->cloud->deleteTenant($id); return redirect()->route('agents.dashboard')->with('success', 'Comercio eliminado correctamente.'); } catch (AgentCloudException $exception) { return back()->withErrors(['commerce' => $exception->getMessage()]); } } public function agents(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.index', ['resellers' => $this->visibleManagedResellers($user), 'user' => $user, 'hierarchyAdmin' => $this->isHierarchyAdmin()]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentsMap(): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); $allResellers = $this->cloud->resellers(); $resellers = $this->visibleManagedResellers($user); if (! empty($user['resellerId'])) { $manager = collect($allResellers)->firstWhere('id', $user['resellerId']); if ($manager) { $hierarchy = collect([$manager]); $ancestor = $manager; while (! empty($ancestor['parentId'])) { $ancestor = collect($allResellers)->firstWhere('id', $ancestor['parentId']); if (! $ancestor) break; $hierarchy->prepend($ancestor); } $resellers = $hierarchy->merge($resellers)->unique('id')->values()->all(); } } $nodes = collect($resellers)->map(function (array $reseller) use ($resellers) { $coordinates = $this->resellerCoordinates($reseller, $resellers); $level = (int) ($reseller['level'] ?? 3); return [ 'id' => $reseller['id'], 'parentId' => $reseller['parentId'] ?? null, 'name' => $reseller['name'], 'email' => $reseller['email'], 'level' => $level, 'levelLabel' => [1 => 'Nacional', 2 => 'Provincial / Estatal', 3 => 'Ciudad / Zona'][$level] ?? 'Socio', 'country' => $reseller['country'] ?? 'AR', 'province' => $reseller['province'] ?? null, 'city' => $reseller['city'] ?? null, 'zone' => $reseller['zone'] ?? null, 'status' => $reseller['status'] ?? 'ACTIVE', 'commissionPercent' => $reseller['commissionPercent'] ?? 70, 'counts' => $reseller['counts'] ?? [], 'latitude' => $coordinates['latitude'], 'longitude' => $coordinates['longitude'], 'location' => collect([$reseller['province'] ?? null, $reseller['city'] ?? null, $reseller['zone'] ?? null])->filter()->implode(' · ') ?: (($reseller['country'] ?? 'AR') === 'AR' ? 'Argentina' : ($reseller['country'] ?? '')), ]; })->values()->all(); return view('agents.agents.map', [ 'nodes' => $nodes, 'managerName' => 'COMERSOFT Global', 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function agentDetail(string $id): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); if (($user['resellerId'] ?? null) === $id) { return redirect()->route('agents.agents'); } $resellers = $this->cloud->resellers(); $reseller = collect($resellers)->firstWhere('id', $id); if (! $reseller) { abort(404, 'Socio no encontrado.'); } $summary = $this->cloud->resellerSummary($id); return view('agents.agents.show', [ 'reseller' => $reseller, 'children' => collect($resellers)->where('parentId', $id)->values()->all(), 'summary' => $summary, 'users' => $summary['users'] ?? [], 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgentForm(Request $request): View|RedirectResponse { $this->ensureHierarchyAdmin(); try { $user = session('agent.user', []); return view('agents.agents.create', [ 'resellers' => ($user['role'] ?? null) === 'SUPERADMIN' ? $this->cloud->resellers() : $this->visibleManagedResellers($user), 'selectedParentId' => $request->query('parentId') ?: data_get($user, 'resellerId'), 'hierarchyAdmin' => $this->isHierarchyAdmin(), 'user' => $user, ]); } catch (AgentCloudException $exception) { return $this->handleCloudError($exception); } } public function createAgent(Request $request): RedirectResponse { $this->ensureHierarchyAdmin(); $payload = $request->validate([ 'name' => ['required', 'string', 'min:2', 'max:120'], 'email' => ['required', 'email'], 'companyName' => ['nullable', 'string', 'max:160'], 'phone' => ['nullable', 'string', 'max:40'], 'level' => ['required', 'integer', 'in:1,2,3'], 'country' => ['required', 'string', 'size:2'], 'province' => ['nullable', 'string', 'max:120'], 'city' => ['nullable', 'string', 'max:120'], 'zone' => ['nullable', 'string', 'max:120'], 'parentId' => ['nullable', 'string', 'max:80'], 'password' => ['nullable', 'string', 'min:12', 'max:200'], ]); if (session('agent.user.role') === 'RESELLER') { $payload['parentId'] = session('agent.user.resellerId'); $payload['level'] = max(2, (int) data_get(session('agent.user'), 'reseller.level', 1) + 1); } $payload['commissionPercent'] = 70; try { $data = $this->cloud->createReseller($payload); return redirect()->route('agents.agents')->with([ 'success' => 'Agente creado correctamente con 1.000 licencias disponibles.', 'temporary_password' => $data['temporaryPassword'] ?? null, ]); } catch (AgentCloudException $exception) { return back()->withInput()->withErrors(['agent' => $exception->getMessage()]); } } private function resellerCoordinates(array $reseller, array $resellers = []): array { $normalize = static function (?string $value): string { return strtr(mb_strtolower(trim((string) $value)), [ 'á' => 'a', 'é' => 'e', 'í' => 'i', 'ó' => 'o', 'ú' => 'u', 'ü' => 'u', 'ñ' => 'n', ]); }; $locations = [ 'buenos aires' => [-36.67, -60.56], 'caba' => [-34.60, -58.38], 'ciudad autonoma de buenos aires' => [-34.60, -58.38], 'cordoba' => [-31.42, -64.19], 'santa fe' => [-31.63, -60.70], 'mendoza' => [-32.89, -68.84], 'tucuman' => [-26.82, -65.22], 'entre rios' => [-31.74, -60.51], 'salta' => [-24.78, -65.41], 'misiones' => [-27.36, -55.90], 'chaco' => [-27.45, -58.99], 'corrientes' => [-27.47, -58.83], 'jujuy' => [-24.18, -65.30], 'la pampa' => [-36.62, -64.29], 'neuquen' => [-38.95, -68.06], 'rio negro' => [-40.81, -63.00], 'chubut' => [-43.30, -65.10], 'santa cruz' => [-50.00, -68.00], 'tierra del fuego' => [-54.80, -68.30], 'san juan' => [-31.54, -68.52], 'san luis' => [-33.30, -66.34], 'la rioja' => [-29.41, -66.86], 'catamarca' => [-28.47, -65.78], 'formosa' => [-26.18, -58.18], 'santiago del estero' => [-27.78, -64.26], ]; $city = $normalize($reseller['city'] ?? null); $province = $normalize($reseller['province'] ?? null); if ($city && isset($locations[$city])) return ['latitude' => $locations[$city][0], 'longitude' => $locations[$city][1]]; if ($province && isset($locations[$province])) return ['latitude' => $locations[$province][0], 'longitude' => $locations[$province][1]]; if (! empty($reseller['parentId'])) { $parent = collect($resellers)->firstWhere('id', $reseller['parentId']); if ($parent) return $this->resellerCoordinates($parent, $resellers); } return ($reseller['country'] ?? 'AR') === 'AR' ? ['latitude' => -34.6037, 'longitude' => -58.3816] : ['latitude' => 0, 'longitude' => 0]; } private function visibleManagedResellers(array $user): array { $currentResellerId = $user['resellerId'] ?? null; return collect($this->cloud->resellers()) ->reject(fn (array $reseller) => $currentResellerId && ($reseller['id'] ?? null) === $currentResellerId) ->values() ->all(); } private function ensureSuperAdmin(): void { abort_unless(session('agent.user.role') === 'SUPERADMIN', 403); } private function ensureLicenseManager(): void { abort_unless(in_array(session('agent.user.role'), ['SUPERADMIN', 'LICENSE_MANAGER'], true), 403); } private function isHierarchyAdmin(): bool { return session('agent.user.role') === 'SUPERADMIN' || (session('agent.user.role') === 'RESELLER' && (int) data_get(session('agent.user'), 'reseller.level', 0) === 1); } private function ensureHierarchyAdmin(): void { abort_unless($this->isHierarchyAdmin(), 403); } private function handleCloudError(AgentCloudException $exception): RedirectResponse { if ($exception->getCode() === 401) { session()->forget(['agent.access_token', 'agent.user']); return redirect()->route('agents.login')->withErrors(['email' => $exception->getMessage()]); } abort(502, $exception->getMessage()); } }