Add all files

This commit is contained in:
root
2025-11-30 11:57:22 +01:00
parent 2ee51cac4f
commit 044271db3d
1476 changed files with 142544 additions and 0 deletions

25
app/Models/ApiKey.php Normal file
View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ApiKey extends Model
{
protected $fillable = [
'name',
'permissions',
'token',
'user_id',
'type',
'ip_addresses',
'last_used_at',
'enabled',
];
protected $casts = [
'permissions' => 'array',
'ip_addresses' => 'array',
'last_used_at' => 'datetime',
];
}

42
app/Models/AuditLog.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AuditLog extends Model
{
use HasFactory;
protected $fillable = [
'action',
'description',
'model_type',
'model_id',
'user_id',
'changes',
'ip_address',
'user_agent',
];
protected $casts = [
'changes' => 'array',
];
/**
* Get the model that was changed.
*/
public function model()
{
return $this->morphTo();
}
/**
* Get the user that performed the action.
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

42
app/Models/Category.php Normal file
View File

@@ -0,0 +1,42 @@
<?php
namespace App\Models;
use App\Observers\CategoryObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
#[ObservedBy(CategoryObserver::class)]
class Category extends Model
{
use HasFactory;
protected $guarded = [];
/**
* Get the products of the category.
*/
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
/**
* Get the parent category of the category.
*/
public function parent(): BelongsTo
{
return $this->belongsTo(Category::class, 'parent_id');
}
/**
* Get the children categories of the category.
*/
public function children(): HasMany
{
return $this->hasMany(Category::class, 'parent_id');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Models;
use App\Models\Traits\HasPlans;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ConfigOption extends Model
{
use HasFactory, HasPlans;
protected $dontShowUnavailablePrice = true;
protected $fillable = [
'name',
'env_variable',
'type',
'sort',
'hidden',
'parent_id',
'upgradable',
];
/**
* Get the parent option.
*/
public function parent()
{
return $this->belongsTo(ConfigOption::class, 'parent_id');
}
/**
* Get the options that belong to the parent. (children or options)
*/
public function children()
{
return $this->hasMany(ConfigOption::class, 'parent_id')->orderBy('sort');
}
/**
* Get the products that belong to the option.
*/
public function products()
{
return $this->belongsToMany(Product::class, 'config_option_products');
}
/**
* Get the service configs that belong to the option.
*/
public function serviceConfigs()
{
return $this->hasMany(ServiceConfig::class, 'config_option_id');
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ConfigOptionProduct extends Model
{
use HasFactory;
protected $fillable = [
'config_option_id',
'product_id',
];
public $timestamps = false;
/**
* Get the option of the product.
*/
public function configOption()
{
return $this->belongsTo(ConfigOption::class);
}
/**
* Get the product of the option.
*/
public function product()
{
return $this->belongsTo(Product::class);
}
}

59
app/Models/Coupon.php Normal file
View File

@@ -0,0 +1,59 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Coupon extends Model
{
use HasFactory;
protected $fillable = [
'type',
'time',
'code',
'value',
'max_uses',
'max_uses_per_user',
'starts_at',
'expires_at',
'recurring',
];
protected $casts = [
'starts_at' => 'datetime',
'expires_at' => 'datetime',
'max_uses' => 'integer',
'max_uses_per_user' => 'integer',
];
/**
* Get the products that belong to the option.
*/
public function products()
{
return $this->belongsToMany(Product::class, 'coupon_products');
}
public function services()
{
return $this->hasMany(Service::class);
}
/**
* Check if the user has exceeded the maximum allowed uses of this coupon
*
* @param int $userId
*/
public function hasExceededMaxUsesPerUser($userId): bool
{
if (empty($this->max_uses_per_user)) {
return false;
}
return $this->services()
->where('user_id', $userId)
->count() >= $this->max_uses_per_user;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CouponProduct extends Model
{
use HasFactory;
protected $fillable = [
'coupon_id',
'product_id',
];
public function coupon()
{
return $this->belongsTo(Coupon::class);
}
public function product()
{
return $this->belongsTo(Product::class);
}
}

32
app/Models/Credit.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use App\Classes\Price;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Model;
class Credit extends Model
{
protected $fillable = [
'currency_code',
'amount',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function currency()
{
return $this->belongsTo(Currency::class, 'currency_code', 'code');
}
public function formattedAmount(): Attribute
{
return Attribute::make(
get: fn () => new Price(['price' => $this->amount, 'currency' => $this->currency])
);
}
}

26
app/Models/Currency.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Currency extends Model
{
use HasFactory;
public $timestamps = false;
public $incrementing = false;
protected $keyType = 'string';
protected $primaryKey = 'code';
protected $fillable = [
'code',
'prefix',
'suffix',
'format',
];
}

View File

@@ -0,0 +1,19 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class CustomProperty extends Model
{
use HasFactory;
public $timestamps = false;
public $guarded = [];
public $casts = [
'allowed_values' => 'array',
];
}

17
app/Models/DebugLog.php Normal file
View File

@@ -0,0 +1,17 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class DebugLog extends Model
{
protected $fillable = [
'context',
'type',
];
protected $casts = [
'context' => 'json',
];
}

22
app/Models/EmailLog.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class EmailLog extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'subject',
'body',
'to',
'sent_at',
'status',
'error',
'job_uuid',
];
}

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class EmailTemplate extends Model
{
use HasFactory;
protected $fillable = [
'key',
'name',
'subject',
'enabled',
'body',
'cc',
'bcc',
];
protected $casts = [
'enabled' => 'boolean',
'cc' => 'array',
'bcc' => 'array',
];
}

44
app/Models/Extension.php Normal file
View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Extension extends Model
{
use HasFactory;
protected $fillable = [
'name',
'enabled',
// Name of extension class (e.g. 'Stripe' or 'Paypal')
'extension',
'type',
];
protected $guarded = [];
/**
* Get the extension's settings.
*/
public function settings()
{
return $this->morphMany(Setting::class, 'settingable');
}
public function path(): Attribute
{
return Attribute::make(
get: fn () => ucfirst($this->type) . 's/' . ucfirst($this->extension)
);
}
public function namespace(): Attribute
{
return Attribute::make(
get: fn () => 'Paymenter\\Extensions\\' . ucfirst($this->type) . 's\\' . ucfirst($this->extension)
);
}
}

36
app/Models/FailedJob.php Normal file
View File

@@ -0,0 +1,36 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Artisan;
class FailedJob extends Model
{
use HasFactory;
protected $fillable = [
'uuid',
'connection',
'queue',
'payload',
'exception',
'failed_at',
];
protected $casts = [
'failed_at' => 'datetime',
];
public function retry()
{
try {
Artisan::call('queue:retry', ['id' => $this->uuid]);
} catch (\Exception $e) {
return $e->getMessage();
}
// $this->delete();
}
}

13
app/Models/Gateway.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
namespace App\Models;
class Gateway extends Extension
{
protected $table = 'extensions';
public function newQuery($excludeDeleted = true)
{
return parent::newQuery($excludeDeleted)->where('type', 'gateway');
}
}

102
app/Models/Invoice.php Normal file
View File

@@ -0,0 +1,102 @@
<?php
namespace App\Models;
use App\Classes\PDF;
use App\Classes\Price;
use App\Observers\InvoiceObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[ObservedBy([InvoiceObserver::class])]
class Invoice extends Model
{
use HasFactory;
public const STATUS_PENDING = 'pending';
public const STATUS_PAID = 'paid';
public const STATUS_CANCELLED = 'cancelled';
protected $fillable = ['number', 'user_id', 'currency_code', 'due_at', 'status'];
protected $casts = [
'due_at' => 'date',
];
public bool $send_create_email = true;
/**
* Total of the invoice.
*
* @return string
*/
public function total(): Attribute
{
return Attribute::make(
get: fn () => $this->items->sum(fn ($item) => $item->price * $item->quantity)
);
}
/**
* Total of the invoice.
*
* @return string
*/
public function formattedTotal(): Attribute
{
return Attribute::make(
get: fn () => new Price(['price' => $this->total, 'currency' => $this->currency])
);
}
/**
* Formatted remaining amount of the invoice.
*/
public function formattedRemaining(): Attribute
{
return Attribute::make(
get: fn () => new Price(['price' => $this->remaining, 'currency' => $this->currency])
);
}
/**
* Remaining amount of the invoice.
*/
public function remaining(): Attribute
{
return Attribute::make(
get: fn () => $this->total - $this->transactions->sum('amount')
);
}
public function currency()
{
return $this->belongsTo(Currency::class, 'currency_code');
}
public function user()
{
return $this->belongsTo(User::class);
}
public function items()
{
return $this->hasMany(InvoiceItem::class);
}
public function transactions()
{
return $this->hasMany(InvoiceTransaction::class);
}
public function pdf(): Attribute
{
return Attribute::make(
get: fn () => PDF::generateInvoice($this)
);
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace App\Models;
use App\Classes\Price;
use App\Observers\InvoiceItemObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[ObservedBy([InvoiceItemObserver::class])]
class InvoiceItem extends Model
{
use HasFactory;
protected $fillable = [
'invoice_id',
'service_id',
'quantity',
'price',
'description',
'gateway_id',
'reference_id',
'reference_type',
];
public function invoice()
{
return $this->belongsTo(Invoice::class);
}
public function reference()
{
return $this->morphTo();
}
public function gateway()
{
return $this->belongsTo(Gateway::class);
}
public function total()
{
return $this->price * $this->quantity;
}
public function formattedTotal(): Attribute
{
return Attribute::make(
get: fn () => new Price(['price' => $this->total(), 'currency' => $this->invoice->currency])
);
}
public function formattedPrice(): Attribute
{
return Attribute::make(
get: fn () => new Price(['price' => $this->price, 'currency' => $this->invoice->currency])
);
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace App\Models;
use App\Classes\Price;
use App\Observers\InvoiceTransactionObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[ObservedBy([InvoiceTransactionObserver::class])]
class InvoiceTransaction extends Model
{
use HasFactory;
protected $fillable = [
'invoice_id',
'gateway_id',
'amount',
'fee',
'transaction_id',
];
public function invoice()
{
return $this->belongsTo(Invoice::class);
}
public function gateway()
{
return $this->belongsTo(Gateway::class);
}
/**
* Formatted remaining amount of the invoice.
*/
public function formattedFee(): Attribute
{
return Attribute::make(
get: fn () => new Price(['price' => $this->fee, 'currency' => $this->invoice->currency])
);
}
/**
* Formatted remaining amount of the invoice.
*/
public function formattedAmount(): Attribute
{
return Attribute::make(
get: fn () => new Price(['price' => $this->amount, 'currency' => $this->invoice->currency])
);
}
}

View File

@@ -0,0 +1,7 @@
<?php
namespace App\Models;
use Laravel\Passport\Client as PassportClient;
class OauthClient extends PassportClient {}

75
app/Models/Order.php Normal file
View File

@@ -0,0 +1,75 @@
<?php
namespace App\Models;
use App\Classes\Price;
use App\Observers\OrderObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[ObservedBy([OrderObserver::class])]
class Order extends Model
{
use HasFactory;
protected $fillable = ['user_id', 'currency_code'];
public bool $send_create_email = true;
public function user()
{
return $this->belongsTo(User::class);
}
public function services()
{
return $this->hasMany(Service::class);
}
/**
* Get the currency corresponding to the service.
*/
public function currency()
{
return $this->hasOne(Currency::class, 'code', 'currency_code');
}
/**
* Total of the order.
*
* @return float
*/
public function total(): Attribute
{
return Attribute::make(
get: fn () => $this->services->sum(fn ($service) => $service->price * $service->quantity)
);
}
/**
* Total of the order.
*
* @return string
*/
public function formattedTotal(): Attribute
{
return Attribute::make(
get: fn () => new Price(['price' => $this->total, 'currency' => $this->currency])
);
}
/**
* Get all invoices for the order.
*/
public function invoices(): Attribute
{
// Each service has invoices (it is a hasManyThrough relationship order -> service -> invoiceItem -> invoice)
$invoicesId = $this->services->map(fn ($service) => $service->invoiceItems->map(fn ($invoiceItem) => $invoiceItem->invoice_id))->flatten();
return new Attribute(
get: fn () => Invoice::whereIn('id', $invoicesId)
);
}
}

79
app/Models/Plan.php Normal file
View File

@@ -0,0 +1,79 @@
<?php
namespace App\Models;
use App\Classes\Price as PriceClass;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Plan extends Model
{
use HasFactory;
public $timestamps = false;
protected $fillable = [
'name',
'type',
'billing_period',
'billing_unit',
];
/**
* Get the available prices of the plan.
*/
public function prices()
{
return $this->hasMany(Price::class);
}
/**
* Get the priceable model of the plan.
*/
public function priceable()
{
return $this->morphTo();
}
/**
* Get the price of the plan.
*/
public function price()
{
if ($this->type === 'free') {
return new PriceClass(['currency' => Currency::find(session('currency', config('settings.default_currency')))], free: true);
}
$currency = session('currency', config('settings.default_currency'));
$price = $this->prices->where('currency_code', $currency)->first();
return new PriceClass((object) [
'price' => $price,
'setup_fee' => $price->setup_fee,
'currency' => $price->currency,
]);
}
// Time between billing periods
public function billingDuration(): Attribute
{
if ($this->type === 'free' || $this->type == 'one-time') {
return Attribute::make(get: fn () => 0);
}
$diffInDays = match ($this->billing_unit) {
'day' => 1,
'week' => 7,
'month' => 30,
'year' => 365,
};
return Attribute::make(
get: fn () => $diffInDays * $this->billing_period
);
}
public function services()
{
return $this->hasMany(Service::class);
}
}

32
app/Models/Price.php Normal file
View File

@@ -0,0 +1,32 @@
<?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\MorphTo;
class Price extends Model
{
use HasFactory;
public $timestamps = false;
protected $guarded = [];
public function priceable(): MorphTo
{
return $this->morphTo();
}
public function currency(): BelongsTo
{
return $this->belongsTo(Currency::class);
}
public function plan(): BelongsTo
{
return $this->belongsTo(Plan::class);
}
}

81
app/Models/Product.php Normal file
View File

@@ -0,0 +1,81 @@
<?php
namespace App\Models;
use App\Models\Traits\HasPlans;
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\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\MorphMany;
class Product extends Model
{
use HasFactory, HasPlans;
protected $guarded = [];
protected $auditInclude = [
'name',
'description',
'category_id',
'enabled',
];
/**
* Get the category of the product.
*/
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
/**
* Get the configurable options of the product.
*/
public function configOptions(): HasManyThrough
{
return $this->hasManyThrough(ConfigOption::class, ConfigOptionProduct::class, 'product_id', 'id', 'id', 'config_option_id')->where('hidden', false)->orderBy('sort', 'asc')->orderBy('id', 'desc');
}
/**
* Get the extension of the product.
*/
public function server()
{
return $this->belongsTo(Server::class);
}
/**
* Get all services using this product.
*/
public function services(): HasMany
{
return $this->hasMany(Service::class);
}
/**
* Get the settings of the product.
*/
public function settings(): MorphMany
{
return $this->morphMany(Setting::class, 'settingable');
}
/**
* Get all available products upgrades
*/
public function upgrades()
{
return $this->belongsToMany(Product::class, 'product_upgrades', 'product_id', 'upgrade_id');
}
/**
* Gets all upgradable config options for the product.
*/
public function upgradableConfigOptions(): HasManyThrough
{
return $this->hasManyThrough(ConfigOption::class, ConfigOptionProduct::class, 'product_id', 'id', 'id', 'config_option_id')->where('hidden', false)->where('upgradable', true)->orderBy('sort', 'asc')->orderBy('id', 'desc');
}
}

View File

@@ -0,0 +1,23 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ProductUpgrade extends Model
{
use HasFactory;
protected $guarded = [];
public function product()
{
return $this->belongsTo(Product::class);
}
public function upgrade()
{
return $this->belongsTo(Product::class, 'upgrade_id');
}
}

26
app/Models/Property.php Normal file
View File

@@ -0,0 +1,26 @@
<?php
namespace App\Models;
use App\Observers\PropertyObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[ObservedBy([PropertyObserver::class])]
class Property extends Model
{
use HasFactory;
public $guarded = [];
public function parent_property()
{
return $this->belongsTo(CustomProperty::class, 'custom_property_id');
}
public function model()
{
return $this->morphTo();
}
}

35
app/Models/Role.php Normal file
View File

@@ -0,0 +1,35 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Role extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'permissions',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'permissions' => 'array',
];
public function users()
{
return $this->belongsToMany(User::class);
}
}

13
app/Models/Server.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
namespace App\Models;
class Server extends Extension
{
protected $table = 'extensions';
public function newQuery($excludeDeleted = true)
{
return parent::newQuery($excludeDeleted)->where('type', 'server');
}
}

220
app/Models/Service.php Normal file
View File

@@ -0,0 +1,220 @@
<?php
namespace App\Models;
use App\Classes\Price;
use App\Models\Traits\HasProperties;
use App\Observers\ServiceObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Paymenter\Extensions\Servers\Proxmox\Models\Server as ProxmoxServer;
#[ObservedBy([ServiceObserver::class])]
class Service extends Model
{
use HasFactory, HasProperties;
public const STATUS_PENDING = 'pending';
public const STATUS_ACTIVE = 'active';
public const STATUS_CANCELLED = 'cancelled';
public const STATUS_SUSPENDED = 'suspended';
protected $fillable = [
'order_id',
'product_id',
'plan_id',
'quantity',
'price',
'expires_at',
'subscription_id',
'status',
'coupon_id',
'user_id',
'currency_code',
];
protected $casts = [
'expires_at' => 'date',
];
/**
* Get the order that owns the service.
*/
public function order()
{
return $this->belongsTo(Order::class);
}
/**
* Get the coupon that owns the service.
*/
public function coupon()
{
return $this->belongsTo(Coupon::class);
}
/**
* Get the currency corresponding to the service.
*/
public function currency()
{
return $this->hasOne(Currency::class, 'code', 'currency_code');
}
/**
* Get the user that owns the service.
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Price of the service.
*
* @return string
*/
public function formattedPrice(): Attribute
{
return Attribute::make(
get: fn () => new Price(['price' => $this->price * $this->quantity, 'currency' => $this->currency])
);
}
/**
* Get the description for the next invoice item.
*/
public function description(): Attribute
{
if ($this->plan->type == 'free' || $this->plan->type == 'one-time') {
return Attribute::make(
get: fn () => $this->product->name
);
}
$date = $this->expires_at ?? now();
$endDate = $date->copy()->{'add' . ucfirst($this->plan->billing_unit) . 's'}($this->plan->billing_period);
return Attribute::make(
get: fn () => $this->product->name . ' (' . $date->format('M d, Y') . ' - ' . $endDate->format('M d, Y') . ')'
);
}
/**
* Calculate next due date.
*/
public function calculateNextDueDate()
{
if ($this->plan->type == 'one-time' || $this->plan->type == 'free') {
return null;
}
$date = $this->expires_at ?? now();
return $date->{'add' . ucfirst($this->plan->billing_unit) . 's'}($this->plan->billing_period);
}
/**
* Get the product corresponding to the service.
*/
public function product()
{
return $this->belongsTo(Product::class);
}
/**
* Get the plan corresponding to the service.
*/
public function plan()
{
return $this->belongsTo(Plan::class);
}
public function proxmoxServer(): HasOne
{
return $this->hasOne(ProxmoxServer::class);
}
/**
* Get the service's configurations.
*/
public function configs()
{
return $this->morphMany(ServiceConfig::class, 'configurable');
}
/**
* Get invoiceItems
*/
public function invoiceItems()
{
return $this->morphMany(InvoiceItem::class, 'reference');
}
/**
* Get invoices
*/
public function invoices()
{
return $this->hasManyThrough(Invoice::class, InvoiceItem::class, 'reference_id', 'id', 'id', 'invoice_id')->where('reference_type', Service::class);
}
/**
* Get cancellation requests
*/
public function cancellation()
{
return $this->hasOne(ServiceCancellation::class);
}
public function cancellable(): Attribute
{
return Attribute::make(
get: fn () => $this->status !== 'cancelled' && $this->plan->type != 'free' && $this->plan->type != 'one-time' && !$this->cancellation?->exists()
);
}
public function upgradable(): Attribute
{
return Attribute::make(
get: fn () => ($this->productUpgrades()->count() > 0 || $this->product->upgradableConfigOptions()->count() > 0) && $this->status == 'active' && $this->upgrade->where('status', ServiceUpgrade::STATUS_PENDING)->count() == 0);
}
public function productUpgrades()
{
return $this->product->upgrades->filter(function ($product) {
$plan = $product->plans()->where('billing_unit', $this->plan->billing_unit)->where('billing_period', $this->plan->billing_period)->get();
// Only get the upgrades that have the exact same billing cycle as the service
if ($plan->count() > 0) {
$product->plan = $plan->first();
return $product;
}
return null;
});
}
public function recalculatePrice()
{
// Calculate the price based on the plan and quantity and config options
$price = $this->plan->price()->price * $this->quantity;
$this->configs->each(function ($config) use (&$price) {
$configValue = $config->configValue;
if ($configValue) {
$price += $configValue->price(null, $this->plan->billing_period, $this->plan->billing_unit, $this->currency_code)->price;
}
});
$this->price = $price;
$this->save();
}
public function upgrade()
{
return $this->hasMany(ServiceUpgrade::class);
}
}

View File

@@ -0,0 +1,25 @@
<?php
namespace App\Models;
use App\Observers\ServiceCancellationObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[ObservedBy([ServiceCancellationObserver::class])]
class ServiceCancellation extends Model
{
use HasFactory;
protected $fillable = [
'service_id',
'reason',
'type',
];
public function service()
{
return $this->belongsTo(Service::class);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ServiceConfig extends Model
{
use HasFactory;
protected $fillable = [
'service_id',
'config_option_id',
'config_value_id',
];
/**
* Get the service that owns the service config.
*/
public function service()
{
return $this->belongsTo(Service::class);
}
/**
* Get the config option that owns the service config.
*/
public function configOption()
{
return $this->belongsTo(ConfigOption::class);
}
/**
* Get the config value that owns the service config.
*/
public function configValue()
{
return $this->belongsTo(ConfigOption::class, 'config_value_id');
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace App\Models;
use App\Classes\Price;
use App\Observers\ServiceUpgradeObserver;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[ObservedBy([ServiceUpgradeObserver::class])]
class ServiceUpgrade extends Model
{
use HasFactory;
public const STATUS_PENDING = 'pending';
public const STATUS_COMPLETED = 'completed';
public $guarded = [];
public function service()
{
return $this->belongsTo(Service::class);
}
public function product()
{
return $this->belongsTo(Product::class);
}
public function plan()
{
return $this->belongsTo(Plan::class);
}
public function invoice()
{
return $this->belongsTo(Invoice::class);
}
public function configs()
{
return $this->morphMany(ServiceConfig::class, 'configurable');
}
public function calculateProratedAmount($oldItem, $newItem): Price
{
// Calculate the total number of days in the billing period
$billingPeriodDays = match ($this->service->plan->billing_unit) {
'day' => $this->service->plan->billing_period,
'week' => $this->service->plan->billing_period * 7,
'month' => $this->service->plan->billing_period * 30,
'year' => $this->service->plan->billing_period * 365,
default => 0,
};
// Calculate the remaining days until the service expires
$expiresAt = $this->service->expires_at->copy()->startOfDay();
$now = Carbon::now()->startOfDay();
$remainingDays = $expiresAt->diffInDays($now, true);
$remainingDays = min($remainingDays, $billingPeriodDays);
// Calculate the prorated amount
$newPrice = $newItem->price(null, $this->service->plan->billing_period, $this->service->plan->billing_unit, $this->service->currency_code);
if (empty($oldItem)) {
$oldPrice = 0;
} else {
$oldPrice = $oldItem->price(null, $this->service->plan->billing_period, $this->service->plan->billing_unit, $this->service->currency_code)->price;
}
$priceDifference = $newPrice->price - $oldPrice;
$total = ($priceDifference / $billingPeriodDays) * $remainingDays;
return new Price([
'price' => $total,
'currency' => $this->service->currency,
]);
}
public function calculatePrice(): Price
{
$total = $this->calculateProratedAmount(
$this->service->product,
$this->product
)->price;
foreach ($this->configs as $config) {
$configValue = $config->configValue;
if (!$configValue) {
continue;
}
$oldPrice = $this->service->configs->where('config_option_id', $config->config_option_id)->first();
$ctotal = $this->calculateProratedAmount(
$oldPrice ? $oldPrice->configValue : null,
$configValue
);
$total += $ctotal->price;
}
return new Price([
'price' => $total,
'currency' => $currency ?? $this->service->currency,
]);
}
}

109
app/Models/Session.php Normal file
View File

@@ -0,0 +1,109 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\Session as FacadesSession;
class Session extends Model
{
use HasFactory;
public $incrementing = false;
public $timestamps = false;
protected $fillable = [
'user_agent',
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected function casts(): array
{
return [
'last_activity' => 'datetime',
];
}
public function impersonating(): bool
{
try {
$payload = $this->payload;
$decoded = config('session.encrypt')
? Crypt::decryptString($payload)
: base64_decode($payload);
$data = unserialize($decoded);
return !empty($data['impersonating']);
} catch (\Throwable $e) {
return false;
}
}
public function user()
{
return $this->belongsTo(User::class);
}
public function getIsCurrentDeviceAttribute()
{
return $this->id === FacadesSession::getId();
}
public function getFormattedLastActiveAttribute()
{
return $this->last_activity->diffForHumans();
}
public function getIsMobileAttribute()
{
return preg_match('/(android|webos|avantgo|blackberry|bolt|boost|cricket|docomo|fone|hiptop|mini|mobi|palm|phone|pie|tablet|up\.browser|up\.link|webos|wos)/i', $this->user_agent);
}
public function getFormattedDeviceAttribute()
{
if (preg_match('/Linux/i', $this->user_agent)) {
$os = 'Linux';
} elseif (preg_match('/Mac/i', $this->user_agent)) {
$os = 'Mac';
} elseif (preg_match('/iPhone/i', $this->user_agent)) {
$os = 'iPhone';
} elseif (preg_match('/iPad/i', $this->user_agent)) {
$os = 'iPad';
} elseif (preg_match('/Droid/i', $this->user_agent)) {
$os = 'Droid';
} elseif (preg_match('/Unix/i', $this->user_agent)) {
$os = 'Unix';
} elseif (preg_match('/Windows/i', $this->user_agent)) {
$os = 'Windows';
} else {
$os = 'Unknown';
}
// Browser Detection
if (preg_match('/Firefox/i', $this->user_agent)) {
$br = 'Firefox';
} elseif (preg_match('/Mac/i', $this->user_agent)) {
$br = 'Mac';
} elseif (preg_match('/Chrome/i', $this->user_agent)) {
$br = 'Chrome';
} elseif (preg_match('/Opera/i', $this->user_agent)) {
$br = 'Opera';
} elseif (preg_match('/MSIE/i', $this->user_agent)) {
$br = 'IE';
} else {
$br = 'Unknown';
}
return "{$os} - {$br}";
}
}

32
app/Models/Setting.php Normal file
View File

@@ -0,0 +1,32 @@
<?php
namespace App\Models;
use App\Events\Setting\Retrieved;
use App\Events\Setting\Saved;
use App\Events\Setting\Saving;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Setting extends Model
{
use HasFactory;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'key',
'value',
'type',
'encrypted',
];
protected $dispatchesEvents = [
'retrieved' => Retrieved::class,
'saving' => Saving::class,
'saved' => Saved::class,
];
}

13
app/Models/TaxRate.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TaxRate extends Model
{
use HasFactory;
protected $fillable = ['name', 'rate', 'country'];
}

45
app/Models/Ticket.php Normal file
View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use App\Observers\TicketObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[ObservedBy([TicketObserver::class])]
class Ticket extends Model
{
use HasFactory;
protected $fillable = [
'subject',
'status',
'priority',
'department',
'user_id',
'assigned_to',
'service_id',
];
public function user()
{
return $this->belongsTo(User::class);
}
public function assignedTo()
{
return $this->belongsTo(User::class, 'assigned_to');
}
public function service()
{
return $this->belongsTo(Service::class);
}
public function messages()
{
return $this->hasMany(TicketMessage::class);
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Models;
use App\Observers\TicketMessageObserver;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
#[ObservedBy([TicketMessageObserver::class])]
class TicketMessage extends Model
{
use HasFactory;
protected $fillable = [
'ticket_id',
'user_id',
'message',
];
public function ticket()
{
return $this->belongsTo(Ticket::class);
}
public function user()
{
return $this->belongsTo(User::class);
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace App\Models\Traits;
use App\Classes\Price;
use App\Models\Plan;
trait HasPlans
{
/**
* Get the plans (prices) of the product.
*/
public function plans()
{
return $this->morphMany(Plan::class, 'priceable')->orderBy('sort');
}
/**
* Get available plans of the product.
*/
public function availablePlans($currency = null)
{
$currency = $currency ?? session('currency', config('settings.default_currency'));
return $this->plans->filter(function ($plan) use ($currency) {
if ($plan->type === 'free') {
return true;
}
return $plan->prices->when($currency, function ($query) use ($currency) {
// Or where plan is free
return $query->where('currency_code', $currency);
})->isNotEmpty();
});
}
/**
* Get first price of the plan.
*/
public function price($plan_id = null, $billing_period = null, $billing_unit = null, $currency = null)
{
$priceAndCurrency = [
'price' => null,
'currency' => null,
];
// Check for free plan
if ($this->availablePlans()->where('type', 'free')->isNotEmpty()) {
return new Price(free: true, dontShowUnavailablePrice: $this->dontShowUnavailablePrice ?? false);
}
// If plan_id is not provided, and billing_period is provided, get the first plan with the billing period and time interval
if (!$plan_id && $billing_period && $billing_unit) {
$plan = $this->availablePlans()->where('billing_period', $billing_period)->where('billing_unit', $billing_unit)->first();
$plan_id = $plan->id ?? null;
}
$currency = $currency ?? session('currency', config('settings.default_currency'));
foreach ($this->availablePlans()->when($plan_id, function ($query) use ($plan_id) {
return $query->where('id', $plan_id);
}) as $plan) {
foreach ($plan->prices->when($currency, function ($query) use ($currency) {
return $query->where('currency_code', $currency);
}) as $price) {
if ($price->price < $priceAndCurrency['price'] || $priceAndCurrency['price'] === null) {
$priceAndCurrency['price'] = $price;
$priceAndCurrency['currency'] = $price->currency;
}
}
if ($priceAndCurrency['price']) {
break;
}
}
return new Price($priceAndCurrency, dontShowUnavailablePrice: $this->dontShowUnavailablePrice ?? false);
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace App\Models\Traits;
use App\Models\Property;
trait HasProperties
{
/**
* Get the properties of the model which has this trait
*/
public function properties()
{
return $this->morphMany(Property::class, 'model');
}
}

View File

@@ -0,0 +1,8 @@
<?php
namespace App\Models\Traits;
trait Settingable
{
//
}

195
app/Models/User.php Normal file
View File

@@ -0,0 +1,195 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Models\Traits\HasProperties;
use App\Observers\UserObserver;
use Dedoc\Scramble\Attributes\SchemaName;
use Filament\Models\Contracts\FilamentUser;
use Filament\Models\Contracts\HasAvatar;
use Filament\Panel;
use Illuminate\Database\Eloquent\Attributes\ObservedBy;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Passport\HasApiTokens;
#[SchemaName('UserModel')]
#[ObservedBy([UserObserver::class])]
class User extends Authenticatable implements FilamentUser, HasAvatar
{
use HasApiTokens, HasFactory, HasProperties, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'first_name',
'last_name',
'email',
'password',
'role_id',
'tfa_secret',
'email_verified_at',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
'tfa_secret',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
'tfa_secret' => 'encrypted',
];
}
/**
* Initials of the user.
*
* @return string
*/
public function initials(): Attribute
{
return Attribute::make(
get: fn () => strtoupper(substr($this->first_name, 0, 1) . substr($this->last_name, 0, 1)),
);
}
/**
* Avatar URL for the user.
*
* @return string
*/
public function avatar(): Attribute
{
return Attribute::make(
get: fn () => 'https://www.gravatar.com/avatar/' . md5(strtolower($this->email)) . '?d=' . urlencode((string) config('settings.gravatar_default')),
);
}
public function getFilamentAvatarUrl(): ?string
{
return $this->avatar;
}
/**
* Get the display name for the user.
*
* @return string
*/
public function name(): Attribute
{
return Attribute::make(
get: fn () => ($this->first_name . ' ' . $this->last_name) ?: $this->email,
);
}
public function hasPermission($permission): bool
{
if (is_null($this->role)) {
return false;
}
// If the user has all permissions, return true
if (in_array('*', $this->role->permissions)) {
return true;
}
return in_array($permission, $this->role->permissions);
}
/** Relationships */
/**
* Get the role that the user belongs to.
* Can be null if the user is a normal user (non-admin).
*/
public function role()
{
return $this->belongsTo(Role::class);
}
/**
* Get the user's sessions.
*/
public function sessions()
{
return $this->hasMany(Session::class);
}
/**
* Get the user's orders.
*/
public function orders()
{
return $this->hasMany(Order::class);
}
/**
* Get the user's services
*/
public function services()
{
return $this->hasMany(Service::class);
}
/**
* Get the user's audit logs.
*/
public function audits()
{
return $this->morphMany(AuditLog::class, 'model');
}
/**
* Get the user's invoices.
*/
public function invoices()
{
return $this->hasMany(Invoice::class);
}
public function canAccessPanel(Panel $panel): bool
{
if ($panel->getId() === 'admin') {
return !is_null($this->role);
}
return false;
}
/**
* Get the user tickets
*/
public function tickets()
{
return $this->hasMany(Ticket::class);
}
/**
* Get the user's credits
*/
public function credits()
{
return $this->hasMany(Credit::class);
}
}