where('unit_id', $unitId) ->when( isset($filters['status']), fn ($q) => $q->where('status', $filters['status']), ) ->when( isset($filters['student_id']), fn ($q) => $q->where('student_id', $filters['student_id']), ) ->when( isset($filters['type']), fn ($q) => $q->where('type', $filters['type']), ) ->orderBy('due_date') ->orderBy('installment_number') ->get(); } public function findForUnit(int $id, int $unitId): ?StudentContractInstallment { return StudentContractInstallment::where('id', $id) ->where('unit_id', $unitId) ->first(); } /** * Baixa manual: marca a parcela como paga. */ public function settle(StudentContractInstallment $installment, array $data): StudentContractInstallment { if ($installment->status === 'paid') { throw ValidationException::withMessages([ 'status' => 'Esta parcela já está paga.', ]); } if ($installment->status === 'cancelled') { throw ValidationException::withMessages([ 'status' => 'Esta parcela está cancelada e não pode receber baixa.', ]); } $discount = isset($data['discount']) ? (float) $data['discount'] : (float) $installment->discount; $fine = isset($data['fine']) ? (float) $data['fine'] : (float) $installment->fine; $paidValue = isset($data['paid_value']) ? (float) $data['paid_value'] : round((float) $installment->value - $discount + $fine, 2); $installment->update([ 'status' => 'paid', 'discount' => $discount, 'fine' => $fine, 'paid_value' => $paidValue, 'payment_date' => $data['payment_date'] ?? Carbon::today()->format('Y-m-d'), ]); return $installment->fresh(['student']); } /** * Gera a cobrança da parcela na conta Asaas da unidade (sob demanda). */ public function generateCharge(StudentContractInstallment $installment): StudentContractInstallment { if ($installment->status === 'cancelled') { throw ValidationException::withMessages([ 'status' => 'Parcela cancelada não pode ser cobrada.', ]); } if ($installment->asaas_id) { throw ValidationException::withMessages([ 'asaas' => 'Esta parcela já possui cobrança gerada.', ]); } $account = UnitPaymentAccount::where('unit_id', $installment->unit_id)->first(); if (!$account || empty($account->asaas_api_key)) { throw ValidationException::withMessages([ 'asaas' => 'A unidade não possui chave Asaas configurada.', ]); } try { SyncStudentChargeJob::dispatchSync($installment); } catch (AsaasException $e) { throw ValidationException::withMessages([ 'asaas' => 'Não foi possível gerar a cobrança na Asaas. Tente novamente.', ]); } return $installment->fresh(['student']); } /** * Reabre uma parcela paga (estorno da baixa manual). */ public function reopen(StudentContractInstallment $installment): StudentContractInstallment { if ($installment->status !== 'paid') { throw ValidationException::withMessages([ 'status' => 'Só é possível reabrir parcelas que estão pagas.', ]); } $installment->update([ 'status' => 'pending', 'paid_value' => 0, 'payment_date' => null, ]); return $installment->fresh(['student']); } // ------------------------------------------------------------------ // Recebíveis avulsos (manuais) — tabela unit_account_receivables // ------------------------------------------------------------------ public function listManual(int $unitId, array $filters = []): Collection { return UnitAccountReceivable::with('student') ->where('unit_id', $unitId) ->when( isset($filters['status']), fn ($q) => $q->where('status', $filters['status']), ) ->when( isset($filters['student_id']), fn ($q) => $q->where('student_id', $filters['student_id']), ) ->orderBy('due_date') ->get(); } public function findManualForUnit(int $id, int $unitId): ?UnitAccountReceivable { return UnitAccountReceivable::where('id', $id) ->where('unit_id', $unitId) ->first(); } public function createManual(int $unitId, array $data): UnitAccountReceivable { return UnitAccountReceivable::create([ 'unit_id' => $unitId, 'origin' => UnitAccountReceivable::ORIGIN_MANUAL, 'student_id' => $data['student_id'] ?? null, 'financial_plan_account_id' => $data['financial_plan_account_id'] ?? null, 'history' => $data['history'], 'value' => $data['value'], 'due_date' => $data['due_date'], 'obs' => $data['obs'] ?? null, 'paid_value' => 0, 'discount' => 0, 'fine' => 0, 'status' => 'pending', ])->load('student'); } public function settleManual(UnitAccountReceivable $receivable, array $data): UnitAccountReceivable { if ($receivable->status === 'paid') { throw ValidationException::withMessages([ 'status' => 'Este recebível já está pago.', ]); } if ($receivable->status === 'cancelled') { throw ValidationException::withMessages([ 'status' => 'Este recebível está cancelado e não pode receber baixa.', ]); } $discount = isset($data['discount']) ? (float) $data['discount'] : (float) $receivable->discount; $fine = isset($data['fine']) ? (float) $data['fine'] : (float) $receivable->fine; $paidValue = isset($data['paid_value']) ? (float) $data['paid_value'] : round((float) $receivable->value - $discount + $fine, 2); $receivable->update([ 'status' => 'paid', 'discount' => $discount, 'fine' => $fine, 'paid_value' => $paidValue, 'payment_date' => $data['payment_date'] ?? Carbon::today()->format('Y-m-d'), ]); return $receivable->fresh('student'); } public function reopenManual(UnitAccountReceivable $receivable): UnitAccountReceivable { if ($receivable->status !== 'paid') { throw ValidationException::withMessages([ 'status' => 'Só é possível reabrir recebíveis que estão pagos.', ]); } $receivable->update([ 'status' => 'pending', 'paid_value' => 0, 'payment_date' => null, ]); return $receivable->fresh('student'); } }