UnitReceivableService.php 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  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\UnitAccountReceivable;
  7. use App\Models\UnitPaymentAccount;
  8. use Carbon\Carbon;
  9. use Illuminate\Database\Eloquent\Collection;
  10. use Illuminate\Validation\ValidationException;
  11. /**
  12. * Contas a Receber da unidade.
  13. *
  14. * Decisão de modelagem (A): as parcelas de contrato de aluno
  15. * (student_contract_installments) SÃO a Contas a Receber de alunos da unidade.
  16. * A baixa é manual aqui; quando houver Asaas na unidade, a baixa também chega
  17. * pelo webhook (ramo STU_).
  18. */
  19. class UnitReceivableService
  20. {
  21. public function list(int $unitId, array $filters = []): Collection
  22. {
  23. return StudentContractInstallment::with(['student'])
  24. ->where('unit_id', $unitId)
  25. ->when(
  26. isset($filters['status']),
  27. fn ($q) => $q->where('status', $filters['status']),
  28. )
  29. ->when(
  30. isset($filters['student_id']),
  31. fn ($q) => $q->where('student_id', $filters['student_id']),
  32. )
  33. ->when(
  34. isset($filters['type']),
  35. fn ($q) => $q->where('type', $filters['type']),
  36. )
  37. ->orderBy('due_date')
  38. ->orderBy('installment_number')
  39. ->get();
  40. }
  41. public function findForUnit(int $id, int $unitId): ?StudentContractInstallment
  42. {
  43. return StudentContractInstallment::where('id', $id)
  44. ->where('unit_id', $unitId)
  45. ->first();
  46. }
  47. /**
  48. * Baixa manual: marca a parcela como paga.
  49. */
  50. public function settle(StudentContractInstallment $installment, array $data): StudentContractInstallment
  51. {
  52. if ($installment->status === 'paid') {
  53. throw ValidationException::withMessages([
  54. 'status' => 'Esta parcela já está paga.',
  55. ]);
  56. }
  57. if ($installment->status === 'cancelled') {
  58. throw ValidationException::withMessages([
  59. 'status' => 'Esta parcela está cancelada e não pode receber baixa.',
  60. ]);
  61. }
  62. $discount = isset($data['discount']) ? (float) $data['discount'] : (float) $installment->discount;
  63. $fine = isset($data['fine']) ? (float) $data['fine'] : (float) $installment->fine;
  64. $paidValue = isset($data['paid_value'])
  65. ? (float) $data['paid_value']
  66. : round((float) $installment->value - $discount + $fine, 2);
  67. $installment->update([
  68. 'status' => 'paid',
  69. 'discount' => $discount,
  70. 'fine' => $fine,
  71. 'paid_value' => $paidValue,
  72. 'payment_date' => $data['payment_date'] ?? Carbon::today()->format('Y-m-d'),
  73. ]);
  74. return $installment->fresh(['student']);
  75. }
  76. /**
  77. * Gera a cobrança da parcela na conta Asaas da unidade (sob demanda).
  78. */
  79. public function generateCharge(StudentContractInstallment $installment): StudentContractInstallment
  80. {
  81. if ($installment->status === 'cancelled') {
  82. throw ValidationException::withMessages([
  83. 'status' => 'Parcela cancelada não pode ser cobrada.',
  84. ]);
  85. }
  86. if ($installment->asaas_id) {
  87. throw ValidationException::withMessages([
  88. 'asaas' => 'Esta parcela já possui cobrança gerada.',
  89. ]);
  90. }
  91. $account = UnitPaymentAccount::where('unit_id', $installment->unit_id)->first();
  92. if (!$account || empty($account->asaas_api_key)) {
  93. throw ValidationException::withMessages([
  94. 'asaas' => 'A unidade não possui chave Asaas configurada.',
  95. ]);
  96. }
  97. try {
  98. SyncStudentChargeJob::dispatchSync($installment);
  99. } catch (AsaasException $e) {
  100. throw ValidationException::withMessages([
  101. 'asaas' => 'Não foi possível gerar a cobrança na Asaas. Tente novamente.',
  102. ]);
  103. }
  104. return $installment->fresh(['student']);
  105. }
  106. /**
  107. * Reabre uma parcela paga (estorno da baixa manual).
  108. */
  109. public function reopen(StudentContractInstallment $installment): StudentContractInstallment
  110. {
  111. throw ValidationException::withMessages([
  112. 'status' => 'Uma parcela paga não pode voltar para um status anterior.',
  113. ]);
  114. }
  115. // ------------------------------------------------------------------
  116. // Recebíveis avulsos (manuais) — tabela unit_account_receivables
  117. // ------------------------------------------------------------------
  118. public function listManual(int $unitId, array $filters = []): Collection
  119. {
  120. return UnitAccountReceivable::with('student')
  121. ->where('unit_id', $unitId)
  122. ->when(
  123. isset($filters['status']),
  124. fn ($q) => $q->where('status', $filters['status']),
  125. )
  126. ->when(
  127. isset($filters['student_id']),
  128. fn ($q) => $q->where('student_id', $filters['student_id']),
  129. )
  130. ->orderBy('due_date')
  131. ->get();
  132. }
  133. public function findManualForUnit(int $id, int $unitId): ?UnitAccountReceivable
  134. {
  135. return UnitAccountReceivable::where('id', $id)
  136. ->where('unit_id', $unitId)
  137. ->first();
  138. }
  139. public function createManual(int $unitId, array $data): UnitAccountReceivable
  140. {
  141. return UnitAccountReceivable::create([
  142. 'unit_id' => $unitId,
  143. 'origin' => UnitAccountReceivable::ORIGIN_MANUAL,
  144. 'student_id' => $data['student_id'] ?? null,
  145. 'financial_plan_account_id' => $data['financial_plan_account_id'] ?? null,
  146. 'history' => $data['history'],
  147. 'value' => $data['value'],
  148. 'due_date' => $data['due_date'],
  149. 'obs' => $data['obs'] ?? null,
  150. 'paid_value' => 0,
  151. 'discount' => 0,
  152. 'fine' => 0,
  153. 'status' => 'pending',
  154. ])->load('student');
  155. }
  156. public function settleManual(UnitAccountReceivable $receivable, array $data): UnitAccountReceivable
  157. {
  158. if ($receivable->status === 'paid') {
  159. throw ValidationException::withMessages([
  160. 'status' => 'Este recebível já está pago.',
  161. ]);
  162. }
  163. if ($receivable->status === 'cancelled') {
  164. throw ValidationException::withMessages([
  165. 'status' => 'Este recebível está cancelado e não pode receber baixa.',
  166. ]);
  167. }
  168. $discount = isset($data['discount']) ? (float) $data['discount'] : (float) $receivable->discount;
  169. $fine = isset($data['fine']) ? (float) $data['fine'] : (float) $receivable->fine;
  170. $paidValue = isset($data['paid_value'])
  171. ? (float) $data['paid_value']
  172. : round((float) $receivable->value - $discount + $fine, 2);
  173. $receivable->update([
  174. 'status' => 'paid',
  175. 'discount' => $discount,
  176. 'fine' => $fine,
  177. 'paid_value' => $paidValue,
  178. 'payment_date' => $data['payment_date'] ?? Carbon::today()->format('Y-m-d'),
  179. ]);
  180. return $receivable->fresh('student');
  181. }
  182. public function reopenManual(UnitAccountReceivable $receivable): UnitAccountReceivable
  183. {
  184. throw ValidationException::withMessages([
  185. 'status' => 'Um recebível pago não pode voltar para um status anterior.',
  186. ]);
  187. }
  188. }