<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Notifications\Notifiable; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Support\Collection; use Illuminate\Database\Eloquent\Collection as DatabaseCollection; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Session; use Laravel\Passport\HasApiTokens; use Leenooks\Traits\ScopeActive; use Leenooks\Traits\UserSwitch; use App\Interfaces\IDs; use App\Models\Scopes\SiteScope; use App\Notifications\ResetPassword as ResetPasswordNotification; use App\Traits\{QueryCacheableConfig,SiteID}; /** * Class User * * Attributes for users: * + role : User's role */ class User extends Authenticatable implements IDs { use HasFactory,HasApiTokens,Notifiable,UserSwitch,QueryCacheableConfig,SiteID,ScopeActive; private const CACHE_TIME = 3600; protected $dates = [ 'created_at', 'updated_at', 'last_access' ]; /** * The attributes that are mass assignable. * * @var array */ protected $fillable = [ 'name', 'email', 'password', ]; /** * The attributes that should be hidden for arrays. * * @var array */ protected $hidden = [ 'password', 'remember_token', ]; /** * Role hierarchy order * * @var array */ public const role_order = [ 'wholesaler', 'reseller', 'customer', ]; /* OVERRIDES */ /** * Users password reset email notification * * @param string $token */ public function sendPasswordResetNotification($token) { $this->notify((new ResetPasswordNotification($token))->onQueue('high')); } /* INTERFACES */ public function getLIDAttribute(): string { return sprintf('#%04s',$this->id); } public function getSIDAttribute(): string { return sprintf('%02s-%s',$this->site_id,$this->getLIDAttribute()); } /* RELATIONS */ /** * The accounts that this user manages * * @return \Illuminate\Database\Eloquent\Relations\HasMany * @note This cannot be loaded with "with"? */ public function accounts() { return $this->hasMany(Account::class) ->orWhereIn('id',$this->rtm_accounts()->pluck('id')) ->active(); } /** * This users invoices * * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough * @deprecated Accounts have invoices, not users */ public function invoices() { return $this->hasManyThrough(Invoice::class,Account::class) ->active(); } /** * This users language configuration * * @return \Illuminate\Database\Eloquent\Relations\BelongsTo */ public function language() { return $this->belongsTo(Language::class); } /** * Return the routes to market account for this user * * @return \Illuminate\Database\Eloquent\Relations\HasOneThrough */ public function rtm() { return $this->hasOneThrough(Rtm::class,Account::class); } /** * The services this user has * * @return \Illuminate\Database\Eloquent\Relations\HasManyThrough * @deprecated Accounts have services, not users */ public function services() { return $this->hasManyThrough(Service::class,Account::class) ->with(['product.type']) ->active(); } /** * Supplier configuration for this user * * @return \Illuminate\Database\Eloquent\Relations\BelongsToMany * @deprecated Move to account->suppliers() */ public function suppliers() { return $this->belongsToMany(Supplier::class) ->where('supplier_user.site_id',$this->site_id) ->withPivot('id','created_at'); } /* ATTRIBUTES */ /** * This is an alias method, as it is used by the framework * * @return string */ public function getNameAttribute(): string { return $this->full_name; } /** * Logged in users full name * * @return string */ public function getFullNameAttribute(): string { return sprintf('%s %s',$this->firstname,$this->lastname); } /** * Return my accounts, but only those accounts with the same group_id * * @note Users can only manage accounts with the same group ID, thereby ensuring they dont see different * pricing options - since prices can be controlled by groups * @return Collection */ public function getMyAccountsAttribute(): Collection { $result = $this->accounts->where('user_id',$this->id); if (! $result->count()) return $result; return $this->isReseller() ? $result : $result->groupBy('group.id')->first(); } /** * Return a friendly string of this persons role * @return string */ public function getRoleAttribute(): string { return ucfirst($this->role()); } public function getSurFirstNameAttribute() { return sprintf('%s, %s',$this->lastname,$this->firstname); } /* SCOPES */ /** * Search for a record * * @param $query * @param string $term */ public function scopeSearch($query,string $term) { // Build our where clause // First Name, Last name if (preg_match('/\ /',$term)) { [$fn,$ln] = explode(' ',$term,2); $query->where(function($query1) use ($fn,$ln,$term) { $query1->where(function($query2) use ($fn,$ln) { return $query2 ->where('firstname','like','%'.$fn.'%') ->where('lastname','like','%'.$ln.'%'); }); }); } elseif (is_numeric($term)) { $query->where('users.id','like','%'.$term.'%'); } elseif (preg_match('/\@/',$term)) { $query->where('email','like','%'.$term.'%'); } else { $query ->Where('firstname','like','%'.$term.'%') ->orWhere('lastname','like','%'.$term.'%'); } return $query; } /* METHODS */ /** * Show this user's clients with service movements * * A service movement, is an active service where the status is not ACTIVE * * @return DatabaseCollection */ public function client_service_movements(): DatabaseCollection { return Service::active() ->serviceUserAuthorised($this) ->where('order_status','!=','ACTIVE') ->with(['account','product']) ->get(); } /** * Determine if the user is an admin of the user with $id * * @param User|null $user * @return bool */ public function isAdmin(User $user=NULL): bool { return $user->exists AND $this->isReseller() AND $this->accounts->pluck('user_id')->contains($user->id); } /** * Determine if the logged in user is a reseller or wholesaler * * @return bool */ public function isReseller(): bool { return in_array($this->role(),['wholesaler','reseller']); } /** * Determine if the logged in user is a wholesaler * * @return bool */ public function isWholesaler(): bool { return in_array($this->role(),['wholesaler']); } /** * Get all the items for the next invoice * * @param bool $future * @return DatabaseCollection */ public function next_invoice_items(bool $future=FALSE): DatabaseCollection { $result = new DatabaseCollection; $this->services->load(['invoice_items.taxes']); foreach ($this->services as $o) { if ($future) { if ($o->invoice_next->subDays(config('app.invoice_inadvance'))->isPast()) continue; } else { if ($o->invoice_next->subDays(config('app.invoice_inadvance'))->isFuture()) continue; } foreach ($o->next_invoice_items($future) as $oo) $result->push($oo); } $result->load([ 'product.translate', 'service.type', ]); return $result; } /** * Return an SQL query that will return a list of invoices * * @return \Illuminate\Database\Query\Builder */ private function query_invoice_items() { return DB::table('invoice_items') ->select([ 'invoice_id', DB::raw('invoice_items.id AS invoice_item_id'), DB::raw('IFNULL(invoice_items.discount_amt,0) AS discount'), DB::raw('ROUND(CAST(quantity*price_base AS decimal(8,2)),2) AS base'), DB::raw('ROUND(invoice_item_taxes.amount,2) AS tax'), ]) ->leftjoin('invoice_item_taxes',['invoice_item_taxes.invoice_item_id'=>'invoice_items.id']) ->where('active',TRUE); } /** * Return an SQL query that will return payment summaries by invoices. * * @return \Illuminate\Database\Query\Builder */ private function query_payment_items() { return DB::table('payment_items') ->select([ 'payment_id', 'invoice_id', DB::raw('SUM(amount) AS allocate'), ]) ->where('amount','>',0) ->groupBy(['invoice_id','payment_id']); } /** * Return an SQL query that will summarise invoices with payments * * @return \Illuminate\Database\Query\Builder * @todo change this to just return outstanding invoices as a collection. */ public function query_invoice_summary() { $invoices = (new Invoice) ->select([ 'invoice_id', DB::raw('SUM(discount) AS discount'), DB::raw('SUM(base) AS base'), DB::raw('SUM(tax) AS tax'), DB::raw('ROUND(SUM(base)+SUM(tax)-SUM(discount),2) AS total'), DB::raw('false AS payments'), DB::raw('false AS payment_fees'), ]) ->from($this->query_invoice_items(),'II') ->join('invoices',['invoices.id'=>'II.invoice_id']) ->whereIN('account_id',$this->accounts->pluck('id')) ->where('invoices.active',TRUE) ->groupBy(['invoice_id']); $payments = (new Payment) ->select([ 'invoice_id', DB::raw('false AS discount'), DB::raw('false AS base'), DB::raw('false AS tax'), DB::raw('false AS total'), DB::raw('SUM(allocate) AS payments'), DB::raw('SUM(fees_amt) AS payment_fees'), ]) ->from($this->query_payment_items(),'PI') ->join('payments',['payments.id'=>'PI.payment_id']) ->whereIN('account_id',$this->accounts->pluck('id')) //->where('payments.active',TRUE) // @todo To implement ->groupBy(['invoice_id']); $summary = (new Invoice) ->withoutGlobalScope(SiteScope::class) ->select([ 'invoice_id', DB::raw('SUM(discount) AS discount'), DB::raw('SUM(base) AS invoice_base'), DB::raw('SUM(tax) AS invoice_tax'), DB::raw('SUM(total) AS invoice_total'), DB::raw('SUM(payments) AS payments'), DB::raw('SUM(payment_fees) AS payment_fees'), ]) ->from($invoices->unionAll($payments),'invoices') ->groupBy(['invoice_id']); return (new Invoice) ->select([ 'account_id', 'id', 'due_at', 'created_at', 'discount', 'invoice_base', 'invoice_tax', 'invoice_total', 'payments', 'payment_fees', DB::raw('ROUND(invoice_total-payments,2) AS balance'), ]) ->join('invoices',['invoices.id'=>'invoice_id']) ->with(['items.taxes']) ->from($summary,'summary'); } /** * Determine what the logged in user's role is * + Wholesaler - aka Super User * + Reseller - services accounts on behalf of their customers * + Customer - end user customer * * @return string */ public function role() { // Cache our role for this session $cache_key = sprintf('%s:%s:%s',$this->id,__METHOD__,Session::getId()); return Cache::remember($cache_key,self::CACHE_TIME,function() { // Get the RTM for our accounts $rtms = Rtm::whereIn('account_id',$this->accounts->pluck('id'))->get(); // If I have no parent, I am the wholesaler if ($rtms->whereNull('parent_id')->count()) return 'wholesaler'; // If I exist in the RTM table, I'm a reseller else if ($rtms->count()) return 'reseller'; // Otherwise a client else return 'customer'; }); } /** * Return the accounts that this user can manage * This method is a helper to User::accounts() - use $user->accounts instead * * @return Collection */ private function rtm_accounts(): Collection { return Account::whereIn('rtm_id',$this->rtm_list()->pluck('id')) ->get(); } /** * Return the RTM hierarchy that this user can manage * * @param Rtm|null $rtm * @return Collection */ public function rtm_list(Rtm $rtm=NULL): Collection { // If this user doesnt manage any accounts if (! $this->exists || ! $this->rtm) return collect(); $list = collect(); // Add this RTM to the list if (! $rtm) { $list->push($this->rtm); $children = $this->rtm->children; } else { $list->push($rtm); $children =$rtm->children; } // Capture any children foreach ($children as $child) $list->push($this->rtm_list($child)); return $rtm ? $list : $list->flatten()->unique(function($item) { return $item->id; }); } }