UnitReceivableService.php 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  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. if ($installment->status !== 'paid') {
  112. throw ValidationException::withMessages([
  113. 'status' => 'Só é possível reabrir parcelas que estão pagas.',
  114. ]);
  115. }
  116. $installment->update([
  117. 'status' => 'pending',
  118. 'paid_value' => 0,
  119. 'payment_date' => null,
  120. ]);
  121. return $installment->fresh(['student']);
  122. }
  123. // ------------------------------------------------------------------
  124. // Recebíveis avulsos (manuais) — tabela unit_account_receivables
  125. // ------------------------------------------------------------------
  126. public function listManual(int $unitId, array $filters = []): Collection
  127. {
  128. return UnitAccountReceivable::with('student')
  129. ->where('unit_id', $unitId)
  130. ->when(
  131. isset($filters['status']),
  132. fn ($q) => $q->where('status', $filters['status']),
  133. )
  134. ->when(
  135. isset($filters['student_id']),
  136. fn ($q) => $q->where('student_id', $filters['student_id']),
  137. )
  138. ->orderBy('due_date')
  139. ->get();
  140. }
  141. public function findManualForUnit(int $id, int $unitId): ?UnitAccountReceivable
  142. {
  143. return UnitAccountReceivable::where('id', $id)
  144. ->where('unit_id', $unitId)
  145. ->first();
  146. }
  147. public function createManual(int $unitId, array $data): UnitAccountReceivable
  148. {
  149. return UnitAccountReceivable::create([
  150. 'unit_id' => $unitId,
  151. 'origin' => UnitAccountReceivable::ORIGIN_MANUAL,
  152. 'student_id' => $data['student_id'] ?? null,
  153. 'financial_plan_account_id' => $data['financial_plan_account_id'] ?? null,
  154. 'history' => $data['history'],
  155. 'value' => $data['value'],
  156. 'due_date' => $data['due_date'],
  157. 'obs' => $data['obs'] ?? null,
  158. 'paid_value' => 0,
  159. 'discount' => 0,
  160. 'fine' => 0,
  161. 'status' => 'pending',
  162. ])->load('student');
  163. }
  164. public function settleManual(UnitAccountReceivable $receivable, array $data): UnitAccountReceivable
  165. {
  166. if ($receivable->status === 'paid') {
  167. throw ValidationException::withMessages([
  168. 'status' => 'Este recebível já está pago.',
  169. ]);
  170. }
  171. if ($receivable->status === 'cancelled') {
  172. throw ValidationException::withMessages([
  173. 'status' => 'Este recebível está cancelado e não pode receber baixa.',
  174. ]);
  175. }
  176. $discount = isset($data['discount']) ? (float) $data['discount'] : (float) $receivable->discount;
  177. $fine = isset($data['fine']) ? (float) $data['fine'] : (float) $receivable->fine;
  178. $paidValue = isset($data['paid_value'])
  179. ? (float) $data['paid_value']
  180. : round((float) $receivable->value - $discount + $fine, 2);
  181. $receivable->update([
  182. 'status' => 'paid',
  183. 'discount' => $discount,
  184. 'fine' => $fine,
  185. 'paid_value' => $paidValue,
  186. 'payment_date' => $data['payment_date'] ?? Carbon::today()->format('Y-m-d'),
  187. ]);
  188. return $receivable->fresh('student');
  189. }
  190. public function reopenManual(UnitAccountReceivable $receivable): UnitAccountReceivable
  191. {
  192. if ($receivable->status !== 'paid') {
  193. throw ValidationException::withMessages([
  194. 'status' => 'Só é possível reabrir recebíveis que estão pagos.',
  195. ]);
  196. }
  197. $receivable->update([
  198. 'status' => 'pending',
  199. 'paid_value' => 0,
  200. 'payment_date' => null,
  201. ]);
  202. return $receivable->fresh('student');
  203. }
  204. }