Add feature tests, unit tests, and configuration for Laravel application

- Created ExampleTest and RoutesTest for basic application response and route testing.
- Implemented IsolatedDatabaseTestCase for isolated database testing.
- Added unit tests for various models and services including Contract, CPE, and LeadDetail.
- Developed request validation tests for CreateClientRequest, UpdateClientRequest, and ContractRequest.
- Introduced Vite configuration for asset management in the Laravel application.
This commit is contained in:
Luis Medon
2026-10-29 11:30:06 +00:00
parent d886ebd3c5
commit e488ae3342
375 changed files with 60502 additions and 0 deletions

18
.editorconfig Normal file
View File

@@ -0,0 +1,18 @@
root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4

66
.env.example Normal file
View File

@@ -0,0 +1,66 @@
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
API_URL=http://localhost/api
APP_LOCALE=en
APP_FALLBACK_LOCALE=en
APP_FAKER_LOCALE=en_US
APP_MAINTENANCE_DRIVER=file
# APP_MAINTENANCE_STORE=database
PHP_CLI_SERVER_WORKERS=4
BCRYPT_ROUNDS=12
LOG_CHANNEL=stack
LOG_STACK=single
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mariadb
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=volcerto
DB_USERNAME=root
DB_PASSWORD=
SESSION_DRIVER=database
SESSION_LIFETIME=120
SESSION_ENCRYPT=false
SESSION_PATH=/
SESSION_DOMAIN=null
BROADCAST_CONNECTION=log
FILESYSTEM_DISK=local
QUEUE_CONNECTION=database
CACHE_STORE=database
# CACHE_PREFIX=
MEMCACHED_HOST=127.0.0.1
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=log
MAIL_SCHEME=null
MAIL_HOST=127.0.0.1
MAIL_PORT=2525
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
VITE_APP_NAME="${APP_NAME}"

11
.gitattributes vendored Normal file
View File

@@ -0,0 +1,11 @@
* text=auto eol=lf
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore

25
.gitignore vendored Normal file
View File

@@ -0,0 +1,25 @@
/.phpunit.cache
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/storage/pail
/vendor
.env
.env.backup
.env.production
.env.testing
.phpactor.json
.phpunit.result.cache
Homestead.json
Homestead.yaml
npm-debug.log
yarn-error.log
/auth.json
/.fleet
/.idea
/.nova
/.vscode
/.zed
lang/vendor/backup/

21
SECURITY.md Normal file
View File

@@ -0,0 +1,21 @@
# Security Policy
## Supported Versions
Use this section to tell people about which versions of your project are
currently being supported with security updates.
| Version | Supported |
| ------- | ------------------ |
| 5.1.x | :white_check_mark: |
| 5.0.x | :x: |
| 4.0.x | :white_check_mark: |
| < 4.0 | :x: |
## Reporting a Vulnerability
Use this section to tell people how to report a vulnerability.
Tell them where to go, how often they can expect to get an update on a
reported vulnerability, what to expect if the vulnerability is accepted or
declined, etc.

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class CleanMinioBackups extends Command
{
protected $signature = 'backup:clean-minio';
protected $description = 'Keeps only the last 5 backups in MinIO, safely removing older files';
public function handle()
{
$disk = Storage::disk(env('FILESYSTEM_DISK', 's3'));
$folder = 'laravel-backup';
$keep = 5;
try {
$this->info('Connecting to MinIO bucket...');
$allFiles = $disk->allFiles($folder);
if (empty($allFiles)) {
$this->info('No backup files found in the directory.');
return 0;
}
$this->info('Found ' . count($allFiles) . ' files. Processing...');
$zipFiles = collect($allFiles)
->filter(fn($file) => str_ends_with($file, '.zip'))
->values();
if ($zipFiles->isEmpty()) {
$this->info('No ZIP backup files found.');
return 0;
}
$this->info('Found ' . $zipFiles->count() . ' ZIP files.');
$backups = $zipFiles->map(function ($file) use ($disk) {
try {
$lastModified = (int) $disk->lastModified($file);
return [
'file' => $file,
'lastModified' => $lastModified,
'date' => date('Y-m-d H:i:s', $lastModified),
'estimated' => false
];
} catch (\Exception $e) {
$this->warn("Cannot read metadata for {$file}, using filename for sorting");
preg_match('/(\d{4}-\d{2}-\d{2}-\d{2}-\d{2}-\d{2})/', $file, $matches);
$timestamp = isset($matches[1])
? \DateTime::createFromFormat('Y-m-d-H-i-s', $matches[1])->getTimestamp()
: time();
return [
'file' => $file,
'lastModified' => $timestamp,
'date' => date('Y-m-d H:i:s', $timestamp),
'estimated' => true
];
}
});
$backups = $backups->sortByDesc('lastModified')->values();
$totalBackups = $backups->count();
$toDelete = $backups->slice($keep);
$this->info("Valid backups found: {$totalBackups}");
if ($toDelete->isEmpty()) {
$this->info("Only {$totalBackups} backup(s) found (less than {$keep}), nothing to delete.");
return 0;
}
$this->info("Keeping the latest {$keep}, deleting {$toDelete->count()} old backups.");
$this->info("Backups scheduled for deletion:");
foreach ($toDelete as $item) {
$this->info(" - {$item['file']} ({$item['date']})" . ($item['estimated'] ? ' [estimated]' : ''));
}
$deletedCount = 0;
foreach ($toDelete as $item) {
try {
$disk->delete($item['file']);
$this->info("✓ Deleted: {$item['file']}" . ($item['estimated'] ? ' [estimated]' : ''));
$deletedCount++;
} catch (\Exception $e) {
$this->error("✗ Failed to delete {$item['file']}: {$e->getMessage()}");
}
}
$this->info("Cleanup finished. Deleted {$deletedCount} files. Kept the last {$keep} backups.");
} catch (\Exception $e) {
$this->error("Failed to access backup directory: {$e->getMessage()}");
$this->error('Debug info:');
$this->error('Bucket: ' . env('AWS_BUCKET'));
$this->error('Endpoint: ' . env('AWS_ENDPOINT'));
$this->error('Key: ' . substr(env('AWS_ACCESS_KEY_ID', ''), 0, 5) . '...');
return 1;
}
return 0;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Http\Controllers\EmailTestController;
class SendTestEmail extends Command
{
/**
* Nome do comando Artisan
*/
protected $signature = 'email:send-test';
/**
* Descrição do comando
*/
protected $description = 'Envia o e-mail de teste automaticamente';
/**
* Executa o comando
*/
public function handle()
{
try {
(new EmailTestController)->sendTestEmail();
} catch (\Throwable $e) {
$this->error('Erro ao enviar e-mail: ' . $e->getMessage());
}
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace App\Exports;
use App\Models\Contract;
use Maatwebsite\Excel\Concerns\FromCollection;
use Maatwebsite\Excel\Concerns\WithHeadings;
class ContractsExport implements FromCollection, WithHeadings
{
public function collection()
{
return Contract::with(['client', 'seller'])->get()->map(function ($contract) {
return [
'Client' => $contract->client->name ?? '',
'Seller' => $contract->seller->name ?? '',
'Code' => $contract->code,
'Supply Type' => strtoupper($contract->supply_type),
'Power/CUI' => $contract->supply_type === 'cpe'
? ($contract->powerTier->name ?? '')
: ($contract->cuiTier->name ?? ''),
'Consumption' => $contract->annual_consumption ?? '',
'Contract Type'=> $contract->contractType->description ?? '',
'PVP' => $contract->contract_value,
'Commission' => $contract->commission_value,
'Status' => $contract->status->description ?? '',
'Activation' => optional($contract->activation_date)->format('d/m/Y'),
'Validity' => optional($contract->validity)->format('d/m/Y'),
'Contract End' => optional($contract->contract_end_date)->format('d/m/Y'),
];
});
}
public function headings(): array
{
return [
'Cliente',
'Vendedor',
'Código',
'Tipo Fornecimento',
'Potência / Escalão',
'Consumo Anual',
'Tipo Contrato',
'PVP',
'Comissão',
'Estado',
'Data Ativação',
'Validade',
'Fim de Contrato',
];
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers;
use App\Services\AdminService;
class AdminController extends Controller
{
public function main(AdminService $admin) {
return view('web.admin.system', $admin->info());
}
public function phpInfo(AdminService $admin)
{
return response($admin->phpInfo())->header('Content-Type', 'text/html');
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Http\Controllers;
use App\Services\AnalyticalService;
use Illuminate\Http\Request;
class AnalyticalController extends Controller
{
public function __construct(private AnalyticalService $analytics)
{
}
public function index(Request $request)
{
return view('web.analytical.index', $this->analytics->index($request));
}
}

View File

@@ -0,0 +1,33 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\RolePermissionService;
use App\Services\UserAccessService;
use Illuminate\Http\Request;
use Spatie\Permission\Models\Role;
class AccessController extends Controller
{
public function roles(RolePermissionService $access)
{
return response()->json($access->index());
}
public function users(UserAccessService $access)
{
return response()->json($access->index());
}
public function updatePermissions(Request $request, Role $role, RolePermissionService $access)
{
return response()->json(['role' => $access->updatePermissions($request, $role)]);
}
public function updateRoles(Request $request, User $user, UserAccessService $access)
{
return response()->json(['user' => $access->updateRoles($request, $user)]);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\AdminService;
use Illuminate\Http\Request;
class AdminController extends Controller
{
public function __construct(private AdminService $admin)
{
}
public function info()
{
return response()->json($this->admin->info());
}
public function phpInfo()
{
return response($this->admin->phpInfo())->header('Content-Type', 'text/html');
}
public function runMigrations()
{
$result = $this->admin->runMigrations();
return response()->json($result, $result['success'] ? 200 : 500);
}
public function testEmail(Request $request)
{
$data = $request->validate(['email' => 'required|email']);
return response()->json($this->admin->testEmail($data['email']));
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\AuthService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Laravel\Sanctum\PersonalAccessToken;
class AuthController extends Controller
{
public function __construct(private AuthService $authentication)
{
}
public function login(Request $request)
{
$credentials = $request->validate(['email' => 'required|email', 'password' => 'required|string']);
return response()->json($this->authentication->issueToken($this->authentication->authenticate($credentials)));
}
private function token(Request $request): PersonalAccessToken
{
$token = $request->user()->currentAccessToken();
abort_unless($token instanceof PersonalAccessToken, 401, 'Token necessario.');
return $token;
}
public function refresh(Request $request)
{
return response()->json($this->authentication->refresh($this->token($request)));
}
public function checkToken(Request $request)
{
return response()->json($this->authentication->check($this->token($request)));
}
public function logout(Request $request)
{
$this->authentication->revoke($this->token($request));
return response()->json(['message' => 'Logout realizado com sucesso', 'revoked' => true]);
}
public function user(Request $request)
{
return response()->json($request->user()->load('roles.permissions'));
}
public function sendResetLink(Request $request)
{
$credentials = $request->validate(['email' => 'required|email']);
$status = $this->authentication->sendResetLink($credentials);
return response()->json(['message' => __($status)], $status === Password::RESET_LINK_SENT ? 200 : 422);
}
public function resetPassword(Request $request)
{
$credentials = $request->validate([
'token' => 'required|string', 'email' => 'required|email', 'password' => 'required|string|min:3|confirmed',
]);
$status = $this->authentication->resetPassword($credentials);
return response()->json(['message' => __($status)], $status === Password::PASSWORD_RESET ? 200 : 422);
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\CreateClientRequest;
use App\Http\Requests\UpdateClientRequest;
use App\Services\ClientService;
use Illuminate\Http\Request;
class ClientController extends Controller
{
public function __construct(private ClientService $clients)
{
}
/**
* List all clients
*
* @author Luis Medon <luismedon@gmail.com>
* @since 2025/11
* @version 2025/11
*/
public function index(Request $request)
{
if ($request->has('per_page')) {
return response()->json(['clients' => $this->clients->listing($request, true)]);
}
$clients = $this->clients->listing($request)->map(function ($client) {
return [
'id' => $client->id,
'type' => $client->type,
'lead_status_id' => $client->lead_status_id,
'client_type_id' => $client->client_type_id,
'name' => $client->name,
'tax_number' => $client->tax_number,
'address' => $client->address,
'phone' => $client->phone,
'email' => $client->email,
'seller_id' => $client->seller_id,
'status' => $client->status,
'notes' => $client->notes,
'contact_info' => $client->contact_info,
'created_at' => $client->created_at,
'updated_at' => $client->updated_at,
];
});
return response()->json(['clients' => $clients]);
}
/**
* Apply filters to the clients query
*
* @author Luis Medon <luismedon@gmail.com>
* @since 2025/11
* @version 2025/11
*/
/**
* Show a single client
*
* @author Luis Medon <luismedon@gmail.com>
* @since 2025/11
* @version 2025/11
*/
public function show($id)
{
return response()->json($this->clients->find($id));
}
/**
* Store a new client
*
* @author Luis Medon <luismedon@gmail.com>
* @since 2025/11
* @version 2025/11
*/
public function store(CreateClientRequest $request)
{
$client = $this->clients->create($request->validated());
return response()->json([
'message' => 'Client created successfully',
'client' => $client
], 201);
}
/**
* Update an existing client
*
* @author Luis Medon <luismedon@gmail.com>
* @since 2025/11
* @version 2025/11
*/
public function update(UpdateClientRequest $request, $id)
{
$client = $this->clients->update($id, $request->validated());
return response()->json([
'message' => 'Client updated successfully',
'client' => $client
]);
}
/**
* Delete a client (soft delete by changing status)
*
* @author Luis Medon <luismedon@gmail.com>
* @since 2025/11
* @version 2025/11
*/
public function destroy($id)
{
$this->clients->delete($id);
return response()->json(['message' => 'Client deleted successfully']);
}
public function options(Request $request)
{
return response()->json($request->filled('type')
? $this->clients->form($request->input('type'), $request->user())
: ['clientTypes' => $this->clients->types()]);
}
public function detail($id)
{
return response()->json($this->clients->detail($id));
}
public function searchName(Request $request)
{
return response()->json($this->clients->searchName($request->input('term', '')));
}
public function searchID(Request $request)
{
return response()->json($this->clients->searchID($request->input('id')));
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\ContractRequest;
use App\Http\Requests\RenewContractRequest;
use App\Services\ContractService;
use Illuminate\Http\Request;
class ContractController extends Controller
{
public function __construct(private ContractService $contracts)
{
}
public function index(Request $request)
{
return response()->json($this->contracts->index($request));
}
public function options(Request $request)
{
return response()->json($this->contracts->create($request));
}
public function show(string $id)
{
return response()->json($this->contracts->show($id));
}
public function edit(string $id)
{
return response()->json($this->contracts->edit($id));
}
public function store(ContractRequest $request)
{
return response()->json(['contract' => $this->contracts->store($request)], 201);
}
public function update(ContractRequest $request, string $id)
{
return response()->json(['contract' => $this->contracts->update($request, $id)]);
}
public function renewalOptions(string $id)
{
return response()->json($this->contracts->renewForm($id));
}
public function renew(RenewContractRequest $request, $id)
{
return response()->json(['contract' => $this->contracts->renew($request, $id)]);
}
public function history($id)
{
return response()->json($this->contracts->history($id));
}
public function showHistory($id, $historyId)
{
return response()->json($this->contracts->editHistory($id, $historyId));
}
public function updateHistory(Request $request, $id, $historyId)
{
return response()->json(['history' => $this->contracts->updateHistory($request, $id, $historyId)]);
}
public function destroyHistory($id, $historyId)
{
$this->contracts->destroyHistory($id, $historyId);
return response()->json(['message' => 'Registo eliminado com sucesso.']);
}
public function destroy(string $id)
{
return response()->json($this->contracts->destroy($id));
}
public function bulkDestroy(Request $request)
{
return response()->json($this->contracts->bulkDestroy($request));
}
public function export(Request $request)
{
return $this->contracts->export($request);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\HomeService;
use App\Services\AnalyticalService;
use Illuminate\Http\Request;
class DashboardController extends Controller
{
public function index(HomeService $dashboard)
{
return response()->json($dashboard->index());
}
public function analytics(Request $request, AnalyticalService $analytics)
{
return response()->json($analytics->index($request));
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Http\Requests\LeadRequest;
use App\Services\LeadService;
class LeadController extends Controller
{
public function __construct(private LeadService $leads)
{
}
public function index()
{
return response()->json($this->leads->list());
}
public function options()
{
return response()->json($this->leads->create());
}
public function show($id)
{
return response()->json($this->leads->show($id));
}
public function store(LeadRequest $request)
{
return response()->json(['lead' => $this->leads->store($request)], 201);
}
public function update(LeadRequest $request, $id)
{
return response()->json(['lead' => $this->leads->update($request, $id)]);
}
public function convert($id)
{
return response()->json(['client' => $this->leads->convert($id)]);
}
public function destroy($id)
{
return response()->json($this->leads->destroy($id));
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Services\ResourceService;
use Illuminate\Http\Request;
class ResourceController extends Controller
{
public function __construct(private ResourceService $resources)
{
}
public function index(string $resource)
{
return response()->json(['data' => $this->resources->all($resource)]);
}
public function show(string $resource, $id)
{
return response()->json(['data' => $this->resources->find($resource, $id)]);
}
public function store(Request $request, string $resource)
{
return response()->json(['data' => $this->resources->save($resource, $request)], 201);
}
public function update(Request $request, string $resource, $id)
{
return response()->json(['data' => $this->resources->save($resource, $request, $id)]);
}
public function destroy(string $resource, $id)
{
$this->resources->delete($resource, $id);
return response()->json(['message' => 'Registo eliminado com sucesso.']);
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
class UserController extends Controller
{
public function __construct(private UserService $users)
{
}
public function index()
{
return response()->json($this->users->index());
}
public function options()
{
return response()->json($this->users->create());
}
public function show(User $user)
{
return response()->json($this->users->edit($user));
}
public function store(Request $request)
{
return response()->json(['user' => $this->users->store($request)], 201);
}
public function update(Request $request, User $user)
{
return response()->json(['user' => $this->users->update($request, $user)]);
}
public function destroy(User $user)
{
$this->users->destroy($user);
return response()->json(['message' => 'Utilizador removido com sucesso.']);
}
public function sendPasswordResetLink(User $user)
{
$status = $this->users->sendPasswordResetLink($user);
return response()->json(['message' => __($status)], $status === Password::RESET_LINK_SENT ? 200 : 422);
}
public function savePin(Request $request)
{
return response()->json($this->users->savePin($request));
}
public function checkPin(Request $request)
{
return response()->json($this->users->checkPin($request));
}
}

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Services\AuthService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpKernel\Exception\HttpException;
class ApiLoginController extends Controller
{
public function __construct(private AuthService $authentication)
{
}
/**
* @author Luis Medon <luismedon@gmail.com>
* @since 2025/11
* @version 2025/11
*/
public function showLoginForm()
{
// Prevent proxies/CDN from caching a page with a CSRF token tied to another visitor's session
return response()->view('auth.login')
->header('Cache-Control', 'no-store, no-cache, must-revalidate, private')
->header('Pragma', 'no-cache');
}
/**
* @author Luis Medon <luismedon@gmail.com>
* @since 2025/11
* @version 2025/11
*/
public function login(Request $request)
{
$request->validate([
'email' => 'required|email',
'password' => 'required',
]);
try {
$user = $this->authentication->authenticate($request->only('email', 'password'));
} catch (HttpException $exception) {
return back()->withInput($request->only('email'))->with('error', 'Credenciais invalidas.');
}
$data = $this->authentication->issueToken($user);
Auth::login($user);
$request->session()->regenerate();
session([
'api_token' => $data['token'],
]);
return redirect()->route('home');
}
/**
* @author Luis Medon <luismedon@gmail.com>
* @since 2025/11
* @version 2025/11
*/
public function logout(Request $request)
{
if ($request->user() && session('api_token')) {
$this->authentication->revoke($this->authentication->sessionToken($request->user(), session('api_token')));
}
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login');
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ConfirmsPasswords;
class ConfirmPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Confirm Password Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password confirmations and
| uses a simple trait to include the behavior. You're free to explore
| this trait and override any functions that require customization.
|
*/
use ConfirmsPasswords;
/**
* Where to redirect users when the intended url fails.
*
* @var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use App\Services\AuthService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
class ForgotPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
use SendsPasswordResetEmails;
public function sendResetLinkEmail(Request $request, AuthService $authentication)
{
$credentials = $request->validate(['email' => 'required|email']);
$status = $authentication->sendResetLink($credentials);
return $status === Password::RESET_LINK_SENT
? back()->with('status', __($status))
: back()->withErrors(['email' => __($status)]);
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = '/';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
$this->middleware('auth')->only('logout');
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use RegistersUsers;
/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = '/';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return \App\Models\User
*/
protected function create(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\ResetsPasswords;
use App\Services\AuthService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Where to redirect users after resetting their password.
*
* @var string
*/
protected $redirectTo = '/';
public function reset(Request $request, AuthService $authentication)
{
$credentials = $request->validate($this->rules());
$status = $authentication->resetPassword($credentials);
return $status === Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withErrors(['email' => __($status)])->withInput($request->only('email'));
}
protected function rules()
{
return [
'token' => 'required',
'email' => 'required|email',
'password' => ['required', 'string', 'min:3', 'confirmed'],
];
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\VerifiesEmails;
class VerificationController extends Controller
{
/*
|--------------------------------------------------------------------------
| Email Verification Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling email verification for any
| user that recently registered with the application. Emails may also
| be re-sent if the user didn't receive the original email message.
|
*/
use VerifiesEmails;
/**
* Where to redirect users after verification.
*
* @var string
*/
protected $redirectTo = '/home';
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\CreateClientRequest;
use App\Http\Requests\UpdateClientRequest;
use App\Services\ClientService;
use Illuminate\Http\Request;
class ClientController extends Controller
{
public function __construct(private ClientService $clients)
{
}
public function list(Request $request)
{
$clients = $this->clients->listing($request, true);
$clientTypes = $this->clients->types();
return view('web.clients.list', compact('clients', 'clientTypes'));
}
public function destroy($id)
{
$this->clients->delete($id);
return response()->json([
'description' => 'Cliente eliminado com sucesso',
'status' => 'success'
], 200);
}
public function add()
{
$clientTypes = $this->clients->types();
return view('web.clients.create.add', compact('clientTypes'));
}
public function add2step(Request $request, $type)
{
return view('web.clients.create.form', $this->clients->form($type, $request->user()));
}
public function store(CreateClientRequest $request)
{
$client = $this->clients->create($request->validated());
return redirect()->route('client.detail', ['id' => $client->id])
->with('success', 'Cliente criado com sucesso!');
}
public function detail($clientId)
{
return view('web.clients.detail', $this->clients->detail($clientId));
}
public function update(UpdateClientRequest $request, $id)
{
$this->clients->update($id, $request->validated());
return redirect()->back()->with('success', 'Cliente atualizado com sucesso!');
}
public function searchName(Request $request)
{
return response()->json($this->clients->searchName($request->input('term', '')));
}
public function searchID(Request $request)
{
return response()->json($this->clients->searchID($request->input('id')));
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\ContractRequest;
use App\Http\Requests\RenewContractRequest;
use App\Services\ContractService;
use Illuminate\Http\Request;
class ContractController extends Controller
{
public function __construct(private ContractService $contracts = new ContractService())
{
}
public function index(Request $request)
{
return view('web.contract.list', $this->contracts->index($request));
}
public function create(Request $request)
{
return view('web.contract.add_edit', $this->contracts->create($request));
}
public function store(ContractRequest $request)
{
$contract = $this->contracts->store($request);
return redirect()->route('client.detail', ['id' => $contract->client_id])->with('success', 'Contrato criado com sucesso!');
}
public function edit(string $id)
{
return view('web.contract.add_edit', $this->contracts->edit($id));
}
public function update(ContractRequest $request, string $id)
{
$this->contracts->update($request, $id);
return redirect()->route('contract.list')->with('success', 'Contrato atualizado com sucesso!');
}
public function show(string $id)
{
return view('web.contract.detail', $this->contracts->show($id));
}
public function renewForm(string $id)
{
return view('web.contract.renew', $this->contracts->renewForm($id));
}
public function renew(RenewContractRequest $request, $id)
{
$contract = $this->contracts->renew($request, $id);
return redirect()->route('contract.edit', ['id' => $contract->id])->with('success', 'Contrato renovado com sucesso!');
}
public function history($id)
{
return view('web.contract.history', $this->contracts->history($id));
}
public function createHistory($id)
{
return view('web.contract.history_create', $this->contracts->createHistory($id));
}
public function storeHistory(Request $request, $id)
{
$this->contracts->storeHistory($request, $id);
return redirect()->route('contract.history', $id)->with('success', 'Registo do histórico criado com sucesso.');
}
public function editHistory($id, $historyId)
{
return view('web.contract.history_edit', $this->contracts->editHistory($id, $historyId));
}
public function updateHistory(Request $request, $id, $historyId)
{
$this->contracts->updateHistory($request, $id, $historyId);
return redirect()->route('contract.history', $id)->with('success', 'Registo do histórico atualizado com sucesso.');
}
public function destroyHistory($id, $historyId)
{
$this->contracts->destroyHistory($id, $historyId);
return redirect()->route('contract.history', $id)->with('success', 'Registo do histórico eliminado com sucesso.');
}
public function export(Request $request)
{
return $this->contracts->export($request);
}
public function destroy(Request $request, string $id)
{
$result = $this->contracts->destroy($id);
return $request->ajax() ? response()->json($result) : back()->with('success', $result['message']);
}
public function bulkDestroy(Request $request)
{
return response()->json($this->contracts->bulkDestroy($request));
}
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, ValidatesRequests;
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\View;
class EmailTestController extends Controller
{
public function sendTestEmail()
{
$start = microtime(true);
$emailData = [
'to' => 'luismedon@gmail.com',
'subject' => 'Teste Completo de Envio de E-mail',
'body' => 'Este é um e-mail de teste completo enviado via Laravel.',
'status' => 'pendente',
'error' => null,
'sent_at' => now()->toDateTimeString(),
];
try {
Mail::raw($emailData['body'], function ($message) use (&$emailData) {
$message->to($emailData['to'])
->subject($emailData['subject']);
});
$emailData['status'] = 'enviado com sucesso';
} catch (\Exception $e) {
$emailData['status'] = 'falha ao enviar';
$emailData['error'] = $e->getMessage();
Log::error('Erro ao enviar e-mail de teste: ' . $e->getMessage());
}
$emailData['execution_time'] = round(microtime(true) - $start, 4);
$emailData['mail_config'] = [
'driver' => config('mail.default'),
'host' => config('mail.mailers.smtp.host'),
'port' => config('mail.mailers.smtp.port'),
'encryption' => config('mail.mailers.smtp.encryption'),
'username' => config('mail.mailers.smtp.username'),
'from' => config('mail.from.address'),
'from_name' => config('mail.from.name'),
'timeout' => config('mail.mailers.smtp.timeout') ?? 'padrão',
];
$emailData['laravel_info'] = [
'laravel_version' => app()->version(),
'php_version' => phpversion(),
'env' => App::environment(),
'mailer_class' => get_class(app()->make('mailer')->getSymfonyTransport()),
];
return view('emails.test-status', compact('emailData'));
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace App\Http\Controllers;
use App\Services\HomeService;
class HomeController extends Controller
{
public function __construct(private HomeService $dashboard)
{
}
public function index()
{
return view('home', $this->dashboard->index());
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers;
use App\Http\Requests\LeadRequest;
use App\Services\LeadService;
class LeadController extends Controller
{
public function __construct(private LeadService $leads)
{
}
public function list()
{
return view('web.lead.list', $this->leads->list());
}
public function show($id)
{
return view('web.lead.show', $this->leads->show($id));
}
public function create()
{
return view('web.lead.add_edit', $this->leads->create());
}
public function edit($id)
{
return view('web.lead.add_edit', $this->leads->edit($id));
}
public function store(LeadRequest $request)
{
$this->leads->store($request);
return redirect()->route('lead.list')->with('success', 'Lead criado com sucesso!');
}
public function update(LeadRequest $request, $id)
{
$this->leads->update($request, $id);
return redirect()->route('lead.list')->with('success', 'Lead atualizado com sucesso!');
}
public function convert($id)
{
$this->leads->convert($id);
return redirect()->route('lead.list')->with('success', 'Lead successfully converted to client!');
}
public function destroy($id)
{
return response()->json($this->leads->destroy($id));
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers\Resources;
class ContractStatusController extends ResourceController
{
protected string $resource = 'contract-statuses';
protected string $viewPath = 'web.resources.contract_status';
protected string $collectionName = 'statuses';
protected string $itemName = 'contractStatus';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers\Resources;
class ContractTypeController extends ResourceController
{
protected string $resource = 'contract_types';
protected string $viewPath = 'web.resources.contract_types';
protected string $collectionName = 'types';
protected string $itemName = 'contract_type';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers\Resources;
class CuiTierController extends ResourceController
{
protected string $resource = 'cui_tiers';
protected string $viewPath = 'web.resources.cui_tiers';
protected string $collectionName = 'cuiTiers';
protected string $itemName = 'cuiTier';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers\Resources;
class PaymentMethodController extends ResourceController
{
protected string $resource = 'payment_methods';
protected string $viewPath = 'web.resources.payment_methods';
protected string $collectionName = 'methods';
protected string $itemName = 'payment_method';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers\Resources;
class PowerTierController extends ResourceController
{
protected string $resource = 'power_tiers';
protected string $viewPath = 'web.resources.power_tiers';
protected string $collectionName = 'tiers';
protected string $itemName = 'power_tier';
}

View File

@@ -0,0 +1,57 @@
<?php
namespace App\Http\Controllers\Resources;
use App\Http\Controllers\Controller;
use App\Services\ResourceService;
use Illuminate\Http\Request;
abstract class ResourceController extends Controller
{
protected string $resource;
protected string $viewPath;
protected string $collectionName;
protected string $itemName;
public function __construct(private ResourceService $resources)
{
}
public function index()
{
return view($this->viewPath . '.index', [$this->collectionName => $this->resources->all($this->resource)]);
}
public function create()
{
return view($this->viewPath . '.add_edit');
}
public function show($id)
{
return $this->edit($id);
}
public function edit($id)
{
return view($this->viewPath . '.add_edit', [$this->itemName => $this->resources->find($this->resource, $id)]);
}
public function store(Request $request)
{
$this->resources->save($this->resource, $request);
return redirect()->route($this->resource . '.index')->with('success', 'Registo criado com sucesso.');
}
public function update(Request $request, $id)
{
$this->resources->save($this->resource, $request, $id);
return redirect()->route($this->resource . '.index')->with('success', 'Registo atualizado com sucesso.');
}
public function destroy($id)
{
$this->resources->delete($this->resource, $id);
return redirect()->route($this->resource . '.index')->with('success', 'Registo eliminado com sucesso.');
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers\Resources;
class SupplierController extends ResourceController
{
protected string $resource = 'suppliers';
protected string $viewPath = 'web.resources.suppliers';
protected string $collectionName = 'suppliers';
protected string $itemName = 'supplier';
}

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Http\Controllers\Resources;
class TypologyController extends ResourceController
{
protected string $resource = 'typologies';
protected string $viewPath = 'web.resources.typology';
protected string $collectionName = 'typologies';
protected string $itemName = 'typology';
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers;
use App\Services\RolePermissionService;
use Illuminate\Http\Request;
use Spatie\Permission\Models\Role;
class RolePermissionController extends Controller
{
public function __construct(private RolePermissionService $access)
{
}
public function index()
{
return view('web.admin.access.roles_permissions', $this->access->index());
}
public function updatePermissions(Request $request, Role $role)
{
$this->access->updatePermissions($request, $role);
return back()->with('success', 'Permissoes atualizadas.');
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Services\UserAccessService;
use Illuminate\Http\Request;
class UserAccessController extends Controller
{
public function __construct(private UserAccessService $access)
{
}
public function index()
{
return view('web.admin.access.users', $this->access->index());
}
public function updateRoles(Request $request, User $user)
{
$this->access->updateRoles($request, $user);
return back()->with('success', 'Roles atualizadas.');
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace App\Http\Controllers;
use App\Models\User;
use App\Services\UserService;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
class UserController extends Controller
{
public function __construct(private UserService $users)
{
}
public function index()
{
return view('web.admin.user.index', $this->users->index());
}
public function create()
{
return view('auth.register', $this->users->create());
}
public function store(Request $request)
{
$this->users->store($request);
return redirect()->route('users.index')->with('success', 'Utilizador criado com sucesso.');
}
public function edit(User $user)
{
return view('web.admin.user.add_edit', $this->users->edit($user));
}
public function update(Request $request, User $user)
{
$this->users->update($request, $user);
return redirect()->route('users.index')->with('success', 'Utilizador atualizado com sucesso.');
}
public function destroy(User $user)
{
$this->users->destroy($user);
return redirect()->route('users.index')->with('success', 'Utilizador removido com sucesso.');
}
public function sendPasswordResetLink(User $user)
{
$status = $this->users->sendPasswordResetLink($user);
return redirect()->route('users.edit', $user)->with($status === Password::RESET_LINK_SENT ? 'success' : 'error', __($status));
}
public function savePin(Request $request)
{
$result = $this->users->savePin($request);
return back()->with('success', $result['message']);
}
public function checkPin(Request $request)
{
return response()->json($this->users->checkPin($request));
}
}

40
app/Http/Kernel.php Normal file
View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
protected $middleware = [
\Illuminate\Foundation\Http\Middleware\HandlePrecognitiveRequests::class,
\Illuminate\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
protected $middlewareGroups = [
'web' => [
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class
],
'api' => [
\Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
\Illuminate\Routing\Middleware\ThrottleRequests::class.':api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
protected $middlewareAliases = [
'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'auth.session' => \App\Http\Middleware\AuthSession::class
];
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
class ApiResponse
{
private const FINANCIAL_FIELDS = [
'contract_value', 'commission_value', 'profit_value', 'chargeback_value', 'chargeback_date',
'paid', 'chargeback', 'contract_snapshot', 'payments', 'installments',
'totalPvp', 'totalCommission', 'totalChargeback', 'totalMargin', 'totalReceived', 'totalPending',
'commission', 'profit', 'pvp', 'net', 'commission_rate', 'amount', 'profit_margin', 'total_installments', 'paid_installments',
'total_vendas', 'total_lucro', 'total_comissao', 'vendasMensais', 'vendasPorTipo',
];
public function handle(Request $request, Closure $next)
{
$request->headers->set('Accept', 'application/json');
$response = $next($request);
if ($response instanceof JsonResponse && $request->is('api/contracts*', 'api/clients*', 'api/dashboard', 'api/analytics', 'api/leads*')
&& $request->user() && !$request->user()->can('view_contract_summary')) {
$response->setData($this->withoutFinancials($response->getData(true)));
}
$response->headers->set('Cache-Control', 'no-store, private');
return $response;
}
private function withoutFinancials($data)
{
if (!is_array($data)) {
return $data;
}
foreach ($data as $key => $value) {
if (in_array($key, self::FINANCIAL_FIELDS, true)) {
unset($data[$key]);
} else {
$data[$key] = $this->withoutFinancials($value);
}
}
return $data;
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace App\Http\Middleware;
use App\Services\AuthService;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthSession
{
public function __construct(private AuthService $authentication)
{
}
public function handle(Request $request, Closure $next)
{
$user = $request->user();
$plainText = $request->session()->get('api_token');
$token = $user && $plainText ? $this->authentication->sessionToken($user, $plainText) : null;
if (!$token) {
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login');
}
$result = $this->authentication->refresh($token);
$request->session()->put('token_expires_at', $result['expires_at']);
return $next($request);
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class EnsureApiUser
{
public function handle(Request $request, Closure $next)
{
abort_if($request->user()->can_login === false, 401, 'Conta desativada.');
return $next($request);
}
}

View File

@@ -0,0 +1,196 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Validator;
use Carbon\Carbon;
class ContractRequest extends FormRequest
{
protected function localizedMessage(string $key, string $fallback): string
{
if (function_exists('app') && app()->bound('translator')) {
return __($key, [], app()->getLocale()) ?: $fallback;
}
return $fallback;
}
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
foreach (['validity', 'contract_end_date', 'contract_date', 'activation_date', 'chargeback_date', 'payment_date', 'renewal_date'] as $field) {
$value = $this->input($field);
if ($value) {
try {
$date = str_contains((string) $value, '/')
? Carbon::createFromFormat('d/m/Y', $value)
: Carbon::parse($value);
$this->merge([$field => $date->format('Y-m-d')]);
} catch (\Throwable) {
// Mantém o valor original para o validador apresentar a mensagem adequada.
}
}
}
if ($this->filled('chargeback_value')) {
$this->merge(['chargeback' => true]);
}
}
public function rules(): array
{
$rules = [
'client_id' => ['required', 'exists:clients,id'],
'contract_value' => ['nullable', 'numeric', 'min:0'],
'commission_value' => ['nullable', 'numeric',],
'profit_value' => ['nullable', 'numeric'],
'seller_id' => ['nullable', 'exists:users,id'],
'supply_type' => ['required', 'in:cpe,cui'],
'supply_address' => ['nullable', 'string', 'max:255'],
'annual_consumption' => ['nullable', 'numeric', 'min:0'],
'code' => ['required', 'string', 'max:255'],
'contract_type_id' => ['required', 'exists:contract_types,id'],
'validity' => ['nullable', 'date'],
'contract_end_date' => ['nullable', 'date'],
'contract_date' => ['nullable', 'date'],
'activation_date' => ['nullable', 'date'],
'status_id' => ['required', 'exists:contract_status,id'],
'commission_rate' => ['nullable', 'numeric', 'between:0,100'],
'notes' => ['nullable', 'string'],
'power_tier_id' => ['nullable', 'exists:power_tiers,id'],
'payment_method_id' => ['required', 'exists:payment_methods,id'],
'delivery_method' => ['required', 'in:correio,email'],
'delivery_email' => ['nullable', 'email', 'max:255'],
'previous_supplier_id' => ['nullable', 'exists:suppliers,id'],
'new_supplier_id' => ['nullable', 'exists:suppliers,id'],
'typology_id' => ['nullable', 'exists:typologies,id'],
'cui_tier_id' => ['nullable', 'exists:cui_tiers,id'],
'go_ki' => ['required', 'numeric'],
'paid' => ['nullable', 'boolean'],
'chargeback' => ['nullable', 'boolean'],
'chargeback_value' => ['required_if:chargeback,1', 'nullable', 'numeric', 'max:-0.01'],
'chargeback_date' => ['required_if:chargeback,1', 'nullable', 'date'],
// Parcelas
'installments.*.due_date' => ['nullable', 'date'],
'installments.*.amount' => ['nullable', 'numeric', 'min:0'],
'installments.*.paid' => ['nullable', 'boolean'],
];
if (! $this->user()?->can('view_contract_summary')) {
foreach (['contract_value', 'commission_value', 'profit_value', 'paid', 'chargeback', 'chargeback_value', 'chargeback_date', 'installments.*.due_date', 'installments.*.amount', 'installments.*.paid'] as $field) {
unset($rules[$field]);
}
}
return $rules;
}
public function withValidator(Validator $validator)
{
$validator->after(function ($validator) {
if (! $this->user()?->can('view_contract_summary')) {
return;
}
$installments = $this->input('installments', []);
$validity = $this->input('validity');
$previousDate = null;
foreach ($installments as $index => $installment) {
if (!empty($installment['due_date'])) {
$dueDate = Carbon::parse($installment['due_date']);
// Data da parcela não pode ser superior à validade do contrato
// if (!empty($validity)) {
// $validityDate = Carbon::createFromFormat('d/m/Y', $validity);
// if ($dueDate->gt($validityDate)) {
// $validator->errors()->add(
// "installments.$index.due_date",
// "A data da parcela $index não pode ser superior à data de renovação do contrato."
// );
// }
// }
// Data da parcela deve ser superior à anterior
if ($previousDate && $dueDate->lte($previousDate)) {
$validator->errors()->add(
"installments.$index.due_date",
"A data da parcela $index deve ser superior à parcela anterior."
);
}
$previousDate = $dueDate;
}
}
});
}
public function messages(): array
{
return [
'cui_tier_id.exists' => $this->localizedMessage('validation.custom.cui_tier_id.exists', 'O escalão CUI selecionado não é válido.'),
'client_id.required' => $this->localizedMessage('validation.custom.client_id.required', 'O cliente é obrigatório.'),
'client_id.exists' => $this->localizedMessage('validation.custom.client_id.exists', 'O cliente selecionado não é válido.'),
'contract_value.numeric' => $this->localizedMessage('validation.custom.contract_value.numeric', 'O valor do contrato deve ser um número.'),
'contract_value.min' => $this->localizedMessage('validation.custom.contract_value.min', 'O valor do contrato não pode ser negativo.'),
'go_ki.required' => $this->localizedMessage('validation.custom.go_ki.required', 'O campo GO/KI é obrigatório.'),
'go_ki.numeric' => $this->localizedMessage('validation.custom.go_ki.numeric', 'O campo GO/KI deve ser numérico.'),
'commission_value.numeric' => $this->localizedMessage('validation.custom.commission_value.numeric', 'O valor da comissão deve ser um número.'),
'profit_value.numeric' => $this->localizedMessage('validation.custom.profit_value.numeric', 'O lucro deve ser um número.'),
'seller_id.exists' => $this->localizedMessage('validation.custom.seller_id.exists', 'O vendedor selecionado não é válido.'),
'supply_type.required' => $this->localizedMessage('validation.custom.supply_type.required', 'O tipo de fornecimento é obrigatório.'),
'supply_type.in' => $this->localizedMessage('validation.custom.supply_type.in', 'O tipo de fornecimento deve ser CPE ou cui.'),
'supply_address.string' => $this->localizedMessage('validation.custom.supply_address.string', 'O endereço de fornecimento deve ser texto.'),
'supply_address.max' => $this->localizedMessage('validation.custom.supply_address.max', 'O endereço de fornecimento não pode exceder 255 caracteres.'),
'annual_consumption.numeric' => $this->localizedMessage('validation.custom.annual_consumption.numeric', 'O consumo anual deve ser numérico.'),
'annual_consumption.min' => $this->localizedMessage('validation.custom.annual_consumption.min', 'O consumo anual não pode ser negativo.'),
'contract_type_id.required' => $this->localizedMessage('validation.custom.contract_type_id.required', 'O tipo de contrato é obrigatório.'),
'contract_type_id.exists' => $this->localizedMessage('validation.custom.contract_type_id.exists', 'O tipo de contrato selecionado não é válido.'),
'validity.date' => $this->localizedMessage('validation.custom.validity.date', 'A validade deve ser uma data válida.'),
'validity.date_format' => $this->localizedMessage('validation.custom.validity.date_format', 'A validade deve estar no formato DD/MM/AAAA.'),
'contract_end_date.date' => $this->localizedMessage('validation.custom.contract_end_date.date', 'A data de fim do contrato deve ser válida.'),
'contract_end_date.date_format' => $this->localizedMessage('validation.custom.contract_end_date.date_format', 'A data de fim de contrato deve estar no formato DD/MM/AAAA.'),
'contract_date.date' => $this->localizedMessage('validation.custom.contract_date.date', 'A data do contrato deve ser válida.'),
'contract_date.date_format' => $this->localizedMessage('validation.custom.contract_date.date_format', 'A data do contrato deve estar no formato DD/MM/AAAA.'),
'activation_date.date' => $this->localizedMessage('validation.custom.activation_date.date', 'A data de ativação deve ser válida.'),
'activation_date.date_format' => $this->localizedMessage('validation.custom.activation_date.date_format', 'A data de ativação deve estar no formato DD/MM/AAAA.'),
'status_id.required' => $this->localizedMessage('validation.custom.status_id.required', 'O estado do contrato é obrigatório.'),
'status_id.exists' => $this->localizedMessage('validation.custom.status_id.exists', 'O estado do contrato selecionado não é válido.'),
'commission_rate.numeric' => $this->localizedMessage('validation.custom.commission_rate.numeric', 'A percentagem da comissão deve ser um número.'),
'commission_rate.between' => $this->localizedMessage('validation.custom.commission_rate.between', 'A percentagem da comissão deve estar entre 0 e 100.'),
'notes.string' => $this->localizedMessage('validation.custom.notes.string', 'As notas devem ser texto.'),
'power_tier_id.exists' => $this->localizedMessage('validation.custom.power_tier_id.exists', 'O escalão de potência selecionado não é válido.'),
'payment_method_id.required' => $this->localizedMessage('validation.custom.payment_method_id.required', 'O método de pagamento é obrigatório.'),
'payment_method_id.exists' => $this->localizedMessage('validation.custom.payment_method_id.exists', 'O método de pagamento selecionado não é válido.'),
'delivery_method.required' => $this->localizedMessage('validation.custom.delivery_method.required', 'O método de envio é obrigatório.'),
'delivery_method.in' => $this->localizedMessage('validation.custom.delivery_method.in', 'O método de envio deve ser "correio" ou "email".'),
'delivery_email.email' => $this->localizedMessage('validation.custom.delivery_email.email', 'O e-mail de envio deve ser válido.'),
'delivery_email.max' => $this->localizedMessage('validation.custom.delivery_email.max', 'O e-mail de envio não pode exceder 255 caracteres.'),
'previous_supplier_id.exists' => $this->localizedMessage('validation.custom.previous_supplier_id.exists', 'O fornecedor anterior selecionado não é válido.'),
'new_supplier_id.exists' => $this->localizedMessage('validation.custom.new_supplier_id.exists', 'O novo fornecedor selecionado não é válido.'),
'paid.required' => $this->localizedMessage('validation.custom.paid.required', 'O campo "Pago" é obrigatório.'),
'paid.boolean' => $this->localizedMessage('validation.custom.paid.boolean', 'O campo "Pago" deve ser verdadeiro ou falso.'),
'chargeback.boolean' => $this->localizedMessage('validation.custom.chargeback.boolean', 'O campo "Chargeback" deve ser verdadeiro ou falso.'),
'chargeback_value.required_if' => $this->localizedMessage('validation.custom.chargeback_value.required_if', 'O valor do chargeback é obrigatório.'),
'chargeback_value.numeric' => $this->localizedMessage('validation.custom.chargeback_value.numeric', 'O valor do chargeback deve ser numérico.'),
'chargeback_value.max' => $this->localizedMessage('validation.custom.chargeback_value.max', 'O valor do chargeback deve ser negativo.'),
'chargeback_date.required_if' => $this->localizedMessage('validation.custom.chargeback_date.required_if', 'A data de chargeback é obrigatória.'),
'chargeback_date.date' => $this->localizedMessage('validation.custom.chargeback_date.date', 'A data de chargeback deve ser uma data válida.'),
'chargeback_date.date_format' => $this->localizedMessage('validation.custom.chargeback_date.date_format', 'A data de chargeback deve estar no formato DD/MM/AAAA.'),
'installments.*.due_date.required_with' => $this->localizedMessage('validation.custom.installments_due_date.required_with', 'A data da parcela é obrigatória quando o valor está preenchido.'),
'installments.*.due_date.date' => $this->localizedMessage('validation.custom.installments_due_date.date', 'A data da parcela deve ser uma data válida.'),
'installments.*.due_date.date_format' => $this->localizedMessage('validation.custom.installments_due_date.date_format', 'A data da parcela deve estar no formato DD/MM/AAAA.'),
'installments.*.amount.required_with' => $this->localizedMessage('validation.custom.installments_amount.required_with', 'O valor da parcela é obrigatório quando a data está preenchida.'),
'installments.*.amount.numeric' => $this->localizedMessage('validation.custom.installments_amount.numeric', 'O valor da parcela deve ser numérico.'),
'installments.*.amount.min' => $this->localizedMessage('validation.custom.installments_amount.min', 'O valor da parcela não pode ser negativo.'),
];
}
}

View File

@@ -0,0 +1,81 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class CreateClientRequest extends FormRequest
{
protected function localizedMessage(string $key, string $fallback): string
{
if (function_exists('app') && app()->bound('translator')) {
return __($key, [], app()->getLocale()) ?: $fallback;
}
return $fallback;
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'tax_number' => 'required|digits:9|unique:clients,tax_number',
'email' => 'required|email',
'phone' => 'required|string|max:20',
'address' => 'required|string|max:255',
'contact_info' => 'nullable|string|max:255',
'notes' => 'nullable|string|max:500',
'client_type_id' => 'required|exists:client_types,id',
'seller_id' => 'required|exists:users,id',
'locality' => 'nullable|string|max:255'
];
}
/**
* Get custom error messages for validation rules.
*
* @return array
*/
public function messages(): array
{
return [
'name.required' => $this->localizedMessage('validation.custom.name.required', 'O nome é obrigatório.'),
'name.string' => $this->localizedMessage('validation.custom.name.string', 'O nome deve ser texto.'),
'name.max' => $this->localizedMessage('validation.custom.name.max', 'O nome não pode ter mais de 255 caracteres.'),
'tax_number.required' => $this->localizedMessage('validation.custom.tax_number.required', 'O NIF é obrigatório.'),
'tax_number.digits' => $this->localizedMessage('validation.custom.tax_number.digits', 'O NIF deve ter 9 dígitos.'),
'tax_number.unique' => $this->localizedMessage('validation.custom.tax_number.unique', 'Já existe um cliente com este NIF.'),
'email.required' => $this->localizedMessage('validation.custom.email.required', 'O e-mail é obrigatório.'),
'email.email' => $this->localizedMessage('validation.custom.email.email', 'O e-mail deve ser válido.'),
'phone.required' => $this->localizedMessage('validation.custom.phone.required', 'O telefone é obrigatório.'),
'phone.string' => $this->localizedMessage('validation.custom.phone.string', 'O telefone deve ser texto.'),
'phone.max' => $this->localizedMessage('validation.custom.phone.max', 'O telefone não pode ter mais de 20 caracteres.'),
'address.required' => $this->localizedMessage('validation.custom.address.required', 'A morada é obrigatória.'),
'address.string' => $this->localizedMessage('validation.custom.address.string', 'A morada deve ser texto.'),
'address.max' => $this->localizedMessage('validation.custom.address.max', 'A morada não pode ter mais de 255 caracteres.'),
'contact_info.string' => $this->localizedMessage('validation.custom.contact_info.string', 'As informações de contacto devem ser texto.'),
'contact_info.max' => $this->localizedMessage('validation.custom.contact_info.max', 'As informações de contacto não podem exceder 255 caracteres.'),
'notes.string' => $this->localizedMessage('validation.custom.notes.string', 'As notas devem ser texto.'),
'notes.max' => $this->localizedMessage('validation.custom.notes.max', 'As notas não podem exceder 500 caracteres.'),
'client_type_id.required' => $this->localizedMessage('validation.custom.client_type_id.required', 'O tipo de cliente é obrigatório.'),
'client_type_id.exists' => $this->localizedMessage('validation.custom.client_type_id.exists', 'O tipo de cliente selecionado não é válido.'),
'seller_id.required' => $this->localizedMessage('validation.custom.seller_id.required', 'O vendedor é obrigatório.'),
'seller_id.exists' => $this->localizedMessage('validation.custom.seller_id.exists', 'O vendedor selecionado não é válido.'),
'locality.string' => $this->localizedMessage('validation.custom.locality.string', 'A localidade deve ser texto.'),
'locality.max' => $this->localizedMessage('validation.custom.locality.max', 'A localidade não pode ter mais de 255 caracteres.'),
];
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class LeadRequest extends FormRequest
{
protected function localizedMessage(string $key, string $fallback): string
{
if (function_exists('app') && app()->bound('translator')) {
return __($key, [], app()->getLocale()) ?: $fallback;
}
return $fallback;
}
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'email' => 'nullable|email|max:255',
'phone' => 'nullable|string|max:20',
'lead_status_id' => 'nullable|exists:lead_status,id',
'notes' => 'nullable|string',
'contact_info' => 'nullable|string',
'address' => 'nullable|string|max:255',
'contact_date' => 'nullable|date_format:d/m/Y',
'contract_end_date' => 'nullable|date_format:d/m/Y|after_or_equal:contact_date',
'contract_type' => 'nullable|in:CPE,CUI',
'delivery_point_code' => 'nullable'
];
}
public function messages(): array
{
return [
'name.required' => $this->localizedMessage('validation.custom.name.required', 'O nome é obrigatório.'),
'name.string' => $this->localizedMessage('validation.custom.name.string', 'O nome deve ser texto.'),
'name.max' => $this->localizedMessage('validation.custom.name.max', 'O nome não pode ter mais de 255 caracteres.'),
'email.email' => $this->localizedMessage('validation.custom.email.email', 'O e-mail deve ser válido.'),
'email.max' => $this->localizedMessage('validation.custom.email.max', 'O e-mail não pode ter mais de 255 caracteres.'),
'phone.string' => $this->localizedMessage('validation.custom.phone.string', 'O telefone deve ser texto.'),
'phone.max' => $this->localizedMessage('validation.custom.phone.max', 'O telefone não pode ter mais de 20 caracteres.'),
'lead_status_id.exists' => $this->localizedMessage('validation.custom.lead_status_id.exists', 'O estado do lead selecionado não é válido.'),
'notes.string' => $this->localizedMessage('validation.custom.notes.string', 'As notas devem ser texto.'),
'contact_info.string' => $this->localizedMessage('validation.custom.contact_info.string', 'As informações de contacto devem ser texto.'),
'contact_info.max' => $this->localizedMessage('validation.custom.contact_info.max', 'As informações de contacto não podem exceder 255 caracteres.'),
'address.string' => $this->localizedMessage('validation.custom.address.string', 'A morada deve ser texto.'),
'address.max' => $this->localizedMessage('validation.custom.address.max', 'A morada não pode ter mais de 255 caracteres.'),
'contact_date.date' => $this->localizedMessage('validation.custom.contact_date.date', 'A data de contacto deve ser válida.'),
'contact_date.date_format' => $this->localizedMessage('validation.custom.contact_date.date_format', 'A data de contacto deve estar no formato DD/MM/AAAA.'),
'contract_end_date.date' => $this->localizedMessage('validation.custom.contract_end_date.date', 'A data de fim do contrato deve ser válida.'),
'contract_end_date.after_or_equal' => $this->localizedMessage('validation.custom.contract_end_date.after_or_equal', 'A data de fim de contrato deve ser igual ou posterior à data de contacto.'),
'contract_end_date.date_format' => $this->localizedMessage('validation.custom.contract_end_date.date_format', 'A data de fim de contrato deve estar no formato DD/MM/AAAA.'),
'contract_type.in' => $this->localizedMessage('validation.custom.contract_type.in', 'O tipo de contrato deve ser CPE ou CUI.'),
];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Http\Requests;
use Carbon\Carbon;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
class RenewContractRequest extends FormRequest
{
protected function localizedMessage(string $key, string $fallback): string
{
if (function_exists('app') && app()->bound('translator')) {
return __($key, [], app()->getLocale()) ?: $fallback;
}
return $fallback;
}
public function authorize(): bool
{
return true;
}
protected function prepareForValidation(): void
{
foreach (['next_due_date', 'contract_date', 'activation_date'] as $field) {
$value = $this->input($field);
if ($value && str_contains($value, '/')) {
try {
$this->merge([
$field => Carbon::createFromFormat('d/m/Y', $value)->format('Y-m-d'),
]);
} catch (\Throwable) {
// Mantém o valor original para o validator apresentar a mensagem adequada.
}
}
}
}
public function rules(): array
{
$rules = [
'next_due_date' => ['required', 'date_format:Y-m-d'],
'annual_consumption' => ['nullable', 'numeric'],
'contract_type_id' => ['required', 'exists:contract_types,id'],
'typology_id' => ['nullable', 'exists:typologies,id'],
'contract_date' => ['nullable', 'date_format:Y-m-d'],
'activation_date' => ['nullable', 'date_format:Y-m-d'],
'renewal_type' => ['required', Rule::exists('renewal_types', 'value')],
'new_supplier_id' => ['required_if:change_supplier,1', 'nullable', 'exists:suppliers,id'],
];
if ($this->user()?->can('view_contract_summary')) {
$rules['amount_paid'] = ['required', 'numeric'];
$rules['commission_value'] = ['nullable', 'numeric'];
}
return $rules;
}
public function messages(): array
{
return [
'amount_paid.required' => $this->localizedMessage('validation.custom.amount_paid.required', 'O valor do contrato é obrigatório.'),
'amount_paid.numeric' => $this->localizedMessage('validation.custom.amount_paid.numeric', 'O valor do contrato deve ser um número.'),
'commission_value.numeric' => $this->localizedMessage('validation.custom.commission_value.numeric', 'A comissão deve ser um número.'),
'next_due_date.required' => $this->localizedMessage('validation.custom.next_due_date.required', 'A próxima renovação é obrigatória.'),
'next_due_date.date_format' => $this->localizedMessage('validation.custom.next_due_date.date_format', 'A próxima renovação deve estar no formato DD/MM/AAAA.'),
'renewal_type.required' => $this->localizedMessage('validation.custom.renewal_type.required', 'O tipo de renovação é obrigatório.'),
'renewal_type.exists' => $this->localizedMessage('validation.custom.renewal_type.exists', 'O tipo de renovação selecionado não é válido.'),
'annual_consumption.numeric' => $this->localizedMessage('validation.custom.annual_consumption.numeric', 'O consumo anual deve ser numérico.'),
'contract_type_id.required' => $this->localizedMessage('validation.custom.contract_type_id.required', 'O tipo de contrato é obrigatório.'),
'contract_type_id.exists' => $this->localizedMessage('validation.custom.contract_type_id.exists', 'O tipo de contrato selecionado não é válido.'),
'typology_id.exists' => $this->localizedMessage('validation.custom.typology_id.exists', 'A tipologia selecionada não é válida.'),
'contract_date.date_format' => $this->localizedMessage('validation.custom.contract_date.date_format', 'A data do contrato deve estar no formato DD/MM/AAAA.'),
'activation_date.date_format' => $this->localizedMessage('validation.custom.activation_date.date_format', 'A data de ativação deve estar no formato DD/MM/AAAA.'),
'new_supplier_id.required_if' => $this->localizedMessage('validation.custom.new_supplier_id.required_if', 'O novo fornecedor é obrigatório quando a alteração está ativa.'),
'new_supplier_id.exists' => $this->localizedMessage('validation.custom.new_supplier_id.exists', 'O novo fornecedor selecionado não é válido.'),
];
}
}

View File

@@ -0,0 +1,83 @@
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class UpdateClientRequest extends FormRequest
{
protected function localizedMessage(string $key, string $fallback): string
{
if (function_exists('app') && app()->bound('translator')) {
return __($key, [], app()->getLocale()) ?: $fallback;
}
return $fallback;
}
/**
* Determine if the user is authorized to make this request.
*
* @return bool
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => 'required|string|max:255',
'tax_number' => 'required|digits:9|unique:clients,tax_number,' . $this->route('id'),
'email' => 'required|email',
'phone' => 'required|string|max:20',
'address' => 'required|string|max:255',
'contact_info' => 'nullable|string|max:255',
'notes' => 'nullable|string|max:500',
'seller_id' => 'required|exists:users,id',
'client_type_id' => 'required|exists:client_types,id',
'locality' => 'nullable|string|max:255'
];
}
/**
* Get custom error messages for validation rules.
*
* @return array
*/
public function messages(): array
{
return [
'name.required' => $this->localizedMessage('validation.custom.name.required', 'O nome é obrigatório.'),
'name.string' => $this->localizedMessage('validation.custom.name.string', 'O nome deve ser texto.'),
'name.max' => $this->localizedMessage('validation.custom.name.max', 'O nome não pode ter mais de 255 caracteres.'),
'tax_number.required' => $this->localizedMessage('validation.custom.tax_number.required', 'O NIF é obrigatório.'),
'tax_number.digits' => $this->localizedMessage('validation.custom.tax_number.digits', 'O NIF deve ter 9 dígitos.'),
'tax_number.unique' => $this->localizedMessage('validation.custom.tax_number.unique', 'Já existe um cliente com este NIF.'),
'email.required' => $this->localizedMessage('validation.custom.email.required', 'O e-mail é obrigatório.'),
'email.email' => $this->localizedMessage('validation.custom.email.email', 'O e-mail deve ser válido.'),
'phone.required' => $this->localizedMessage('validation.custom.phone.required', 'O telefone é obrigatório.'),
'phone.string' => $this->localizedMessage('validation.custom.phone.string', 'O telefone deve ser texto.'),
'phone.max' => $this->localizedMessage('validation.custom.phone.max', 'O telefone não pode ter mais de 20 caracteres.'),
'address.required' => $this->localizedMessage('validation.custom.address.required', 'A morada é obrigatória.'),
'address.string' => $this->localizedMessage('validation.custom.address.string', 'A morada deve ser texto.'),
'address.max' => $this->localizedMessage('validation.custom.address.max', 'A morada não pode ter mais de 255 caracteres.'),
'contact_info.string' => $this->localizedMessage('validation.custom.contact_info.string', 'As informações de contacto devem ser texto.'),
'contact_info.max' => $this->localizedMessage('validation.custom.contact_info.max', 'As informações de contacto não podem exceder 255 caracteres.'),
'notes.string' => $this->localizedMessage('validation.custom.notes.string', 'As notas devem ser texto.'),
'notes.max' => $this->localizedMessage('validation.custom.notes.max', 'As notas não podem exceder 500 caracteres.'),
'seller_id.required' => $this->localizedMessage('validation.custom.seller_id.required', 'O vendedor é obrigatório.'),
'seller_id.exists' => $this->localizedMessage('validation.custom.seller_id.exists', 'O vendedor selecionado não é válido.'),
'client_type_id.required' => $this->localizedMessage('validation.custom.client_type_id.required', 'O tipo de cliente é obrigatório.'),
'client_type_id.exists' => $this->localizedMessage('validation.custom.client_type_id.exists', 'O tipo de cliente selecionado não é válido.'),
'locality.string' => $this->localizedMessage('validation.custom.locality.string', 'A localidade deve ser texto.'),
'locality.max' => $this->localizedMessage('validation.custom.locality.max', 'A localidade não pode ter mais de 255 caracteres.'),
];
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use App\Models\User;
class NewUserWelcome extends Mailable
{
use Queueable, SerializesModels;
public $user;
public $password;
public function __construct(User $user, string $password)
{
$this->user = $user;
$this->password = $password;
}
public function build()
{
return $this->subject('Conta criada com sucesso')
->view('emails.new_user_welcome');
}
}

87
app/Models/CPE.php Normal file
View File

@@ -0,0 +1,87 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CPE extends Model
{
use HasFactory;
protected $table = 'cpe';
protected $fillable = [
'client_id',
'pvp',
'net',
'power',
'tier',
'annual_consumption',
'contract_type_id',
'validity',
'activation_date',
'renewal_date',
'notes',
'payment_date',
'previous_supplier',
'status_id',
];
protected $dates = [
'validity',
'activation_date',
'renewal_date',
'payment_date',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'id'
];
public function client()
{
return $this->belongsTo(Clients::class, 'client_id');
}
public function contractType()
{
return $this->belongsTo(ContractType::class, 'contract_type_id');
}
public function status()
{
return $this->belongsTo(ContractStatus::class, 'status_id');
}
public function payments()
{
return $this->hasMany(CpePayment::class, 'cpe_id');
}
public function totalPaid()
{
return $this->payments()->where('status', 'paid')->sum('amount');
}
public function isPaid()
{
return $this->totalPaid() >= $this->pvp;
}
public function isCurrentRenewalPaid()
{
return $this->payments()
->where('reference_period', $this->renewal_date?->format('Y-m-d'))
->where('status', 'paid')
->exists();
}
}

15
app/Models/ClientType.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ClientType extends Model
{
protected $fillable = ['description', 'icon', 'color'];
public function clients()
{
return $this->hasMany(Clients::class);
}
}

71
app/Models/Clients.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Clients extends Model
{
protected $fillable = [
'name',
'tax_number',
'address',
'phone',
'email',
'seller_id',
'receive_invoice_id',
'client_type_id',
'contact_info',
'notes',
'type',
'lead_status_id',
'locality'
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
];
public function contracts()
{
return $this->hasMany(Contract::class, 'client_id');
}
public function seller(): BelongsTo {
return $this->belongsTo(User::class, 'seller_id');
}
public function cpes()
{
return $this->hasMany(Cpe::class);
}
public function hasUnpaidCpes()
{
return $this->cpes->contains(function ($cpe) {
return !$cpe->isPaid();
});
}
public function clientType()
{
return $this->belongsTo(ClientType::class, 'client_type_id');
}
public function leadStatus()
{
return $this->belongsTo(LeadStatus::class, 'lead_status_id');
}
public function leadDetail()
{
return $this->hasOne(LeadDetail::class, 'client_id', 'id');
}
}

224
app/Models/Contract.php Normal file
View File

@@ -0,0 +1,224 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Contract extends Model
{
protected $fillable = [
'client_id',
'paid',
'chargeback',
'chargeback_value',
'chargeback_date',
'contract_value',
'commission_value',
'profit_value',
'seller_id',
'supply_type',
'supply_address',
'annual_consumption',
'code',
'contract_type_id',
'status_id',
'commission_rate',
'notes',
'power_tier_id',
'payment_method_id',
'delivery_method',
'delivery_email',
'previous_supplier_id',
'new_supplier_id',
'typology_id',
'cui_tier_id',
'activation_date',
'contract_date',
'validity',
'contract_end_date',
'go_ki'
];
public function histories()
{
return $this->hasMany(ContractHistory::class)->orderBy('created_at', 'desc');
}
public function client(): BelongsTo
{
return $this->belongsTo(Clients::class);
}
public function contractType(): BelongsTo
{
return $this->belongsTo(ContractType::class);
}
public function status(): BelongsTo
{
return $this->belongsTo(ContractStatus::class);
}
public function payments(): HasMany
{
return $this->hasMany(Payment::class);
}
public function powerTier(): BelongsTo
{
return $this->belongsTo(PowerTier::class);
}
public function cuiTier(): BelongsTo
{
return $this->belongsTo(CuiTier::class);
}
public function paymentMethod(): BelongsTo
{
return $this->belongsTo(PaymentMethod::class);
}
public function previousSupplier(): BelongsTo
{
return $this->belongsTo(Supplier::class, 'previous_supplier_id');
}
public function newSupplier(): BelongsTo
{
return $this->belongsTo(Supplier::class, 'new_supplier_id');
}
public function typology()
{
return $this->belongsTo(Typology::class);
}
public function installments()
{
return $this->hasMany(ContractInstallment::class);
}
// Gerar parcelas automaticamente
public function createInstallments()
{
$amount = $this->contract_value / $this->installment_count;
for ($i = 1; $i <= $this->installment_count; $i++) {
$this->installments()->create([
'installment_number' => $i,
'amount' => $amount,
'due_date' => now()->addMonths($i - 1),
]);
}
}
// Scopes
public function scopeCpe($query)
{
return $query->where('type', 'cpe');
}
public function scopeCpi($query)
{
return $query->where('type', 'cpi');
}
public function seller()
{
return $this->belongsTo(User::class, 'seller_id');
}
public function scopeBetweenDates($query, $from, $to)
{
return $query->when($from, fn($q) => $q->where('contract_date', '>=', $from))
->when($to, fn($q) => $q->where('contract_date', '<=', $to));
}
public static function getMonthlyGrowth($from = null, $to = null)
{
$driver = \Illuminate\Support\Facades\DB::getDriverName();
$monthExpression = $driver === 'sqlite'
? "strftime('%Y-%m', contract_date)"
: "DATE_FORMAT(contract_date, '%Y-%m')";
return self::query()
->betweenDates($from, $to)
->selectRaw($monthExpression . ' as mes, SUM(contract_value) as total')
->groupByRaw($monthExpression)
->orderByRaw($monthExpression)
->get();
}
// Mutators & Accessors
protected function activationDate(): Attribute
{
return Attribute::make(
// Getter: returns a Carbon instance or null
get: fn ($value) => $value ? Carbon::parse($value) : null,
// Setter: accepts 'd/m/Y' from form, converts to 'Y-m-d' for DB
set: fn ($value) => $this->convertDateToDbFormat($value)
);
}
protected function contractDate(): Attribute
{
return Attribute::make(
// Getter: returns a Carbon instance or null
get: fn ($value) => $value ? Carbon::parse($value) : null,
// Setter: accepts 'd/m/Y' from form, converts to 'Y-m-d' for DB
set: fn ($value) => $this->convertDateToDbFormat($value)
);
}
protected function validity(): Attribute
{
return Attribute::make(
// Getter: returns a Carbon instance or null
get: fn ($value) => $value ? Carbon::parse($value) : null,
// Setter: accepts 'd/m/Y' from form, converts to 'Y-m-d' for DB
set: fn ($value) => $this->convertDateToDbFormat($value)
);
}
protected function contractEndDate(): Attribute
{
return Attribute::make(
get: fn ($value) => $value ? Carbon::parse($value) : null,
set: fn ($value) => $this->convertDateToDbFormat($value)
);
}
/**
* Converts a date string from 'd/m/Y' or other formats to 'Y-m-d' for DB storage.
*
* @param string|null $value
* @return string|null
*/
private function convertDateToDbFormat($value)
{
$value = trim($value);
if (!$value) return null;
try {
// Try parsing d/m/Y format
return Carbon::createFromFormat('d/m/Y', $value)->format('Y-m-d');
} catch (\Exception $e) {
try {
// Fallback: parse standard date formats (e.g., Y-m-d)
return Carbon::parse($value)->format('Y-m-d');
} catch (\Exception $e) {
// If parsing fails, store null
return null;
}
}
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace App\Models;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
class ContractHistory extends Model
{
use HasFactory;
protected $table = 'contract_histories';
// Campos que podem ser preenchidos em massa
protected $fillable = [
'contract_id',
'previous_supplier_id',
'current_supplier_id',
'annual_consumption',
'seller_id',
'contract_type_id',
'typology_id',
'contract_date',
'activation_date',
'renewal_date',
'renewal_type',
'go_ki',
'validity',
'contract_value',
'commission_value',
'profit_value',
'chargeback',
'chargeback_value',
'chargeback_date',
'profit_margin',
'total_installments',
'paid_installments',
'notes',
'contract_snapshot'
];
public function contract()
{
return $this->belongsTo(Contract::class);
}
public function previousSupplier()
{
return $this->belongsTo(Supplier::class, 'previous_supplier_id');
}
public function currentSupplier()
{
return $this->belongsTo(Supplier::class, 'current_supplier_id');
}
public function seller()
{
return $this->belongsTo(User::class, 'seller_id');
}
public function contractType()
{
return $this->belongsTo(ContractType::class);
}
protected function validity(): Attribute
{
return Attribute::make(
// Getter: returns a Carbon instance or null
get: fn ($value) => $value ? Carbon::parse($value) : null,
// Setter: accepts 'd/m/Y' from form, converts to 'Y-m-d' for DB
set: fn ($value) => $this->convertDateToDbFormat($value)
);
}
protected function chargebackDate(): Attribute
{
return Attribute::make(
get: fn ($value) => $value ? Carbon::parse($value) : null,
set: fn ($value) => $this->convertDateToDbFormat($value)
);
}
/**
* Converts a date string from 'd/m/Y' or other formats to 'Y-m-d' for DB storage.
*
* @param string|null $value
* @return string|null
*/
private function convertDateToDbFormat($value)
{
$value = trim($value);
if (!$value) return null;
try {
// Try parsing d/m/Y format
return Carbon::createFromFormat('d/m/Y', $value)->format('Y-m-d');
} catch (\Exception $e) {
try {
// Fallback: parse standard date formats (e.g., Y-m-d)
return Carbon::parse($value)->format('Y-m-d');
} catch (\Exception $e) {
// If parsing fails, store null
return null;
}
}
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ContractInstallment extends Model
{
protected $fillable = [
'contract_id',
'installment_number',
'amount',
'due_date',
'paid',
'paid_at'
];
public function contract()
{
return $this->belongsTo(Contract::class);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ContractStatus extends Model
{
protected $table = "contract_status";
protected $fillable = [
'description',
'code',
'color'
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'id'
];
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ContractType extends Model
{
protected $table = "contract_types";
protected $fillable = [
'code',
'description',
'status'
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'id'
];
}

21
app/Models/CpePayment.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class CpePayment extends Model
{
protected $fillable = [
'cpe_id',
'amount',
'reference_period',
'payment_date',
'status',
];
public function cpe()
{
return $this->belongsTo(CPE::class);
}
}

13
app/Models/CuiTier.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CuiTier extends Model
{
use HasFactory;
protected $fillable = ['name', 'status'];
}

55
app/Models/LeadDetail.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Carbon\Carbon;
class LeadDetail extends Model
{
use HasFactory;
protected $fillable = [
'client_id',
'contact_date',
'contract_end_date',
'contract_type',
'delivery_point_code'
];
public function client()
{
return $this->belongsTo(Clients::class);
}
/**
* Accessor - quando for buscar do BD converte para d/m/Y
*/
public function getContactDateAttribute($value)
{
return $value ? Carbon::parse($value)->format('d/m/Y') : null;
}
public function getContractEndDateAttribute($value)
{
return $value ? Carbon::parse($value)->format('d/m/Y') : null;
}
public function setContactDateAttribute($value)
{
$this->attributes['contact_date'] = $value
? Carbon::createFromFormat('d/m/Y', $value)->format('Y-m-d')
: null;
}
public function setContractEndDateAttribute($value)
{
$this->attributes['contract_end_date'] = $value
? Carbon::createFromFormat('d/m/Y', $value)->format('Y-m-d')
: null;
}
}

20
app/Models/LeadStatus.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class LeadStatus extends Model
{
use HasFactory;
protected $table = 'lead_status';
protected $fillable = ['name'];
public function clients()
{
return $this->hasMany(Clients::class, 'lead_status_id');
}
}

15
app/Models/Payment.php Normal file
View File

@@ -0,0 +1,15 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Payment extends Model
{
protected $fillable = ['contract_id', 'amount', 'reference_period', 'payment_date', 'status'];
public function contract()
{
return $this->belongsTo(Contract::class);
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PaymentMethod extends Model
{
use HasFactory;
protected $fillable = [
'name',
'icon',
'status'
];
public function cpe()
{
return $this->hasMany(cpe::class);
}
}

10
app/Models/PowerTier.php Normal file
View File

@@ -0,0 +1,10 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class PowerTier extends Model
{
protected $fillable = ['name', 'status'];
}

View File

@@ -0,0 +1,12 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ReceiveInvoiceTypes extends Model
{
protected $fillable = [
'description',
];
}

View File

@@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class RenewalType extends Model
{
protected $fillable = [
'value',
'description',
];
}

25
app/Models/Supplier.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Supplier extends Model
{
protected $fillable = [
'name',
'status',
'color'
];
// Opcional: se tiveres uma relação com contracts
public function previousContracts()
{
return $this->hasMany(Contract::class, 'previous_supplier_id');
}
public function newContracts()
{
return $this->hasMany(Contract::class, 'new_supplier_id');
}
}

23
app/Models/Typology.php Normal file
View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Typology extends Model
{
protected $fillable = [
'code',
'description',
'status'
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'id'
];
}

55
app/Models/User.php Normal file
View File

@@ -0,0 +1,55 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Hash;
use Spatie\Permission\Traits\HasRoles;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasApiTokens, HasFactory, Notifiable, HasRoles;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
'pin',
'can_login',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
'pin',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'can_login' => 'boolean',
];
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Providers;
use Carbon\Carbon;
use Illuminate\Pagination\Paginator;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\URL;
use Illuminate\Support\ServiceProvider;
use Maatwebsite\Excel\Excel;
use Illuminate\Support\Facades\Route;
use Spatie\Permission\Middleware\RoleMiddleware;
class AppServiceProvider extends ServiceProvider
{
/**
* Register any application services.
*/
public function register(): void
{
//
}
/**
* Bootstrap any application services.
*/
public function boot(): void
{
App::setLocale('pt');
Carbon::setLocale('pt');
Paginator::useBootstrap();
//URL::forceScheme('https');
Route::aliasMiddleware('role', RoleMiddleware::class);
Route::aliasMiddleware('permission', \Spatie\Permission\Middleware\PermissionMiddleware::class);
Route::aliasMiddleware('role_or_permission', \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class);
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Route;
use Illuminate\Foundation\Support\Providers\RouteServiceProvider as ServiceProvider;
class RouteServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->group(base_path('routes/api.php'));
Route::middleware('web')
->group(base_path('routes/web.php'));
});
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace App\Services;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Redis;
use Throwable;
class AdminService
{
public function info(): array
{
$databaseVersion = DB::connection()->getDriverName() === 'mysql'
? DB::select('SELECT VERSION() as version')[0]->version
: DB::connection()->getDriverName();
try {
$redisVersion = Redis::connection()->client()->info()['redis_version'] ?? 'N/A';
} catch (Throwable $exception) {
$redisVersion = 'N/A';
}
return [
'php_version' => PHP_VERSION, 'php_sapi' => PHP_SAPI, 'laravel_version' => app()->version(),
'environment' => app()->environment(), 'debug_mode' => config('app.debug'),
'database_driver' => config('database.default'), 'database_version' => $databaseVersion,
'redis_version' => $redisVersion, 'redis_database' => config('database.redis.cache.database'),
'cache_driver' => config('cache.default'), 'queue_driver' => config('queue.default'),
'session_driver' => config('session.driver'), 'memory_limit' => ini_get('memory_limit'), 'system' => php_uname(),
];
}
public function phpInfo(): string
{
ob_start();
phpinfo();
return ob_get_clean();
}
public function runMigrations(): array
{
$exitCode = Artisan::call('migrate', ['--force' => true]);
return ['success' => $exitCode === 0, 'exit_code' => $exitCode, 'output' => trim(Artisan::output())];
}
public function testEmail(string $recipient): array
{
Mail::raw('Teste de envio de email.', function ($message) use ($recipient) {
$message->to($recipient)->subject('Teste de envio de email');
});
return ['success' => true, 'message' => 'Email de teste enviado com sucesso.'];
}
}

View File

@@ -0,0 +1,283 @@
<?php
namespace App\Services;
use App\Models\Contract;
use App\Models\ContractHistory;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class AnalyticalService
{
/**
* @author Luis Medon <luismedon@gmail.com>
* @version 2026/03
*/
public function index(Request $request)
{
$from = $this->parseDate($request->validity_from ?? $request->from ?? null);
$to = $this->parseDate($request->validity_to ?? $request->to ?? null);
$baseContractQuery = $this->applyFilters(Contract::query(), $request, 'contract_date');
$baseHistoryQuery = $this->applyFilters(ContractHistory::query(), $request, 'contract_date');
$contractTotals = (clone $baseContractQuery)
->selectRaw('COALESCE(SUM(contract_value), 0) as total_vendas, COALESCE(SUM(profit_value), 0) as total_lucro, COALESCE(SUM(commission_value), 0) as total_comissao, COUNT(*) as contagem, COUNT(DISTINCT client_id) as total_clients')
->first();
$historyTotals = (clone $baseHistoryQuery)
->selectRaw('COALESCE(SUM(contract_value), 0) as total_vendas, COALESCE(SUM(profit_value), 0) as total_lucro, COALESCE(SUM(commission_value), 0) as total_comissao, COUNT(*) as contagem, COUNT(DISTINCT contract_id) as total_clients')
->first();
$totalVendas = (float) ($contractTotals->total_vendas ?? 0) + (float) ($historyTotals->total_vendas ?? 0);
$totalLucro = (float) ($contractTotals->total_lucro ?? 0) + (float) ($historyTotals->total_lucro ?? 0);
$totalComissao = (float) ($contractTotals->total_comissao ?? 0) + (float) ($historyTotals->total_comissao ?? 0);
$contagem = (int) (($contractTotals->contagem ?? 0) + ($historyTotals->contagem ?? 0));
$totalClients = (int) max($contractTotals->total_clients ?? 0, $historyTotals->total_clients ?? 0);
$stats = [
'total_vendas' => $totalVendas,
'total_lucro' => $totalLucro,
'total_comissao' => $totalComissao,
'contagem' => $contagem,
'total_clients' => $totalClients,
];
$vendasMensais = $this->buildMonthlyGrowth($request);
$vendasPorTipo = $this->buildClientTypeBreakdown($request);
$totaisPorAno = $this->buildYearBreakdown($request);
$totaisPorComercializadora = $this->buildSupplierBreakdown($request);
return compact('stats', 'vendasMensais', 'vendasPorTipo', 'totaisPorAno', 'totaisPorComercializadora');
}
private function applyFilters($query, Request $request, string $dateField = 'contract_date')
{
if (!str_contains($dateField, '.')) {
$dateField = $query->getModel()->getTable() . '.' . $dateField;
}
$from = $this->parseDate($request->validity_from ?? $request->from ?? null);
$to = $this->parseDate($request->validity_to ?? $request->to ?? null);
if ($from) {
$query->whereDate($dateField, '>=', $from);
}
if ($to) {
$query->whereDate($dateField, '<=', $to);
}
foreach (['status_id', 'seller_id', 'supply_type', 'contract_type_id', 'year'] as $field) {
$value = $request->input($field);
if ($value === null || $value === '' || $value === 'all') {
continue;
}
switch ($field) {
case 'status_id':
$query->where('status_id', $value);
break;
case 'seller_id':
$query->where('seller_id', $value);
break;
case 'supply_type':
$query->where('supply_type', $value);
break;
case 'contract_type_id':
$query->where('contract_type_id', $value);
break;
case 'year':
$query->whereRaw($this->yearExpression($dateField) . ' = ?', [(int) $value]);
break;
}
}
if ($request->filled('client_type_id')) {
$query->whereHas('client', fn ($q) => $q->where('client_type_id', $request->client_type_id));
}
if ($request->filled('supplier_id')) {
$query->where(function ($q) use ($request) {
$q->where('new_supplier_id', $request->supplier_id)
->orWhere('previous_supplier_id', $request->supplier_id);
});
}
return $query;
}
private function buildMonthlyGrowth(Request $request)
{
$from = $this->parseDate($request->validity_from ?? $request->from ?? null);
$to = $this->parseDate($request->validity_to ?? $request->to ?? null);
$contractSeries = Contract::query();
$historySeries = ContractHistory::query();
$this->applyFilters($contractSeries, $request, 'contract_date');
$this->applyFilters($historySeries, $request, 'contract_date');
$monthExpression = $this->monthExpression('contract_date');
$contractTotals = (clone $contractSeries)
->selectRaw($monthExpression . ' as mes, SUM(contract_value) as total')
->whereNotNull('contract_date')
->groupByRaw($monthExpression)
->orderByRaw($monthExpression)
->get();
$historyTotals = (clone $historySeries)
->selectRaw($monthExpression . ' as mes, SUM(contract_value) as total')
->whereNotNull('contract_date')
->groupByRaw($monthExpression)
->orderByRaw($monthExpression)
->get();
$merged = [];
foreach ($contractTotals as $row) {
$merged[$row->mes] = (float) ($row->total ?? 0);
}
foreach ($historyTotals as $row) {
$merged[$row->mes] = ($merged[$row->mes] ?? 0) + (float) ($row->total ?? 0);
}
ksort($merged);
return collect(array_map(fn ($month, $total) => ['mes' => $month, 'total' => $total], array_keys($merged), array_values($merged)));
}
private function buildClientTypeBreakdown(Request $request)
{
$contractBreakdown = (clone $this->applyFilters(Contract::query(), $request, 'contract_date'))
->join('clients', 'contracts.client_id', '=', 'clients.id')
->join('client_types', 'clients.client_type_id', '=', 'client_types.id')
->selectRaw('client_types.description as tipo, SUM(contracts.contract_value) as total')
->groupBy('client_types.description')
->get();
$historyBreakdown = (clone $this->applyFilters(ContractHistory::query(), $request, 'contract_date'))
->join('contracts', 'contract_histories.contract_id', '=', 'contracts.id')
->join('clients', 'contracts.client_id', '=', 'clients.id')
->join('client_types', 'clients.client_type_id', '=', 'client_types.id')
->selectRaw('client_types.description as tipo, SUM(contract_histories.contract_value) as total')
->groupBy('client_types.description')
->get();
$merged = [];
foreach ($contractBreakdown as $row) {
$merged[$row->tipo] = (float) ($row->total ?? 0);
}
foreach ($historyBreakdown as $row) {
$key = $row->tipo ?? 'Sem tipo';
$merged[$key] = ($merged[$key] ?? 0) + (float) ($row->total ?? 0);
}
return collect(array_map(fn ($tipo, $total) => ['tipo' => $tipo, 'total' => $total], array_keys($merged), array_values($merged)));
}
private function buildYearBreakdown(Request $request)
{
$yearExpression = $this->yearExpression('contract_date');
$contractYears = (clone $this->applyFilters(Contract::query(), $request, 'contract_date'))
->selectRaw($yearExpression . ' as ano, SUM(contract_value) as total')
->whereNotNull('contract_date')
->groupByRaw($yearExpression)
->orderByRaw($yearExpression)
->get();
$historyYears = (clone $this->applyFilters(ContractHistory::query(), $request, 'contract_date'))
->selectRaw($yearExpression . ' as ano, SUM(contract_value) as total')
->whereNotNull('contract_date')
->groupByRaw($yearExpression)
->orderByRaw($yearExpression)
->get();
$merged = [];
foreach ($contractYears as $row) {
$merged[(string) $row->ano] = (float) ($row->total ?? 0);
}
foreach ($historyYears as $row) {
$key = (string) ($row->ano ?? '');
$merged[$key] = ($merged[$key] ?? 0) + (float) ($row->total ?? 0);
}
ksort($merged);
return collect(array_map(fn ($ano, $total) => ['ano' => $ano, 'total' => $total], array_keys($merged), array_values($merged)));
}
private function buildSupplierBreakdown(Request $request)
{
$contractSuppliers = (clone $this->applyFilters(Contract::query(), $request, 'contract_date'))
->join('suppliers', 'contracts.new_supplier_id', '=', 'suppliers.id')
->selectRaw('suppliers.name as fornecedor, SUM(contracts.contract_value) as total')
->groupBy('suppliers.name')
->orderBy('total', 'desc')
->get();
$historySuppliers = (clone $this->applyFilters(ContractHistory::query(), $request, 'contract_date'))
->join('suppliers', 'contract_histories.current_supplier_id', '=', 'suppliers.id')
->selectRaw('suppliers.name as fornecedor, SUM(contract_histories.contract_value) as total')
->groupBy('suppliers.name')
->orderBy('total', 'desc')
->get();
$merged = [];
foreach ($contractSuppliers as $row) {
$merged[$row->fornecedor] = (float) ($row->total ?? 0);
}
foreach ($historySuppliers as $row) {
$key = $row->fornecedor ?? 'Sem fornecedor';
$merged[$key] = ($merged[$key] ?? 0) + (float) ($row->total ?? 0);
}
return collect(array_map(fn ($fornecedor, $total) => ['fornecedor' => $fornecedor, 'total' => $total], array_keys($merged), array_values($merged)))
->sortByDesc('total')->values();
}
private function yearExpression(string $column): string
{
if (DB::getDriverName() === 'sqlite') {
return "CAST(strftime('%Y', {$column}) AS INTEGER)";
}
return "YEAR({$column})";
}
private function monthExpression(string $column): string
{
if (DB::getDriverName() === 'sqlite') {
return "strftime('%Y-%m', {$column})";
}
return "DATE_FORMAT({$column}, '%Y-%m')";
}
private function parseDate($date)
{
if ($date === null || $date === '') {
return null;
}
$date = trim((string) $date);
try {
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date)) {
return Carbon::createFromFormat('Y-m-d', $date)->format('Y-m-d');
}
if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $date)) {
return Carbon::createFromFormat('d/m/Y', $date)->format('Y-m-d');
}
return Carbon::parse($date)->format('Y-m-d');
} catch (\Exception $e) {
return null;
}
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Laravel\Sanctum\PersonalAccessToken;
class AuthService
{
public function authenticate(array $credentials): User
{
$user = User::where('email', $credentials['email'])->first();
abort_unless($user && Hash::check($credentials['password'], $user->password) && $user->can_login !== false, 401, 'Credenciais invalidas.');
return $user;
}
public function issueToken(User $user): array
{
$token = $user->createToken('api-token', ['*'], now()->addMinutes(20));
return ['token' => $token->plainTextToken, 'user' => $user->only(['id', 'name', 'email'])];
}
public function sessionToken(User $user, string $plainText): ?PersonalAccessToken
{
$token = PersonalAccessToken::findToken($plainText);
if (!$token || $token->tokenable_type !== $user->getMorphClass() || (int) $token->tokenable_id !== (int) $user->id
|| ($token->expires_at && $token->expires_at->isPast()) || $user->can_login === false) {
return null;
}
return $token;
}
public function refresh(PersonalAccessToken $token): array
{
abort_if($token->expires_at && $token->expires_at->isPast(), 401, 'Token expirado.');
$token->update(['expires_at' => now()->addMinutes(20)]);
return [
'message' => 'Token renovado com sucesso',
'expires_at' => $token->expires_at->toISOString(),
'user' => $token->tokenable->only(['id', 'name', 'email']),
];
}
public function check(PersonalAccessToken $token): array
{
$valid = !$token->expires_at || $token->expires_at->isFuture();
$seconds = $valid && $token->expires_at ? max(0, now()->diffInSeconds($token->expires_at, false)) : 0;
return ['valid' => $valid, 'expires_at' => $token->expires_at?->toISOString(), 'expires_in' => $seconds, 'expires_in_minutes' => ceil($seconds / 60)];
}
public function revoke(?PersonalAccessToken $token): void
{
$token?->delete();
}
public function sendResetLink(array $credentials): string
{
return Password::sendResetLink($credentials);
}
public function resetPassword(array $credentials): string
{
return Password::reset($credentials, function (User $user, string $password) {
$user->forceFill(['password' => Hash::make($password), 'remember_token' => Str::random(60)])->save();
$user->tokens()->delete();
event(new PasswordReset($user));
});
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Services;
use App\Models\Clients;
use App\Models\ClientType;
use App\Models\Contract;
use App\Models\User;
use App\Traits\ResourcesCache;
use Illuminate\Http\Request;
class ClientService
{
use ResourcesCache;
public function listing(Request $request, bool $paginated = false)
{
$query = Clients::query()->where('status', 'A');
if ($paginated) {
$query->where('type', 'client');
}
foreach (['name', 'tax_number', 'email'] as $field) {
if ($request->filled($field)) {
$query->where($field, 'like', '%' . $request->input($field) . '%');
}
}
if ($request->filled('client_type_id')) {
$query->where('client_type_id', $request->input('client_type_id'));
}
if ($request->filled('contract_code')) {
$query->whereHas('contracts', fn ($contracts) => $contracts->where('code', 'like', '%' . $request->input('contract_code') . '%'));
}
return $paginated
? $query->orderByDesc('id')->paginate(max(1, min(100, (int) $request->input('per_page', 10))))->appends($request->query())
: $query->orderBy('id')->get();
}
public function types()
{
return ClientType::all();
}
public function form($type, User $user): array
{
return ['type' => ClientType::findOrFail($type), 'sellers' => $user->id === 1 ? User::all() : $this->_getSellers()];
}
public function find($id)
{
return Clients::with('contracts')->findOrFail($id);
}
public function detail($id): array
{
$contracts = Contract::with(['contractType', 'status'])->where('client_id', $id)->orderByDesc('activation_date')->get();
return [
'client' => $this->find($id),
'contractsByStatus' => $contracts->groupBy(fn ($contract) => $contract->status->description ?? 'Sem status'),
'clientTypes' => $this->types(),
'sellers' => $this->_getSellers(),
];
}
public function create(array $data)
{
return Clients::create($data);
}
public function update($id, array $data)
{
$client = $this->find($id);
$client->update($data);
return $client;
}
public function delete($id): void
{
$client = $this->find($id);
$client->status = 'I';
$client->save();
}
public function searchName(string $term)
{
return Clients::with('clientType')->where('type', 'client')->where('status', 'A')
->where('name', 'like', '%' . $term . '%')->orderBy('name')->limit(10)->get()
->map(fn ($client) => ['id' => $client->id, 'label' => $client->name . ' (' . optional($client->clientType)->description . ')']);
}
public function searchID($id)
{
abort_unless(ctype_digit((string) $id), 400, 'ID invalido');
return Clients::where('type', 'client')->where('status', 'A')->findOrFail((int) $id);
}
}

View File

@@ -0,0 +1,735 @@
<?php
namespace App\Services;
use App\Exports\ContractsExport;
use App\Http\Requests\ContractRequest;
use App\Http\Requests\RenewContractRequest;
use App\Models\{
ClientType, Contract, ContractHistory, RenewalType
};
use App\Traits\ResourcesCache;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Maatwebsite\Excel\Facades\Excel;
class ContractService
{
use ResourcesCache;
/**
* Displays a paginated list of contracts with applied filters.
*
* @param Request $request The HTTP request object containing filter and pagination data.
*
* @return \Illuminate\View\View
*
* @author Luis Medon <luismedon@gmail.com>
* @version 2025/06
* @since 2025/06
*/
public function index(Request $request)
{
$perPage = max(1, min(100, (int) $request->get('per_page', 10)));
$query = Contract::query();
$this->_applyFilters($query, $request);
$filteredQuery = clone $query;
$contracts = $query->paginate($perPage)->appends($request->query());
# Values for the full filtered set, not only the current page
$totalPvp = (float) $filteredQuery->sum('contract_value');
$totalCommission = (float) $filteredQuery->sum('commission_value');
$totalChargeback = (float) $filteredQuery->sum('chargeback_value');
$totalMargin = $totalPvp - $totalCommission + $totalChargeback;
return [
'contracts' => $contracts,
'statuses' => $this->_getContractStatuses(),
'contractTypes' => $this->_getContractTypes(),
'sellers' => $this->_getSellers(),
'totalPvp' => $totalPvp,
'totalCommission' => $totalCommission,
'totalChargeback' => $totalChargeback,
'totalMargin' => $totalMargin,
'suppliers' => $this->_getSuppliers(),
'clientTypes' => ClientType::orderBy('description')->get(),
];
}
/**
* Applies filter parameters to the contract query.
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param Request $request
*
* @return void
*
* @author Luis Medon
*/
private function _applyFilters($query, Request $request)
{
$filters = [
'contract_id' => fn($q, $v) => $q->where('id', $v),
'contract_code' => fn($q, $v) => $q->where('code', 'like', "%$v%"),
'status' => fn($q, $v) => $q->where('status_id', $v),
'contract_type' => fn($q, $v) => $q->where('contract_type_id', $v),
'client_type_id' => fn($q, $v) => $q->whereHas(
'client',
fn($clientQuery) => $clientQuery->where('client_type_id', $v)
),
'supply_type' => fn($q, $v) => $q->where('supply_type', $v),
'validity_from' => fn($q, $v) => $q->whereDate('contract_date', '>=', $this->normalizeDate($v)),
'validity_to' => fn($q, $v) => $q->whereDate('contract_date', '<=', $this->normalizeDate($v)),
'activation_from' => fn($q, $v) => $q->whereDate('activation_date', '>=', $this->normalizeDate($v)),
'activation_to' => fn($q, $v) => $q->whereDate('activation_date', '<=', $this->normalizeDate($v)),
'renewal_from' => fn($q, $v) => $q->whereDate('validity', '>=', $this->normalizeDate($v)),
'renewal_to' => fn($q, $v) => $q->whereDate('validity', '<=', $this->normalizeDate($v)),
'seller_id' => fn($q, $v) => $q->where('seller_id', $v),
'supplier_id' => fn($q, $v) => $q->where('new_supplier_id', $v),
];
foreach ($filters as $key => $callback) {
if ($request->filled($key)) {
$callback($query, $request->input($key));
}
}
if ($request->filled('client_name')) {
$query->whereHas('client', fn($q) => $q->where('name', 'like', '%' . $request->input('client_name') . '%'));
}
if ($request->filled('sort_by')) {
$allowedSortColumns = [
'id', 'code', 'status_id', 'contract_type_id', 'supply_type',
'validity', 'activation_date', 'renewal_date', 'seller_id'
];
$sortBy = $request->input('sort_by');
if (in_array($sortBy, $allowedSortColumns)) {
$order = strtolower($request->input('order', 'desc'));
$order = in_array($order, ['asc', 'desc']) ? $order : 'desc';
$query->orderBy($sortBy, $order);
} else {
// Fallback to default sorting if sort_by is invalid
$query->orderBy('activation_date', 'desc');
}
} else {
// Always apply default sorting when sort_by is not provided
// Last added contracts first
$query->orderBy('id', 'desc');
}
}
/**
* Exports filtered contracts to Excel.
*
* @param Request $request
*
* @return \Symfony\Component\HttpFoundation\BinaryFileResponse
*
* @author Luis Medon
*/
public function export(Request $request)
{
$filename = 'contratos_' . now()->format('Ymd_His') . '.xlsx';
return Excel::download(new ContractsExport($request), $filename);
}
/**
* Shows the contract creation form.
*
* @param Request $request
*
* @return \Illuminate\View\View
*
* @author Luis Medon
*/
public function create(Request $request)
{
return [
'clientId' => $request->query('client_id'),
'contract' => null,
'paymentMethods' => $this->_getPaymentMethods(),
'contractTypes' => $this->_getContractTypes(),
'contractStatus' => $this->_getContractStatuses(),
'powerTiers' => $this->_getPowerTiers(),
'cui_tiers' => $this->_getCuiTiers(),
'suppliers' => $this->_getSuppliers(),
'typology' => $this->_getTypologies(),
'sellers' => $this->_getSellers(),
'installments_count' => 12,
];
}
/**
* Stores a newly created contract.
*
* @param ContractRequest $request
*
* @return \Illuminate\Http\RedirectResponse
*
* @author Luis Medon
*/
public function store(ContractRequest $request)
{
$data = $request->validated();
if (! $request->user()?->can('view_contract_summary')) {
foreach (['contract_value', 'commission_value', 'profit_value', 'paid', 'chargeback', 'chargeback_value', 'chargeback_date'] as $field) {
unset($data[$field]);
}
$installmentsInput = [];
} else {
$installmentsInput = $request->input('installments', []);
}
foreach (['contract_date', 'activation_date', 'validity', 'contract_end_date', 'renewal_date', 'payment_date', 'chargeback_date'] as $field) {
if (!empty($data[$field])) {
$data[$field] = $this->normalizeDate($data[$field]);
}
}
if (($data['delivery_method'] ?? null) !== 'email') {
$data['delivery_email'] = null;
}
if (empty($data['chargeback'])) {
$data['chargeback_value'] = null;
$data['chargeback_date'] = null;
}
if ($request->user()?->can('view_contract_summary')) {
$data['profit_value'] = (float) ($data['contract_value'] ?? 0)
- abs((float) ($data['commission_value'] ?? 0))
+ (float) ($data['chargeback_value'] ?? 0);
}
$contract = Contract::create($data);
foreach ($installmentsInput as $i => $installment) {
if (!empty($installment['due_date']) && !empty($installment['amount'])) {
$dueDate = $this->normalizeDate($installment['due_date']);
if ($dueDate === null) {
continue;
}
$contract->installments()->create([
'installment_number' => $i,
'due_date' => $dueDate,
'amount' => $installment['amount'],
'paid' => isset($installment['paid']) && $installment['paid'] ? 1 : 0,
]);
}
}
return $contract;
}
/**
* Shows the contract editing form.
*
* @param string $id
*
* @return \Illuminate\View\View
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*
* @author Luis Medon
*/
public function edit(string $id)
{
$contract = Contract::findOrFail($id);
$suppliers = $this->includeCurrentOption($this->_getSuppliers(), $contract->previousSupplier);
$suppliers = $this->includeCurrentOption($suppliers, $contract->newSupplier);
return [
'contract' => $contract,
'clientId' => $contract->client_id,
'paymentMethods' => $this->includeCurrentOption($this->_getPaymentMethods(), $contract->paymentMethod),
'contractTypes' => $this->includeCurrentOption($this->_getContractTypes(), $contract->contractType),
'contractStatus' => $this->_getContractStatuses(),
'powerTiers' => $this->includeCurrentOption($this->_getPowerTiers(), $contract->powerTier),
'cui_tiers' => $this->includeCurrentOption($this->_getCuiTiers(), $contract->cuiTier),
'suppliers' => $suppliers,
'typology' => $this->includeCurrentOption($this->_getTypologies(), $contract->typology),
'sellers' => $this->includeCurrentOption($this->_getSellers(), $contract->seller),
'installments_count' => 12,
];
}
private function includeCurrentOption(Collection $options, ?Model $current): Collection
{
$options = clone $options;
if ($current && ! $options->contains(fn (Model $option) => $option->is($current))) {
$options->prepend($current);
}
return $options;
}
/**
* Updates an existing contract.
*
* @param ContractRequest $request
* @param string $id
*
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*
* @author Luis Medon <luismedon@gmail.com>
*/
public function update(ContractRequest $request, string $id)
{
$contract = Contract::findOrFail($id);
$data = $request->validated();
if (! $request->user()?->can('view_contract_summary')) {
foreach (['contract_value', 'commission_value', 'profit_value', 'paid', 'chargeback', 'chargeback_value', 'chargeback_date'] as $field) {
unset($data[$field]);
}
$installmentsInput = [];
} else {
$installmentsInput = $request->input('installments', []);
}
// Campos de data do contrato
foreach (['contract_date', 'activation_date', 'validity', 'contract_end_date', 'renewal_date', 'payment_date', 'chargeback_date'] as $field) {
if (!empty($data[$field])) {
$data[$field] = $this->normalizeDate($data[$field]);
}
}
if (($data['delivery_method'] ?? null) !== 'email') {
$data['delivery_email'] = null;
}
if ($request->user()?->can('view_contract_summary') && empty($data['chargeback'])) {
$data['chargeback_value'] = null;
$data['chargeback_date'] = null;
}
if ($request->user()?->can('view_contract_summary')) {
$data['profit_value'] = (float) ($data['contract_value'] ?? 0)
- abs((float) ($data['commission_value'] ?? 0))
+ (float) ($data['chargeback_value'] ?? 0);
}
if (! $contract->update($data)) {
throw \Illuminate\Validation\ValidationException::withMessages([
'contract' => 'O contrato não foi atualizado. Reveja os dados e tente novamente.',
]);
}
// Parcelas
$processedIds = [];
foreach ($installmentsInput as $i => $installment) {
$dueDateRaw = trim($installment['due_date'] ?? '');
$amount = $installment['amount'] ?? null;
if ($dueDateRaw !== '' && $amount !== null) {
$dueDate = $this->normalizeDate($dueDateRaw);
if ($dueDate === null) {
continue;
}
$payload = [
'installment_number' => $i,
'due_date' => $dueDate,
'amount' => $amount,
'paid' => isset($installment['paid']) && $installment['paid'] ? 1 : 0,
];
$existing = $contract->installments()->where('installment_number', $i)->first();
if ($existing) {
$existing->update($payload);
$processedIds[] = $existing->id;
} else {
$new = $contract->installments()->create($payload);
$processedIds[] = $new->id;
}
}
}
// Deleta parcelas não enviadas
if ($request->user()?->can('view_contract_summary')) {
$contract->installments()->whereNotIn('id', $processedIds)->delete();
}
return $contract;
}
/**
* Shows contract details including financial summary.
*
* @param string $id
*
* @return \Illuminate\View\View
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*
* @author Luis Medon
*/
public function show(string $id)
{
$contract = Contract::with('payments')->findOrFail($id);
$lastRef = $contract->payments()->orderByDesc('reference_period')->value('reference_period');
$totalReceived = $totalPending = $commission = $profit = 0;
if ($lastRef) {
$payments = $contract->payments()->whereDate('reference_period', $lastRef)->get();
$totalReceived = $payments->where('status', 'paid')->sum('amount');
$totalPending = $payments->where('status', 'pending')->sum('amount');
$commission = $contract->pvp * ($contract->commission_rate / 100);
$profit = ($contract->pvp - $commission) - $contract->net
+ (float) ($contract->chargeback_value ?? 0);
}
return [
'contract' => $contract,
'contractTypes' => $this->_getContractTypes(),
'statuses' => $this->_getContractStatuses(),
'totalReceived' => $totalReceived,
'totalPending' => $totalPending,
'commission' => $commission,
'profit' => $profit,
];
}
/**
* Displays the contract renewal form.
*
* @param string $id
*
* @return \Illuminate\View\View
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*
* @author Luis Medon
*/
public function renewForm(string $id)
{
$contract = Contract::findOrFail($id);
return [
'contract' => $contract,
'renewalTypes' => RenewalType::orderBy('id')->get(),
'suppliers' => $this->_getSuppliers(),
'contractTypes' => $this->_getContractTypes(),
'typology' => $this->_getTypologies(),
];
}
/**
* Performs contract renewal and creates payment.
*
* @param string $id
*
* @return \Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*
* @author Luis Medon <luismedon@gmail.com>
*/
public function renew(RenewContractRequest $request, $id)
{
# Obtain contract information
$contract = Contract::findOrFail($id);
# Obtain contract installemnts
$installments = $contract->installments()->get();
$totalInstallments = $installments->count();
$totalPaidInstallments = $installments->where('paid', true)->count();
$canViewFinancials = $request->user()?->can('view_contract_summary') ?? false;
$currentSupplierId = $request->boolean('change_supplier')
? $request->new_supplier_id
: $contract->new_supplier_id;
# Create Contract History Record
$historyData = [
'contract_id' => $contract->id,
'previous_supplier_id' => $contract->new_supplier_id,
'current_supplier_id' => $currentSupplierId,
'seller_id' => $contract->seller_id,
'contract_type_id' => $request->contract_type_id,
'typology_id' => $request->typology_id,
'contract_date' => $contract->contract_date,
'activation_date' => $contract->activation_date,
'renewal_date' => $contract->validity,
'renewal_type' => $request->renewal_type,
'go_ki' => $contract->go_ki,
'validity' => $contract->validity,
'contract_value' => $contract->contract_value,
'commission_value' => $contract->commission_value,
'profit_value' => $contract->profit_value,
'annual_consumption' => $contract->annual_consumption,
'total_installments' => $totalInstallments,
'paid_installments' => $totalPaidInstallments,
'notes' => 'Renovação automática',
'contract_snapshot' => $contract->toJson(),
];
if ($canViewFinancials) {
$historyData['chargeback'] = (bool) $contract->chargeback;
$historyData['chargeback_value'] = $contract->chargeback_value;
$historyData['chargeback_date'] = $contract->chargeback_date;
}
ContractHistory::create($historyData);
# Update current contract data
$contractData = [
'annual_consumption' => $request->annual_consumption,
'contract_date' => $request->contract_date,
'activation_date' => $request->activation_date,
'contract_end_date' => $request->contract_end_date,
'validity' => $request->next_due_date,
'contract_type_id' => $request->contract_type_id,
'typology_id' => $request->typology_id,
'go_ki' => $request->go_ki,
'paid' => true,
'previous_supplier_id' => $contract->new_supplier_id,
];
if ($canViewFinancials) {
$contractData['contract_value'] = $request->amount_paid;
$contractData['commission_value'] = $request->commission_value;
$contractData['profit_value'] = (float) $request->amount_paid - abs((float) $request->commission_value) + (float) ($contract->chargeback_value ?? 0);
}
if ($request->boolean('change_supplier')) {
$contractData['new_supplier_id'] = $request->new_supplier_id;
}
$contract->update($contractData);
return $contract;
}
public function history($id)
{
$contract = Contract::findOrFail($id);
$histories = ContractHistory::with(['previousSupplier', 'currentSupplier', 'seller', 'contractType'])
->where('contract_id', $contract->id)
->orderByRaw('COALESCE(validity, contract_date, created_at) ASC')
->paginate(10);
$totals = ContractHistory::where('contract_id', $contract->id)
->selectRaw('COUNT(*) as renewals_count')
->selectRaw('COALESCE(SUM(total_installments), 0) as total_installments')
->selectRaw('COALESCE(SUM(paid_installments), 0) as paid_installments')
->selectRaw('COALESCE(SUM(contract_value), 0) as contract_value')
->selectRaw('COALESCE(SUM(commission_value), 0) as commission_value')
->selectRaw('COALESCE(SUM(chargeback_value), 0) as chargeback_value')
->selectRaw('COALESCE(SUM(profit_value), 0) as profit_value')
->first();
return compact('histories', 'totals', 'contract');
}
public function createHistory($id)
{
$contract = Contract::findOrFail($id);
return [
'contract' => $contract,
'history' => null,
'suppliers' => $this->_getSuppliers(),
];
}
public function storeHistory(Request $request, $id)
{
$contract = Contract::findOrFail($id);
$rules = [
'previous_supplier_id' => ['nullable', 'exists:suppliers,id'],
'current_supplier_id' => ['nullable', 'exists:suppliers,id'],
'validity' => ['nullable', 'date'],
'annual_consumption' => ['nullable', 'numeric'],
'seller_id' => ['nullable', 'exists:users,id'],
'contract_type_id' => ['nullable', 'exists:contract_types,id'],
'typology_id' => ['nullable', 'exists:typologies,id'],
'contract_date' => ['nullable', 'date'],
'activation_date' => ['nullable', 'date'],
'renewal_date' => ['nullable', 'date'],
'renewal_type' => ['nullable', 'string'],
'go_ki' => ['nullable', 'numeric'],
'contract_value' => ['nullable', 'numeric'],
'commission_value' => ['nullable', 'numeric'],
'profit_value' => ['nullable', 'numeric'],
'chargeback' => ['nullable', 'boolean'],
'chargeback_value' => ['nullable', 'numeric', 'max:-0.01'],
'chargeback_date' => ['nullable', 'date'],
'notes' => ['nullable', 'string'],
];
if (! $request->user()?->can('view_contract_summary')) {
foreach (['contract_value', 'commission_value', 'profit_value', 'chargeback', 'chargeback_value', 'chargeback_date'] as $field) {
unset($rules[$field]);
}
}
$data = $request->validate($rules);
foreach (['validity', 'contract_date', 'activation_date', 'renewal_date', 'chargeback_date'] as $field) {
if (! empty($data[$field])) {
$data[$field] = $this->normalizeDate($data[$field]);
}
}
if (empty($data['chargeback'] ?? null)) {
$data['chargeback'] = false;
$data['chargeback_value'] = null;
$data['chargeback_date'] = null;
}
$data['contract_id'] = $contract->id;
if ($request->user()?->can('view_contract_summary') && ! empty($data['contract_value']) && ! empty($data['commission_value'])) {
$data['profit_value'] = (float) ($data['contract_value'] ?? 0)
- abs((float) ($data['commission_value'] ?? 0))
+ (float) ($data['chargeback_value'] ?? 0);
}
return ContractHistory::create($data);
}
public function editHistory($id, $historyId)
{
$contract = Contract::findOrFail($id);
$history = ContractHistory::where('contract_id', $contract->id)->findOrFail($historyId);
return [
'contract' => $contract,
'history' => $history,
'suppliers' => $this->_getSuppliers(),
];
}
private function normalizeDate($value): ?string
{
if ($value === null || $value === '') {
return null;
}
$value = trim((string) $value);
try {
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $value)) {
return Carbon::createFromFormat('Y-m-d', $value)->format('Y-m-d');
}
if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $value)) {
return Carbon::createFromFormat('d/m/Y', $value)->format('Y-m-d');
}
return Carbon::parse($value)->format('Y-m-d');
} catch (\Exception $e) {
return null;
}
}
public function updateHistory(Request $request, $id, $historyId)
{
$contract = Contract::findOrFail($id);
$history = ContractHistory::where('contract_id', $contract->id)->findOrFail($historyId);
$data = $request->validate($this->getHistoryUpdateRules($request));
if (! $request->user()?->can('view_contract_summary')) {
foreach (['contract_value', 'commission_value', 'profit_value', 'chargeback', 'chargeback_value', 'chargeback_date'] as $field) {
unset($data[$field]);
}
}
$history->update($data);
return $history;
}
protected function getHistoryUpdateRules(Request $request): array
{
$rules = [
'previous_supplier_id' => ['nullable', 'exists:suppliers,id'],
'validity' => ['nullable', 'date'],
'annual_consumption' => ['nullable', 'numeric'],
'notes' => ['nullable', 'string'],
];
if ($request->user()?->can('view_contract_summary')) {
$rules['contract_value'] = ['required', 'numeric'];
$rules['commission_value'] = ['nullable', 'numeric'];
$rules['profit_value'] = ['nullable', 'numeric'];
$rules['chargeback'] = ['nullable', 'boolean'];
$rules['chargeback_value'] = ['nullable', 'numeric', 'max:-0.01'];
$rules['chargeback_date'] = ['nullable', 'date'];
}
return $rules;
}
public function destroyHistory($id, $historyId)
{
$contract = Contract::findOrFail($id);
$history = ContractHistory::where('contract_id', $contract->id)->findOrFail($historyId);
$history->delete();
return $history;
}
/**
* Deletes a contract from the system.
*
* @param string $id
*
* @return \Illuminate\Http\JsonResponse|\Illuminate\Http\RedirectResponse
*
* @throws \Illuminate\Database\Eloquent\ModelNotFoundException
*
* @author Luis Medon
*/
public function destroy(string $id)
{
$contract = Contract::findOrFail($id);
$contract->delete();
return ['success' => true, 'message' => 'Contrato removido com sucesso.', 'icon' => 'success'];
}
public function bulkDestroy(Request $request)
{
$data = $request->validate([
'ids' => ['required', 'array', 'min:1'],
'ids.*' => ['integer', 'distinct', 'exists:contracts,id'],
]);
DB::transaction(function () use ($data) {
Contract::whereIn('id', $data['ids'])->get()->each->delete();
});
return [
'success' => true,
'message' => count($data['ids']) === 1
? 'Contrato removido com sucesso.'
: 'Contratos removidos com sucesso.',
'icon' => 'success',
];
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Services;
use App\Models\Contract;
class HomeService
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
}
/**
* Show the application dashboard.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function index()
{
$today = now();
$next60Days = now()->addDays(60);
$activeStatus = 1;
$pendingStatus = 3;
// === Contracts grouped by client type (for the chart) ===
$contractsByClientType = Contract::with('client.clientType')
->where('status_id', $activeStatus)
->get()
->groupBy(function ($contract) {
return $contract->client->clientType->description ?? 'Undefined';
})
->map(function ($group) {
return $group->count();
});
$clientLabels = $contractsByClientType->keys();
$clientData = $contractsByClientType->values();
// === Pending Contracts ===
$pendingContracts = Contract::with(['client', 'contractType'])
->where('status_id', $pendingStatus)
->orderBy('id')
->take(5)
->get();
// === Active Contracts ===
$activeContracts = Contract::with(['client', 'contractType'])
->where('status_id', $activeStatus)
->orderByDesc('id')
->take(5)
->get();
// === Upcoming Renewals (next 2 month) ===
$upcomingRenewals = Contract::with(['client', 'contractType', 'seller', 'installments' => function ($query) use ($today, $next60Days) {
$query->whereBetween('due_date', [$today, $next60Days])
->orderBy('due_date');
}])
->where('status_id', $activeStatus)
->whereBetween('validity', [$today, $next60Days])
->orderByDesc('contracts.id')
->take(5)
->get();
return [
'clientLabels' => $clientLabels,
'clientData' => $clientData,
'activeContracts' => $activeContracts,
'pendingContracts' => $pendingContracts,
'upcomingRenewals' => $upcomingRenewals
];
}
}

View File

@@ -0,0 +1,133 @@
<?php
namespace App\Services;
use App\Http\Requests\LeadRequest;
use App\Models\Clients as Client;
use App\Models\LeadDetail;
use App\Models\LeadStatus;
class LeadService
{
/**
* Display a listing of leads.
*/
public function list()
{
// Fetch only records with type = 'lead' and eager load leadStatus relation
$leads = Client::with('leadStatus')
->where('type', 'lead')
->orderBy('created_at', 'desc')
->paginate(10);
return compact('leads');
}
/**
* Display a specific lead.
*/
public function show($id)
{
// Fetch a single lead with its leadStatus
$lead = Client::with(['leadStatus', 'leadDetail'])->where('type', 'lead')->findOrFail($id);
return compact('lead');
}
/**
* Show the form for creating a new lead.
*/
public function create()
{
// Use unified add_edit view
return ['leadStatuses' => LeadStatus::all()];
}
/**
* Store a newly created lead in storage.
*/
public function store(LeadRequest $request)
{
$validated = $request->validated();
$validated['type'] = 'lead';
$validated['seller_id'] = $request->user()->id;
$client = Client::create($validated);
// Save lead details
LeadDetail::create([
'client_id' => $client->id,
'contact_date' => $request->contact_date,
'contract_end_date' => $request->contract_end_date,
'contract_type' => $request->contract_type,
'delivery_point_code' => $request->delivery_point_code,
]);
return $client->load('leadDetail');
}
/**
* Show the form for editing the specified lead.
*/
public function edit($id)
{
// Find lead by id
$lead = Client::with('leadDetail')->where('type', 'lead')->findOrFail($id);
// Use unified add_edit view and pass the lead
return ['lead' => $lead, 'leadStatuses' => LeadStatus::all()];
}
/**
* Update the specified lead in storage.
*/
public function update(LeadRequest $request, $id)
{
$lead = Client::where('type', 'lead')->findOrFail($id);
$lead->update($request->validated());
// Update or create lead details
$lead->leadDetail()->updateOrCreate(
['client_id' => $lead->id],
[
'contact_date' => $request->contact_date,
'contract_end_date' => $request->contract_end_date,
'contract_type' => $request->contract_type,
'delivery_point_code' => $request->delivery_point_code,
]
);
return $lead->load('leadDetail');
}
/**
* Convert a lead into a client.
*/
public function convert($id)
{
$lead = Client::where('type', 'lead')->findOrFail($id);
$lead->update([
'type' => 'client',
'lead_status_id' => null,
'status' => 'A',
]);
return $lead;
}
/**
* Remove the specified lead from storage.
*/
public function destroy($id)
{
$lead = Client::where('type', 'lead')->findOrFail($id);
$lead->delete();
return [
'status' => 'success',
'message' => 'Lead removed successfully!'
];
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Services;
use App\Models\{PowerTier, ContractType, PaymentMethod, Typology, CuiTier, Supplier, ContractStatus};
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Validation\Rule;
class ResourceService
{
private const MODELS = [
'power_tiers' => PowerTier::class,
'contract_types' => ContractType::class,
'payment_methods' => PaymentMethod::class,
'typologies' => Typology::class,
'cui_tiers' => CuiTier::class,
'suppliers' => Supplier::class,
'contract-statuses' => ContractStatus::class,
];
private function model(string $resource): string
{
abort_unless(isset(self::MODELS[$resource]), 404);
return self::MODELS[$resource];
}
public function all(string $resource)
{
return $this->model($resource)::orderBy('id')->get();
}
public function find(string $resource, $id): Model
{
$model = $this->model($resource)::findOrFail($id);
if ($resource === 'suppliers') {
abort_unless($model->status === 'A', 403, 'Este fornecedor esta inativo.');
}
return $model;
}
public function save(string $resource, Request $request, $id = null): Model
{
$modelClass = $this->model($resource);
$model = $id === null ? new $modelClass() : $this->find($resource, $id);
$rules = match ($resource) {
'contract_types', 'typologies' => ['description' => 'required|string|max:255'],
'payment_methods' => ['name' => 'required|string|max:255', 'icon' => 'required|string|max:255'],
'suppliers' => ['name' => 'required|string|max:255', 'color' => 'nullable|string|max:20'],
'contract-statuses' => [
'code' => ['required', 'string', 'max:10', Rule::unique($model->getTable(), 'code')->ignore($model->getKey())],
'description' => 'required|string|max:255',
'color' => ['nullable', 'string', 'regex:/^#([A-Fa-f0-9]{6})$/'],
],
default => ['name' => 'required|string|max:255'],
};
$model->fill($request->validate($rules));
if ($resource === 'suppliers' && $id === null) {
$model->status = 'A';
}
$model->save();
$this->invalidate($resource);
return $model;
}
public function delete(string $resource, $id): void
{
$model = $this->model($resource)::findOrFail($id);
$resource === 'contract-statuses' ? $model->delete() : $model->update(['status' => 'I']);
$this->invalidate($resource);
}
private function invalidate(string $resource): void
{
Cache::forget($resource === 'contract-statuses' ? 'contract_statuses' : $resource);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Services;
use Illuminate\Http\Request;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
class RolePermissionService
{
public function index()
{
return [
'roles' => Role::with('permissions')->get(),
'permissions' => Permission::all(),
];
}
public function updatePermissions(Request $request, Role $role)
{
$request->validate(['permissions' => 'sometimes|array', 'permissions.*' => 'exists:permissions,name']);
$role->syncPermissions($request->input('permissions', []));
return $role->load('permissions');
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Services;
use Illuminate\Http\Request;
use App\Models\User;
use Spatie\Permission\Models\Role;
class UserAccessService
{
public function index()
{
return [
'users' => User::with('roles')->get(),
'roles' => Role::all(),
];
}
public function updateRoles(Request $request, User $user)
{
$request->validate(['roles' => 'sometimes|array', 'roles.*' => 'exists:roles,name']);
$user->syncRoles($request->input('roles', []));
return $user->load('roles');
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace App\Services;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Str;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Mail;
use App\Mail\NewUserWelcome;
use Illuminate\Support\Facades\Cache;
use Spatie\Permission\Models\Role;
class UserService
{
public function __construct(private AuthService $authentication)
{
}
public function index()
{
$users = User::with('roles')->paginate(20);
return compact('users');
}
public function create()
{
$roles = Role::where('id', '!=', 1)->get();
return compact('roles');
}
public function store(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email',
'roles' => 'array|nullable',
'roles.*' => 'exists:roles,name',
]);
$validator->validate();
$canLogin = $request->boolean('can_login');
$password = Str::random(12);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($password),
'can_login' => $canLogin,
]);
if ($request->filled('roles')) {
$user->syncRoles($request->roles);
}
if ($canLogin) {
Mail::to($user->email)->send(new NewUserWelcome($user, $password));
}
Cache::forget('sellers');
return $user;
}
public function edit(User $user)
{
$roles = Role::where('id', '!=', 1)->get();
return compact('user', 'roles');
}
public function update(Request $request, User $user)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|max:255',
'email' => 'required|email|unique:users,email,' . $user->id,
'roles' => 'array|nullable',
'roles.*' => 'exists:roles,name',
]);
$validator->validate();
$user->update([
'name' => $request->name,
'email' => $request->email,
'can_login' => $request->boolean('can_login'),
]);
if ($request->has('roles')) {
$user->syncRoles($request->input('roles', []));
}
Cache::forget('sellers');
return $user;
}
public function sendPasswordResetLink(User $user)
{
$status = $this->authentication->sendResetLink(['email' => $user->email]);
return $status;
}
public function savePin(Request $request)
{
$request->validate([
'pin' => ['required', 'digits:4'],
]);
$user = $request->user();
/** @var User $user */
$user->pin = Hash::make($request->pin);
$user->save();
return ['message' => 'PIN saved successfully.'];
}
public function checkPin(Request $request)
{
$request->validate([
'pin' => ['required', 'digits:4']
]);
$user = $request->user();
if ($user->pin && Hash::check($request->pin, $user->pin)) {
return ['message' => 'PIN valid'];
}
throw \Illuminate\Validation\ValidationException::withMessages(['pin' => 'Invalid PIN']);
}
public function destroy(User $user): void
{
$user->tokens()->delete();
$user->delete();
Cache::forget('sellers');
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace App\Traits;
use Illuminate\Support\Facades\Cache;
use App\Models\{
CuiTier, Clients, ContractStatus,
ContractType, PaymentMethod,
PowerTier, Supplier, Typology, User
};
/**
* Trait ResourcesCache
*
* This trait provides cached access to frequently used resources
* such as contract statuses, contract types, payment methods,
* suppliers, and others. It helps to avoid hitting the database
* repeatedly by caching results for a configurable period of time.
*/
trait ResourcesCache
{
/**
* Defines the cache lifetime in seconds.
*
* It retrieves the session lifetime from the configuration
* and converts it into seconds (minutes * 60).
*
* @return int
*/
private function cacheTime(): int
{
return config('session.lifetime') * 60;
}
/**
* Get all contract statuses, cached.
*/
public function _getContractStatuses()
{
return Cache::remember(
'contract_statuses',
$this->cacheTime(),
fn() => ContractStatus::all()
);
}
/**
* Get active contract types, cached.
*/
public function _getContractTypes()
{
return Cache::remember(
'contract_types',
$this->cacheTime(),
fn() => ContractType::where('status', 'A')->get()
);
}
/**
* Get active payment methods, cached.
*/
public function _getPaymentMethods()
{
return Cache::remember(
'payment_methods',
$this->cacheTime(),
fn() => PaymentMethod::where('status', 'A')->get()
);
}
/**
* Get active power tiers, cached.
*/
public function _getPowerTiers()
{
return Cache::remember(
'power_tiers',
$this->cacheTime(),
fn() => PowerTier::where('status', 'A')->get()
);
}
/**
* Get active CUI tiers, cached.
*/
public function _getCuiTiers()
{
return Cache::remember(
'cui_tiers',
$this->cacheTime(),
fn() => CuiTier::where('status', 'A')->get()
);
}
/**
* Get active suppliers, cached.
*/
public function _getSuppliers()
{
return Cache::remember(
'suppliers',
$this->cacheTime(),
fn() => Supplier::where('status', 'A')->get()
);
}
/**
* Get active typologies, cached.
*/
public function _getTypologies()
{
return Cache::remember(
'typologies',
$this->cacheTime(),
fn() => Typology::where('status', 'A')->get()
);
}
/**
* Get all sellers except the one with ID = 1, cached.
*/
public function _getSellers()
{
return Cache::remember(
'sellers',
$this->cacheTime(),
fn() => User::where('id', '!=', 1)->get()
);
}
}

18
artisan Normal file
View File

@@ -0,0 +1,18 @@
#!/usr/bin/env php
<?php
use Illuminate\Foundation\Application;
use Symfony\Component\Console\Input\ArgvInput;
define('LARAVEL_START', microtime(true));
// Register the Composer autoloader...
require __DIR__.'/vendor/autoload.php';
// Bootstrap Laravel and handle the command...
/** @var Application $app */
$app = require_once __DIR__.'/bootstrap/app.php';
$status = $app->handleCommand(new ArgvInput);
exit($status);

79
bootstrap/app.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Facades\URL;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
api: __DIR__.'/../routes/api.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware) {
$middleware->api(prepend: [\App\Http\Middleware\ApiResponse::class]);
# Register Middleware
$middleware->alias([
'auth.session' => \App\Http\Middleware\AuthSession::class,
]);
})
->withExceptions(function (Exceptions $exceptions) {
$exceptions->reportable(function (Throwable $e) {
});
$exceptions->render(function (Throwable $e, $request) {
if ($request->is('api/*') || $request->expectsJson()) {
$status = 500;
$message = $e->getMessage() ?: 'Erro interno do servidor';
if ($e instanceof \Illuminate\Validation\ValidationException) {
return response()->json([
'message' => 'Erro de validação.',
'errors' => $e->errors(),
], 422);
}
if ($e instanceof \Illuminate\Auth\AuthenticationException) {
return response()->json(['message' => 'Nao autenticado.'], 401);
}
if ($e instanceof \Illuminate\Auth\Access\AuthorizationException) {
return response()->json(['message' => 'Acesso negado.'], 403);
}
if ($e instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) {
return response()->json(['message' => 'Registo nao encontrado.'], 404);
}
if ($e instanceof \Symfony\Component\HttpKernel\Exception\HttpExceptionInterface) {
$status = $e->getStatusCode();
$message = $e->getMessage() ?: 'Erro HTTP';
}
return response()->json([
'message' => $status >= 500 && app()->environment('production') ? 'Erro interno do servidor.' : $message,
'exception' => app()->environment('production') ? null : get_class($e),
], $status);
}
return null;
});
})
->withSchedule(function (Schedule $schedule) {
// $schedule->command('backup:run')->dailyAt('13:00');
// $schedule->command('backup:clean-minio')->dailyAt('13:30');
$schedule->command('sanctum:prune-expired --hours=24')->dailyAt('05:00');
})
->booted(function () {
if (config('app.force_ssl')) {
URL::forceScheme('https');
}
})
->create();

2
bootstrap/cache/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

6
bootstrap/providers.php Normal file
View File

@@ -0,0 +1,6 @@
<?php
return [
App\Providers\AppServiceProvider::class,
App\Providers\TelescopeServiceProvider::class,
];

82
composer.json Normal file
View File

@@ -0,0 +1,82 @@
{
"$schema": "https://getcomposer.org/schema.json",
"name": "laravel/laravel",
"type": "project",
"description": "The skeleton application for the Laravel framework.",
"keywords": [
"laravel",
"framework"
],
"license": "MIT",
"require": {
"php": "^8.2",
"doctrine/dbal": "^4.3",
"laravel/framework": "^12.0",
"laravel/sanctum": "^4.2",
"laravel/tinker": "^2.10.1",
"laravel/ui": "^4.6",
"league/flysystem-aws-s3-v3": "^3.0",
"maatwebsite/excel": "^3.1",
"nesbot/carbon": "^3.9",
"spatie/laravel-backup": "^9.3",
"spatie/laravel-permission": "^6.17"
},
"require-dev": {
"fakerphp/faker": "^1.24",
"laravel/pail": "^1.2.2",
"laravel/pint": "^1.13",
"laravel/sail": "^1.41",
"mockery/mockery": "^1.6",
"nunomaduro/collision": "^8.6",
"phpunit/phpunit": "^11.5.3"
},
"autoload": {
"psr-4": {
"App\\": "app/",
"Database\\Factories\\": "database/factories/",
"Database\\Seeders\\": "database/seeders/"
}
},
"autoload-dev": {
"psr-4": {
"Tests\\": "tests/"
}
},
"scripts": {
"post-autoload-dump": [
"Illuminate\\Foundation\\ComposerScripts::postAutoloadDump",
"@php artisan package:discover --ansi"
],
"post-update-cmd": [
"@php artisan vendor:publish --tag=laravel-assets --ansi --force"
],
"post-root-package-install": [
"@php -r \"file_exists('.env') || copy('.env.example', '.env');\""
],
"post-create-project-cmd": [
"@php artisan key:generate --ansi",
"@php -r \"file_exists('database/database.sqlite') || touch('database/database.sqlite');\"",
"@php artisan migrate --graceful --ansi"
],
"dev": [
"Composer\\Config::disableProcessTimeout",
"npx concurrently -c \"#93c5fd,#c4b5fd,#fb7185,#fdba74\" \"php artisan serve\" \"php artisan queue:listen --tries=1\" \"php artisan pail --timeout=0\" \"npm run dev\" --names=server,queue,logs,vite"
]
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"config": {
"optimize-autoloader": true,
"preferred-install": "dist",
"sort-packages": true,
"allow-plugins": {
"pestphp/pest-plugin": true,
"php-http/discovery": true
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

10102
composer.lock generated Normal file

File diff suppressed because it is too large Load Diff

234
config/app.php Normal file
View File

@@ -0,0 +1,234 @@
<?php
return [
'force_ssl' => env('FORCE_SSL', false),
/*
|--------------------------------------------------------------------------
| Application Name
|--------------------------------------------------------------------------
|
| This value is the name of your application, which will be used when the
| framework needs to place the application's name in a notification or
| other UI elements where an application name needs to be displayed.
|
*/
'name' => env('APP_NAME', 'Laravel'),
/*
|--------------------------------------------------------------------------
| Application Environment
|--------------------------------------------------------------------------
|
| This value determines the "environment" your application is currently
| running in. This may determine how you prefer to configure various
| services the application utilizes. Set this in your ".env" file.
|
*/
'env' => env('APP_ENV', 'production'),
/*
|--------------------------------------------------------------------------
| Application Debug Mode
|--------------------------------------------------------------------------
|
| When your application is in debug mode, detailed error messages with
| stack traces will be shown on every error that occurs within your
| application. If disabled, a simple generic error page is shown.
|
*/
'debug' => (bool) env('APP_DEBUG', false),
/*
|--------------------------------------------------------------------------
| Application URL
|--------------------------------------------------------------------------
|
| This URL is used by the console to properly generate URLs when using
| the Artisan command line tool. You should set this to the root of
| the application so that it's available within Artisan commands.
|
*/
'url' => env('APP_URL', 'http://localhost'),
'api_url' => env('API_URL', 'http://localhost/api'),
/*
|--------------------------------------------------------------------------
| Application Timezone
|--------------------------------------------------------------------------
|
| Here you may specify the default timezone for your application, which
| will be used by the PHP date and date-time functions. The timezone
| is set to "UTC" by default as it is suitable for most use cases.
|
*/
'timezone' => env('APP_TIMEZONE', 'UTC'),
/*
|--------------------------------------------------------------------------
| Application Locale Configuration
|--------------------------------------------------------------------------
|
| The application locale determines the default locale that will be used
| by Laravel's translation / localization methods. This option can be
| set to any locale for which you plan to have translation strings.
|
*/
'locale' => env('APP_LOCALE', 'pt'),
'fallback_locale' => env('APP_FALLBACK_LOCALE', 'pt'),
'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
/*
|--------------------------------------------------------------------------
| Encryption Key
|--------------------------------------------------------------------------
|
| This key is utilized by Laravel's encryption services and should be set
| to a random, 32 character string to ensure that all encrypted values
| are secure. You should do this prior to deploying the application.
|
*/
'cipher' => 'AES-256-CBC',
'key' => env('APP_KEY'),
'previous_keys' => [
...array_filter(
explode(',', env('APP_PREVIOUS_KEYS', ''))
),
],
/*
|--------------------------------------------------------------------------
| Maintenance Mode Driver
|--------------------------------------------------------------------------
|
| These configuration options determine the driver used to determine and
| manage Laravel's "maintenance mode" status. The "cache" driver will
| allow maintenance mode to be controlled across multiple machines.
|
| Supported drivers: "file", "cache"
|
*/
'maintenance' => [
'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
'store' => env('APP_MAINTENANCE_STORE', 'database'),
],
/*
|--------------------------------------------------------------------------
| Autoloaded Service Providers
|--------------------------------------------------------------------------
|
| The service providers listed here will be automatically loaded on the
| request to your application. Feel free to add your own services to
| this array to grant expanded functionality to your applications.
|
*/
'providers' => [
/*
* Laravel Framework Service Providers...
*/
Illuminate\Auth\AuthServiceProvider::class,
Illuminate\Broadcasting\BroadcastServiceProvider::class,
Illuminate\Bus\BusServiceProvider::class,
Illuminate\Cache\CacheServiceProvider::class,
Illuminate\Foundation\Providers\ConsoleSupportServiceProvider::class,
Illuminate\Cookie\CookieServiceProvider::class,
Illuminate\Database\DatabaseServiceProvider::class,
Illuminate\Encryption\EncryptionServiceProvider::class,
Illuminate\Filesystem\FilesystemServiceProvider::class, // 👈 Importante
Illuminate\Foundation\Providers\FoundationServiceProvider::class,
Illuminate\Hashing\HashServiceProvider::class,
Illuminate\Mail\MailServiceProvider::class,
Illuminate\Notifications\NotificationServiceProvider::class,
Illuminate\Pagination\PaginationServiceProvider::class,
Illuminate\Pipeline\PipelineServiceProvider::class,
Illuminate\Queue\QueueServiceProvider::class,
Illuminate\Redis\RedisServiceProvider::class,
Illuminate\Auth\Passwords\PasswordResetServiceProvider::class,
Illuminate\Session\SessionServiceProvider::class,
Illuminate\Translation\TranslationServiceProvider::class,
Illuminate\Validation\ValidationServiceProvider::class,
Illuminate\View\ViewServiceProvider::class,
/*
* Package Service Providers...
*/
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
// App\Providers\AuthServiceProvider::class,
// App\Providers\BroadcastServiceProvider::class,
// App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
],
/*
|--------------------------------------------------------------------------
| Class Aliases
|--------------------------------------------------------------------------
|
| This array of class aliases will be registered when this application
| is started. However, feel free to register as many as you wish as
| the aliases are "lazy" loaded so they don't hinder performance.
|
*/
'aliases' => [
'App' => Illuminate\Support\Facades\App::class,
'Arr' => Illuminate\Support\Arr::class,
'Artisan' => Illuminate\Support\Facades\Artisan::class,
'Auth' => Illuminate\Support\Facades\Auth::class,
'Blade' => Illuminate\Support\Facades\Blade::class,
'Broadcast' => Illuminate\Support\Facades\Broadcast::class,
'Bus' => Illuminate\Support\Facades\Bus::class,
'Cache' => Illuminate\Support\Facades\Cache::class,
'Config' => Illuminate\Support\Facades\Config::class,
'Cookie' => Illuminate\Support\Facades\Cookie::class,
'Crypt' => Illuminate\Support\Facades\Crypt::class,
'DB' => Illuminate\Support\Facades\DB::class,
'Eloquent' => Illuminate\Database\Eloquent\Model::class,
'Event' => Illuminate\Support\Facades\Event::class,
'File' => Illuminate\Support\Facades\File::class,
'Gate' => Illuminate\Support\Facades\Gate::class,
'Hash' => Illuminate\Support\Facades\Hash::class,
'Http' => Illuminate\Support\Facades\Http::class,
'Lang' => Illuminate\Support\Facades\Lang::class,
'Log' => Illuminate\Support\Facades\Log::class,
'Mail' => Illuminate\Support\Facades\Mail::class,
'Notification' => Illuminate\Support\Facades\Notification::class,
'Password' => Illuminate\Support\Facades\Password::class,
'Queue' => Illuminate\Support\Facades\Queue::class,
'Redirect' => Illuminate\Support\Facades\Redirect::class,
'Request' => Illuminate\Support\Facades\Request::class,
'Response' => Illuminate\Support\Facades\Response::class,
'Route' => Illuminate\Support\Facades\Route::class,
'Schema' => Illuminate\Support\Facades\Schema::class,
'Session' => Illuminate\Support\Facades\Session::class,
'Storage' => Illuminate\Support\Facades\Storage::class,
'Str' => Illuminate\Support\Str::class,
'URL' => Illuminate\Support\Facades\URL::class,
'Validator' => Illuminate\Support\Facades\Validator::class,
'View' => Illuminate\Support\Facades\View::class,
],
];

115
config/auth.php Normal file
View File

@@ -0,0 +1,115 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Defaults
|--------------------------------------------------------------------------
|
| This option defines the default authentication "guard" and password
| reset "broker" for your application. You may change these values
| as required, but they're a perfect start for most applications.
|
*/
'defaults' => [
'guard' => env('AUTH_GUARD', 'web'),
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
],
/*
|--------------------------------------------------------------------------
| Authentication Guards
|--------------------------------------------------------------------------
|
| Next, you may define every authentication guard for your application.
| Of course, a great default configuration has been defined for you
| which utilizes session storage plus the Eloquent user provider.
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| Supported: "session"
|
*/
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
],
/*
|--------------------------------------------------------------------------
| User Providers
|--------------------------------------------------------------------------
|
| All authentication guards have a user provider, which defines how the
| users are actually retrieved out of your database or other storage
| system used by the application. Typically, Eloquent is utilized.
|
| If you have multiple user tables or models you may configure multiple
| providers to represent the model / table. These providers may then
| be assigned to any extra authentication guards you have defined.
|
| Supported: "database", "eloquent"
|
*/
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => env('AUTH_MODEL', App\Models\User::class),
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
/*
|--------------------------------------------------------------------------
| Resetting Passwords
|--------------------------------------------------------------------------
|
| These configuration options specify the behavior of Laravel's password
| reset functionality, including the table utilized for token storage
| and the user provider that is invoked to actually retrieve users.
|
| The expiry time is the number of minutes that each reset token will be
| considered valid. This security feature keeps tokens short-lived so
| they have less time to be guessed. You may change this as needed.
|
| The throttle setting is the number of seconds a user must wait before
| generating more password reset tokens. This prevents the user from
| quickly generating a very large amount of password reset tokens.
|
*/
'passwords' => [
'users' => [
'provider' => 'users',
'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
'expire' => 60,
'throttle' => 60,
],
],
/*
|--------------------------------------------------------------------------
| Password Confirmation Timeout
|--------------------------------------------------------------------------
|
| Here you may define the amount of seconds before a password confirmation
| window expires and users are asked to re-enter their password via the
| confirmation screen. By default, the timeout lasts for three hours.
|
*/
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
];

341
config/backup.php Normal file
View File

@@ -0,0 +1,341 @@
<?php
return [
'backup' => [
/*
* The name of this application. You can use this name to monitor
* the backups.
*/
'name' => 'laravel-backup',
'source' => [
'files' => [
/*
* The list of directories and files that will be included in the backup.
*/
'include' => [
// base_path(),
],
/*
* These directories and files will be excluded from the backup.
*
* Directories used by the backup process will automatically be excluded.
*/
'exclude' => [
// base_path('vendor'),
// base_path('node_modules'),
],
/*
* Determines if symlinks should be followed.
*/
'follow_links' => false,
/*
* Determines if it should avoid unreadable folders.
*/
'ignore_unreadable_directories' => false,
/*
* This path is used to make directories in resulting zip-file relative
* Set to `null` to include complete absolute path
* Example: base_path()
*/
'relative_path' => null,
],
/*
* The names of the connections to the databases that should be backed up
* MySQL, PostgreSQL, SQLite and Mongo databases are supported.
*
* The content of the database dump may be customized for each connection
* by adding a 'dump' key to the connection settings in config/database.php.
* E.g.
* 'mysql' => [
* ...
* 'dump' => [
* 'excludeTables' => [
* 'table_to_exclude_from_backup',
* 'another_table_to_exclude'
* ]
* ],
* ],
*
* If you are using only InnoDB tables on a MySQL server, you can
* also supply the useSingleTransaction option to avoid table locking.
*
* E.g.
* 'mysql' => [
* ...
* 'dump' => [
* 'useSingleTransaction' => true,
* ],
* ],
*
* For a complete list of available customization options, see https://github.com/spatie/db-dumper
*/
'databases' => [
env('DB_CONNECTION', 'mysql'),
],
],
/*
* The database dump can be compressed to decrease disk space usage.
*
* Out of the box Laravel-backup supplies
* Spatie\DbDumper\Compressors\GzipCompressor::class.
*
* You can also create custom compressor. More info on that here:
* https://github.com/spatie/db-dumper#using-compression
*
* If you do not want any compressor at all, set it to null.
*/
'database_dump_compressor' => Spatie\DbDumper\Compressors\GzipCompressor::class,
/*
* If specified, the database dumped file name will contain a timestamp (e.g.: 'Y-m-d-H-i-s').
*/
'database_dump_file_timestamp_format' => null,
/*
* The base of the dump filename, either 'database' or 'connection'
*
* If 'database' (default), the dumped filename will contain the database name.
* If 'connection', the dumped filename will contain the connection name.
*/
'database_dump_filename_base' => 'database',
/*
* The file extension used for the database dump files.
*
* If not specified, the file extension will be .archive for MongoDB and .sql for all other databases
* The file extension should be specified without a leading .
*/
'database_dump_file_extension' => '',
'destination' => [
/*
* The compression algorithm to be used for creating the zip archive.
*
* If backing up only database, you may choose gzip compression for db dump and no compression at zip.
*
* Some common algorithms are listed below:
* ZipArchive::CM_STORE (no compression at all; set 0 as compression level)
* ZipArchive::CM_DEFAULT
* ZipArchive::CM_DEFLATE
* ZipArchive::CM_BZIP2
* ZipArchive::CM_XZ
*
* For more check https://www.php.net/manual/zip.constants.php and confirm it's supported by your system.
*/
'compression_method' => ZipArchive::CM_DEFAULT,
/*
* The compression level corresponding to the used algorithm; an integer between 0 and 9.
*
* Check supported levels for the chosen algorithm, usually 1 means the fastest and weakest compression,
* while 9 the slowest and strongest one.
*
* Setting of 0 for some algorithms may switch to the strongest compression.
*/
'compression_level' => 9,
/*
* The filename prefix used for the backup zip file.
*/
'filename_prefix' => '',
/*
* The disk names on which the backups will be stored.
*/
'disks' => [
env('FILESYSTEM_DISK', 'local'),
],
],
/*
* The directory where the temporary files will be stored.
*/
'temporary_directory' => storage_path('app/backup-temp'),
/*
* The password to be used for archive encryption.
* Set to `null` to disable encryption.
*/
'password' => env('BACKUP_ARCHIVE_PASSWORD'),
/*
* The encryption algorithm to be used for archive encryption.
* You can set it to `null` or `false` to disable encryption.
*
* When set to 'default', we'll use ZipArchive::EM_AES_256 if it is
* available on your system.
*/
'encryption' => 'default',
/*
* The number of attempts, in case the backup command encounters an exception
*/
'tries' => 1,
/*
* The number of seconds to wait before attempting a new backup if the previous try failed
* Set to `0` for none
*/
'retry_delay' => 0,
],
/*
* You can get notified when specific events occur. Out of the box you can use 'mail' and 'slack'.
* For Slack you need to install laravel/slack-notification-channel.
*
* You can also use your own notification classes, just make sure the class is named after one of
* the `Spatie\Backup\Notifications\Notifications` classes.
*/
'notifications' => [
'notifications' => [
\Spatie\Backup\Notifications\Notifications\BackupHasFailedNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\UnhealthyBackupWasFoundNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\CleanupHasFailedNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\BackupWasSuccessfulNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\HealthyBackupWasFoundNotification::class => ['mail'],
\Spatie\Backup\Notifications\Notifications\CleanupWasSuccessfulNotification::class => ['mail'],
],
/*
* Here you can specify the notifiable to which the notifications should be sent. The default
* notifiable will use the variables specified in this config file.
*/
'notifiable' => \Spatie\Backup\Notifications\Notifiable::class,
'mail' => [
'to' => ['luismedon@gmail.com'],
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name' => env('MAIL_FROM_NAME', 'Example'),
],
],
'slack' => [
'webhook_url' => '',
/*
* If this is set to null the default channel of the webhook will be used.
*/
'channel' => null,
'username' => null,
'icon' => null,
],
'discord' => [
'webhook_url' => '',
/*
* If this is an empty string, the name field on the webhook will be used.
*/
'username' => '',
/*
* If this is an empty string, the avatar on the webhook will be used.
*/
'avatar_url' => '',
],
],
/*
* Here you can specify which backups should be monitored.
* If a backup does not meet the specified requirements the
* UnHealthyBackupWasFound event will be fired.
*/
'monitor_backups' => [
[
'name' => 'laravel-backup',
'disks' => [env('FILESYSTEM_DISK', 'local')],
'health_checks' => [
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumAgeInDays::class => 1,
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumStorageInMegabytes::class => 5000,
],
],
/*
[
'name' => 'name of the second app',
'disks' => ['local', 's3'],
'health_checks' => [
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumAgeInDays::class => 1,
\Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumStorageInMegabytes::class => 5000,
],
],
*/
],
'cleanup' => [
/*
* The strategy that will be used to cleanup old backups. The default strategy
* will keep all backups for a certain amount of days. After that period only
* a daily backup will be kept. After that period only weekly backups will
* be kept and so on.
*
* No matter how you configure it the default strategy will never
* delete the newest backup.
*/
'strategy' => \Spatie\Backup\Tasks\Cleanup\Strategies\DefaultStrategy::class,
'default_strategy' => [
/*
* The number of days for which backups must be kept.
*/
'keep_all_backups_for_days' => 0,
/*
* After the "keep_all_backups_for_days" period is over, the most recent backup
* of that day will be kept. Older backups within the same day will be removed.
* If you create backups only once a day, no backups will be removed yet.
*/
'keep_daily_backups_for_days' => 5,
/*
* After the "keep_daily_backups_for_days" period is over, the most recent backup
* of that week will be kept. Older backups within the same week will be removed.
* If you create backups only once a week, no backups will be removed yet.
*/
'keep_weekly_backups_for_weeks' => 0,
/*
* After the "keep_weekly_backups_for_weeks" period is over, the most recent backup
* of that month will be kept. Older backups within the same month will be removed.
*/
'keep_monthly_backups_for_months' => 0,
/*
* After the "keep_monthly_backups_for_months" period is over, the most recent backup
* of that year will be kept. Older backups within the same year will be removed.
*/
'keep_yearly_backups_for_years' => 0,
/*
* After cleaning up the backups remove the oldest backup until
* this amount of megabytes has been reached.
* Set null for unlimited size.
*/
'delete_oldest_backups_when_using_more_megabytes_than' => null,
],
/*
* The number of attempts, in case the cleanup command encounters an exception
*/
'tries' => 1,
/*
* The number of seconds to wait before attempting a new cleanup if the previous try failed
* Set to `0` for none
*/
'retry_delay' => 0,
],
];

108
config/cache.php Normal file
View File

@@ -0,0 +1,108 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Cache Store
|--------------------------------------------------------------------------
|
| This option controls the default cache store that will be used by the
| framework. This connection is utilized if another isn't explicitly
| specified when running a cache operation inside the application.
|
*/
'default' => env('CACHE_DRIVER', 'database'),
/*
|--------------------------------------------------------------------------
| Cache Stores
|--------------------------------------------------------------------------
|
| Here you may define all of the cache "stores" for your application as
| well as their drivers. You may even define multiple stores for the
| same cache driver to group types of items stored in your caches.
|
| Supported drivers: "array", "database", "file", "memcached",
| "redis", "dynamodb", "octane", "null"
|
*/
'stores' => [
'array' => [
'driver' => 'array',
'serialize' => false,
],
'database' => [
'driver' => 'database',
'connection' => env('DB_CACHE_CONNECTION'),
'table' => env('DB_CACHE_TABLE', 'cache'),
'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
'lock_table' => env('DB_CACHE_LOCK_TABLE'),
],
'file' => [
'driver' => 'file',
'path' => storage_path('framework/cache/data'),
'lock_path' => storage_path('framework/cache/data'),
],
'memcached' => [
'driver' => 'memcached',
'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
'sasl' => [
env('MEMCACHED_USERNAME'),
env('MEMCACHED_PASSWORD'),
],
'options' => [
// Memcached::OPT_CONNECT_TIMEOUT => 2000,
],
'servers' => [
[
'host' => env('MEMCACHED_HOST', '127.0.0.1'),
'port' => env('MEMCACHED_PORT', 11211),
'weight' => 100,
],
],
],
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
],
'dynamodb' => [
'driver' => 'dynamodb',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
'endpoint' => env('DYNAMODB_ENDPOINT'),
],
'octane' => [
'driver' => 'octane',
],
],
/*
|--------------------------------------------------------------------------
| Cache Key Prefix
|--------------------------------------------------------------------------
|
| When utilizing the APC, database, memcached, Redis, and DynamoDB cache
| stores, there might be other applications using the same cache. For
| that reason, you may prefix every cache key to avoid collisions.
|
*/
'prefix' => env('CACHE_PREFIX'),
];

174
config/database.php Normal file
View File

@@ -0,0 +1,174 @@
<?php
use Illuminate\Support\Str;
return [
/*
|--------------------------------------------------------------------------
| Default Database Connection Name
|--------------------------------------------------------------------------
|
| Here you may specify which of the database connections below you wish
| to use as your default connection for database operations. This is
| the connection which will be utilized unless another connection
| is explicitly specified when you execute a query / statement.
|
*/
'default' => env('DB_CONNECTION', 'sqlite'),
/*
|--------------------------------------------------------------------------
| Database Connections
|--------------------------------------------------------------------------
|
| Below are all of the database connections defined for your application.
| An example configuration is provided for each database system which
| is supported by Laravel. You're free to add / remove connections.
|
*/
'connections' => [
'sqlite' => [
'driver' => 'sqlite',
'url' => env('DB_URL'),
'database' => env('DB_DATABASE', database_path('database.sqlite')),
'prefix' => '',
'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
'busy_timeout' => null,
'journal_mode' => null,
'synchronous' => null,
],
'mysql' => [
'driver' => 'mysql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'mariadb' => [
'driver' => 'mariadb',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => env('DB_CHARSET', 'utf8mb4'),
'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
'prefix' => '',
'prefix_indexes' => true,
'strict' => true,
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
'pgsql' => [
'driver' => 'pgsql',
'url' => env('DB_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '5432'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
'search_path' => 'public',
'sslmode' => 'prefer',
],
'sqlsrv' => [
'driver' => 'sqlsrv',
'url' => env('DB_URL'),
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '1433'),
'database' => env('DB_DATABASE', 'laravel'),
'username' => env('DB_USERNAME', 'root'),
'password' => env('DB_PASSWORD', ''),
'charset' => env('DB_CHARSET', 'utf8'),
'prefix' => '',
'prefix_indexes' => true,
// 'encrypt' => env('DB_ENCRYPT', 'yes'),
// 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
],
],
/*
|--------------------------------------------------------------------------
| Migration Repository Table
|--------------------------------------------------------------------------
|
| This table keeps track of all the migrations that have already run for
| your application. Using this information, we can determine which of
| the migrations on disk haven't actually been run on the database.
|
*/
'migrations' => [
'table' => 'migrations',
'update_date_on_publish' => true,
],
/*
|--------------------------------------------------------------------------
| Redis Databases
|--------------------------------------------------------------------------
|
| Redis is an open source, fast, and advanced key-value store that also
| provides a richer body of commands than a typical key-value system
| such as Memcached. You may define your connection settings here.
|
*/
'redis' => [
'client' => env('REDIS_CLIENT', 'phpredis'),
'options' => [
'cluster' => env('REDIS_CLUSTER', 'redis'),
'prefix' => env('REDIS_PREFIX', Str::slug(env('APP_NAME', 'laravel'), '_').'_database_'),
'persistent' => env('REDIS_PERSISTENT', false),
],
'default' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_DB', '0'),
],
'cache' => [
'url' => env('REDIS_URL'),
'host' => env('REDIS_HOST', '127.0.0.1'),
'username' => env('REDIS_USERNAME'),
'password' => env('REDIS_PASSWORD'),
'port' => env('REDIS_PORT', '6379'),
'database' => env('REDIS_CACHE_DB', '1'),
],
],
];

79
config/filesystems.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Default Filesystem Disk
|--------------------------------------------------------------------------
|
| Here you may specify the default filesystem disk that should be used
| by the framework. The "local" disk, as well as a variety of cloud
| based disks are available to your application for file storage.
|
*/
'default' => env('FILESYSTEM_DISK', 'local'),
/*
|--------------------------------------------------------------------------
| Filesystem Disks
|--------------------------------------------------------------------------
|
| Below you may configure as many filesystem disks as necessary, and you
| may even configure multiple disks for the same driver. Examples for
| most supported storage drivers are configured here for reference.
|
| Supported drivers: "local", "ftp", "sftp", "s3"
|
*/
'disks' => [
'local' => [
'driver' => 'local',
'root' => storage_path('app/private'),
'serve' => true,
'throw' => false,
'report' => false,
],
'public' => [
'driver' => 'local',
'root' => storage_path('app/public'),
'url' => env('APP_URL').'/storage',
'visibility' => 'public',
'throw' => false,
'report' => false,
],
's3' => [
'driver' => 's3',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION'),
'bucket' => env('AWS_BUCKET'),
'url' => env('AWS_URL'),
'endpoint' => env('AWS_ENDPOINT'),
'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', true),
],
],
/*
|--------------------------------------------------------------------------
| Symbolic Links
|--------------------------------------------------------------------------
|
| Here you may configure the symbolic links that will be created when the
| `storage:link` Artisan command is executed. The array keys should be
| the locations of the links and the values should be their targets.
|
*/
'links' => [
public_path('storage') => storage_path('app/public'),
],
];

132
config/logging.php Normal file
View File

@@ -0,0 +1,132 @@
<?php
use Monolog\Handler\NullHandler;
use Monolog\Handler\StreamHandler;
use Monolog\Handler\SyslogUdpHandler;
use Monolog\Processor\PsrLogMessageProcessor;
return [
/*
|--------------------------------------------------------------------------
| Default Log Channel
|--------------------------------------------------------------------------
|
| This option defines the default log channel that is utilized to write
| messages to your logs. The value provided here should match one of
| the channels present in the list of "channels" configured below.
|
*/
'default' => env('LOG_CHANNEL', 'stack'),
/*
|--------------------------------------------------------------------------
| Deprecations Log Channel
|--------------------------------------------------------------------------
|
| This option controls the log channel that should be used to log warnings
| regarding deprecated PHP and library features. This allows you to get
| your application ready for upcoming major versions of dependencies.
|
*/
'deprecations' => [
'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
'trace' => env('LOG_DEPRECATIONS_TRACE', false),
],
/*
|--------------------------------------------------------------------------
| Log Channels
|--------------------------------------------------------------------------
|
| Here you may configure the log channels for your application. Laravel
| utilizes the Monolog PHP logging library, which includes a variety
| of powerful log handlers and formatters that you're free to use.
|
| Available drivers: "single", "daily", "slack", "syslog",
| "errorlog", "monolog", "custom", "stack"
|
*/
'channels' => [
'stack' => [
'driver' => 'stack',
'channels' => explode(',', env('LOG_STACK', 'single')),
'ignore_exceptions' => false,
],
'single' => [
'driver' => 'single',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'daily' => [
'driver' => 'daily',
'path' => storage_path('logs/laravel.log'),
'level' => env('LOG_LEVEL', 'debug'),
'days' => env('LOG_DAILY_DAYS', 14),
'replace_placeholders' => true,
],
'slack' => [
'driver' => 'slack',
'url' => env('LOG_SLACK_WEBHOOK_URL'),
'username' => env('LOG_SLACK_USERNAME', 'Laravel Log'),
'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
'level' => env('LOG_LEVEL', 'critical'),
'replace_placeholders' => true,
],
'papertrail' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
'handler_with' => [
'host' => env('PAPERTRAIL_URL'),
'port' => env('PAPERTRAIL_PORT'),
'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
],
'processors' => [PsrLogMessageProcessor::class],
],
'stderr' => [
'driver' => 'monolog',
'level' => env('LOG_LEVEL', 'debug'),
'handler' => StreamHandler::class,
'formatter' => env('LOG_STDERR_FORMATTER'),
'with' => [
'stream' => 'php://stderr',
],
'processors' => [PsrLogMessageProcessor::class],
],
'syslog' => [
'driver' => 'syslog',
'level' => env('LOG_LEVEL', 'debug'),
'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
'replace_placeholders' => true,
],
'errorlog' => [
'driver' => 'errorlog',
'level' => env('LOG_LEVEL', 'debug'),
'replace_placeholders' => true,
],
'null' => [
'driver' => 'monolog',
'handler' => NullHandler::class,
],
'emergency' => [
'path' => storage_path('logs/laravel.log'),
],
],
];

Some files were not shown because too many files have changed in this diff Show More