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:
48
website/app/Models/AccountCredit.php
Normal file
48
website/app/Models/AccountCredit.php
Normal 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');
|
||||
}
|
||||
}
|
||||
79
website/app/Models/Affiliate.php
Normal file
79
website/app/Models/Affiliate.php
Normal 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;
|
||||
}
|
||||
}
|
||||
50
website/app/Models/AffiliateCommission.php
Normal file
50
website/app/Models/AffiliateCommission.php
Normal 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);
|
||||
}
|
||||
}
|
||||
37
website/app/Models/AffiliatePayout.php
Normal file
37
website/app/Models/AffiliatePayout.php
Normal 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);
|
||||
}
|
||||
}
|
||||
47
website/app/Models/AffiliateReferral.php
Normal file
47
website/app/Models/AffiliateReferral.php
Normal 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);
|
||||
}
|
||||
}
|
||||
27
website/app/Models/ArticleRevision.php
Normal file
27
website/app/Models/ArticleRevision.php
Normal 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');
|
||||
}
|
||||
}
|
||||
36
website/app/Models/ArticleVote.php
Normal file
36
website/app/Models/ArticleVote.php
Normal 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);
|
||||
}
|
||||
}
|
||||
36
website/app/Models/CannedResponse.php
Normal file
36
website/app/Models/CannedResponse.php
Normal 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');
|
||||
}
|
||||
}
|
||||
44
website/app/Models/CartItem.php
Normal file
44
website/app/Models/CartItem.php
Normal 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);
|
||||
}
|
||||
}
|
||||
61
website/app/Models/CreditNote.php
Normal file
61
website/app/Models/CreditNote.php
Normal 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');
|
||||
}
|
||||
}
|
||||
46
website/app/Models/Currency.php
Normal file
46
website/app/Models/Currency.php
Normal 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);
|
||||
}
|
||||
}
|
||||
56
website/app/Models/DebitNote.php
Normal file
56
website/app/Models/DebitNote.php
Normal 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');
|
||||
}
|
||||
}
|
||||
39
website/app/Models/Department.php
Normal file
39
website/app/Models/Department.php
Normal 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');
|
||||
}
|
||||
}
|
||||
93
website/app/Models/KnowledgeBaseArticle.php
Normal file
93
website/app/Models/KnowledgeBaseArticle.php
Normal 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);
|
||||
}
|
||||
}
|
||||
75
website/app/Models/KnowledgeBaseCategory.php
Normal file
75
website/app/Models/KnowledgeBaseCategory.php
Normal 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;
|
||||
}
|
||||
}
|
||||
49
website/app/Models/OrderRiskAssessment.php
Normal file
49
website/app/Models/OrderRiskAssessment.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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']);
|
||||
|
||||
96
website/app/Models/Quote.php
Normal file
96
website/app/Models/Quote.php
Normal 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;
|
||||
}
|
||||
}
|
||||
25
website/app/Models/SlaBusinessHours.php
Normal file
25
website/app/Models/SlaBusinessHours.php
Normal 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',
|
||||
];
|
||||
}
|
||||
}
|
||||
35
website/app/Models/SlaPolicy.php
Normal file
35
website/app/Models/SlaPolicy.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
40
website/app/Models/TicketCustomField.php
Normal file
40
website/app/Models/TicketCustomField.php
Normal 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');
|
||||
}
|
||||
}
|
||||
27
website/app/Models/TicketCustomFieldValue.php
Normal file
27
website/app/Models/TicketCustomFieldValue.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
28
website/app/Models/TicketSatisfactionRating.php
Normal file
28
website/app/Models/TicketSatisfactionRating.php
Normal 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);
|
||||
}
|
||||
}
|
||||
24
website/app/Models/TicketTag.php
Normal file
24
website/app/Models/TicketTag.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user