Przeglądaj źródła

feat: add Taxa de Contratação / Entrada to student contracts

down_payment_value + down_payment_date abate o Total do Curso antes de
dividir as parcelas restantes, e geram sua própria parcela (REF.
ENTRADA), igual à matrícula.
alvesantos 2 dni temu
rodzic
commit
c93fc0c0d9

+ 2 - 0
app/Http/Requests/StudentContractRequest.php

@@ -42,6 +42,8 @@ public function rules(): array
             'total_value'            => 'sometimes|nullable|numeric|min:0',
             'total_value'            => 'sometimes|nullable|numeric|min:0',
             'total_installments'     => 'sometimes|nullable|integer|min:1|max:13',
             'total_installments'     => 'sometimes|nullable|integer|min:1|max:13',
             'total_due_date'         => 'sometimes|nullable|date_format:Y-m-d',
             'total_due_date'         => 'sometimes|nullable|date_format:Y-m-d',
+            'down_payment_value'     => 'sometimes|nullable|numeric|min:0',
+            'down_payment_date'      => 'sometimes|nullable|date_format:Y-m-d|required_with:down_payment_value',
             'payment_method'         => 'sometimes|nullable|string|in:pix,credit_card,debit_card',
             'payment_method'         => 'sometimes|nullable|string|in:pix,credit_card,debit_card',
             'fine_cancelled'         => 'sometimes|nullable|numeric|min:0|max:100',
             'fine_cancelled'         => 'sometimes|nullable|numeric|min:0|max:100',
         ];
         ];

+ 6 - 0
app/Http/Resources/StudentContractResource.php

@@ -67,6 +67,12 @@ public function toArray(Request $request): array
                 ? Carbon::parse($this->total_due_date)->format('d/m/Y')
                 ? Carbon::parse($this->total_due_date)->format('d/m/Y')
                 : null,
                 : null,
 
 
+            'down_payment_value' => $this->down_payment_value,
+
+            'down_payment_date' => $this->down_payment_date
+                ? Carbon::parse($this->down_payment_date)->format('d/m/Y')
+                : null,
+
             'payment_method'         => $this->payment_method,
             'payment_method'         => $this->payment_method,
             'fine_cancelled'         => $this->fine_cancelled,
             'fine_cancelled'         => $this->fine_cancelled,
             'status'                 => $this->status,
             'status'                 => $this->status,

+ 3 - 0
app/Models/StudentContract.php

@@ -22,6 +22,8 @@
  * @property string|null $total_value
  * @property string|null $total_value
  * @property int|null $total_installments
  * @property int|null $total_installments
  * @property \Illuminate\Support\Carbon|null $total_due_date
  * @property \Illuminate\Support\Carbon|null $total_due_date
+ * @property string|null $down_payment_value
+ * @property \Illuminate\Support\Carbon|null $down_payment_date
  * @property \Illuminate\Support\Carbon|null $end_date
  * @property \Illuminate\Support\Carbon|null $end_date
  * @property string|null $start_time
  * @property string|null $start_time
  * @property string|null $end_time
  * @property string|null $end_time
@@ -124,6 +126,7 @@ class StudentContract extends Model
         'package_due_date' => 'date:Y-m-d',
         'package_due_date' => 'date:Y-m-d',
         'materials_due_date' => 'date:Y-m-d',
         'materials_due_date' => 'date:Y-m-d',
         'total_due_date' => 'date:Y-m-d',
         'total_due_date' => 'date:Y-m-d',
+        'down_payment_date' => 'date:Y-m-d',
         'created_at' => 'datetime',
         'created_at' => 'datetime',
         'updated_at' => 'datetime',
         'updated_at' => 'datetime',
     ];
     ];

+ 14 - 1
app/Services/StudentContractService.php

@@ -409,6 +409,7 @@ private function buildInstallmentDates(string $firstDate, int $recurringDay, int
      */
      */
     private const INSTALLMENT_BLOCKS = [
     private const INSTALLMENT_BLOCKS = [
         ['value' => 'tax_register', 'installments' => 'installments', 'due_date' => 'enrollment_due_date', 'type' => 'enrollment', 'history' => 'REF. MATRÍCULA'],
         ['value' => 'tax_register', 'installments' => 'installments', 'due_date' => 'enrollment_due_date', 'type' => 'enrollment', 'history' => 'REF. MATRÍCULA'],
+        ['value' => 'down_payment_value', 'installments' => null, 'due_date' => 'down_payment_date', 'type' => 'down_payment', 'history' => 'REF. ENTRADA'],
         ['value' => 'package_value', 'installments' => 'package_installments', 'due_date' => 'package_due_date', 'type' => 'package', 'history' => 'REF. PACOTE'],
         ['value' => 'package_value', 'installments' => 'package_installments', 'due_date' => 'package_due_date', 'type' => 'package', 'history' => 'REF. PACOTE'],
         ['value' => 'materials_value', 'installments' => 'materials_installments', 'due_date' => 'materials_due_date', 'type' => 'materials', 'history' => 'REF. MATERIAIS'],
         ['value' => 'materials_value', 'installments' => 'materials_installments', 'due_date' => 'materials_due_date', 'type' => 'materials', 'history' => 'REF. MATERIAIS'],
         ['value' => 'total_value', 'installments' => 'total_installments', 'due_date' => 'total_due_date', 'type' => 'total', 'history' => 'REF. TOTAL DO CURSO'],
         ['value' => 'total_value', 'installments' => 'total_installments', 'due_date' => 'total_due_date', 'type' => 'total', 'history' => 'REF. TOTAL DO CURSO'],
@@ -422,11 +423,23 @@ private function generateInstallments(StudentContract $contract): void
 
 
         $now = now();
         $now = now();
 
 
+        // A entrada abate do Total do Curso: quem cobre aquele valor é a
+        // própria parcela de entrada (bloco "down_payment" abaixo).
+        $downPayment = (float) ($contract->down_payment_value ?? 0);
+
         foreach (self::INSTALLMENT_BLOCKS as $block) {
         foreach (self::INSTALLMENT_BLOCKS as $block) {
             $value = $contract->{$block['value']};
             $value = $contract->{$block['value']};
-            $installments = $contract->{$block['installments']};
+
+            // A entrada é sempre paga de uma vez; os demais blocos usam a
+            // quantidade de parcelas configurada no próprio contrato.
+            $installments = $block['installments'] ? $contract->{$block['installments']} : 1;
+
             $dueDate = $contract->{$block['due_date']};
             $dueDate = $contract->{$block['due_date']};
 
 
+            if ($block['type'] === 'total' && $downPayment > 0) {
+                $value = max(0, (float) $value - $downPayment);
+            }
+
             if (! $value || ! $installments || ! $dueDate) continue;
             if (! $value || ! $installments || ! $dueDate) continue;
 
 
             $installmentValue = round($value / $installments, 2);
             $installmentValue = round($value / $installments, 2);

+ 28 - 0
database/migrations/2026_09_10_000001_add_down_payment_to_student_contracts_table.php

@@ -0,0 +1,28 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+/**
+ * Taxa de Contratação / Entrada: valor pago à vista que abate do Total do
+ * Curso antes de dividir as parcelas restantes. Gera sua própria parcela
+ * (REF. ENTRADA), igual a matrícula.
+ */
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('student_contracts', function (Blueprint $table) {
+            $table->decimal('down_payment_value', 10, 2)->nullable()->after('total_due_date');
+            $table->date('down_payment_date')->nullable()->after('down_payment_value');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('student_contracts', function (Blueprint $table) {
+            $table->dropColumn(['down_payment_value', 'down_payment_date']);
+        });
+    }
+};

+ 120 - 0
tests/Unit/StudentContractDownPaymentTest.php

@@ -0,0 +1,120 @@
+<?php
+
+namespace Tests\Unit;
+
+use App\Models\City;
+use App\Models\Country;
+use App\Models\State;
+use App\Models\Student;
+use App\Models\StudentContractInstallment;
+use App\Models\Unit;
+use App\Services\StudentContractService;
+use Illuminate\Foundation\Testing\RefreshDatabase;
+use Tests\TestCase;
+
+/**
+ * Taxa de Contratação / Entrada: abate do Total do Curso antes de dividir as
+ * parcelas restantes, e gera sua própria parcela (REF. ENTRADA), do mesmo
+ * jeito que a matrícula já funciona.
+ */
+class StudentContractDownPaymentTest extends TestCase
+{
+    use RefreshDatabase;
+
+    private Unit $unit;
+
+    private StudentContractService $service;
+
+    protected function setUp(): void
+    {
+        parent::setUp();
+
+        $this->unit = $this->makeUnit();
+        $this->service = new StudentContractService();
+    }
+
+    public function test_down_payment_reduces_total_installments_basis_and_creates_its_own_installment(): void
+    {
+        $student = Student::create([
+            'name'    => 'Aluno Teste',
+            'unit_id' => $this->unit->id,
+            'status'  => 'active',
+        ]);
+
+        $contract = $this->service->create([
+            'student_id'         => $student->id,
+            'unit_id'            => $this->unit->id,
+            'status'             => 'pending',
+            'total_value'        => 5843.00,
+            'total_installments' => 13,
+            'total_due_date'     => '2026-10-10',
+            'down_payment_value' => 500.00,
+            'down_payment_date'  => '2026-09-15',
+        ]);
+
+        $installments = StudentContractInstallment::where('student_contract_id', $contract->id)
+            ->orderBy('type')
+            ->get();
+
+        $downPayment = $installments->firstWhere('type', 'down_payment');
+
+        $this->assertNotNull($downPayment);
+        $this->assertSame('REF. ENTRADA', $downPayment->history);
+        $this->assertSame(1, $downPayment->total_installments);
+        $this->assertEquals(500.00, $downPayment->value);
+        $this->assertSame('2026-09-15', $downPayment->due_date->format('Y-m-d'));
+
+        $totalInstallments = $installments->where('type', 'total');
+
+        $this->assertCount(13, $totalInstallments);
+        // (5843 - 500) / 13 = 411.00
+        $this->assertEquals(411.00, $totalInstallments->first()->value);
+    }
+
+    public function test_without_down_payment_total_installments_use_full_value(): void
+    {
+        $student = Student::create([
+            'name'    => 'Aluno Sem Entrada',
+            'unit_id' => $this->unit->id,
+            'status'  => 'active',
+        ]);
+
+        $contract = $this->service->create([
+            'student_id'         => $student->id,
+            'unit_id'            => $this->unit->id,
+            'status'             => 'pending',
+            'total_value'        => 1300.00,
+            'total_installments' => 13,
+            'total_due_date'     => '2026-10-10',
+        ]);
+
+        $installments = StudentContractInstallment::where('student_contract_id', $contract->id)->get();
+
+        $this->assertNull($installments->firstWhere('type', 'down_payment'));
+
+        $totalInstallments = $installments->where('type', 'total');
+
+        $this->assertCount(13, $totalInstallments);
+        $this->assertEquals(100.00, $totalInstallments->first()->value);
+    }
+
+    private function makeUnit(): Unit
+    {
+        $country = Country::create(['name' => 'Brasil', 'code' => 'BR']);
+        $state   = State::create(['name' => 'Paraná', 'code' => 'PR', 'country_id' => $country->id]);
+        $city    = City::create(['name' => 'Maringá', 'country_id' => $country->id, 'state_id' => $state->id]);
+
+        return Unit::create([
+            'fantasy_name'     => 'Unidade Teste',
+            'social_reason'    => 'Unidade Teste LTDA',
+            'cnpj'             => '00000000000191',
+            'street'           => 'Rua Teste',
+            'neighborhood'     => 'Centro',
+            'postal_code'      => '87000000',
+            'city_id'          => $city->id,
+            'state_id'         => $state->id,
+            'email'            => 'unidade@teste.com',
+            'name_responsible' => 'Responsável Teste',
+        ]);
+    }
+}