Files
voltcerto/app/Services/ContractService.php
Luis Medon e488ae3342 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.
2026-10-29 11:30:06 +00:00

736 lines
27 KiB
PHP

<?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',
];
}
}