fn ($query) => $query ->withTrashed() ->with(['city', 'state']), 'classPackageUnit', 'createdBy', ]) ->latestVersion() ->where('unit_id', $unitId) ->when($studentId, fn ($q) => $q->where('student_id', $studentId)) ->orderBy('created_at', 'desc') ->get(); } public function findById(int $id): ?StudentContract { $contract = StudentContract::with(['student', 'classPackageUnit', 'createdBy'])->find($id); if (! $contract) { return null; } $this->loadVersionHistory($contract); return $contract; } public function create(array $data): StudentContract { if (! empty($data['due_day'])) { $data['recurring_day'] = (int) $data['due_day']; } unset($data['due_day']); $contract = StudentContract::create($data); $this->generateInstallments($contract); return $contract; } public function update(int $id, array $data, ?int $createdByUserId = null): ?StudentContract { if (! empty($data['due_day'])) { $data['recurring_day'] = (int) $data['due_day']; } unset($data['due_day']); return DB::transaction(function () use ($id, $data, $createdByUserId): ?StudentContract { $contract = StudentContract::query()->lockForUpdate()->find($id); if (! $contract) { return null; } $originalAttributes = $contract->getAttributes(); $contract->fill($data); $dirty = Arr::except( $contract->getDirty(), array_merge(self::FILE_ATTRIBUTES, self::VERSION_ATTRIBUTES), ); if ($dirty === []) { return $contract->refresh(); } if ($contract->derivedVersions()->withTrashed()->lockForUpdate()->first()) { throw ValidationException::withMessages([ 'contract' => __('validation.student_contract_old_version'), ]); } $submittedAttributes = array_keys($data); $copiedAttributes = Arr::except( $originalAttributes, array_merge(self::FILE_ATTRIBUTES, self::VERSION_ATTRIBUTES, $submittedAttributes), ); $editedAttributes = Arr::only($contract->getAttributes(), $submittedAttributes); $newVersion = StudentContract::create(array_merge( $copiedAttributes, $editedAttributes, [ 'version' => $contract->version + 1, 'derived_from_contract_id' => $contract->id, 'created_by_user_id' => $createdByUserId, ], )); StudentContractInstallment::where('student_contract_id', $contract->id) ->update(['student_contract_id' => $newVersion->id]); return $newVersion->fresh(); }); } public function delete(int $id): bool { $model = $this->findById($id); if (! $model) { return false; } if ($model->file_url) { Storage::delete($model->file_url); } if ($model->signed_file_url) { Storage::delete($model->signed_file_url); } return $model->delete(); } // public function getFranchisorSummary(array $unitIds = []): array { $base = StudentContract::query()->latestVersion() ->when(! empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds)); return [ 'active' => (clone $base)->where('status', 'active')->count(), 'frozen' => (clone $base)->where('status', 'frozen')->count(), 'cancelled' => (clone $base)->where('status', 'cancelled')->count(), ]; } public function getFranchisorByStatus(string $status, array $unitIds = []): Collection { return StudentContract::with(['student', 'unit', 'createdBy'])->latestVersion() ->where('status', $status) ->when(! empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds)) ->orderBy('created_at', 'desc') ->get(); } public function getInstallments(int $contractId): Collection { return StudentContractInstallment::where('student_contract_id', $contractId) ->where('status', 'pending') ->orderBy('due_date') ->orderBy('installment_number') ->get(); } // public function attachFile(int $id, UploadedFile $file, bool $signed = false): ?StudentContract { $model = StudentContract::find($id); if (! $model) { return null; } $path = $file->store('student-media'); StudentMedia::create([ 'student_id' => $model->student_id, 'student_contract_id' => $model->id, 'url' => $path, 'file_type' => $file->getMimeType(), 'type' => $signed ? 'signed_contract' : 'contract', ]); $urlAttribute = $signed ? 'signed_file_url' : 'file_url'; $typeAttribute = $signed ? 'signed_file_type' : 'file_type'; $model->update([ $urlAttribute => $path, $typeAttribute => $file->getMimeType(), ]); return $model->fresh(); } // public function cancel(int $id, ?int $createdByUserId = null): ?StudentContract { return $this->update($id, ['status' => 'cancelled'], $createdByUserId); } public function freeze(int $id, int $months = 0, ?int $createdByUserId = null): ?StudentContract { return DB::transaction(function () use ($id, $months, $createdByUserId): ?StudentContract { $contract = $this->update($id, ['status' => 'frozen'], $createdByUserId); if (! $contract) { return null; } if ($months > 0) { StudentContractInstallment::where('student_contract_id', $contract->id) ->where('status', 'pending') ->get() ->each(function ($installment) use ($months) { $newDate = Carbon::parse($installment->due_date)->addMonths($months); $installment->update(['due_date' => $newDate->format('Y-m-d')]); }); } return $contract->fresh(); }); } public function reactivate(int $id, ?int $createdByUserId = null): ?StudentContract { return $this->update($id, ['status' => 'active'], $createdByUserId); } // // monta o array de datas para n parcelas // primeira parcela usa exatamente firstdate, data escolhida pelo usuario // segunda parcela em diante usa recurringday avancando mes a mes a partir do mes da primeira // exemplo: firstdate=25/05/2026, recurringday=5, count=3 // resultado: [25/05/2026, 05/06/2026, 05/07/2026] 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); $dates[] = $next; } return $dates; } private function generateInstallments(StudentContract $contract): void { $recurringDay = $contract->recurring_day ?? 1; $rows = []; $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, ]; } } // Pacote if ($contract->package_value && $contract->package_installments && $contract->package_due_date) { $value = round($contract->package_value / $contract->package_installments, 2); $dates = $this->buildInstallmentDates( $contract->package_due_date->format('Y-m-d'), $recurringDay, $contract->package_installments, ); foreach ($dates as $i => $date) { $rows[] = [ 'student_contract_id' => $contract->id, 'unit_id' => $contract->unit_id, 'student_id' => $contract->student_id, 'type' => 'package', 'history' => 'REF. PACOTE', 'installment_number' => $i + 1, 'total_installments' => $contract->package_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, ]; } } if (! empty($rows)) { // As parcelas são registradas/calculadas normalmente. A cobrança no Asaas // da franquia será reintroduzida no módulo Franchisee (conta própria por unidade). StudentContractInstallment::insert($rows); } } private function loadVersionHistory(StudentContract $contract): void { $root = $contract; while ($root->derived_from_contract_id !== null) { $parent = StudentContract::with('createdBy')->find($root->derived_from_contract_id); if (! $parent) { break; } $root = $parent; } $history = new Collection; $current = StudentContract::with('createdBy')->find($root->id); while ($current) { $current->loadMissing('createdBy'); $history->push($current); $current = $current->derivedVersions()->with('createdBy')->first(); } $contract->setRelation('versionHistory', $history->sortByDesc('version')->values()); } }