Passed
Push — master ( 73ff1c...c96fe8 )
by Thomas
07:34
created

Enrollment   F

Complexity

Total Complexity 71

Size/Duplication

Total Lines 399
Duplicated Lines 0 %

Importance

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

49 Methods

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

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;
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
            ->whereIn('status_id', ['1', '2'])
57
            ->get();
58
    }
59
60
    public function scopeWithoutChildren($query)
61
    {
62
        return $query
63
            ->where(function ($query) {
64
                $query->whereDoesntHave('childrenEnrollments')
65
                ->where('parent_id', null);
66
            })
67
            ->orWhere(function ($query) {
68
                $query->where('parent_id', null);
69
            })
70
            ->get();
71
    }
72
73
    /** only pending enrollments */
74
    public function scopePending($query)
75
    {
76
        return $query
77
            ->where('status_id', 1)
78
            ->where('parent_id', null)
79
            ->get();
80
    }
81
82
    public function scopeNoresult($query)
83
    {
84
        return $query->doesntHave('result');
85
    }
86
87
    public function scopePeriod(Builder $query, int $periodId)
88
    {
89
        return $query->whereHas('course', function ($q) use ($periodId) {
90
            $q->where('period_id', $periodId);
91
        });
92
    }
93
94
    public function scopeCourse(Builder $query, int $courseId)
95
    {
96
        return $query->where('course_id', $courseId);
97
    }
98
99
    /** FUNCTIONS */
100
    public function changeCourse(Course $newCourse)
101
    {
102
        $this->course_id = $newCourse->id;
103
        $this->save();
104
    }
105
106
    public function markAsPaid()
107
    {
108
        $this->status_id = 2;
109
        $this->save();
110
111
        // also mark children as paid
112
        foreach ($this->childrenEnrollments as $child) {
113
            $child->status_id = 2;
114
            $child->save();
115
        }
116
    }
117
118
    public function isPaid()
119
    {
120
        return $this->status_id === 2;
121
    }
122
123
    public function markAsUnpaid()
124
    {
125
        $this->status_id = 1;
126
        $this->save();
127
128
        $this->invoiceDetails()->delete();
129
130
        // also mark children as unpaid
131
        foreach ($this->childrenEnrollments as $child) {
132
            $child->status_id = 1;
133
            $child->invoiceDetails()->delete();
134
            $child->save();
135
        }
136
    }
137
138
    public function addScholarship(Scholarship $scholarship)
139
    {
140
        $this->scholarships()->sync($scholarship);
141
        if (config('invoicing.adding_scholarship_marks_as_paid')) {
142
            $this->markAsPaid();
143
        }
144
    }
145
146
    public function removeScholarship($scholarship)
147
    {
148
        $this->scholarships()->detach($scholarship);
149
        if (config('invoicing.adding_scholarship_marks_as_paid')) {
150
            $this->markAsUnpaid();
151
        }
152
    }
153
154
    /** RELATIONS */
155
    public function student()
156
    {
157
        return $this->belongsTo(Student::class, 'student_id');
158
    }
159
160
    public function user()
161
    {
162
        return $this->belongsTo(User::class, 'student_id');
163
    }
164
165
    public function course()
166
    {
167
        return $this->belongsTo(Course::class, 'course_id');
168
    }
169
170
    public function invoiceDetails()
171
    {
172
        return $this->morphMany(InvoiceDetail::class, 'product');
173
    }
174
175
    public function invoices()
176
    {
177
        return $this->invoiceDetails->map(fn(InvoiceDetail $invoiceDetail) => $invoiceDetail->invoice)->filter();
178
    }
179
180
    // also includes invoices for this enrollment's scheduled payments.
181
    public function relatedInvoices()
182
    {
183
        $scheduledPaymentsInvoices = $this->scheduledPayments->map(fn (ScheduledPayment $scheduledPayment) => $scheduledPayment->invoices());
184
185
        return $this->invoices()->concat($scheduledPaymentsInvoices)->flatten(1);
186
    }
187
188
    public function comments()
189
    {
190
        return $this->morphMany(Comment::class, 'commentable');
191
    }
192
193
    public function scholarships()
194
    {
195
        return $this->belongsToMany(Scholarship::class);
196
    }
197
198
    public function result()
199
    {
200
        return $this->hasOne(Result::class)
201
            ->with('result_name')
202
            ->with('comments');
203
    }
204
205
    public function childrenEnrollments()
206
    {
207
        return $this->hasMany(self::class, 'parent_id');
208
    }
209
210
    public function enrollmentStatus()
211
    {
212
        return $this->belongsTo(EnrollmentStatusType::class, 'status_id');
213
    }
214
215
    public function grades()
216
    {
217
        return $this->hasMany(Grade::class);
218
    }
219
220
    public function scheduledPayments()
221
    {
222
        return $this->hasMany(ScheduledPayment::class);
223
    }
224
225
    public function saveScheduledPayments($payments)
226
    {
227
        $this->scheduledPayments()->delete();
228
        foreach ($payments as $payment) {
229
            $this->scheduledPayments()->create([
230
                'date' => $payment->date,
231
                'value' => $payment->value,
232
                'status' => $payment->status,
233
            ]);
234
        }
235
    }
236
237
    /* Accessors */
238
239
    public function getResultNameAttribute()
240
    {
241
        return $this->result->result_name->name ?? '-';
242
    }
243
244
    public function skill_evaluations()
245
    {
246
        return $this->hasMany(SkillEvaluation::class);
247
    }
248
249
    public function getStudentNameAttribute()
250
    {
251
        return $this->student->name ?? '';
252
    }
253
254
    public function getNameAttribute()
255
    {
256
        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

256
        return /** @scrutinizer ignore-type */ __('Enrollment for').' '.$this->student_name;
Loading history...
257
    }
258
259
    public function getTypeAttribute()
260
    {
261
        return 'enrollment';
262
    }
263
264
    /*     public function getStudentIdAttribute()
265
        {
266
            return $this->student['id'];
267
        } */
268
269
    public function getStudentAgeAttribute()
270
    {
271
        return $this->student->student_age;
272
    }
273
274
    public function getStudentBirthdateAttribute()
275
    {
276
        return $this->student->birthdate;
277
    }
278
279
    public function getStudentEmailAttribute()
280
    {
281
        return $this->student['email'];
282
    }
283
284
    public function getDateAttribute()
285
    {
286
        return Carbon::parse($this->created_at, 'UTC')->locale(App::getLocale())->isoFormat('LL');
287
    }
288
289
    public function getChildrenCountAttribute()
290
    {
291
        return self::where('parent_id', $this->id)->count();
292
    }
293
294
    public function getChildrenAttribute()
295
    {
296
        return self::where('parent_id', $this->id)->with('course')->get();
297
    }
298
299
    public function getStatusAttribute()
300
    {
301
        return $this->enrollmentStatus->name;
302
    }
303
304
    public function getProductCodeAttribute()
305
    {
306
        return $this->course->rhythm->product_code ?? ' ';
307
    }
308
309
    public function getAttendanceRatioAttribute()
310
    {
311
        $courseEventIds = $this->course->events->pluck('id');
312
        $attendances = $this->student->attendance()->with('event')->get()->whereIn('event_id', $courseEventIds);
313
        if ($attendances->count() > 0) {
314
            return round(100 * (($attendances->where('attendance_type_id', 1)->count() + $attendances->where('attendance_type_id', 2)->count() * 0.75) / $attendances->count()));
315
        } else {
316
            return;
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) {
0 ignored issues
show
Bug introduced by
The property invoices does not exist on App\Models\Enrollment. Did you mean invoice_id?
Loading history...
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) {
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
        return number_format($balance, 2);
420
    }
421
}
422