Quellcode durchsuchen

feat: add pricing mode and related fields to student contracts, update resources and views for new structure

alvesantos vor 5 Stunden
Ursprung
Commit
ef6ed27bed

+ 5 - 1
app/Http/Controllers/SelectController.php

@@ -82,7 +82,11 @@ public function franchiseePackages(): JsonResponse
     {
         $unitId = $this->activeUnitId();
         $items = $unitId
-            ? ClassPackageUnit::where('unit_id', $unitId)->where('visible', true)->orderBy('name')->get()
+            ? ClassPackageUnit::with(['pavaoProposalUnit', 'irrecusableProposalUnit'])
+                ->where('unit_id', $unitId)
+                ->where('visible', true)
+                ->orderBy('name')
+                ->get()
             : collect();
 
         return $this->items(PackageSelectResource::collection($items));

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

@@ -21,6 +21,7 @@ public function rules(): array
             '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",
+            'pricing_mode'           => 'sometimes|nullable|string|in:pavao,irrecusavel',
             'class_quantity'         => 'sometimes|nullable|integer|min:1',
             'weekday'                => 'sometimes|nullable|integer|min:0|max:6',
             'start_time'             => 'sometimes|nullable|date_format:H:i',
@@ -35,6 +36,12 @@ public function rules(): array
             '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',
+            'materials_value'        => 'sometimes|nullable|numeric|min:0',
+            'materials_installments' => 'sometimes|nullable|integer|min:1|max:13',
+            'materials_due_date'     => 'sometimes|nullable|date_format:Y-m-d',
+            'total_value'            => 'sometimes|nullable|numeric|min:0',
+            'total_installments'     => 'sometimes|nullable|integer|min:1|max:13',
+            'total_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',

+ 5 - 0
app/Http/Resources/Select/PackageSelectResource.php

@@ -2,11 +2,14 @@
 
 namespace App\Http\Resources\Select;
 
+use App\Http\Resources\Concerns\SerializesProposalPricing;
 use Illuminate\Http\Request;
 use Illuminate\Http\Resources\Json\JsonResource;
 
 class PackageSelectResource extends JsonResource
 {
+    use SerializesProposalPricing;
+
     public function toArray(Request $request): array
     {
         return [
@@ -18,6 +21,8 @@ public function toArray(Request $request): array
             'start_time' => $this->start_time,
             'second_weekday' => $this->second_weekday,
             'second_start_time' => $this->second_start_time,
+            'pavao' => $this->whenLoaded('pavaoProposalUnit', fn () => $this->proposalPricingArray($this->pavaoProposalUnit)),
+            'irrecusavel' => $this->whenLoaded('irrecusableProposalUnit', fn () => $this->proposalPricingArray($this->irrecusableProposalUnit, withCondition: true)),
         ];
     }
 }

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

@@ -23,6 +23,12 @@ public function toArray(Request $request): array
             'student_id'               => $this->student_id,
             'unit_id'                  => $this->unit_id,
             'class_package_unit_id'    => $this->class_package_unit_id,
+            'pricing_mode'             => $this->pricing_mode,
+            'pricing_mode_label'       => match ($this->pricing_mode) {
+                'pavao'       => 'Pavão',
+                'irrecusavel' => 'Irrecusável',
+                default       => null,
+            },
             'signature_date'           => $this->signature_date ? Carbon::parse($this->signature_date)->format('d/m/Y') : null,
             'end_date'                 => $this->end_date ? Carbon::parse($this->end_date)->format('d/m/Y') : null,
             'class_quantity'           => $this->class_quantity,
@@ -47,6 +53,20 @@ public function toArray(Request $request): array
                 ? Carbon::parse($this->package_due_date)->format('d/m/Y')
                 : null,
 
+            'materials_value'        => $this->materials_value,
+            'materials_installments' => $this->materials_installments,
+
+            'materials_due_date' => $this->materials_due_date
+                ? Carbon::parse($this->materials_due_date)->format('d/m/Y')
+                : null,
+
+            'total_value'        => $this->total_value,
+            'total_installments' => $this->total_installments,
+
+            'total_due_date' => $this->total_due_date
+                ? Carbon::parse($this->total_due_date)->format('d/m/Y')
+                : null,
+
             'early_payment_discount' => $this->early_payment_discount,
             'interest_rate'          => $this->interest_rate,
             'payment_method'         => $this->payment_method,

+ 9 - 0
app/Models/StudentContract.php

@@ -15,6 +15,13 @@
  * @property int $student_id
  * @property int $unit_id
  * @property int|null $class_package_unit_id
+ * @property string|null $pricing_mode
+ * @property string|null $materials_value
+ * @property int|null $materials_installments
+ * @property \Illuminate\Support\Carbon|null $materials_due_date
+ * @property string|null $total_value
+ * @property int|null $total_installments
+ * @property \Illuminate\Support\Carbon|null $total_due_date
  * @property \Illuminate\Support\Carbon|null $end_date
  * @property string|null $start_time
  * @property string|null $end_time
@@ -117,6 +124,8 @@ class StudentContract extends Model
         'end_date' => 'date:Y-m-d',
         'enrollment_due_date' => 'date:Y-m-d',
         'package_due_date' => 'date:Y-m-d',
+        'materials_due_date' => 'date:Y-m-d',
+        'total_due_date' => 'date:Y-m-d',
         'created_at' => 'datetime',
         'updated_at' => 'datetime',
     ];

+ 29 - 41
app/Services/StudentContractService.php

@@ -231,7 +231,8 @@ public function generateFile(int $id): ?StudentContract
             }
 
             $model->load([
-                'classPackageUnit',
+                'classPackageUnit.pavaoProposalUnit',
+                'classPackageUnit.irrecusableProposalUnit',
                 'student.city',
                 'student.state',
                 'student.responsibles.city',
@@ -336,6 +337,21 @@ private function buildInstallmentDates(string $firstDate, int $recurringDay, int
         return $dates;
     }
 
+    /**
+     * Cada bloco de valor do contrato (Matrícula, Pacote/Aulas, Materiais, Total
+     * do Curso) gera seu próprio grupo de parcelas quando tem valor, quantidade
+     * de parcelas e data de vencimento preenchidos. "Total do Curso" concentra
+     * o que foi marcado como "incluso no valor do curso" na proposta; os
+     * demais blocos só existem quando a modalidade escolhida marcou aquele
+     * item como "permite parcelar" (parcela separada, fora do Total).
+     */
+    private const INSTALLMENT_BLOCKS = [
+        ['value' => 'tax_register', 'installments' => 'installments', 'due_date' => 'enrollment_due_date', 'type' => 'enrollment', 'history' => 'REF. MATRÍCULA'],
+        ['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' => 'total_value', 'installments' => 'total_installments', 'due_date' => 'total_due_date', 'type' => 'total', 'history' => 'REF. TOTAL DO CURSO'],
+    ];
+
     private function generateInstallments(StudentContract $contract): void
     {
         $recurringDay = $contract->recurring_day ?? 1;
@@ -344,47 +360,19 @@ private function generateInstallments(StudentContract $contract): void
 
         $now = now();
 
-        // Matrícula
-
-        if ($contract->tax_register && $contract->installments && $contract->enrollment_due_date) {
-            $value = round($contract->tax_register / $contract->installments, 2);
-
-            $dates = $this->buildInstallmentDates(
-                $contract->enrollment_due_date->format('Y-m-d'),
-                $recurringDay,
-                $contract->installments,
-            );
-
-            foreach ($dates as $i => $date) {
-                $rows[] = [
-                    'student_contract_id' => $contract->id,
-                    'unit_id'             => $contract->unit_id,
-                    'student_id'          => $contract->student_id,
-                    'type'                => 'enrollment',
-                    'history'             => 'REF. MATRÍCULA',
-                    'installment_number'  => $i + 1,
-                    'total_installments'  => $contract->installments,
-                    'value'               => $value,
-                    'paid_value'          => 0,
-                    'discount'            => 0,
-                    'fine'                => 0,
-                    'due_date'            => $date->format('Y-m-d'),
-                    'status'              => 'pending',
-                    'created_at'          => $now,
-                    'updated_at'          => $now,
-                ];
-            }
-        }
+        foreach (self::INSTALLMENT_BLOCKS as $block) {
+            $value = $contract->{$block['value']};
+            $installments = $contract->{$block['installments']};
+            $dueDate = $contract->{$block['due_date']};
 
-        // Pacote
+            if (! $value || ! $installments || ! $dueDate) continue;
 
-        if ($contract->package_value && $contract->package_installments && $contract->package_due_date) {
-            $value = round($contract->package_value / $contract->package_installments, 2);
+            $installmentValue = round($value / $installments, 2);
 
             $dates = $this->buildInstallmentDates(
-                $contract->package_due_date->format('Y-m-d'),
+                $dueDate->format('Y-m-d'),
                 $recurringDay,
-                $contract->package_installments,
+                $installments,
             );
 
             foreach ($dates as $i => $date) {
@@ -392,11 +380,11 @@ private function generateInstallments(StudentContract $contract): void
                     'student_contract_id' => $contract->id,
                     'unit_id'             => $contract->unit_id,
                     'student_id'          => $contract->student_id,
-                    'type'                => 'package',
-                    'history'             => 'REF. PACOTE',
+                    'type'                => $block['type'],
+                    'history'             => $block['history'],
                     'installment_number'  => $i + 1,
-                    'total_installments'  => $contract->package_installments,
-                    'value'               => $value,
+                    'total_installments'  => $installments,
+                    'value'               => $installmentValue,
                     'paid_value'          => 0,
                     'discount'            => 0,
                     'fine'                => 0,

+ 45 - 0
database/migrations/2026_08_18_000002_add_pricing_mode_to_student_contracts.php

@@ -0,0 +1,45 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+/**
+ * O contrato passa a registrar qual modalidade da proposta comercial foi
+ * escolhida (Pavão/Irrecusável) e ganha os blocos de Materiais e Total do
+ * Curso — espelhando o que já existe para Matrícula/Pacote — para poder
+ * gerar contas a receber com base nos valores da modalidade escolhida.
+ * Ver docs/proposta_comercial_v2_ajustes.md.
+ */
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('student_contracts', function (Blueprint $table) {
+            $table->string('pricing_mode', 20)->nullable()->after('class_package_unit_id');
+
+            $table->decimal('materials_value', 10, 2)->nullable()->after('package_due_date');
+            $table->tinyInteger('materials_installments')->unsigned()->nullable()->after('materials_value');
+            $table->date('materials_due_date')->nullable()->after('materials_installments');
+
+            $table->decimal('total_value', 10, 2)->nullable()->after('materials_due_date');
+            $table->tinyInteger('total_installments')->unsigned()->nullable()->after('total_value');
+            $table->date('total_due_date')->nullable()->after('total_installments');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::table('student_contracts', function (Blueprint $table) {
+            $table->dropColumn([
+                'pricing_mode',
+                'materials_value',
+                'materials_installments',
+                'materials_due_date',
+                'total_value',
+                'total_installments',
+                'total_due_date',
+            ]);
+        });
+    }
+};

+ 52 - 24
resources/views/student-contracts/contract.blade.php

@@ -106,23 +106,35 @@
             : null,
     ])->filter()->implode(' e ');
 
-    $materialValue = (float) ($package?->contract_material_value ?? 0);
-    $classValue = (float) ($contract->package_value ?? 0);
+    $pricingModeLabels = ['pavao' => 'Pavão', 'irrecusavel' => 'Irrecusável'];
+    $pricingModeLabel = $pricingModeLabels[$contract->pricing_mode] ?? null;
+    $modality = match ($contract->pricing_mode) {
+        'pavao' => $package?->pavaoProposalUnit,
+        'irrecusavel' => $package?->irrecusableProposalUnit,
+        default => null,
+    };
+
     $enrollmentValue = (float) ($contract->tax_register ?? 0);
-    $totalContract = $materialValue + $classValue + $enrollmentValue;
-    $classDiscount = (float) ($package?->contrat_discount_value ?? 0);
-    $classOriginalValue = $classValue + $classDiscount;
-    $classDiscountPercentage = $classOriginalValue > 0
-        ? ($classDiscount / $classOriginalValue) * 100
-        : 0;
+    $classValue = (float) ($contract->package_value ?? 0);
+    $materialValue = (float) ($contract->materials_value ?? 0);
+    $totalContract = (float) ($contract->total_value ?? ($enrollmentValue + $classValue + $materialValue));
+    $classDiscountPercentage = (float) ($modality?->classes_discount_percentage ?? 0);
+
     $packageInstallments = max(1, (int) ($contract->package_installments ?? 1));
-    $enrollmentInstallments = max(0, (int) ($contract->installments ?? 0));
     $classInstallmentValue = $classValue / $packageInstallments;
     $punctualityValue = $classInstallmentValue * (1 - ((float) ($contract->early_payment_discount ?? 0) / 100));
-    $totalInstallments = $packageInstallments + $enrollmentInstallments;
-    $installmentDescription = $enrollmentInstallments > 0
-        ? ' (Matrícula + '.$packageInstallments.'x)'
-        : '';
+
+    // Um bloco por valor que efetivamente gera cobrança (Matrícula/Aulas/
+    // Materiais só aparecem aqui quando a modalidade marcou "permite
+    // parcelar"; o que está "incluso no valor do curso" some daqui e entra
+    // só no bloco "Total do Curso").
+    $installmentBlocks = collect([
+        ['label' => 'Matrícula', 'value' => $enrollmentValue, 'installments' => (int) ($contract->installments ?? 0), 'due_date' => $contract->enrollment_due_date],
+        ['label' => 'Aulas', 'value' => $classValue, 'installments' => (int) ($contract->package_installments ?? 0), 'due_date' => $contract->package_due_date],
+        ['label' => 'Materiais', 'value' => $materialValue, 'installments' => (int) ($contract->materials_installments ?? 0), 'due_date' => $contract->materials_due_date],
+        ['label' => 'Total do Curso', 'value' => $totalContract, 'installments' => (int) ($contract->total_installments ?? 0), 'due_date' => $contract->total_due_date],
+    ])->filter(fn ($block) => $block['value'] > 0 && $block['installments'] > 0)
+        ->map(fn ($block) => [...$block, 'installment_value' => $block['value'] / $block['installments']]);
 
     $issueDate = ($contract->signature_date ?? now())->copy();
     $studentAge = $student?->birth_date
@@ -204,23 +216,39 @@
     <tr><td colspan="3"><span class="label">Dia(s) e horário(s):</span> {{ $schedule }}</td></tr>
     <tr>
         <td colspan="2"><span class="label">DURAÇÃO:</span> DE {{ $date($contract->signature_date) }} ATÉ {{ $date($contract->end_date) }}</td>
-        <td><span class="label">MODALIDADE:</span> {{ $package?->name }}</td>
+        <td>
+            <span class="label">MODALIDADE:</span> {{ $package?->name }}
+            @if ($pricingModeLabel)
+                — Contrato {{ $pricingModeLabel }}
+            @endif
+        </td>
     </tr>
     <tr>
-        <td><span class="label">TOTAL CONTRATO:</span> {{ $money($totalContract) }}</td>
-        <td><span class="label">DESCONTO AULAS:</span> {{ $money($classDiscount) }} ({{ $percentage($classDiscountPercentage) }})</td>
-        <td><span class="label">AULAS COM DESCONTO:</span> {{ $money($classValue) }}</td>
+        <td colspan="3">
+            <span class="label">TOTAL CONTRATO:</span> {{ $money($totalContract) }}
+            @if ($classDiscountPercentage > 0)
+                &nbsp;&nbsp;<span class="label">DESCONTO AULAS (pontualidade):</span> {{ $percentage($classDiscountPercentage) }}
+            @endif
+        </td>
     </tr>
     <tr>
         <td colspan="3">
             <div class="line"><span class="label">TAXA DE MATRÍCULA:</span> {{ $money($enrollmentValue) }}</div>
-            <div class="line"><span class="label">MATERIAL:</span> {{ $money($materialValue) }} EM 1 vez</div>
-            <div class="line"><span class="label">AULAS (- {{ $percentage($classDiscountPercentage) }}):</span> {{ $money($classValue) }}</div>
-            <div class="line"><span class="label">Nº DE PARCELAS:</span> {{ $totalInstallments }} vezes{{ $installmentDescription }}</div>
-            <div class="line"><span class="label">VALOR:</span> {{ $money($classInstallmentValue) }}</div>
-            <div class="line"><span class="label">VENCIMENTO:</span> todo dia {{ str_pad((string) ($contract->recurring_day ?? ''), 2, '0', STR_PAD_LEFT) }}</div>
-            <div class="line"><span class="label">DESCONTO POR PONTUALIDADE:</span> {{ $percentage($contract->early_payment_discount) }}</div>
-            <div class="line"><span class="label">VALOR DA PARCELA SE PAGO ATÉ O VENCIMENTO:</span> {{ $money($punctualityValue) }}</div>
+            <div class="line"><span class="label">MATERIAL:</span> {{ $money($materialValue) }}</div>
+            <div class="line"><span class="label">AULAS:</span> {{ $money($classValue) }}</div>
+
+            @foreach ($installmentBlocks as $block)
+                <div class="line">
+                    <span class="label">{{ mb_strtoupper($block['label']) }}:</span>
+                    {{ $block['installments'] }}x de {{ $money($block['installment_value']) }},
+                    1ª parcela em {{ $date($block['due_date']) }}, demais todo dia {{ str_pad((string) ($contract->recurring_day ?? ''), 2, '0', STR_PAD_LEFT) }}
+                </div>
+            @endforeach
+
+            @if ($contract->early_payment_discount)
+                <div class="line"><span class="label">DESCONTO POR PONTUALIDADE:</span> {{ $percentage($contract->early_payment_discount) }}</div>
+                <div class="line"><span class="label">VALOR DA PARCELA DE AULAS SE PAGO ATÉ O VENCIMENTO:</span> {{ $money($punctualityValue) }}</div>
+            @endif
         </td>
     </tr>
 </table>