Files
Ronald Huynen 2547717edb Initial commit
2026-03-23 21:37:59 +01:00

534 lines
17 KiB
PHP

<?php
namespace App\Models;
use App\Helpers\StringHelper;
use App\Models\Locations\Location;
use App\Models\User;
use App\Notifications\NonUserPasswordResetNotification;
use App\Notifications\VerifyProfileEmail;
use App\Traits\ActiveStatesTrait;
use App\Traits\HasPresence;
use App\Traits\LocationTrait;
use App\Traits\ProfileTrait;
use App\Traits\TaggableWithLocale;
use Cog\Contracts\Love\Reactable\Models\Reactable as ReactableInterface;
use Cog\Contracts\Love\Reacterable\Models\Reacterable as ReacterableInterface;
use Cog\Laravel\Love\Reactable\Models\Traits\Reactable;
use Cog\Laravel\Love\Reacterable\Models\Traits\Reacterable;
use Illuminate\Auth\MustVerifyEmail as AuthMustVerifyEmail;
use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Contracts\Auth\CanResetPassword as CanResetPasswordContract;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Facades\Storage;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Scout\Searchable;
use Namu\WireChat\Traits\Chatable;
use Spatie\Activitylog\LogOptions;
use Spatie\Activitylog\Traits\LogsActivity;
use Spatie\Permission\Traits\HasRoles;
class Bank extends Authenticatable implements ReacterableInterface, ReactableInterface, MustVerifyEmail, CanResetPasswordContract
{
use HasFactory;
use HasProfilePhoto;
use AuthMustVerifyEmail;
use Notifiable;
use HasRoles;
use LogsActivity; // Spatie Activity Log
use TaggableWithLocale;
use Reacterable; // cybercog/laravel-love
use Reactable; // cybercog/laravel-love
use Searchable; // laravel/scout with ElasticSearch
use LocationTrait;
use CanResetPassword; // Trait
use Chatable;
use ActiveStatesTrait;
use HasPresence;
use ProfileTrait;
/**
* The attributes that should be hidden for serialization.
* BEWARE: API'S CAN POTENTIALLY EXPOSE ALL VISIBLE FIELDS
*
* @var array
*/
protected $hidden = [
'email',
'email_verified_at',
'full_name',
'password',
'remember_token',
'phone',
'cyclos_id',
'cyclos_salt',
'two_factor_confirmed_at',
'two_factor_recovery_codes',
'two_factor_secret',
'limit_min',
'limit_max',
'lang_preference',
'created_at',
'updated_at',
'last_login_at',
'inactive_at',
'deleted_at',
];
/**
* The attributes that are mass assignable.
*
* @var string[]
*/
protected $fillable = [
'name',
'full_name',
'email',
'profile_photo_path',
'about',
'about_short',
'motivation',
'website',
'phone',
'phone_public',
'password',
'level',
'limit_min',
'limit_max',
'lang_preference',
'last_login_at',
'last_login_ip'
];
/**
* The attributes that should be cast.
*
* @var array
*/
protected $casts = [
'phone_public' => 'boolean',
'date_of_birth' => 'date',
'email_verified_at' => 'datetime',
'inactive_at' => 'datetime',
'deleted_at' => 'datetime',
];
protected $guard_name = 'web';
/**
* Get the index name for the model.
*
* @return string
*/
public function searchableAs()
{
return 'banks_index';
}
/**
* Convert this model to a searchable array.
*
* @return array
*/
public function toSearchableArray()
{
// Prepare eager loaded relationships
$this->load(
'languages',
'locations.district.translations',
'locations.city.translations',
'locations.division.translations',
'locations.country.translations',
'tags.contexts.tags',
'tags.contexts.tags.locale',
'tags.contexts.category.ancestorsAndSelf',
);
return [
'id' => $this->id,
'name' => $this->name,
//TODO: Update to multilang database structure in future
'about_nl' => $this->about,
'about_en' => $this->about,
'about_fr' => $this->about,
'about_de' => $this->about,
'about_es' => $this->about,
'about_short_nl' => $this->about_short,
'about_short_en' => $this->about_short,
'about_short_fr' => $this->about_short,
'about_short_de' => $this->about_short,
'about_short_es' => $this->about_short,
'motivation_nl' => $this->motivation,
'motivation_en' => $this->motivation,
'motivation_fr' => $this->motivation,
'motivation_de' => $this->motivation,
'motivation_es' => $this->motivation,
'website' => $this->website,
'last_login_at' => $this->last_login_at,
'lang_preference' => $this->lang_preference,
'languages' => $this->languages->map(function ($language) { // map() as languages is a collection
return [
'id' => $language->id,
'name' => $language->name,
'lang_code' => $language->lang_code,
];
}),
'locations' => $this->locations->map(function ($location) { // map() as locations is a collection
return [
'id' => $location->id,
'district' => $location->district ? $location->district->translations->map(function ($translation) { // map() as translations is a collection
return $translation->name;
})->toArray() : [],
'city' => $location->city ? $location->city->translations->map(function ($translation) { // map() as translations is a collection
return $translation->name;
})->toArray() : [],
'division' => $location->division ? $location->division->translations->map(function ($translation) { // map() as translations is a collection
return $translation->name;
})->toArray() : [],
'country' => $location->country ? $location->country->translations->map(function ($translation) { // map() as translations is a collection
return $translation->name;
})->toArray() : [],
];
}),
'tags' => $this->tags->map(function ($tag) {
return [
'contexts' => $tag->contexts
->map(function ($context) {
return [
'tags' => $context->tags->map(function ($tag) {
// Include the locale in the field name for tags
return [
'name_' . $tag->locale->locale => StringHelper::DutchTitleCase($tag->normalized),
];
}),
'categories' => Category::with(['translations' => function ($query) { $query->select('category_id', 'locale', 'name');}])
->find([ $context->category->ancestorsAndSelf()->get()->flatMap(function ($related) {
$categoryPath = explode('.', $related->path);
return $categoryPath;
})
->unique()->values()->toArray()
])->map(function ($category) {
// Include the locale in the field name for categories
return $category->translations->mapWithKeys(function ($translation) {
return ['name_' . $translation->locale => StringHelper::DutchTitleCase($translation->name)];
});
}),
];
}),
];
})
];
}
/**
* Get the bank's related admin's.
* Many-to-many.
*/
public function admins()
{
return $this->belongsToMany(Admin::class);
}
/**
* Get all of the bank's accounts.
* One-to-many polymorphic.
*
* @return void
*/
public function accounts()
{
return $this->morphMany(Account::class, 'accountable');
}
/**
* Get the bank's manager(s) that can manage bank profiles.
* Many-to-many.
*/
public function managers()
{
return $this->belongsToMany(User::class, 'bank_user', 'bank_id', 'user_id');
}
/**
* Retrieve all related bank clients.
*
* @return \Illuminate\Support\Collection
*/
public function clients()
{
return $this->userClients->merge($this->organizationClients);
}
public function userClients()
{
return $this->morphedByMany(User::class, 'client', 'bank_clients')
->withPivot('relationship_type');
}
public function organizationClients()
{
return $this->morphedByMany(Organization::class, 'client', 'bank_clients')
->withPivot('relationship_type');
}
/**
* Get all related the locations of the bank.
* One-to-many polymorph.
*/
public function locations()
{
return $this->morphMany(Location::class, 'locatable');
}
/**
* Get all of the languages for the organization.
* Many-to-many polymorphic.
*/
public function languages()
{
return $this->morphToMany(Language::class, 'languagable')->withPivot('competence');
}
/**
* Get all of the social for the organization.
* Many-to-many polymorphic.
*/
public function socials()
{
return $this->morphToMany(Social::class, 'sociable')->withPivot('id', 'user_on_social', 'server_of_social');
}
/**
* Get all of the bank's message settings.
*/
public function message_settings()
{
return $this->morphMany(MessageSetting::class, 'message_settingable');
}
public function sendEmailVerificationNotification()
{
\Mail::to($this->email)->send(new \App\Mail\VerifyProfileEmailMailable($this));
}
/**
* Send the password reset notification.
*
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token)
{
// Construct the full reset URL using the named route
// The route 'non-user.password.reset' expects 'profileType' and 'token' parameters.
$resetUrl = route('non-user.password.reset', [
'profileType' => 'bank', // Hardcode 'admin' for the Admin model
'token' => $token,
'email' => $this->getEmailForPasswordReset(), // Method from CanResetPassword trait
]);
// Pass the fully constructed URL to your notification
$this->notify(new NonUserPasswordResetNotification($resetUrl, $this));
}
/**
* Configure the activity log options for the User model.
*
* This method sets up the logging to:
* - Only log changes to the 'name', 'password', and 'last_login_ip' attributes.
* - Log only when these attributes have been modified (dirty).
* - Prevent submission of empty logs.
* - Use 'user' as the log name.
*
* @return \Spatie\Activitylog\LogOptions
*/
public function getActivitylogOptions(): LogOptions
{
return LogOptions::defaults()
->logOnly([
'name',
'last_login_ip',
'inactive_at',
'deleted_at',
])
->logOnlyDirty() // Only log attributes that have been changed
->dontSubmitEmptyLogs()
->useLogName('Bank');
}
/**
* Wirechat: Returns the URL for the user's cover image (avatar).
* Adjust the 'avatar_url' field to your database setup.
*/
public function getCoverUrlAttribute(): ?string
{
return Storage::url($this->profile_photo_path) ?? null;
}
/**
* Wirechat:Returns the URL for the user's profile page.
* Adjust the 'profile' route as needed for your setup.
*/
public function getProfileUrlAttribute(): ?string
{
return route('profile.show_by_type_and_id', ['type' => __('bank'), 'id' => $this->id]);
}
/**
* Wirechat: Returns the display name for the user.
* Modify this to use your preferred name field.
*/
public function getDisplayNameAttribute(): ?string
{
if ($this->full_name !== $this->name) {
return $this->full_name . ' (' . $this->name . ')';
} elseif ($this->name) {
return $this->name;
} else {
return __('Bank');
}
}
/**
* Wirechat: Search for users when creating a new chat or adding members to a group.
* Customize the search logic to limit results, such as restricting to friends or eligible users only.
*/
public function searchChatables(string $query): ?\Illuminate\Support\Collection
{
$searchableFields = ['name', 'full_name'];
// Search across all profile types
$users = \App\Models\User::where(function ($queryBuilder) use ($searchableFields, $query) {
foreach ($searchableFields as $field) {
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
}
})->limit(6)->get();
$organizations = \App\Models\Organization::where(function ($queryBuilder) use ($searchableFields, $query) {
foreach ($searchableFields as $field) {
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
}
})->limit(6)->get();
$banks = Bank::where(function ($queryBuilder) use ($searchableFields, $query) {
foreach ($searchableFields as $field) {
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
}
})->limit(6)->get();
$admins = \App\Models\Admin::where(function ($queryBuilder) use ($searchableFields, $query) {
foreach ($searchableFields as $field) {
$queryBuilder->orWhere($field, 'LIKE', '%'.$query.'%');
}
})->limit(6)->get();
// Combine all results into a base Collection to avoid serialization issues
$results = collect()
->merge($users->all())
->merge($organizations->all())
->merge($banks->all())
->merge($admins->all());
// Filter out profiles based on configuration
return $results->filter(function ($profile) {
// Check inactive profiles
if (timebank_config('profile_inactive.messenger_hidden') && !$profile->isActive()) {
return false;
}
// Check email unverified profiles
if (timebank_config('profile_email_unverified.messenger_hidden') && !$profile->isEmailVerified()) {
return false;
}
// Check incomplete profiles
if (
timebank_config('profile_incomplete.messenger_hidden')
&& method_exists($profile, 'hasIncompleteProfile')
&& $profile->hasIncompleteProfile($profile)
) {
return false;
}
return true;
})->take(6)->values();
}
/**
* Wirechat: Determine if the user can create new groups.
*/
public function canCreateGroups(): bool
{
return timebank_config('profiles.bank.messenger_can_create_groups');
}
/**
* Check if the Bank has any transactions with another model (User, Organization, Bank).
*
* @param \Illuminate\Database\Eloquent\Model $model
* @return bool
*/
public function hasTransactionsWith($model): bool
{
$modelClass = get_class($model);
$modelId = $model->id;
// Check if this Bank is involved in any transaction where the other model is either the sender or receiver
return Transaction::where(function ($query) use ($modelClass, $modelId) {
$query->whereHas('accountFrom.accountable', function ($q) use ($modelClass, $modelId) {
$q->where('accountable_type', $modelClass)
->where('accountable_id', $modelId);
})
->orWhereHas('accountTo.accountable', function ($q) use ($modelClass, $modelId) {
$q->where('accountable_type', $modelClass)
->where('accountable_id', $modelId);
});
})
->where(function ($query) {
$query->whereHas('accountFrom.accountable', function ($q) {
$q->where('accountable_type', Bank::class)
->where('accountable_id', $this->id);
})
->orWhereHas('accountTo.accountable', function ($q) {
$q->where('accountable_type', Bank::class)
->where('accountable_id', $this->id);
});
})
->exists();
}
}