Test Failed
Branch master (1557a3)
by Adam
08:20
created

Job::currency()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Coyote;
4
5
use Carbon\Carbon;
6
use Coyote\Job\Location;
7
use Coyote\Models\Job\Refer;
0 ignored issues
show
Bug introduced by
The type Coyote\Models\Job\Refer was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
8
use Coyote\Models\Scopes\ForUser;
9
use Coyote\Services\Elasticsearch\CharFilters\JobFilter;
10
use Coyote\Services\Eloquent\HasMany;
11
use Illuminate\Database\Eloquent\Model;
12
use Illuminate\Database\Eloquent\SoftDeletes;
13
use Illuminate\Notifications\RoutesNotifications;
14
15
/**
16
 * @property int $id
17
 * @property int $user_id
18
 * @property int $firm_id
19
 * @property \Carbon\Carbon $created_at
20
 * @property \Carbon\Carbon $updated_at
21
 * @property \Carbon\Carbon $deleted_at
22
 * @property \Carbon\Carbon $deadline_at
23
 * @property \Carbon\Carbon $boost_at
24
 * @property bool $is_expired
25
 * @property int $salary_from
26
 * @property int $salary_to
27
 * @property int $country_id
28
 * @property int $currency_id
29
 * @property int $is_remote
30
 * @property int $remote_range
31
 * @property int $enable_apply
32
 * @property int $visits
33
 * @property int $rate_id
34
 * @property bool $is_gross
35
 * @property int $employment_id
36
 * @property int $views
37
 * @property float $score
38
 * @property float $rank
39
 * @property string $slug
40
 * @property string $title
41
 * @property string $description
42
 * @property string $recruitment
43
 * @property string $requirements
44
 * @property string $email
45
 * @property string $phone
46
 * @property User $user
47
 * @property Firm $firm
48
 * @property Tag[] $tags
49
 * @property Location[] $locations
50
 * @property Currency[] $currency
51
 * @property Feature[] $features
52
 * @property Refer[] $refers
53
 * @property int $plan_id
54
 * @property bool $is_boost
55
 * @property bool $is_publish
56
 * @property bool $is_ads
57
 * @property bool $is_highlight
58
 * @property bool $is_on_top
59
 * @property Plan $plan
60
 */
61
class Job extends Model
62
{
63
    use SoftDeletes, ForUser, RoutesNotifications;
64
    use Searchable {
65
        getIndexBody as parentGetIndexBody;
66
    }
67
68
    const MONTH           = 1;
69
    const YEAR            = 2;
70
    const WEEK            = 3;
71
    const HOUR            = 4;
72
73
    const STUDENT         = 1;
74
    const JUNIOR          = 2;
75
    const MID             = 3;
76
    const SENIOR          = 4;
77
    const LEAD            = 5;
78
    const MANAGER         = 6;
79
80
    const NET             = 0;
81
    const GROSS           = 1;
82
83
    /**
84
     * Filling each field adds points to job offer score.
85
     */
86
    const SCORE_CONFIG = [
87
        'job'             => ['salary_from' => 25, 'salary_to' => 25, 'city' => 15, 'seniority_id' => 5],
88
        'firm'            => ['name' => 15, 'logo' => 5, 'website' => 1, 'description' => 5]
89
    ];
90
91
    /**
92
     * The attributes that are mass assignable.
93
     *
94
     * @var array
95
     */
96
    protected $fillable = [
97
        'title',
98
        'description',
99
        'requirements',
100
        'recruitment',
101
        'is_remote',
102
        'is_gross',
103
        'remote_range',
104
        'country_id',
105
        'salary_from',
106
        'salary_to',
107
        'currency_id',
108
        'rate_id',
109
        'employment_id',
110
        'deadline_at',
111
        'email',
112
        'phone',
113
        'enable_apply',
114
        'seniority_id',
115
        'plan_id'
116
    ];
117
118
    /**
119
     * Default fields values.
120
     *
121
     * @var array
122
     */
123
    protected $attributes = [
124
        'enable_apply'      => true,
125
        'is_remote'         => false,
126
        'title'             => ''
127
    ];
128
129
    /**
130
     * Cast to when calling toArray() (for example before index in elasticsearch).
131
     *
132
     * @var array
133
     */
134
    protected $casts = [
135
        'is_remote'         => 'boolean',
136
        'is_boost'          => 'boolean',
137
        'is_gross'          => 'boolean',
138
        'is_publish'        => 'boolean',
139
        'is_ads'            => 'boolean',
140
        'is_highlight'      => 'boolean',
141
        'is_on_top'         => 'boolean'
142
    ];
143
144
    /**
145
     * @var string
146
     */
147
    protected $dateFormat = 'Y-m-d H:i:se';
148
149
    /**
150
     * @var array
151
     */
152
    protected $dates = ['created_at', 'updated_at', 'deadline_at', 'boost_at'];
153
154
    /**
155
     * Elasticsearch type mapping
156
     *
157
     * @var array
158
     */
159
    protected $mapping = [
160
        "id" => [
161
            "type" => "long"
162
        ],
163
        "locations" => [
164
            "type" => "nested",
165
            "properties" => [
166
                "city" => [
167
                    "type" => "string",
168
                    "analyzer" => "keyword_asciifolding_analyzer",
169
                    "fields" => [
170
                        "original" => ["type" => "text", "analyzer" => "keyword_analyzer", "fielddata" => true]
171
                    ]
172
                ],
173
                "coordinates" => [
174
                    "type" => "geo_point"
175
                ]
176
            ]
177
        ],
178
        "title" => [
179
            "type" => "text",
180
            "analyzer" => "default_analyzer"
181
        ],
182
        "description" => [
183
            "type" => "text",
184
            "analyzer" => "default_analyzer"
185
        ],
186
        "requirements" => [
187
            "type" => "text",
188
            "analyzer" => "default_analyzer"
189
        ],
190
        "is_remote" => [
191
            "type" => "boolean"
192
        ],
193
        "remote_range" => [
194
            "type" => "integer"
195
        ],
196
        "tags" => [
197
            "type" => "text",
198
            "fields" => [
199
                "original" => ["type" => "keyword"]
200
            ]
201
        ],
202
        "firm" => [
203
            "type" => "object",
204
            "properties" => [
205
                "name" => [
206
                    "type" => "text",
207
                    "analyzer" => "default_analyzer",
208
                    "fields" => [
209
                        // filtrujemy firmy po tym polu
210
                        "original" => ["type" => "text", "analyzer" => "keyword_analyzer", "fielddata" => true]
211
                    ]
212
                ],
213
                "slug" => [
214
                    "type" => "text",
215
                    "analyzer" => "keyword_analyzer"
216
                ]
217
            ]
218
        ],
219
        "created_at" => [
220
            "type" => "date",
221
            "format" => "yyyy-MM-dd HH:mm:ss"
222
        ],
223
        "updated_at" => [
224
            "type" => "date",
225
            "format" => "yyyy-MM-dd HH:mm:ss"
226
        ],
227
        "deadline_at" => [
228
            "type" => "date",
229
            "format" => "yyyy-MM-dd HH:mm:ss"
230
        ],
231
        "boost_at" => [
232
            "type" => "date",
233
            "format" => "yyyy-MM-dd HH:mm:ss"
234
        ],
235
        "salary" => [
236
            "type" => "float"
237
        ],
238
        "referral_bonus" => [
239
            "type" => "long"
240
        ],
241
        "score" => [
242
            "type" => "long"
243
        ],
244
        "is_boost" => [
245
            "type" => "boolean"
246
        ],
247
        "is_publish" => [
248
            "type" => "boolean"
249
        ],
250
        "is_ads" => [
251
            "type" => "boolean"
252
        ],
253
        "is_on_top" => [
254
            "type" => "boolean"
255
        ],
256
        "is_highlight" => [
257
            "type" => "boolean"
258
        ]
259
    ];
260
261
    /**
262
     * We need to set firm id to null offer is private
263
     */
264
    public static function boot()
265
    {
266
        parent::boot();
267
268
        static::saving(function (Job $model) {
269
            // nullable column
270
            foreach (['firm_id', 'salary_from', 'salary_to', 'remote_range', 'seniority_id'] as $column) {
271
                if (empty($model->{$column})) {
272
                    $model->{$column} = null;
273
                }
274
            }
275
276
            $model->score = $model->getScore();
277
278
            // field must not be null
279
            $model->is_remote = (int) $model->is_remote;
280
        });
281
282
        static::creating(function (Job $model) {
283
            $model->boost_at = $model->freshTimestamp();
284
        });
285
    }
286
287
    /**
288
     * @return string[]
289
     */
290
    public static function getRatesList()
291
    {
292
        return [self::MONTH => 'miesięcznie', self::YEAR => 'rocznie', self::WEEK => 'tygodniowo', self::HOUR => 'godzinowo'];
293
    }
294
295
    /**
296
     * @return string[]
297
     */
298
    public static function getTaxList()
299
    {
300
        return [self::NET => 'netto', self::GROSS => 'brutto'];
301
    }
302
303
    /**
304
     * @return string[]
305
     */
306
    public static function getEmploymentList()
307
    {
308
        return [1 => 'Umowa o pracę', 2 => 'Umowa zlecenie', 3 => 'Umowa o dzieło', 4 => 'Kontrakt'];
309
    }
310
311
    /**
312
     * @return string[]
313
     */
314
    public static function getSeniorityList()
315
    {
316
        return [self::STUDENT => 'Stażysta', self::JUNIOR => 'Junior', self::MID => 'Mid-Level', self::SENIOR => 'Senior', self::LEAD => 'Lead', self::MANAGER => 'Manager'];
317
    }
318
319
    /**
320
     * @return array
321
     */
322
    public static function getRemoteRangeList()
323
    {
324
        $list = [];
325
326
        for ($i = 100; $i > 0; $i -= 10) {
327
            $list[$i] = "$i%";
328
        }
329
330
        return $list;
331
    }
332
333
    /**
334
     * @return int
335
     */
336
    public function getScore()
337
    {
338
        $score = 0;
339
340
        foreach (self::SCORE_CONFIG['job'] as $column => $point) {
341
            if (!empty($this->{$column})) {
342
                $score += $point;
343
            }
344
        }
345
346
        // 30 points maximum...
347
        $score += min(30, (count($this->tags()->get()) * 10));
348
        $score += min(50, count($this->features()->wherePivot('checked', true)->get()) * 5);
349
350
        if ($this->firm_id) {
351
            $firm = $this->firm;
352
353
            foreach (self::SCORE_CONFIG['firm'] as $column => $point) {
354
                if (!empty($firm->{$column})) {
355
                    $score += $point;
356
                }
357
            }
358
359
            $score += min(25, $firm->benefits()->count() * 5);
0 ignored issues
show
Bug introduced by
The method count() does not exist on Coyote\Services\Eloquent\HasMany. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

359
            $score += min(25, $firm->benefits()->/** @scrutinizer ignore-call */ count() * 5);
Loading history...
360
            $score -= ($firm->is_agency * 15);
361
        } else {
362
            $score -= 15;
363
        }
364
365
        return max(1, $score); // score can't be negative. 1 point min for elasticsearch algorithm
366
    }
367
368
    /**
369
     * Scope for currently active job offers
370
     *
371
     * @param \Illuminate\Database\Query\Builder $query
372
     * @return \Illuminate\Database\Query\Builder
373
     */
374
    public function scopePriorDeadline($query)
375
    {
376
        return $query->where('deadline_at', '>', date('Y-m-d H:i:s'));
377
    }
378
379
    /**
380
     * @return HasMany
381
     */
382
    public function locations()
383
    {
384
        $instance = new Job\Location();
385
386
        return new HasMany($instance->newQuery(), $this, $instance->getTable() . '.' . $this->getForeignKey(), $this->getKeyName());
387
    }
388
389
    /**
390
     * @return \Illuminate\Database\Eloquent\Relations\MorphOne
391
     */
392
    public function page()
393
    {
394
        return $this->morphOne('Coyote\Page', 'content');
395
    }
396
397
    /**
398
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
399
     */
400
    public function firm()
401
    {
402
        return $this->belongsTo('Coyote\Firm');
403
    }
404
405
    /**
406
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
407
     */
408
    public function currency()
409
    {
410
        return $this->belongsTo('Coyote\Currency');
411
    }
412
413
    /**
414
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
415
     */
416
    public function referers()
417
    {
418
        return $this->hasMany('Coyote\Job\Referer');
419
    }
420
421
    /**
422
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
423
     */
424
    public function tags()
425
    {
426
        return $this->belongsToMany('Coyote\Tag', 'job_tags')->withPivot(['priority', 'order']);
427
    }
428
429
    /**
430
     * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany
431
     */
432
    public function features()
433
    {
434
        return $this->belongsToMany('Coyote\Feature', 'job_features')->orderBy('order')->withPivot(['checked', 'value']);
435
    }
436
437
    /**
438
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
439
     */
440
    public function subscribers()
441
    {
442
        return $this->hasMany('Coyote\Job\Subscriber');
443
    }
444
445
    /**
446
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
447
     */
448
    public function applications()
449
    {
450
        return $this->hasMany('Coyote\Job\Application');
451
    }
452
453
    /**
454
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
455
     */
456
    public function refers()
457
    {
458
        return $this->hasMany('Coyote\Job\Refer');
459
    }
460
461
    /**
462
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
463
     */
464
    public function user()
465
    {
466
        return $this->belongsTo('Coyote\User');
467
    }
468
469
    /**
470
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
471
     */
472
    public function country()
473
    {
474
        return $this->belongsTo('Coyote\Country');
475
    }
476
477
    /**
478
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
479
     */
480
    public function payments()
481
    {
482
        return $this->hasMany('Coyote\Payment');
483
    }
484
485
    /**
486
     * @return \Illuminate\Database\Eloquent\Relations\BelongsTo
487
     */
488
    public function plan()
489
    {
490
        return $this->belongsTo(Plan::class);
491
    }
492
493
    /**
494
     * @param string $title
495
     */
496
    public function setTitleAttribute($title)
497
    {
498
        $title = trim($title);
499
500
        $this->attributes['title'] = $title;
501
        $this->attributes['slug'] = str_slug($title, '_');
502
    }
503
504
    /**
505
     * @param string $value
506
     */
507
    public function setSalaryFromAttribute($value)
508
    {
509
        $this->attributes['salary_from'] = $value === null ? null : (int) trim($value);
0 ignored issues
show
introduced by
The condition $value === null can never be true.
Loading history...
510
    }
511
512
    /**
513
     * @param string $value
514
     */
515
    public function setSalaryToAttribute($value)
516
    {
517
        $this->attributes['salary_to'] = $value === null ? null : (int) trim($value);
0 ignored issues
show
introduced by
The condition $value === null can never be true.
Loading history...
518
    }
519
520
    /**
521
     * @return int
522
     */
523
    public function getDeadlineAttribute()
524
    {
525
        return (new Carbon($this->deadline_at))->diff(Carbon::now(), false)->days;
526
    }
527
528
    /**
529
     * @return bool
530
     */
531
    public function getIsExpiredAttribute()
532
    {
533
        return (new Carbon($this->deadline_at))->isPast();
534
    }
535
536
    /**
537
     * @return mixed
538
     */
539
    public function getCityAttribute()
540
    {
541
        return $this->locations->implode('city', ', ');
542
    }
543
544
    /**
545
     * @return string
546
     */
547
    public function getCurrencySymbolAttribute()
548
    {
549
        return $this->currency()->value('symbol');
550
    }
551
552
    /**
553
     * @param int $userId
554
     */
555
    public function setDefaultUserId($userId)
556
    {
557
        if (empty($this->user_id)) {
558
            $this->user_id = $userId;
559
        }
560
    }
561
562
    /**
563
     * @param mixed $features
564
     */
565
    public function setDefaultFeatures($features)
566
    {
567
        if (!count($this->features)) {
568
            foreach ($features as $feature) {
569
                $pivot = $this->features()->newPivot(['checked' => $feature->checked, 'value' => $feature->value]);
570
                $this->features->add($feature->setRelation('pivot', $pivot));
571
            }
572
        }
573
    }
574
575
    /**
576
     * @param int $planId
577
     */
578
    public function setDefaultPlanId($planId)
579
    {
580
        if (empty($this->plan_id)) {
581
            $this->plan_id = $planId;
582
        }
583
    }
584
585
    /**
586
     * @return Payment
587
     */
588
    public function getUnpaidPayment()
589
    {
590
        return $this->payments()->where('status_id', Payment::NEW)->orderBy('created_at', 'DESC')->first();
591
    }
592
593
    /**
594
     * @return string|null
595
     */
596
    public function getPaymentUuid()
597
    {
598
        return $this->payments()->where('status_id', Payment::NEW)->orderBy('created_at', 'DESC')->first(['id']);
599
    }
600
601
    /**
602
     * @param string $url
603
     */
604
    public function addReferer($url)
605
    {
606
        if ($url && mb_strlen($url) < 200) {
607
            $referer = $this->referers()->firstOrNew(['url' => $url]);
608
609
            if (!$referer->id) {
610
                $referer->save();
611
            } else {
612
                $referer->increment('count');
613
            }
614
        }
615
    }
616
617
    /**
618
     * @return string
619
     */
620
    public function routeNotificationForTwilio()
621
    {
622
        return $this->phone;
623
    }
624
625
    /**
626
     * @return array
627
     */
628
    protected function getIndexBody()
629
    {
630
        $this->setCharFilter(JobFilter::class);
631
        $body = $this->parentGetIndexBody();
632
633
        // maximum offered salary
634
        $salary = $this->monthlySalary(max($this->salary_from, $this->salary_to));
635
        $body = array_except($body, ['deleted_at', 'features']);
636
637
        $locations = [];
638
639
        // We need to transform locations to format acceptable by elasticsearch.
640
        // I'm talking here about the coordinates
641
        /** @var \Coyote\Job\Location $location */
642
        foreach ($this->locations()->get(['city', 'longitude', 'latitude']) as $location) {
0 ignored issues
show
Bug introduced by
The method get() does not exist on Coyote\Services\Eloquent\HasMany. Since you implemented __call, consider adding a @method annotation. ( Ignorable by Annotation )

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

642
        foreach ($this->locations()->/** @scrutinizer ignore-call */ get(['city', 'longitude', 'latitude']) as $location) {
Loading history...
643
            $nested = ['city' => $location->city];
644
645
            if ($location->latitude && $location->longitude) {
646
                $nested['coordinates'] = [
647
                    'lat' => $location->latitude,
648
                    'lon' => $location->longitude
649
                ];
650
            }
651
652
            $locations[] = $nested;
653
        }
654
655
        // I don't know why elasticsearch skips documents with empty locations field when we use function_score.
656
        // That's why I add empty object (workaround).
657
        if (empty($locations)) {
658
            $locations[] = (object) [];
659
        }
660
661
        $body = array_merge($body, [
662
            // score must be int
663
            'score'             => (int) $body['score'],
664
            'locations'         => $locations,
665
            'salary'            => $salary,
666
            'salary_from'       => $this->monthlySalary($this->salary_from),
667
            'salary_to'         => $this->monthlySalary($this->salary_to),
668
            // yes, we index currency name so we don't have to look it up in database during search process
669
            'currency_symbol'   => $this->currency()->value('symbol'),
670
            // higher tag's priorities first
671
            'tags'              => $this->tags()->get(['name', 'priority'])->sortByDesc('pivot.priority')->pluck('name')->toArray(),
672
            // index null instead of 100 is job is not remote
673
            'remote_range'      => $this->is_remote ? $this->remote_range : null
674
        ]);
675
676
        if ($this->firm_id) {
677
            // logo is instance of File object. casting to string returns file name.
678
            // cast to (array) if firm is empty.
679
            $body['firm'] = array_map('strval', (array) array_only($this->firm->toArray(), ['name', 'logo', 'slug']));
680
        }
681
682
        return $body;
683
    }
684
685
    /**
686
     * @param float|null $salary
687
     * @return float|null
688
     */
689
    private function monthlySalary($salary)
690
    {
691
        if (empty($salary) || $this->rate_id === self::MONTH) {
692
            return $salary;
693
        }
694
695
        // we need to calculate monthly salary in order to sorting data by salary
696
        if ($this->rate_id == self::YEAR) {
697
            $salary = round($salary / 12);
698
        } elseif ($this->rate_id == self::WEEK) {
699
            $salary = round($salary * 4);
700
        } elseif ($this->rate_id == self::HOUR) {
701
            $salary = round($salary * 8 * 5 * 4);
702
        }
703
704
        return $salary;
705
    }
706
}
707