Browse Source

refactor: regras de atualizacao contrato

Gustavo Mantovani 23 giờ trước cách đây
mục cha
commit
85c92d4bda

+ 135 - 37
app/Http/Requests/StudentContractRequest.php

@@ -2,6 +2,7 @@
 
 namespace App\Http\Requests;
 
+use App\Models\StudentContract;
 use Carbon\CarbonImmutable;
 use Illuminate\Foundation\Http\FormRequest;
 use Illuminate\Validation\Validator;
@@ -10,30 +11,34 @@ class StudentContractRequest extends FormRequest
 {
     public function rules(): array
     {
+        // aluno e pacote sao obrigatorios apenas no cadastro
+
+        $required = $this->isMethod('POST') ? 'required' : 'sometimes';
+
         return [
-            'student_id'               => 'required|exists:students,id',
-            'protocol'                 => 'sometimes|nullable|string|max:255',
-            'signature_date'           => 'sometimes|nullable|date_format:Y-m-d',
-            'end_date'                 => 'sometimes|nullable|date_format:Y-m-d',
-            'class_package_unit_id'    => 'required|exists:class_package_units,id',
-            'class_quantity'           => 'sometimes|nullable|integer|min:1',
-            'weekday'                  => 'sometimes|nullable|integer|min:0|max:6',
-            'start_time'               => 'sometimes|nullable|date_format:H:i',
-            'end_time'                 => 'sometimes|nullable|date_format:H:i',
-            'second_weekday'           => 'sometimes|nullable|integer|min:0|max:6',
-            'second_start_time'        => 'sometimes|nullable|date_format:H:i',
-            'second_end_time'          => 'sometimes|nullable|date_format:H:i',
-            'due_day'                  => 'sometimes|nullable|integer|min:1|max:31',
-            'tax_register'             => 'sometimes|nullable|numeric|min:0',
-            'installments'             => 'sometimes|nullable|integer|min:1|max:12',
-            'enrollment_due_date'      => 'sometimes|nullable|date_format:Y-m-d',
-            'package_value'            => 'sometimes|nullable|numeric|min:0',
-            'package_installments'     => 'sometimes|nullable|integer|min:1|max:13',
-            'package_due_date'         => 'sometimes|nullable|date_format:Y-m-d',
-            'early_payment_discount'   => 'sometimes|nullable|numeric|min:0|max:100',
-            'interest_rate'            => 'sometimes|nullable|numeric|min:0',
-            'payment_method'           => 'sometimes|nullable|string|in:pix,credit_card,debit_card',
-            'fine_cancelled'           => 'sometimes|nullable|numeric|min:0|max:100',
+            'student_id'             => "$required|exists:students,id",
+            'protocol'               => 'sometimes|nullable|string|max:255',
+            'signature_date'         => 'sometimes|nullable|date_format:Y-m-d',
+            'end_date'               => 'sometimes|nullable|date_format:Y-m-d',
+            'class_package_unit_id'  => "$required|exists:class_package_units,id",
+            'class_quantity'         => 'sometimes|nullable|integer|min:1',
+            'weekday'                => 'sometimes|nullable|integer|min:0|max:6',
+            'start_time'             => 'sometimes|nullable|date_format:H:i',
+            'end_time'               => 'sometimes|nullable|date_format:H:i',
+            'second_weekday'         => 'sometimes|nullable|integer|min:0|max:6',
+            'second_start_time'      => 'sometimes|nullable|date_format:H:i',
+            'second_end_time'        => 'sometimes|nullable|date_format:H:i',
+            'due_day'                => 'sometimes|nullable|integer|min:1|max:31',
+            'tax_register'           => 'sometimes|nullable|numeric|min:0',
+            'installments'           => 'sometimes|nullable|integer|min:1|max:12',
+            'enrollment_due_date'    => 'sometimes|nullable|date_format:Y-m-d',
+            'package_value'          => 'sometimes|nullable|numeric|min:0',
+            'package_installments'   => 'sometimes|nullable|integer|min:1|max:13',
+            'package_due_date'       => 'sometimes|nullable|date_format:Y-m-d',
+            'early_payment_discount' => 'sometimes|nullable|numeric|min:0|max:100',
+            'interest_rate'          => 'sometimes|nullable|numeric|min:0',
+            'payment_method'         => 'sometimes|nullable|string|in:pix,credit_card,debit_card',
+            'fine_cancelled'         => 'sometimes|nullable|numeric|min:0|max:100',
         ];
     }
 
@@ -41,24 +46,117 @@ public function after(): array
     {
         return [
             function (Validator $validator): void {
-                if (
-                    ! $this->isMethod('POST')
-                    || ! $this->filled(['signature_date', 'end_date'])
-                    || $validator->errors()->hasAny(['signature_date', 'end_date'])
-                ) {
-                    return;
-                }
+                // o cadastro e a renovacao possuem limites de periodo diferentes
 
-                $signatureDate = CarbonImmutable::createFromFormat('!Y-m-d', $this->string('signature_date')->toString());
-                $endDate = CarbonImmutable::createFromFormat('!Y-m-d', $this->string('end_date')->toString());
+                if ($this->isMethod('POST')) {
+                    $this->validateCreationPeriod($validator);
 
-                if ($endDate->greaterThan($signatureDate->addYearNoOverflow())) {
-                    $validator->errors()->add(
-                        'end_date',
-                        __('validation.student_contract_max_duration'),
-                    );
+                    return;
                 }
+
+                $this->validateRenewalPeriod($validator);
             },
         ];
     }
+
+    private function validateCreationPeriod(Validator $validator): void
+    {
+        // na criacao o contrato pode durar no maximo um ano desde a assinatura
+
+        if (
+            ! $this->filled(['signature_date', 'end_date'])
+            || $validator->errors()->hasAny(['signature_date', 'end_date'])
+        ) {
+            return;
+        }
+
+        $signatureDate = CarbonImmutable::createFromFormat('!Y-m-d', $this->string('signature_date')->toString());
+
+        $endDate = CarbonImmutable::createFromFormat('!Y-m-d', $this->string('end_date')->toString());
+
+        if ($endDate->greaterThan($signatureDate->addYearNoOverflow())) {
+            $validator->errors()->add(
+                'end_date',
+                __('validation.student_contract_max_duration'),
+            );
+        }
+    }
+
+    private function validateRenewalPeriod(Validator $validator): void
+    {
+        // a renovacao sempre considera os dados da versao atual do contrato
+
+        $contract = $this->contractBeingUpdated();
+
+        if (!$contract) {
+            return;
+        }
+
+        // a data inicial fica imutavel depois que o contrato e criado
+
+        if (
+            $this->exists('signature_date')
+            && !$validator->errors()->has('signature_date')
+            && $this->input('signature_date') !== $contract->signature_date?->format('Y-m-d')
+        ) {
+            $validator->errors()->add(
+                'signature_date',
+                __('validation.student_contract_signature_date_immutable'),
+            );
+        }
+
+        // sem uma nova data final valida nao existe periodo para conferir
+
+        if (!$this->exists('end_date') || $validator->errors()->has('end_date')) {
+            return;
+        }
+
+        $currentEndDate = $contract->end_date?->toImmutable();
+
+        $newEndDate = $this->filled('end_date')
+            ? CarbonImmutable::createFromFormat('!Y-m-d', $this->string('end_date')->toString())
+            : null;
+
+        // a data final nunca pode diminuir nem ser removida
+
+        if ($currentEndDate && (!$newEndDate || $newEndDate->lessThan($currentEndDate))) {
+            $validator->errors()->add(
+                'end_date',
+                __('validation.student_contract_end_date_cannot_decrease'),
+            );
+
+            return;
+        }
+
+        // reenviar a mesma data nao caracteriza uma renovacao
+
+        if (
+            (!$currentEndDate && !$newEndDate)
+            || ($currentEndDate && $newEndDate && $newEndDate->equalTo($currentEndDate))
+        ) {
+            return;
+        }
+
+        // a renovacao pode avancar no maximo um ano a partir da data atual
+
+        $maximumEndDate = CarbonImmutable::today()->addYearNoOverflow();
+
+        if ($newEndDate?->greaterThan($maximumEndDate)) {
+            $validator->errors()->add(
+                'end_date',
+                __('validation.student_contract_renewal_max_end_date', [
+                    'date' => $maximumEndDate->format('d/m/Y'),
+                ]),
+            );
+        }
+    }
+
+    protected function contractBeingUpdated(): ?StudentContract
+    {
+        // o id da rota identifica a versao usada como base para a edicao
+
+        $contractId = $this->route('id');
+
+        return is_numeric($contractId) ? StudentContract::find((int) $contractId) : null;
+    }
 }

+ 4 - 1
app/Services/StudentContractService.php

@@ -267,13 +267,16 @@ public function reactivate(int $id, ?int $createdByUserId = null): ?StudentContr
     private function buildInstallmentDates(string $firstDate, int $recurringDay, int $count): array
     {
         $dates = [];
+
         $first = Carbon::createFromFormat('Y-m-d', $firstDate);
+
         $dates[] = $first->copy();
 
         $baseMonth = $first->copy()->startOfMonth();
 
         for ($i = 1; $i < $count; $i++) {
             $next = $baseMonth->copy()->addMonths($i);
+
             $day = min($recurringDay, $next->daysInMonth);
 
             $next->setDay($day);
@@ -380,7 +383,7 @@ private function loadVersionHistory(StudentContract $contract): void
 
         $history = new Collection;
 
-        $current = $root;
+        $current = StudentContract::with('createdBy')->find($root->id);
 
         while ($current) {
             $current->loadMissing('createdBy');

+ 3 - 0
lang/en/validation.php

@@ -102,6 +102,9 @@
     'student_has_active_contracts' => 'A student with active contracts cannot be deleted.',
     'student_contract_max_duration' => 'The student contract must have a maximum duration of 1 year.',
     'student_contract_old_version' => 'Only the latest contract version can be edited.',
+    'student_contract_signature_date_immutable' => 'The contract start date cannot be changed.',
+    'student_contract_end_date_cannot_decrease' => 'The contract end date cannot be reduced.',
+    'student_contract_renewal_max_end_date' => 'The contract end date can be extended at most until :date.',
     'max' => [
         'array' => 'The :attribute field must not have more than :max items.',
         'file' => 'The :attribute field must not be greater than :max kilobytes.',

+ 3 - 0
lang/es/validation.php

@@ -102,6 +102,9 @@
     'student_has_active_contracts' => 'No se puede eliminar un estudiante que tenga contratos activos.',
     'student_contract_max_duration' => 'El contrato del estudiante debe tener una duración máxima de 1 año.',
     'student_contract_old_version' => 'Solo se puede editar la versión más reciente del contrato.',
+    'student_contract_signature_date_immutable' => 'La fecha de inicio del contrato no se puede modificar.',
+    'student_contract_end_date_cannot_decrease' => 'La fecha de finalización del contrato no se puede reducir.',
+    'student_contract_renewal_max_end_date' => 'La fecha de finalización del contrato puede extenderse como máximo hasta :date.',
     'max' => [
         'array' => 'El campo :attribute no debe tener más de :max elementos.',
         'file' => 'El campo :attribute no debe ser mayor que :max kilobytes.',

+ 3 - 0
lang/pt/validation.php

@@ -103,6 +103,9 @@
     'student_has_active_contracts' => 'Não é possível excluir um estudante que possui contratos ativos.',
     'student_contract_max_duration' => 'O contrato do estudante deve ter duração máxima de 1 ano.',
     'student_contract_old_version' => 'Somente a versão mais recente do contrato pode ser editada.',
+    'student_contract_signature_date_immutable' => 'A data de início do contrato não pode ser alterada.',
+    'student_contract_end_date_cannot_decrease' => 'A data de fim do contrato não pode ser reduzida.',
+    'student_contract_renewal_max_end_date' => 'A data de fim do contrato pode ser prorrogada no máximo até :date.',
     'max' => [
         'array' => 'O campo :attribute não deve ter mais de :max itens.',
         'file' => 'O campo :attribute não deve ser maior que :max kilobytes.',

+ 109 - 0
tests/Unit/Http/Requests/StudentContractRequestTest.php

@@ -0,0 +1,109 @@
+<?php
+
+namespace Tests\Unit\Http\Requests;
+
+use App\Http\Requests\StudentContractRequest;
+use App\Models\StudentContract;
+use Carbon\CarbonImmutable;
+use Illuminate\Support\Facades\Validator as ValidatorFacade;
+use Illuminate\Validation\Validator;
+use Tests\TestCase;
+
+class StudentContractRequestTest extends TestCase
+{
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        app()->setLocale('pt');
+        CarbonImmutable::setTestNow('2026-08-10 12:00:00');
+    }
+
+    protected function tearDown(): void
+    {
+        CarbonImmutable::setTestNow();
+
+        parent::tearDown();
+    }
+
+    public function test_it_does_not_allow_changing_the_contract_start_date(): void
+    {
+        $validator = $this->validatorFor([
+            'signature_date' => '2025-01-11',
+        ]);
+
+        $this->assertSame(
+            ['A data de início do contrato não pode ser alterada.'],
+            $validator->errors()->get('signature_date'),
+        );
+    }
+
+    public function test_it_allows_sending_the_unchanged_contract_start_date(): void
+    {
+        $validator = $this->validatorFor([
+            'signature_date' => '2025-01-10',
+        ]);
+
+        $this->assertFalse($validator->errors()->has('signature_date'));
+    }
+
+    public function test_it_does_not_allow_reducing_the_contract_end_date(): void
+    {
+        $validator = $this->validatorFor([
+            'end_date' => '2026-11-30',
+        ]);
+
+        $this->assertSame(
+            ['A data de fim do contrato não pode ser reduzida.'],
+            $validator->errors()->get('end_date'),
+        );
+    }
+
+    public function test_it_allows_renewal_up_to_one_year_from_today(): void
+    {
+        $validator = $this->validatorFor([
+            'end_date' => '2027-08-10',
+        ]);
+
+        $this->assertFalse($validator->errors()->has('end_date'));
+    }
+
+    public function test_it_rejects_renewal_beyond_one_year_from_today(): void
+    {
+        $validator = $this->validatorFor([
+            'end_date' => '2027-08-11',
+        ]);
+
+        $this->assertSame(
+            ['A data de fim do contrato pode ser prorrogada no máximo até 10/08/2027.'],
+            $validator->errors()->get('end_date'),
+        );
+    }
+
+    private function validatorFor(array $data): Validator
+    {
+        $contract = new StudentContract;
+        $contract->signature_date = '2025-01-10';
+        $contract->end_date       = '2026-12-01';
+
+        $request           = TestableStudentContractRequest::create('/', 'PUT', $data);
+        $request->contract = $contract;
+        $validator         = ValidatorFacade::make($request->all(), $request->rules());
+
+        foreach ($request->after() as $callback) {
+            $validator->after($callback);
+        }
+
+        return $validator;
+    }
+}
+
+class TestableStudentContractRequest extends StudentContractRequest
+{
+    public ?StudentContract $contract = null;
+
+    protected function contractBeingUpdated(): ?StudentContract
+    {
+        return $this->contract;
+    }
+}