Passed
Push — master ( 26caeb...0e28f7 )
by Thomas
11:18
created

Enrollment   F

Complexity

Total Complexity 68

Size/Duplication

Total Lines 385
Duplicated Lines 0 %

Importance

Changes 3
Bugs 2 Features 0
Metric Value
eloc 132
c 3
b 2
f 0
dl 0
loc 385
rs 2.96
wmc 68

47 Methods

Rating   Name   Duplication   Size   Complexity  
A getChildrenAttribute() 0 3 1
A course() 0 3 1
B getHasBookForCourseAttribute() 0 16 7
A setTotalPriceAttribute() 0 3 1
A result() 0 5 1
A getStudentAgeAttribute() 0 3 1
A getPriceAttribute() 0 15 4
A getAttendanceRatioAttribute() 0 8 2
A addScholarship() 0 5 2
A getTypeAttribute() 0 3 1
A removeScholarship() 0 5 2
A cancel() 0 19 4
A scopePending() 0 6 1
A scopePeriod() 0 4 1
A getNameAttribute() 0 3 1
A enrollmentStatus() 0 3 1
A changeCourse() 0 4 1
A getBalanceAttribute() 0 12 2
A markAsUnpaid() 0 12 2
A user() 0 3 1
A scopeWithoutChildren() 0 11 1
A skill_evaluations() 0 3 1
A scheduledPayments() 0 3 1
A invoices() 0 3 1
A scopeNoresult() 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 getAbsenceCountAttribute() 0 6 1
A saveScheduledPayments() 0 8 2
A getResultNameAttribute() 0 3 1
A getPriceWithCurrencyAttribute() 0 7 2
A scopeReal() 0 5 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 grades() 0 3 1
A childrenEnrollments() 0 3 1
A markAsPaid() 0 9 2
A getTotalPaidPriceAttribute() 0 8 2

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

242
        return /** @scrutinizer ignore-type */ __('Enrollment for').' '.$this->student_name;
Loading history...
243
    }
244
245
    public function getTypeAttribute()
246
    {
247
        return 'enrollment';
248
    }
249
250
    /*     public function getStudentIdAttribute()
251
        {
252
            return $this->student['id'];
253
        } */
254
255
    public function getStudentAgeAttribute()
256
    {
257
        return $this->student->student_age;
258
    }
259
260
    public function getStudentBirthdateAttribute()
261
    {
262
        return $this->student->birthdate;
263
    }
264
265
    public function getStudentEmailAttribute()
266
    {
267
        return $this->student['email'];
268
    }
269
270
    public function getDateAttribute()
271
    {
272
        return Carbon::parse($this->created_at, 'UTC')->locale(App::getLocale())->isoFormat('LL');
273
    }
274
275
    public function getChildrenCountAttribute()
276
    {
277
        return self::where('parent_id', $this->id)->count();
278
    }
279
280
    public function getChildrenAttribute()
281
    {
282
        return self::where('parent_id', $this->id)->with('course')->get();
283
    }
284
285
    public function getStatusAttribute()
286
    {
287
        return $this->enrollmentStatus->name;
288
    }
289
290
    public function getProductCodeAttribute()
291
    {
292
        return $this->course->rhythm->product_code ?? ' ';
293
    }
294
295
    public function getAttendanceRatioAttribute()
296
    {
297
        $courseEventIds = $this->course->events->pluck('id');
298
        $attendances = $this->student->attendance()->with('event')->get()->whereIn('event_id', $courseEventIds);
0 ignored issues
show
Bug introduced by
The method whereIn() does not exist on Illuminate\Database\Eloquent\Collection. ( Ignorable by Annotation )

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

298
        $attendances = $this->student->attendance()->with('event')->get()->/** @scrutinizer ignore-call */ whereIn('event_id', $courseEventIds);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
299
        if ($attendances->count() > 0) {
300
            return round(100 * (($attendances->where('attendance_type_id', 1)->count() + $attendances->where('attendance_type_id', 2)->count() * 0.75) / $attendances->count()));
301
        } else {
302
            return;
303
        }
304
    }
305
306
    public function getAbsenceCountAttribute()
307
    {
308
        $courseEventIds = $this->course->events->pluck('id');
309
        $attendances = $this->student->attendance()->with('event')->get()->whereIn('event_id', $courseEventIds);
310
311
        return $attendances->where('attendance_type_id', 3)->count() + $attendances->where('attendance_type_id', 4)->count();
312
    }
313
314
    public function getPriceAttribute()
315
    {
316
        if ($this->total_price !== null) {
317
            return $this->total_price / 100;
318
        }
319
320
        // if enabled, retrieve the default price category for the student
321
        if (config('invoicing.price_categories_enabled') && $this->student?->price_category) {
322
            $price_category = $this->student->price_category;
323
324
            return $this->course->$price_category ?? 0;
325
        }
326
327
        // finally, we default to the course price or 0 (because some screens need a value here, it cannot be null)
328
        return $this->course->price ?? 0;
329
    }
330
331
    public function getPriceWithCurrencyAttribute()
332
    {
333
        if (config('app.currency_position') === 'before') {
334
            return config('app.currency_symbol').' '.$this->price;
335
        }
336
337
        return $this->price.' '.config('app.currency_symbol');
338
    }
339
340
    public function getBalanceAttribute()
341
    {
342
        $balance = $this->price;
343
        $paidAmount = 0;
344
345
        foreach ($this->invoices as $invoice) {
346
            $paidAmount += $invoice->payments?->sum('value') ?? 0;
347
        }
348
349
        $balance -= $paidAmount;
350
351
        return number_format($balance, 2);
352
    }
353
354
    public function cancel()
355
    {
356
        // if the enrollment had children, delete them entirely
357
        if ($this->childrenEnrollments->count() > 0) {
0 ignored issues
show
Bug introduced by
The method count() does not exist on Illuminate\Database\Eloquent\Collection. ( Ignorable by Annotation )

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

357
        if ($this->childrenEnrollments->/** @scrutinizer ignore-call */ count() > 0) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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->total_price;
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) {
0 ignored issues
show
Bug introduced by
The method where() does not exist on Illuminate\Database\Eloquent\Collection. ( Ignorable by Annotation )

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

395
                if ($this->student->books->/** @scrutinizer ignore-call */ where('id', $book->id)->count() == 0) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
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