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']) ->where('status', $status) ->when(!empty($unitIds), fn ($q) => $q->whereIn('unit_id', $unitIds)) ->orderBy('created_at', 'desc') ->get(); } public function getAll(int $unitId, ?int $studentId = null): Collection { return StudentContract::with(['student.city', 'student.state', 'classPackageUnit']) ->where('unit_id', $unitId) ->when($studentId, fn ($q) => $q->where('student_id', $studentId)) ->orderBy('created_at', 'desc') ->get(); } public function findById(int $id): ?StudentContract { return StudentContract::with(['student', 'classPackageUnit'])->find($id); } 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; } 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)) { StudentContractInstallment::insert($rows); // Dispatch Asaas sync for the generated installments $installments = StudentContractInstallment::where('student_contract_id', $contract->id) ->where('status', 'pending') ->get(); foreach ($installments as $installment) { \App\Jobs\SyncStudentChargeJob::dispatch($installment->id); } } } /** * Monta o array de datas para N parcelas. * * - 1ª parcela: usa exatamente $firstDate (data escolhida pelo usuário) * - 2ª em diante: dia $recurringDay avançando mês a mês a partir do mês da 1ª * * Exemplo: firstDate=25/05/2026, recurringDay=5, count=3 * → [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; } public function update(int $id, array $data): ?StudentContract { $model = $this->findById($id); if (!$model) { return null; } if (!empty($data['due_day'])) { $data['recurring_day'] = (int) $data['due_day']; } unset($data['due_day']); $model->update($data); return $model->fresh(); } public function attachFile(int $id, $file): ?StudentContract { $model = $this->findById($id); if (!$model) { return null; } /** @var \Illuminate\Http\UploadedFile $file */ $path = $file->store('student-media'); StudentMedia::create([ 'student_id' => $model->student_id, 'student_contract_id' => $model->id, 'url' => $path, 'file_type' => $file->getMimeType(), 'type' => 'contract', ]); $model->update(['file_url' => Storage::url($path), 'file_type' => $file->getMimeType()]); return $model->fresh(); } 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 freeze(int $id, int $months = 0): ?StudentContract { $model = $this->findById($id); if (!$model) { return null; } if ($months > 0) { StudentContractInstallment::where('student_contract_id', $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')]); }); } $model->update(['status' => 'frozen']); return $model->fresh(); } public function cancel(int $id): ?StudentContract { $model = $this->findById($id); if (!$model) { return null; } $model->update(['status' => 'cancelled']); return $model->fresh(); } public function reactivate(int $id): ?StudentContract { $model = $this->findById($id); if (!$model) { return null; } $model->update(['status' => 'active']); return $model->fresh(); } public function delete(int $id): bool { $model = $this->findById($id); if (!$model) { return false; } if ($model->file_url) { Storage::delete($model->file_url); } return $model->delete(); } }