UnitReceivableService.php 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. <?php
  2. namespace App\Services;
  3. use App\Exceptions\AsaasException;
  4. use App\Jobs\SyncStudentChargeJob;
  5. use App\Models\StudentContractInstallment;
  6. use App\Models\UnitPaymentAccount;
  7. use Carbon\Carbon;
  8. use Illuminate\Database\Eloquent\Collection;
  9. use Illuminate\Validation\ValidationException;
  10. /**
  11. * Contas a Receber da unidade.
  12. *
  13. * Decisão de modelagem (A): as parcelas de contrato de aluno
  14. * (student_contract_installments) SÃO a Contas a Receber de alunos da unidade.
  15. * A baixa é manual aqui; quando houver Asaas na unidade, a baixa também chega
  16. * pelo webhook (ramo STU_).
  17. */
  18. class UnitReceivableService
  19. {
  20. public function list(int $unitId, array $filters = []): Collection
  21. {
  22. return StudentContractInstallment::with(['student'])
  23. ->where('unit_id', $unitId)
  24. ->when(
  25. isset($filters['status']),
  26. fn ($q) => $q->where('status', $filters['status']),
  27. )
  28. ->when(
  29. isset($filters['student_id']),
  30. fn ($q) => $q->where('student_id', $filters['student_id']),
  31. )
  32. ->when(
  33. isset($filters['type']),
  34. fn ($q) => $q->where('type', $filters['type']),
  35. )
  36. ->orderBy('due_date')
  37. ->orderBy('installment_number')
  38. ->get();
  39. }
  40. public function findForUnit(int $id, int $unitId): ?StudentContractInstallment
  41. {
  42. return StudentContractInstallment::where('id', $id)
  43. ->where('unit_id', $unitId)
  44. ->first();
  45. }
  46. /**
  47. * Baixa manual: marca a parcela como paga.
  48. */
  49. public function settle(StudentContractInstallment $installment, array $data): StudentContractInstallment
  50. {
  51. if ($installment->status === 'paid') {
  52. throw ValidationException::withMessages([
  53. 'status' => 'Esta parcela já está paga.',
  54. ]);
  55. }
  56. if ($installment->status === 'cancelled') {
  57. throw ValidationException::withMessages([
  58. 'status' => 'Esta parcela está cancelada e não pode receber baixa.',
  59. ]);
  60. }
  61. $discount = isset($data['discount']) ? (float) $data['discount'] : (float) $installment->discount;
  62. $fine = isset($data['fine']) ? (float) $data['fine'] : (float) $installment->fine;
  63. $paidValue = isset($data['paid_value'])
  64. ? (float) $data['paid_value']
  65. : round((float) $installment->value - $discount + $fine, 2);
  66. $installment->update([
  67. 'status' => 'paid',
  68. 'discount' => $discount,
  69. 'fine' => $fine,
  70. 'paid_value' => $paidValue,
  71. 'payment_date' => $data['payment_date'] ?? Carbon::today()->format('Y-m-d'),
  72. ]);
  73. return $installment->fresh(['student']);
  74. }
  75. /**
  76. * Gera a cobrança da parcela na conta Asaas da unidade (sob demanda).
  77. */
  78. public function generateCharge(StudentContractInstallment $installment): StudentContractInstallment
  79. {
  80. if ($installment->status === 'cancelled') {
  81. throw ValidationException::withMessages([
  82. 'status' => 'Parcela cancelada não pode ser cobrada.',
  83. ]);
  84. }
  85. if ($installment->asaas_id) {
  86. throw ValidationException::withMessages([
  87. 'asaas' => 'Esta parcela já possui cobrança gerada.',
  88. ]);
  89. }
  90. $account = UnitPaymentAccount::where('unit_id', $installment->unit_id)->first();
  91. if (!$account || empty($account->asaas_api_key)) {
  92. throw ValidationException::withMessages([
  93. 'asaas' => 'A unidade não possui chave Asaas configurada.',
  94. ]);
  95. }
  96. try {
  97. SyncStudentChargeJob::dispatchSync($installment);
  98. } catch (AsaasException $e) {
  99. throw ValidationException::withMessages([
  100. 'asaas' => 'Não foi possível gerar a cobrança na Asaas. Tente novamente.',
  101. ]);
  102. }
  103. return $installment->fresh(['student']);
  104. }
  105. /**
  106. * Reabre uma parcela paga (estorno da baixa manual).
  107. */
  108. public function reopen(StudentContractInstallment $installment): StudentContractInstallment
  109. {
  110. if ($installment->status !== 'paid') {
  111. throw ValidationException::withMessages([
  112. 'status' => 'Só é possível reabrir parcelas que estão pagas.',
  113. ]);
  114. }
  115. $installment->update([
  116. 'status' => 'pending',
  117. 'paid_value' => 0,
  118. 'payment_date' => null,
  119. ]);
  120. return $installment->fresh(['student']);
  121. }
  122. }