Enrollment   F
last analyzed

Complexity

Total Complexity 71

Size/Duplication

Total Lines 401
Duplicated Lines 0 %

Importance

Changes 3
Bugs 2 Features 0
Metric Value
eloc 135
c 3
b 2
f 0
dl 0
loc 401
rs 2.7199
wmc 71

49 Methods

Rating   Name   Duplication   Size   Complexity  
A scopePending() 0 6 1
A scopePeriod() 0 4 1
A scopeWithoutChildren() 0 11 1
A scopeNoresult() 0 3 1
B getHasBookForCourseAttribute() 0 16 7
A setTotalPriceAttribute() 0 3 1
A cancel() 0 19 4
A getTotalPaidPriceAttribute() 0 8 2
A getPriceAttribute() 0 15 4
A getAbsenceCountAttribute() 0 6 1
A getPriceWithCurrencyAttribute() 0 7 2
A getChildrenAttribute() 0 3 1
A course() 0 3 1
A result() 0 5 1
A getStudentAgeAttribute() 0 3 1
A getAttendanceRatioAttribute() 0 6 2
A relatedInvoices() 0 5 1
A addScholarship() 0 5 2
A getTypeAttribute() 0 3 1
A removeScholarship() 0 5 2
A invoiceDetails() 0 3 1
A enrollmentStatus() 0 3 1
A getNameAttribute() 0 3 1
A changeCourse() 0 4 1
A markAsUnpaid() 0 12 2
A getBalanceAttribute() 0 12 3
A user() 0 3 1
A skill_evaluations() 0 3 1
A scheduledPayments() 0 3 1
A invoices() 0 3 1
A getChildrenCountAttribute() 0 3 1
A getProductCodeAttribute() 0 3 1
A scopeParent() 0 5 1
A getStudentBirthdateAttribute() 0 3 1
A scopeCourse() 0 3 1
A saveScheduledPayments() 0 12 2
A getResultNameAttribute() 0 3 1
A scopeReal() 0 6 1
A isPaid() 0 3 1
A student() 0 3 1
A getStudentEmailAttribute() 0 3 1
A scholarships() 0 3 1
A getStatusAttribute() 0 3 1
A getDateAttribute() 0 3 1
A comments() 0 3 1
A getStudentNameAttribute() 0 3 1
A markAsPaid() 0 9 2
A childrenEnrollments() 0 3 1
A grades() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Enrollment often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Enrollment, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace App\Models;
4
5
use App\Events\EnrollmentCreated;
6
use App\Events\EnrollmentDeleting;
7
use App\Events\EnrollmentUpdated;
8
use App\Events\EnrollmentUpdating;
9
use App\Models\Skills\SkillEvaluation;
10
use Backpack\CRUD\app\Models\Traits\CrudTrait;
11
use Carbon\Carbon;
12
use Illuminate\Database\Eloquent\Builder;
13
use Illuminate\Database\Eloquent\Model;
14
use Illuminate\Support\Facades\App;
15
use Spatie\Activitylog\Traits\LogsActivity;
16
17
/**
18
 * @mixin IdeHelperEnrollment
19
 */
20
class Enrollment extends Model
21
{
22
    use CrudTrait;
0 ignored issues
show
introduced by
The trait Backpack\CRUD\app\Models\Traits\CrudTrait requires some properties which are not provided by App\Models\Enrollment: $fakeColumns, $identifiableAttribute, $Type
Loading history...
23
    use LogsActivity;
24
25
    protected $guarded = ['id'];
26
27
    protected $appends = ['type', 'name', 'result_name', 'product_code', 'price', 'price_with_currency'];
28
29
    protected $with = ['student', 'course', 'childrenEnrollments'];
30
31
    protected static bool $logUnguarded = true;
32
33
    protected $dispatchesEvents = [
34
        'deleting' => EnrollmentDeleting::class,
35
        'created' => EnrollmentCreated::class,
36
        'updating' => EnrollmentUpdating::class,
37
        'updated' => EnrollmentUpdated::class,
38
    ];
39
40
    /**
41
     * return all pending enrollments, without the child enrollments.
42
     */
43
    public function scopeParent($query)
44
    {
45
        return $query
46
        ->where('parent_id', null)
47
        ->get();
48
    }
49
50
    public function scopeReal($query)
51
    {
52
        return $query
53
            ->whereDoesntHave('childrenEnrollments')
54
            ->whereIn('status_id', ['1', '2'])
55
            ->get();
56
    }
57
58
    public function scopeWithoutChildren($query)
59
    {
60
        return $query
61
            ->where(function ($query) {
62
                $query->whereDoesntHave('childrenEnrollments')
63
                ->where('parent_id', null);
64
            })
65
            ->orWhere(function ($query) {
66
                $query->where('parent_id', null);
67
            })
68
            ->get();
69
    }
70
71
    /** only pending enrollments */
72
    public function scopePending($query)
73
    {
74
        return $query
75
            ->where('status_id', 1)
76
            ->where('parent_id', null)
77
            ->get();
78
    }
79
80
    public function scopeNoresult($query)
81
    {
82
        return $query->doesntHave('result');
83
    }
84
85
    public function scopePeriod(Builder $query, int $periodId)
86
    {
87
        return $query->whereHas('course', function ($q) use ($periodId) {
88
            $q->where('period_id', $periodId);
89
        });
90
    }
91
92
    public function scopeCourse(Builder $query, int $courseId)
93
    {
94
        return $query->where('course_id', $courseId);
95
    }
96
97
    /** FUNCTIONS */
98
    public function changeCourse(Course $newCourse)
99
    {
100
        $this->course_id = $newCourse->id;
101
        $this->save();
102
    }
103
104
    public function markAsPaid()
105
    {
106
        $this->status_id = 2;
107
        $this->save();
108
109
        // also mark children as paid
110
        foreach ($this->childrenEnrollments as $child) {
111
            $child->status_id = 2;
112
            $child->save();
113
        }
114
    }
115
116
    public function isPaid()
117
    {
118
        return $this->status_id === 2;
119
    }
120
121
    public function markAsUnpaid()
122
    {
123
        $this->status_id = 1;
124
        $this->save();
125
126
        $this->invoiceDetails()->delete();
127
128
        // also mark children as unpaid
129
        foreach ($this->childrenEnrollments as $child) {
130
            $child->status_id = 1;
131
            $child->invoiceDetails()->delete();
132
            $child->save();
133
        }
134
    }
135
136
    public function addScholarship(Scholarship $scholarship)
137
    {
138
        $this->scholarships()->sync($scholarship);
139
        if (config('invoicing.adding_scholarship_marks_as_paid')) {
140
            $this->markAsPaid();
141
        }
142
    }
143
144
    public function removeScholarship($scholarship)
145
    {
146
        $this->scholarships()->detach($scholarship);
147
        if (config('invoicing.adding_scholarship_marks_as_paid')) {
148
            $this->markAsUnpaid();
149
        }
150
    }
151
152
    /** RELATIONS */
153
    public function student()
154
    {
155
        return $this->belongsTo(Student::class, 'student_id');
156
    }
157
158
    public function user()
159
    {
160
        return $this->belongsTo(User::class, 'student_id');
161
    }
162
163
    public function course()
164
    {
165
        return $this->belongsTo(Course::class, 'course_id');
166
    }
167
168
    public function invoiceDetails()
169
    {
170
        return $this->morphMany(InvoiceDetail::class, 'product');
171
    }
172
173
    public function invoices()
174
    {
175
        return $this->invoiceDetails->map(fn (InvoiceDetail $invoiceDetail) => $invoiceDetail->invoice)->filter();
176
    }
177
178
    // also includes invoices for this enrollment's scheduled payments.
179
    public function relatedInvoices()
180
    {
181
        $scheduledPaymentsInvoices = $this->scheduledPayments->map(fn (ScheduledPayment $scheduledPayment) => $scheduledPayment->invoices());
182
183
        return $this->invoices()->concat($scheduledPaymentsInvoices)->flatten(1);
184
    }
185
186
    public function comments()
187
    {
188
        return $this->morphMany(Comment::class, 'commentable');
189
    }
190
191
    public function scholarships()
192
    {
193
        return $this->belongsToMany(Scholarship::class);
194
    }
195
196
    public function result()
197
    {
198
        return $this->hasOne(Result::class)
199
            ->with('result_name')
200
            ->with('comments');
201
    }
202
203
    public function childrenEnrollments()
204
    {
205
        return $this->hasMany(self::class, 'parent_id');
206
    }
207
208
    public function enrollmentStatus()
209
    {
210
        return $this->belongsTo(EnrollmentStatusType::class, 'status_id');
211
    }
212
213
    public function grades()
214
    {
215
        return $this->hasMany(Grade::class);
216
    }
217
218
    public function scheduledPayments()
219
    {
220
        return $this->hasMany(ScheduledPayment::class);
221
    }
222
223
    public function saveScheduledPayments($payments)
224
    {
225
        $paymentsToDelete = $this->scheduledPayments()->pluck('id')->diff($payments->pluck('id'));
226
        ScheduledPayment::whereIn('id', $paymentsToDelete)->delete();
227
228
        foreach ($payments as $payment) {
229
            $this->scheduledPayments()->updateOrCreate([
230
                'id' => $payment->id,
231
            ], [
232
                'date' => $payment->date,
233
                'value' => $payment->value,
234
                'status' => $payment->status,
235
            ]);
236
        }
237
    }
238
239
    /* Accessors */
240
241
    public function getResultNameAttribute()
242
    {
243
        return $this->result->result_name->name ?? '-';
244
    }
245
246
    public function skill_evaluations()
247
    {
248
        return $this->hasMany(SkillEvaluation::class);
249
    }
250
251
    public function getStudentNameAttribute()
252
    {
253
        return $this->student->name ?? '';
254
    }
255
256
    public function getNameAttribute()
257
    {
258
        return __('Enrollment for').' '.$this->student_name;
0 ignored issues
show
Bug introduced by
Are you sure __('Enrollment for') of type array|string can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

258
        return /** @scrutinizer ignore-type */ __('Enrollment for').' '.$this->student_name;
Loading history...
259
    }
260
261
    public function getTypeAttribute()
262
    {
263
        return 'enrollment';
264
    }
265
266
    /*     public function getStudentIdAttribute()
267
        {
268
            return $this->student['id'];
269
        } */
270
271
    public function getStudentAgeAttribute()
272
    {
273
        return $this->student->student_age;
274
    }
275
276
    public function getStudentBirthdateAttribute()
277
    {
278
        return $this->student->birthdate;
279
    }
280
281
    public function getStudentEmailAttribute()
282
    {
283
        return $this->student['email'];
284
    }
285
286
    public function getDateAttribute()
287
    {
288
        return Carbon::parse($this->created_at, 'UTC')->locale(App::getLocale())->isoFormat('LL');
289
    }
290
291
    public function getChildrenCountAttribute()
292
    {
293
        return self::where('parent_id', $this->id)->count();
294
    }
295
296
    public function getChildrenAttribute()
297
    {
298
        return self::where('parent_id', $this->id)->with('course')->get();
299
    }
300
301
    public function getStatusAttribute()
302
    {
303
        return $this->enrollmentStatus->name;
304
    }
305
306
    public function getProductCodeAttribute()
307
    {
308
        return $this->course->rhythm->product_code ?? ' ';
309
    }
310
311
    public function getAttendanceRatioAttribute()
312
    {
313
        $courseEventIds = $this->course->events->pluck('id');
314
        $attendances = $this->student->attendance()->with('event')->get()->whereIn('event_id', $courseEventIds);
315
        if ($attendances->count() > 0) {
316
            return round(100 * (($attendances->where('attendance_type_id', 1)->count() + $attendances->where('attendance_type_id', 2)->count() * 0.75) / $attendances->count()));
317
        }
318
    }
319
320
    public function getAbsenceCountAttribute()
321
    {
322
        $courseEventIds = $this->course->events->pluck('id');
323
        $attendances = $this->student->attendance()->with('event')->get()->whereIn('event_id', $courseEventIds);
324
325
        return $attendances->where('attendance_type_id', 3)->count() + $attendances->where('attendance_type_id', 4)->count();
326
    }
327
328
    public function getPriceAttribute()
329
    {
330
        if ($this->total_price !== null) {
331
            return $this->total_price / 100;
332
        }
333
334
        // if enabled, retrieve the default price category for the student
335
        if (config('invoicing.price_categories_enabled') && $this->student?->price_category) {
336
            $price_category = $this->student->price_category;
337
338
            return $this->course->$price_category ?? 0;
339
        }
340
341
        // finally, we default to the course price or 0 (because some screens need a value here, it cannot be null)
342
        return $this->course->price ?? 0;
343
    }
344
345
    public function getPriceWithCurrencyAttribute()
346
    {
347
        if (config('app.currency_position') === 'before') {
348
            return config('app.currency_symbol').' '.$this->price;
349
        }
350
351
        return $this->price.' '.config('app.currency_symbol');
352
    }
353
354
    public function cancel()
355
    {
356
        // if the enrollment had children, delete them entirely
357
        if ($this->childrenEnrollments->count() > 0) {
358
            foreach ($this->childrenEnrollments as $child) {
359
                $child->delete();
360
            }
361
        }
362
363
        // delete attendance records related to the enrollment
364
        $attendances = $this->course->attendance->where('student_id', $this->student->id);
365
        Attendance::destroy($attendances->map(fn ($item, $key) => $item->id));
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

365
        Attendance::destroy($attendances->map(fn ($item, /** @scrutinizer ignore-unused */ $key) => $item->id));

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
366
367
        foreach ($this->course->children as $child) {
368
            $attendances = $child->attendance->where('student_id', $this->student->id);
369
            Attendance::destroy($attendances->map(fn ($item, $key) => $item->id));
0 ignored issues
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

369
            Attendance::destroy($attendances->map(fn ($item, /** @scrutinizer ignore-unused */ $key) => $item->id));

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
370
        }
371
372
        $this->delete();
373
    }
374
375
    public function getTotalPaidPriceAttribute()
376
    {
377
        $total = 0;
378
        foreach ($this->invoices() as $invoice) {
379
            $total += $invoice->paidTotal();
380
        }
381
382
        return $total;
383
    }
384
385
    public function setTotalPriceAttribute($value)
386
    {
387
        $this->attributes['total_price'] = $value * 100;
388
    }
389
390
    public function getHasBookForCourseAttribute()
391
    {
392
        if ($this->course->books->count() > 0) {
393
            foreach ($this->course->books as $book) {
394
                // if the student doesn't have one of the course books
395
                if ($this->student->books->where('id', $book->id)->count() == 0) {
396
                    return false;
397
                }
398
399
                // if one book is expired
400
                if ($this->student && $this->student->books->where('id', $book->id)->filter(fn ($book) => $book->pivot->expiry_date == null || $book->pivot->expiry_date > Carbon::now())->count() == 0) {
401
                    return 'EXP';
402
                }
403
            }
404
405
            return 'OK';
406
        }
407
    }
408
409
    public function getBalanceAttribute()
410
    {
411
        if (! config('invoicing.invoices_contain_enrollments_only')) {
412
            abort(422, 'Configuration options forbid to access this value');
413
        }
414
415
        $balance = $this->price;
416
        foreach ($this->invoices() as $invoice) {
417
            $balance -= $invoice->paidTotal();
418
        }
419
420
        return number_format($balance, 2);
421
    }
422
}
423