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

637 lines
18 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\MorphMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use OwenIt\Auditing\Contracts\Auditable as AuditableContract;
use OwenIt\Auditing\Auditable;
class Business extends Model implements AuditableContract
{
use HasFactory, SoftDeletes, Auditable;
// User type (buyer/seller/both)
public const TYPES = [
'buyer' => 'Buyer (Dispensary/Retailer)',
'seller' => 'Seller (Brand/Manufacturer)',
'both' => 'Both (Vertically Integrated)',
];
// Business types aligned with cannabis industry (stored in business_type column)
public const BUSINESS_TYPES = [
'brand' => 'Brand/Manufacturer',
'retailer' => 'Retailer/Dispensary',
'distributor' => 'Distributor',
'cultivator' => 'Cultivator',
'processor' => 'Processor/Manufacturer',
'testing_lab' => 'Testing Laboratory',
'both' => 'Vertically Integrated', // Brand + Retailer
];
// Setup Status constants
// Services that can be offered
public const SERVICES = [
'cultivation',
'manufacturing',
'processing',
'distribution',
'retail',
'delivery',
'testing',
'consulting',
];
protected $fillable = [
// Account Ownership
'owner_user_id',
// Core Company Identity (LeafLink Company equivalent)
'name',
'slug',
'dba_name', // Doing Business As
'description',
'logo',
'website',
// Company Classification
'type', // buyer, seller, or both
'industry_type', // Primary industry type
'business_type', // Business type from onboarding (same as industry_type)
'services', // JSON: Array of services offered
// Corporate Structure
'business_group', // For multi-location operators (corporate grouping)
'parent_business_id', // For subsidiaries
// Tax & Legal Information (LICENSE HOLDER DETAILS)
'tin_ein', // Tax Identification Number
'license_number', // Cannabis retail license number
'legal_name', // Official legal entity name
'legal_structure', // LLC, Corporation, Partnership, etc.
// Compliance Documents (LeafLink requirement)
'business_license_path', // State business license
'cannabis_license_path', // Cannabis operation license
'w9_form_path', // W-9 tax form
'insurance_certificate_path', // General liability insurance
'bond_certificate_path', // Surety bond if required
'ato_document_path', // Arizona Transaction Privilege Tax
'resale_certificate_path', // Resale certificate
'compliance_documents_updated_at', // When docs were last updated
// Corporate Address (headquarters/main office)
'corporate_address',
'corporate_city',
'corporate_state',
'corporate_zipcode',
'corporate_unit',
// Physical Address (business location)
'physical_address',
'physical_city',
'physical_state',
'physical_zipcode',
// Location Contact
'business_phone',
'business_email',
// Billing Address
'billing_address',
'billing_city',
'billing_state',
'billing_zipcode',
// Shipping/Delivery Address
'shipping_address',
'shipping_city',
'shipping_state',
'shipping_zipcode',
// AP Contact
'ap_contact_first_name',
'ap_contact_last_name',
'ap_contact_phone',
'ap_contact_email',
// Delivery Contact
'delivery_contact_first_name',
'delivery_contact_last_name',
'delivery_contact_phone',
// Financial & Payment Settings
'payment_terms', // net_30, net_15, etc.
'commission_rate',
'credit_limit',
'payment_method_preference',
// Onboarding & Application
'referral_source',
'referral_code',
'status', // not_started, in_progress, submitted, approved, rejected
'rejected_at',
'rejection_reason',
'application_submitted_at',
'onboarding_completed',
// Status & Settings (LeafLink archival pattern)
'is_active', // Active vs Archived
'approved_at',
'approved_by',
'archived_at',
'archived_reason',
'settings', // JSON for additional settings
'notes',
// Business Setup Form Fields (from CRM migration)
'payment_method',
'delivery_unit',
'delivery_directions',
'delivery_preferences',
'delivery_schedule',
'buyer_name',
'buyer_phone',
'buyer_email',
'preferred_contact_method',
'tpt_document_path',
'form_5000a_path',
'setup_completed_at',
'setup_progress',
// Additional contact fields from comprehensive CRM form
'physical_address',
'physical_city',
'physical_state',
'physical_zipcode',
'business_phone',
'business_email',
'license_number',
'billing_address',
'billing_city',
'billing_state',
'billing_zipcode',
'shipping_address',
'shipping_city',
'shipping_state',
'shipping_zipcode',
'ap_contact_first_name',
'ap_contact_last_name',
'ap_contact_phone',
'ap_contact_email',
'ap_contact_sms',
'ap_preferred_contact_method',
'delivery_contact_first_name',
'delivery_contact_last_name',
'delivery_contact_phone',
'delivery_contact_email',
'delivery_contact_sms',
];
protected $casts = [
'services' => 'array',
'settings' => 'array',
'setup_progress' => 'array',
'setup_completed_at' => 'datetime',
'is_active' => 'boolean',
'onboarding_completed' => 'boolean',
'approved_at' => 'datetime',
'rejected_at' => 'datetime',
'application_submitted_at' => 'datetime',
'archived_at' => 'datetime',
'compliance_documents_updated_at' => 'datetime',
'commission_rate' => 'decimal:2',
'credit_limit' => 'decimal:2',
];
// LeafLink-aligned Relationships
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'business_user')
->withPivot('contact_type', 'is_primary', 'permissions')
->withTimestamps();
}
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_user_id');
}
public function locations(): HasMany
{
// TODO: Locations not yet implemented - table is empty placeholder
// When implemented, should use 'business_id' foreign key for consistency
return $this->hasMany(Location::class, 'business_id');
}
public function licenses(): HasMany
{
return $this->hasMany(License::class);
}
public function contacts(): HasMany
{
return $this->hasMany(Contact::class);
}
public function addresses(): MorphMany
{
return $this->morphMany(Address::class, 'addressable');
}
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
public function orders(): HasMany
{
return $this->hasMany(Order::class);
}
public function brands(): HasMany
{
return $this->hasMany(Brand::class);
}
// Corporate Structure
public function parentBusiness()
{
return $this->belongsTo(Business::class, 'parent_business_id');
}
public function subsidiaries(): HasMany
{
return $this->hasMany(Business::class, 'parent_business_id');
}
public function approver()
{
return $this->belongsTo(User::class, 'approved_by');
}
// Scopes
public function scopeActive($query)
{
return $query->where('is_active', true);
}
public function scopeApproved($query)
{
return $query->where('is_approved', true);
}
public function scopeSellers($query)
{
return $query->whereIn('type', ['seller', 'both']);
}
public function scopeBuyers($query)
{
return $query->whereIn('type', ['buyer', 'both']);
}
// Helper methods (LeafLink-aligned)
public function isSeller(): bool
{
return in_array($this->type, ['seller', 'both']);
}
public function isBuyer(): bool
{
return in_array($this->type, ['buyer', 'both']);
}
public function isVerticallyIntegrated(): bool
{
return $this->type === 'both';
}
public function hasService(string $service): bool
{
return in_array($service, $this->services ?? []);
}
public function needsOnboarding(): bool
{
return !$this->onboarding_completed;
}
public function isArchived(): bool
{
return !is_null($this->archived_at);
}
public function isPending(): bool
{
return $this->status === 'submitted';
}
public function isApproved(): bool
{
return $this->status === 'approved';
}
public function isRejected(): bool
{
return $this->status === 'rejected';
}
// Corporate Structure Helpers
public function getGroupedCompanies()
{
if (!$this->business_group) {
return collect([$this]);
}
return static::where('business_group', $this->business_group)
->where('is_active', true)
->get();
}
public function getPrimaryLocation()
{
return $this->locations()->where('is_primary', true)->first();
}
public function getCorporateAddress()
{
return $this->addresses()->where('type', 'corporate')->first();
}
public function getActiveLicenses()
{
return $this->licenses()->where('status', 'active')->get();
}
public function getPrimaryContact()
{
return $this->contacts()->where('is_primary', true)->first();
}
public function getAccountOwner()
{
return $this->owner;
}
// COMPLIANCE & LICENSE HOLDER METHODS
/**
* Check if business has all required compliance documents
* (Required for LeafLink-style platform operation)
*/
public function hasRequiredComplianceDocs(): bool
{
$requiredDocs = [
'business_license_path',
'cannabis_license_path',
'w9_form_path',
'insurance_certificate_path',
];
foreach ($requiredDocs as $doc) {
if (empty($this->$doc)) {
return false;
}
}
return true;
}
/**
* Get missing compliance documents
*/
public function getMissingComplianceDocs(): array
{
$requiredDocs = [
'business_license_path' => 'Business License',
'cannabis_license_path' => 'Cannabis License',
'w9_form_path' => 'W-9 Tax Form',
'insurance_certificate_path' => 'Insurance Certificate',
];
$missing = [];
foreach ($requiredDocs as $field => $name) {
if (empty($this->$field)) {
$missing[] = $name;
}
}
return $missing;
}
/**
* Check if company can legally sell cannabis products
* (Has active licenses and compliance docs)
*/
public function canSellCannabis(): bool
{
return $this->isApproved() &&
$this->hasRequiredComplianceDocs() &&
$this->getActiveLicenses()->isNotEmpty() &&
$this->is_active;
}
/**
* Check if compliance documents are up to date
*/
public function hasCurrentComplianceDocs(): bool
{
if (!$this->compliance_documents_updated_at) {
return false;
}
// Documents should be updated within last year
return $this->compliance_documents_updated_at->isAfter(now()->subYear());
}
/**
* Get legal entity information for contracts/invoices
*/
public function getLegalEntityInfo(): array
{
return [
'legal_name' => $this->legal_name ?: $this->name,
'dba_name' => $this->dba_name,
'legal_structure' => $this->legal_structure,
'tin_ein' => $this->tin_ein,
'corporate_address' => $this->getCorporateAddress(),
];
}
/**
* Update compliance documents timestamp
*/
public function updateComplianceDocuments()
{
$this->update(['compliance_documents_updated_at' => now()]);
}
// Archive/Restore (LeafLink pattern)
public function archive(string $reason = null)
{
$this->update([
'is_active' => false,
'archived_at' => now(),
'archived_reason' => $reason,
]);
// Archive related entities
$this->locations()->update(['is_active' => false]);
$this->contacts()->update(['is_active' => false]);
}
public function restore()
{
$this->update([
'is_active' => true,
'archived_at' => null,
'archived_reason' => null,
]);
}
// Company Setup Progress Tracking (Based on CRM Form Analysis)
/**
* Calculate company setup progress percentage and step details
* Based on the 5-step form from the old CRM: Company → Billing → Delivery → Documents → Review
*/
public function getSetupProgress(): array
{
$steps = [
'company_profile' => [
'name' => 'Company Profile',
'weight' => 20,
'fields' => ['name', 'dba_name', 'license_number', 'tin_ein', 'business_type', 'physical_address', 'physical_city', 'physical_state']
],
'contact_billing' => [
'name' => 'Contact & Billing',
'weight' => 25,
'fields' => ['business_phone', 'business_email', 'billing_address', 'billing_city', 'billing_state', 'ap_contact_first_name', 'ap_contact_last_name', 'ap_contact_email']
],
'delivery_info' => [
'name' => 'Delivery Information',
'weight' => 15,
'fields' => ['shipping_address', 'shipping_city', 'delivery_preferences', 'delivery_contact_name', 'delivery_contact_phone']
],
'compliance_docs' => [
'name' => 'Compliance Documents',
'weight' => 30,
'fields' => ['cannabis_license_path', 'w9_form_path', 'insurance_certificate_path', 'business_license_path']
],
'final_review' => [
'name' => 'Final Review',
'weight' => 10,
'fields' => ['buyer_name', 'buyer_email', 'preferred_contact_method']
]
];
$stepResults = [];
$totalProgress = 0;
foreach ($steps as $stepKey => $stepConfig) {
$requiredFields = collect($stepConfig['fields']);
$filledFields = $requiredFields->filter(function($field) {
return !empty($this->$field);
});
$stepPercentage = $requiredFields->count() > 0
? ($filledFields->count() / $requiredFields->count()) * 100
: 0;
$stepResults[$stepKey] = [
'name' => $stepConfig['name'],
'completed' => $stepPercentage >= 100,
'percentage' => $stepPercentage,
'weight' => $stepConfig['weight'],
'filled_fields' => $filledFields->count(),
'total_fields' => $requiredFields->count(),
];
$totalProgress += ($stepPercentage / 100) * $stepConfig['weight'];
}
return [
'percentage' => round($totalProgress, 1),
'steps' => $stepResults,
'is_complete' => $totalProgress >= 95, // 95% threshold for completion
'completed_at' => $this->setup_completed_at,
];
}
/**
* Check if company setup is complete enough to enable purchasing
*/
public function canPurchase(): bool
{
$progress = $this->getSetupProgress();
// Must have minimum required fields
$requiredForPurchase = [
'name', 'license_number', 'business_phone', 'business_email',
'billing_address', 'payment_method',
'cannabis_license_path', 'w9_form_path', 'business_license_path'
];
foreach ($requiredForPurchase as $field) {
if (empty($this->$field)) {
return false;
}
}
return $progress['percentage'] >= 75; // 75% minimum for purchasing
}
/**
* Mark company setup as complete and submit for approval
*/
public function markSetupComplete(): void
{
$this->update([
'setup_completed_at' => now(),
'onboarding_completed' => true,
'status' => 'submitted', // Submit for admin approval
'application_submitted_at' => now(),
]);
}
/**
* Get setup status for dashboard display
*/
public function getSetupStatus(): array
{
$progress = $this->getSetupProgress();
if ($progress['is_complete']) {
return [
'status' => 'complete',
'message' => 'Company setup complete! You can now purchase cannabis products.',
'color' => 'success'
];
} elseif ($progress['percentage'] >= 75) {
return [
'status' => 'ready',
'message' => 'Setup nearly complete. You can start purchasing!',
'color' => 'primary'
];
} elseif ($progress['percentage'] >= 50) {
return [
'status' => 'in_progress',
'message' => 'Good progress! Complete your setup to start purchasing.',
'color' => 'warning'
];
} else {
return [
'status' => 'started',
'message' => 'Complete your company setup to start purchasing cannabis products.',
'color' => 'info'
];
}
}
}