Files
hub/app/Models/Contact.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

348 lines
9.2 KiB
PHP

<?php
namespace App\Models;
use DateTime;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class Contact extends Model
{
use HasFactory, SoftDeletes;
// Contact Types for Cannabis Business (LeafLink-aligned)
public const CONTACT_TYPES = [
'primary' => 'Primary Contact',
'owner' => 'Owner/Executive',
'manager' => 'General Manager',
'buyer' => 'Buyer/Purchasing Manager',
'accounts_payable' => 'Accounts Payable',
'accounts_receivable' => 'Accounts Receivable',
'receiving' => 'Receiving/Delivery Manager',
'compliance' => 'Compliance Officer',
'operations' => 'Operations Manager',
'sales' => 'Sales Representative',
'marketing' => 'Marketing Contact',
'it' => 'IT/Technical Contact',
'legal' => 'Legal Contact',
'staff' => 'Staff Member',
];
// Communication Preferences
public const COMMUNICATION_METHODS = [
'email' => 'Email',
'phone' => 'Phone',
'sms' => 'SMS/Text',
'portal' => 'Online Portal',
'fax' => 'Fax',
];
protected $fillable = [
// Ownership
'company_id',
'location_id', // Optional - can be company-wide or location-specific
'user_id', // Optional - linked user account
// Personal Information
'first_name',
'last_name',
'title', // Mr., Ms., Dr., etc.
'position', // Job title
'department',
// Contact Information
'email',
'phone',
'mobile',
'fax',
'extension',
// Business Details
'contact_type',
'responsibilities', // JSON: array of responsibilities
'permissions', // JSON: what they can approve/access
// Communication Preferences
'preferred_contact_method',
'communication_preferences', // JSON: when/how to contact
'language_preference',
'timezone',
// Schedule & Availability
'work_hours', // JSON: schedule
'availability_notes',
'emergency_contact',
// Status & Settings
'is_primary', // Primary contact for business/location
'is_active',
'is_emergency_contact',
'can_approve_orders',
'can_receive_invoices',
'can_place_orders',
'receive_notifications',
'receive_marketing',
// Internal Notes
'notes',
'last_contact_date',
'next_followup_date',
'relationship_notes',
// Account Management
'archived_at',
'archived_reason',
'created_by',
'updated_by',
];
protected $casts = [
'responsibilities' => 'array',
'permissions' => 'array',
'communication_preferences' => 'array',
'work_hours' => 'array',
'is_primary' => 'boolean',
'is_active' => 'boolean',
'is_emergency_contact' => 'boolean',
'can_approve_orders' => 'boolean',
'can_receive_invoices' => 'boolean',
'can_place_orders' => 'boolean',
'receive_notifications' => 'boolean',
'receive_marketing' => 'boolean',
'last_contact_date' => 'datetime',
'next_followup_date' => 'datetime',
'archived_at' => 'datetime',
];
// Relationships
public function business(): BelongsTo
{
return $this->belongsTo(Business::class);
}
public function location(): BelongsTo
{
return $this->belongsTo(Location::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function addresses(): MorphMany
{
return $this->morphMany(Address::class, 'addressable');
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function updatedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'updated_by');
}
// Scopes
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopePrimary($query)
{
return $query->where('is_primary', true);
}
public function scopeByType($query, string $type)
{
return $query->where('contact_type', $type);
}
public function scopeCanApproveOrders($query)
{
return $query->where('can_approve_orders', true);
}
public function scopeCanPlaceOrders($query)
{
return $query->where('can_place_orders', true);
}
public function scopeReceivesInvoices($query)
{
return $query->where('can_receive_invoices', true);
}
public function scopeEmergencyContacts($query)
{
return $query->where('is_emergency_contact', true);
}
public function scopeForCompany($query, int $companyId)
{
return $query->where('company_id', $companyId);
}
public function scopeForLocation($query, int $locationId)
{
return $query->where('location_id', $locationId);
}
// Helper Methods
public function getFullName(): string
{
return trim("{$this->first_name} {$this->last_name}");
}
public function getDisplayName(): string
{
$name = $this->getFullName();
if ($this->position) {
$name .= " ({$this->position})";
}
return $name;
}
public function getFormattedTitle(): string
{
$parts = array_filter([
$this->title,
$this->getFullName(),
$this->position,
]);
return implode(' ', $parts);
}
public function getContactInfo(): array
{
return array_filter([
'email' => $this->email,
'phone' => $this->phone,
'mobile' => $this->mobile,
'fax' => $this->fax,
]);
}
public function getPreferredContactMethod(): string
{
return $this->preferred_contact_method ?? 'email';
}
public function canBeContactedAt(DateTime $datetime = null): bool
{
$datetime = $datetime ?? now();
if (!$this->work_hours) {
return true; // No restrictions
}
$dayOfWeek = strtolower($datetime->format('l'));
$timeOfDay = $datetime->format('H:i');
$schedule = $this->work_hours[$dayOfWeek] ?? null;
if (!$schedule || !isset($schedule['start'], $schedule['end'])) {
return false; // No schedule for this day
}
return $timeOfDay >= $schedule['start'] && $timeOfDay <= $schedule['end'];
}
public function isBuyer(): bool
{
return $this->contact_type === 'buyer';
}
public function isAccountsPayable(): bool
{
return $this->contact_type === 'accounts_payable';
}
public function isReceiving(): bool
{
return $this->contact_type === 'receiving';
}
public function isPrimary(): bool
{
return $this->is_primary;
}
public function isArchived(): bool
{
return !is_null($this->archived_at);
}
public function hasPermission(string $permission): bool
{
return in_array($permission, $this->permissions ?? []);
}
public function hasResponsibility(string $responsibility): bool
{
return in_array($responsibility, $this->responsibilities ?? []);
}
// Status Management
public function archive(string $reason = null, User $archivedBy = null)
{
$this->update([
'is_active' => false,
'is_primary' => false, // Can't be primary if archived
'archived_at' => now(),
'archived_reason' => $reason,
'updated_by' => $archivedBy?->id,
]);
}
public function restore(User $restoredBy = null)
{
$this->update([
'is_active' => true,
'archived_at' => null,
'archived_reason' => null,
'updated_by' => $restoredBy?->id,
]);
}
public function makePrimary()
{
// Remove primary status from other contacts in same scope
if ($this->location_id) {
// Location-specific primary
static::where('location_id', $this->location_id)
->where('id', '!=', $this->id)
->update(['is_primary' => false]);
} else {
// Company-wide primary
static::where('company_id', $this->company_id)
->whereNull('location_id')
->where('id', '!=', $this->id)
->update(['is_primary' => false]);
}
$this->update(['is_primary' => true]);
}
public function updateLastContact()
{
$this->update(['last_contact_date' => now()]);
}
public function setNextFollowup(DateTime $date, string $notes = null)
{
$this->update([
'next_followup_date' => $date,
'relationship_notes' => $this->relationship_notes . "\n\nFollowup scheduled for {$date->format('Y-m-d')}: {$notes}",
]);
}
}