feat: add advanced features — KB, tickets v2, multi-currency, cart, quotes, affiliates, credits, staff RBAC, fraud detection, service panels

Major additions:
- Knowledge base with categories, articles, revisions, and voting
- Enhanced ticket system: departments, SLA policies, canned responses, tags, custom fields, satisfaction ratings, internal notes
- Multi-currency support with exchange rate sync
- Shopping cart and quote system with PDF generation
- Affiliate program with referrals, commissions, and payouts
- Account credits, credit notes, and debit notes
- Staff management with granular role-based permissions
- Fraud detection and order risk assessment
- ServerHunter SEO integration
- Service lifecycle events (suspend/unsuspend/terminate)
- Service management panels for VPS, Dedicated, Hosting, and Game servers
- Plan lifecycle fields and per-customer overrides
- 30+ migrations, 17 factories, 8 feature test suites

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude Dev
2026-03-17 07:35:10 -04:00
parent 2bf8a5b6bf
commit de8ec69ea0
265 changed files with 29892 additions and 216 deletions

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
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\MorphTo;
class AccountCredit extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'amount',
'currency',
'type',
'description',
'reference_type',
'reference_id',
'created_by',
];
protected function casts(): array
{
return [
'amount' => 'decimal:2',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function referenceable(): MorphTo
{
return $this->morphTo(__FUNCTION__, 'reference_type', 'reference_id');
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
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\HasMany;
use Illuminate\Support\Str;
class Affiliate extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'referral_code',
'status',
'commission_type',
'commission_rate',
'recurring_commissions',
'minimum_payout',
'total_earned',
'total_paid',
'pending_balance',
'approved_at',
];
protected function casts(): array
{
return [
'commission_rate' => 'decimal:2',
'minimum_payout' => 'decimal:2',
'total_earned' => 'decimal:2',
'total_paid' => 'decimal:2',
'pending_balance' => 'decimal:2',
'recurring_commissions' => 'boolean',
'approved_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function referrals(): HasMany
{
return $this->hasMany(AffiliateReferral::class);
}
public function commissions(): HasMany
{
return $this->hasMany(AffiliateCommission::class);
}
public function payouts(): HasMany
{
return $this->hasMany(AffiliatePayout::class);
}
public static function generateReferralCode(): string
{
do {
$code = Str::lower(Str::random(8));
} while (self::where('referral_code', $code)->exists());
return $code;
}
public function referralUrl(): string
{
$marketingDomain = config('app.domains.marketing');
return 'https://'.$marketingDomain.'?ref='.$this->referral_code;
}
}

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AffiliateCommission extends Model
{
use HasFactory;
protected $fillable = [
'affiliate_id',
'referral_id',
'payment_transaction_id',
'amount',
'currency',
'type',
'status',
'approved_at',
'paid_at',
];
protected function casts(): array
{
return [
'amount' => 'decimal:2',
'approved_at' => 'datetime',
'paid_at' => 'datetime',
];
}
public function affiliate(): BelongsTo
{
return $this->belongsTo(Affiliate::class);
}
public function referral(): BelongsTo
{
return $this->belongsTo(AffiliateReferral::class, 'referral_id');
}
public function paymentTransaction(): BelongsTo
{
return $this->belongsTo(PaymentTransaction::class);
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class AffiliatePayout extends Model
{
use HasFactory;
protected $fillable = [
'affiliate_id',
'amount',
'currency',
'method',
'status',
'reference',
'processed_at',
];
protected function casts(): array
{
return [
'amount' => 'decimal:2',
'processed_at' => 'datetime',
];
}
public function affiliate(): BelongsTo
{
return $this->belongsTo(Affiliate::class);
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Cashier\Subscription;
class AffiliateReferral extends Model
{
use HasFactory;
protected $fillable = [
'affiliate_id',
'referred_user_id',
'subscription_id',
'status',
'signup_ip',
'referral_url',
'approved_at',
];
protected function casts(): array
{
return [
'approved_at' => 'datetime',
];
}
public function affiliate(): BelongsTo
{
return $this->belongsTo(Affiliate::class);
}
public function referredUser(): BelongsTo
{
return $this->belongsTo(User::class, 'referred_user_id');
}
public function subscription(): BelongsTo
{
return $this->belongsTo(Subscription::class);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ArticleRevision extends Model
{
protected $fillable = [
'article_id',
'content',
'edited_by',
];
public function article(): BelongsTo
{
return $this->belongsTo(KnowledgeBaseArticle::class, 'article_id');
}
public function editor(): BelongsTo
{
return $this->belongsTo(User::class, 'edited_by');
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ArticleVote extends Model
{
protected $fillable = [
'article_id',
'user_id',
'is_helpful',
'ip_address',
];
/** @return array<string, string> */
protected function casts(): array
{
return [
'is_helpful' => 'boolean',
];
}
public function article(): BelongsTo
{
return $this->belongsTo(KnowledgeBaseArticle::class, 'article_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class CannedResponse extends Model
{
use HasFactory;
protected $fillable = [
'title',
'content',
'category',
'is_shared',
'created_by',
'usage_count',
];
/** @return array<string, string> */
protected function casts(): array
{
return [
'is_shared' => 'boolean',
];
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
}

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class CartItem extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'session_id',
'plan_id',
'billing_cycle',
'quantity',
'config_selections',
'provisioning_config',
'coupon_code',
];
protected function casts(): array
{
return [
'quantity' => 'integer',
'config_selections' => 'array',
'provisioning_config' => 'array',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function plan(): BelongsTo
{
return $this->belongsTo(Plan::class);
}
}

View File

@@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
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\MorphMany;
use Illuminate\Support\Str;
class CreditNote extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'invoice_id',
'number',
'amount',
'currency',
'reason',
'status',
'issued_at',
'created_by',
];
protected function casts(): array
{
return [
'amount' => 'decimal:2',
'issued_at' => 'datetime',
];
}
public static function generateNumber(): string
{
return 'CN-'.strtoupper(Str::random(6));
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function invoice(): BelongsTo
{
return $this->belongsTo(Invoice::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function accountCredits(): MorphMany
{
return $this->morphMany(AccountCredit::class, 'referenceable', 'reference_type', 'reference_id');
}
}

View File

@@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Currency extends Model
{
use HasFactory;
protected $fillable = [
'code',
'symbol',
'name',
'decimal_places',
'exchange_rate',
'is_base',
'is_enabled',
'last_synced_at',
];
protected function casts(): array
{
return [
'decimal_places' => 'integer',
'exchange_rate' => 'decimal:6',
'is_base' => 'boolean',
'is_enabled' => 'boolean',
'last_synced_at' => 'datetime',
];
}
public function scopeEnabled(Builder $query): Builder
{
return $query->where('is_enabled', true);
}
public function scopeBase(Builder $query): Builder
{
return $query->where('is_base', true);
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class DebitNote extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'invoice_id',
'number',
'amount',
'currency',
'reason_type',
'reason',
'status',
'issued_at',
'created_by',
];
protected function casts(): array
{
return [
'amount' => 'decimal:2',
'issued_at' => 'datetime',
];
}
public static function generateNumber(): string
{
return 'DN-'.strtoupper(Str::random(6));
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function invoice(): BelongsTo
{
return $this->belongsTo(Invoice::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
}

View File

@@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
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\HasMany;
class Department extends Model
{
use HasFactory;
protected $fillable = [
'name',
'slug',
'email',
'auto_assign_to',
'sla_policy_id',
'sort_order',
];
public function tickets(): HasMany
{
return $this->hasMany(SupportTicket::class);
}
public function slaPolicy(): BelongsTo
{
return $this->belongsTo(SlaPolicy::class);
}
public function autoAssignee(): BelongsTo
{
return $this->belongsTo(User::class, 'auto_assign_to');
}
}

View File

@@ -0,0 +1,93 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class KnowledgeBaseArticle extends Model
{
use HasFactory;
protected $fillable = [
'category_id',
'author_id',
'title',
'slug',
'content',
'excerpt',
'status',
'is_featured',
'view_count',
'helpful_count',
'not_helpful_count',
'published_at',
];
/** @return array<string, string> */
protected function casts(): array
{
return [
'is_featured' => 'boolean',
'view_count' => 'integer',
'helpful_count' => 'integer',
'not_helpful_count' => 'integer',
'published_at' => 'datetime',
];
}
public function category(): BelongsTo
{
return $this->belongsTo(KnowledgeBaseCategory::class, 'category_id');
}
public function author(): BelongsTo
{
return $this->belongsTo(User::class, 'author_id');
}
public function revisions(): HasMany
{
return $this->hasMany(ArticleRevision::class, 'article_id');
}
public function votes(): HasMany
{
return $this->hasMany(ArticleVote::class, 'article_id');
}
public function scopePublished(Builder $query): Builder
{
return $query->where('status', 'published')
->where('published_at', '<=', now());
}
public function scopeFeatured(Builder $query): Builder
{
return $query->where('is_featured', true);
}
public function scopeSearch(Builder $query, string $search): Builder
{
return $query->whereRaw(
'MATCH(title, content) AGAINST(? IN BOOLEAN MODE)',
[$search.'*']
);
}
public function helpfulPercentage(): float
{
$total = $this->helpful_count + $this->not_helpful_count;
if ($total === 0) {
return 0.0;
}
return round(($this->helpful_count / $total) * 100, 1);
}
}

View File

@@ -0,0 +1,75 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class KnowledgeBaseCategory extends Model
{
use HasFactory;
protected $fillable = [
'parent_id',
'name',
'slug',
'description',
'icon',
'sort_order',
'is_visible',
];
/** @return array<string, string> */
protected function casts(): array
{
return [
'sort_order' => 'integer',
'is_visible' => 'boolean',
];
}
public function parent(): BelongsTo
{
return $this->belongsTo(self::class, 'parent_id');
}
public function children(): HasMany
{
return $this->hasMany(self::class, 'parent_id');
}
public function articles(): HasMany
{
return $this->hasMany(KnowledgeBaseArticle::class, 'category_id');
}
public function scopeVisible(Builder $query): Builder
{
return $query->where('is_visible', true);
}
public function scopeRoot(Builder $query): Builder
{
return $query->whereNull('parent_id');
}
/** @return Collection<int, self> */
public function getAncestors(): Collection
{
$ancestors = new Collection;
$current = $this->parent;
while ($current) {
$ancestors->prepend($current);
$current = $current->parent;
}
return $ancestors;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class OrderRiskAssessment extends Model
{
use HasFactory;
protected $fillable = [
'order_id',
'user_id',
'risk_score',
'risk_level',
'checks',
'auto_action',
'reviewed_by',
'reviewed_at',
];
protected function casts(): array
{
return [
'checks' => 'array',
'risk_score' => 'integer',
'reviewed_at' => 'datetime',
];
}
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function reviewer(): BelongsTo
{
return $this->belongsTo(User::class, 'reviewed_by');
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace App\Models;
use App\Services\Billing\CurrencyService;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
@@ -30,6 +31,10 @@ class Plan extends Model
'stock_quantity',
'status',
'sort_order',
'days_to_suspend',
'days_to_terminate',
'auto_suspend_enabled',
'auto_terminate_enabled',
];
protected function casts(): array
@@ -40,6 +45,10 @@ class Plan extends Model
'provisioning_config' => 'array',
'stock_quantity' => 'integer',
'sort_order' => 'integer',
'days_to_suspend' => 'integer',
'days_to_terminate' => 'integer',
'auto_suspend_enabled' => 'boolean',
'auto_terminate_enabled' => 'boolean',
];
}
@@ -81,6 +90,18 @@ class Plan extends Model
return true;
}
public function priceInCurrency(string $currency): float
{
if (strtoupper($currency) === strtoupper($this->currency ?? 'USD')) {
return (float) $this->price;
}
/** @var CurrencyService $currencyService */
$currencyService = app(CurrencyService::class);
return $currencyService->convert((float) $this->price, $this->currency ?? 'USD', $currency);
}
public function scopePublic(Builder $query): Builder
{
return $query->whereNotIn('status', ['hidden', 'internal', 'inactive']);

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class Quote extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'prospect_email',
'prospect_name',
'number',
'status',
'items',
'subtotal',
'tax',
'total',
'currency',
'notes',
'valid_until',
'accepted_at',
'sent_at',
'created_by',
];
protected function casts(): array
{
return [
'items' => 'array',
'subtotal' => 'decimal:2',
'tax' => 'decimal:2',
'total' => 'decimal:2',
'valid_until' => 'date',
'accepted_at' => 'datetime',
'sent_at' => 'datetime',
];
}
public static function generateNumber(): string
{
return 'QT-'.strtoupper(Str::random(6));
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function creator(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
public function isExpired(): bool
{
if (! $this->valid_until) {
return false;
}
return $this->valid_until->isPast();
}
public function markAccepted(): void
{
$this->update([
'status' => 'accepted',
'accepted_at' => now(),
]);
}
public function markSent(): void
{
$this->update([
'status' => 'sent',
'sent_at' => now(),
]);
}
public function getRecipientEmail(): ?string
{
return $this->user?->email ?? $this->prospect_email;
}
public function getRecipientName(): ?string
{
return $this->user?->name ?? $this->prospect_name;
}
}

View File

@@ -0,0 +1,25 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class SlaBusinessHours extends Model
{
protected $fillable = [
'day_of_week',
'start_time',
'end_time',
'is_holiday',
];
/** @return array<string, string> */
protected function casts(): array
{
return [
'is_holiday' => 'boolean',
];
}
}

View File

@@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class SlaPolicy extends Model
{
use HasFactory;
protected $fillable = [
'name',
'priority',
'first_response_hours',
'resolution_hours',
'business_hours_only',
];
/** @return array<string, string> */
protected function casts(): array
{
return [
'business_hours_only' => 'boolean',
];
}
public function departments(): HasMany
{
return $this->hasMany(Department::class);
}
}

View File

@@ -8,7 +8,9 @@ use Illuminate\Database\Eloquent\Builder;
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\HasOne;
class SupportTicket extends Model
{
@@ -22,6 +24,16 @@ class SupportTicket extends Model
'status',
'priority',
'department',
'department_id',
'assigned_to',
'sla_policy_id',
'first_response_due_at',
'resolution_due_at',
'first_responded_at',
'resolved_at',
'sla_first_response_breached',
'sla_resolution_breached',
'merged_into_ticket_id',
'last_reply_at',
];
@@ -30,6 +42,12 @@ class SupportTicket extends Model
{
return [
'last_reply_at' => 'datetime',
'first_response_due_at' => 'datetime',
'resolution_due_at' => 'datetime',
'first_responded_at' => 'datetime',
'resolved_at' => 'datetime',
'sla_first_response_breached' => 'boolean',
'sla_resolution_breached' => 'boolean',
];
}
@@ -61,4 +79,39 @@ class SupportTicket extends Model
{
return $this->hasMany(TicketReply::class, 'ticket_id');
}
public function departmentRelation(): BelongsTo
{
return $this->belongsTo(Department::class, 'department_id');
}
public function assignee(): BelongsTo
{
return $this->belongsTo(User::class, 'assigned_to');
}
public function slaPolicy(): BelongsTo
{
return $this->belongsTo(SlaPolicy::class);
}
public function tags(): BelongsToMany
{
return $this->belongsToMany(TicketTag::class, 'support_ticket_tag', 'ticket_id', 'tag_id');
}
public function customFieldValues(): HasMany
{
return $this->hasMany(TicketCustomFieldValue::class, 'ticket_id');
}
public function satisfactionRating(): HasOne
{
return $this->hasOne(TicketSatisfactionRating::class, 'ticket_id');
}
public function mergedInto(): BelongsTo
{
return $this->belongsTo(self::class, 'merged_into_ticket_id');
}
}

View File

@@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class TicketCustomField extends Model
{
protected $fillable = [
'name',
'type',
'options',
'is_required',
'department_id',
'sort_order',
];
/** @return array<string, string> */
protected function casts(): array
{
return [
'options' => 'array',
'is_required' => 'boolean',
];
}
public function department(): BelongsTo
{
return $this->belongsTo(Department::class);
}
public function values(): HasMany
{
return $this->hasMany(TicketCustomFieldValue::class, 'custom_field_id');
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TicketCustomFieldValue extends Model
{
protected $fillable = [
'ticket_id',
'custom_field_id',
'value',
];
public function ticket(): BelongsTo
{
return $this->belongsTo(SupportTicket::class, 'ticket_id');
}
public function customField(): BelongsTo
{
return $this->belongsTo(TicketCustomField::class, 'custom_field_id');
}
}

View File

@@ -20,6 +20,7 @@ class TicketReply extends Model
'message_id',
'from_email',
'via_email',
'is_internal',
];
/** @return array<string, string> */
@@ -28,6 +29,7 @@ class TicketReply extends Model
return [
'is_staff_reply' => 'boolean',
'via_email' => 'boolean',
'is_internal' => 'boolean',
];
}

View File

@@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TicketSatisfactionRating extends Model
{
protected $fillable = [
'ticket_id',
'user_id',
'rating',
'comment',
];
public function ticket(): BelongsTo
{
return $this->belongsTo(SupportTicket::class, 'ticket_id');
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
class TicketTag extends Model
{
use HasFactory;
protected $fillable = [
'name',
'color',
];
public function tickets(): BelongsToMany
{
return $this->belongsToMany(SupportTicket::class, 'support_ticket_tag', 'tag_id', 'ticket_id');
}
}

View File

@@ -30,6 +30,10 @@ class User extends Authenticatable implements MustVerifyEmail
'company',
'admin_notes',
'virtfusion_user_id',
'override_days_to_suspend',
'override_days_to_terminate',
'credit_balance',
'currency',
];
/** @var list<string> */
@@ -47,6 +51,9 @@ class User extends Authenticatable implements MustVerifyEmail
'email_verified_at' => 'datetime',
'password' => 'hashed',
'passkey_credentials' => 'json',
'override_days_to_suspend' => 'integer',
'override_days_to_terminate' => 'integer',
'credit_balance' => 'decimal:2',
];
}
@@ -101,9 +108,44 @@ class User extends Authenticatable implements MustVerifyEmail
return $this->hasMany(TrustedDevice::class);
}
public function accountCredits(): HasMany
{
return $this->hasMany(AccountCredit::class);
}
public function creditNotes(): HasMany
{
return $this->hasMany(CreditNote::class);
}
public function debitNotes(): HasMany
{
return $this->hasMany(DebitNote::class);
}
public function cartItems(): HasMany
{
return $this->hasMany(CartItem::class);
}
public function quotes(): HasMany
{
return $this->hasMany(Quote::class);
}
public function affiliate(): HasOne
{
return $this->hasOne(Affiliate::class);
}
public function isAdmin(): bool
{
return $this->hasRole('admin');
return $this->hasRole(['admin', 'super_admin', 'billing_admin', 'support_agent', 'support_lead', 'readonly_admin']);
}
public function isSuperAdmin(): bool
{
return $this->hasRole(['admin', 'super_admin']);
}
public function isCustomer(): bool