Files
hub/app/Models/User.php
Jon Leopard 7e5ea2bc10 checkpoint: marketplace and operational features complete, ready for Week 4 data migration
Summary of completed work:
- Complete buyer portal (browse, cart, checkout, orders, invoices)
- Complete seller portal (orders, manifests, fleet, picking)
- Business onboarding wizards (buyer 4-step, seller 5-step)
- Email verification and registration flows
- Notification system for buyers and sellers
- Payment term surcharges and pickup/delivery workflows
- Filament admin resources (Business, Brand, Product, Order, Invoice, User)
- 51 migrations executed successfully

Renamed Company -> Business throughout codebase for consistency.
Unified authentication flows with password reset.
Added audit trail and telescope for debugging.

Next: Week 4 Data Migration (Days 22-28)
- Product migration (883 products from cannabrands_crm)
- Company migration (81 buyer companies)
- User migration (preserve password hashes)
- Order history migration
2025-10-15 11:17:15 -07:00

253 lines
6.8 KiB
PHP

<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Spatie\Permission\Traits\HasRoles;
use Filament\Models\Contracts\FilamentUser;
use Filament\Panel;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Collection;
class User extends Authenticatable implements FilamentUser
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable, HasRoles;
/**
* User type constants
*/
public const USER_TYPES = [
'buyer' => 'Buyer/Retailer',
'seller' => 'Seller/Brand',
'admin' => 'Super Admin'
];
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'first_name',
'last_name',
'email',
'password',
'phone',
'position', // Job title/role
'user_type', // admin, buyer, seller
'status', // active, inactive, suspended
'business_onboarding_completed',
'temp_business_name',
'temp_market',
'temp_contact_type',
// Contact preferences
'preferred_contact_method', // email, phone, sms
'timezone',
'language_preference',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'status' => 'string',
];
}
/**
* Get the user's full name by combining first and last name
* This accessor ensures Filament can display the user's name
*/
protected function name(): \Illuminate\Database\Eloquent\Casts\Attribute
{
return \Illuminate\Database\Eloquent\Casts\Attribute::make(
get: fn () => trim(($this->first_name ?? '') . ' ' . ($this->last_name ?? '')) ?: 'Unknown User'
);
}
public function canAccessPanel(Panel $panel): bool
{
if ($panel->getId() === 'admin') {
return $this->user_type === 'admin' || $this->hasRole('Super Admin');
}
if ($panel->getId() === 'seller') {
return $this->user_type === 'seller';
}
if ($panel->getId() === 'buyer') {
return $this->user_type === 'buyer';
}
return false;
}
/**
* Primary business association for this user
* (Users can be associated with multiple businesses, but have one primary)
*/
public function primaryBusiness()
{
// First try to get explicitly primary business
$primary = $this->businesses()->wherePivot('is_primary', true)->first();
// If no primary set, return the first business
if (!$primary) {
$primary = $this->businesses()->first();
}
return $primary;
}
public function approver()
{
return $this->belongsTo(User::class, 'approved_by');
}
// Multi-tenancy methods for Filament
public function getTenants(Panel $panel): Collection
{
if ($panel->getId() === 'business') {
return $this->businesses;
}
return collect();
}
public function canAccessTenant(Model $tenant): bool
{
return $this->businesses->contains($tenant);
}
// Relationships (Business-centric approach)
public function companies(): BelongsToMany
{
return $this->belongsToMany(Business::class, 'business_user')
->withPivot('contact_type', 'is_primary', 'permissions')
->withTimestamps();
}
// Alias for backwards compatibility
public function businesses(): BelongsToMany
{
return $this->companies();
}
/**
* Contact records for this user across different businesses
*/
public function contacts(): HasMany
{
return $this->hasMany(Contact::class);
}
// Helper methods for business associations
public function isPrimaryContactFor(Business $business): bool
{
return $this->businesses()
->where('business_id', $business->id)
->wherePivot('is_primary', true)
->exists();
}
public function getRoleFor(Business $business): string
{
$pivot = $this->businesses()->where('business_id', $business->id)->first()?->pivot;
return $pivot?->contact_type ?? '';
}
public function hasContactType($contactType, Business $business = null): bool
{
if ($business) {
return $this->businesses()
->where('business_id', $business->id)
->wherePivot('contact_type', $contactType)
->exists();
}
return $this->businesses()->wherePivot('contact_type', $contactType)->exists();
}
public function canAccessBusiness(Business $business): bool
{
return $this->businesses->contains($business) &&
$business->isActive() &&
$this->isApproved();
}
public function getGroupedBusinesses(): Collection
{
return $this->businesses->flatMap(function ($business) {
return $business->getGroupedBusinesses();
})->unique('id');
}
// Status helper methods
public function isActive(): bool
{
return $this->status === 'active';
}
public function isInactive(): bool
{
return $this->status === 'inactive';
}
public function isSuspended(): bool
{
return $this->status === 'suspended';
}
/**
* Check if user's primary business is approved
*/
public function isApproved(): bool
{
$business = $this->primaryBusiness();
return $business && $business->status === 'approved';
}
/**
* Send the password reset notification with user-type-specific URL.
*
* @param string $token
* @return void
*/
public function sendPasswordResetNotification($token)
{
$routeName = match($this->user_type) {
'seller' => 'seller.password.reset',
'buyer' => 'buyer.password.reset',
default => 'buyer.password.reset', // Default to buyer for safety
};
$url = route($routeName, ['token' => $token, 'email' => $this->email]);
$this->notify(new \App\Notifications\ResetPasswordNotification($token, $url));
}
}