12-05-2026 Frontend dev
Some checks are pending
linter / quality (push) Waiting to run
tests / ci (push) Waiting to run

This commit is contained in:
Kevin Adametz 2026-05-12 18:32:33 +02:00
parent 405df0a122
commit 5b8bdf4182
779 changed files with 480564 additions and 6241 deletions

View file

@ -0,0 +1,41 @@
<?php
namespace App\Models;
use Database\Factories\AdminPresetFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class AdminPreset extends Model
{
/** @use HasFactory<AdminPresetFactory> */
use HasFactory;
public const PRESS_RELEASE_DELETED_PUBLISHED_TEXT = 'press_releases.deleted_published_text';
protected $fillable = [
'key',
'area',
'type',
'label',
'value',
'payload',
'is_active',
];
protected function casts(): array
{
return [
'payload' => 'array',
'is_active' => 'boolean',
];
}
public static function activeValue(string $key, ?string $fallback = null): ?string
{
return static::query()
->where('key', $key)
->where('is_active', true)
->value('value') ?? $fallback;
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Laravel\Sanctum\PersonalAccessToken;
class ApiUsageLog extends Model
{
public $timestamps = false;
protected $fillable = [
'user_id',
'personal_access_token_id',
'method',
'path',
'route_name',
'status_code',
'ip_address',
'user_agent',
'duration_ms',
'requested_at',
];
protected function casts(): array
{
return [
'requested_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function personalAccessToken(): BelongsTo
{
return $this->belongsTo(PersonalAccessToken::class);
}
}

View file

@ -0,0 +1,29 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class BillingAddress extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'salutation_key',
'title',
'name',
'address1',
'address2',
'postal_code',
'city',
'country_code',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

58
app/Models/Category.php Normal file
View file

@ -0,0 +1,58 @@
<?php
namespace App\Models;
use App\Enums\Portal;
use Database\Factories\CategoryFactory;
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;
class Category extends Model
{
/** @use HasFactory<CategoryFactory> */
use HasFactory;
protected $fillable = [
'parent_id',
'portal',
'is_active',
'legacy_portal',
'legacy_id',
];
protected function casts(): array
{
return [
'portal' => Portal::class,
'is_active' => '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 translations(): HasMany
{
return $this->hasMany(CategoryTranslation::class);
}
public function pressReleases(): HasMany
{
return $this->hasMany(PressRelease::class);
}
public function footerCodes(): BelongsToMany
{
return $this->belongsToMany(FooterCode::class, 'category_footer_code');
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Support\Str;
class CategoryTranslation extends Model
{
protected $fillable = [
'category_id',
'locale',
'name',
'slug',
'description',
];
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
/**
* Generates a slug that is unique within a single locale.
* If `$ignoreCategoryId` is given (when editing), a clash with this very
* category's translation is allowed, so renaming back to the same slug
* works.
*/
public static function uniqueSlug(string $source, string $locale, ?int $ignoreCategoryId = null): string
{
$base = Str::slug($source) ?: 'kategorie';
$slug = $base;
$i = 2;
while (
self::query()
->where('locale', $locale)
->where('slug', $slug)
->when(
$ignoreCategoryId !== null,
fn ($q) => $q->where('category_id', '!=', $ignoreCategoryId),
)
->exists()
) {
$slug = $base.'-'.$i++;
}
return $slug;
}
}

137
app/Models/Company.php Normal file
View file

@ -0,0 +1,137 @@
<?php
namespace App\Models;
use App\Enums\CompanyType;
use App\Enums\Portal;
use App\Models\Concerns\HasUniqueSlug;
use App\Scopes\PortalScope;
use Database\Factories\CompanyFactory;
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\SoftDeletes;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
class Company extends Model
{
/** @use HasFactory<CompanyFactory> */
use HasFactory, HasUniqueSlug, SoftDeletes;
/**
* @return list<string>
*/
protected function slugScopeAttributes(): array
{
return ['portal'];
}
protected function slugFallback(): string
{
return 'company';
}
protected static function booted(): void
{
static::addGlobalScope(new PortalScope);
}
protected $fillable = [
'portal',
'owner_user_id',
'type',
'name',
'slug',
'address',
'country_code',
'phone',
'fax',
'email',
'website',
'logo_path',
'logo_variants',
'is_active',
'disable_footer_code',
'legacy_portal',
'legacy_id',
];
protected function casts(): array
{
return [
'portal' => Portal::class,
'type' => CompanyType::class,
'logo_variants' => 'array',
'is_active' => 'boolean',
'disable_footer_code' => 'boolean',
'deleted_at' => 'datetime',
];
}
public function owner(): BelongsTo
{
return $this->belongsTo(User::class, 'owner_user_id');
}
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)
->withPivot('role')
->withTimestamps();
}
public function contacts(): HasMany
{
return $this->hasMany(Contact::class);
}
public function pressReleases(): HasMany
{
return $this->hasMany(PressRelease::class);
}
public function userPaymentOptions(): BelongsToMany
{
return $this->belongsToMany(UserPaymentOption::class, 'user_payment_option_company')
->withPivot('is_active')
->withTimestamps();
}
public function logoUrl(): ?string
{
if (blank($this->logo_path)) {
return null;
}
$logoPath = trim((string) $this->logo_path);
$legacyPortal = $this->legacy_portal ?: $this->portal?->value;
$logoFilename = basename((string) (parse_url($logoPath, PHP_URL_PATH) ?: $logoPath));
$candidates = [];
if (Str::startsWith($logoPath, '/storage/')) {
$candidates[] = Str::after($logoPath, '/storage/');
} elseif (! Str::startsWith($logoPath, ['http://', 'https://'])) {
$candidates[] = ltrim($logoPath, '/');
}
if ($legacyPortal && filled($logoFilename)) {
$candidates[] = "company-logos/{$legacyPortal}/{$this->id}/{$logoFilename}";
$candidates[] = "{$legacyPortal}/company/{$logoFilename}";
}
foreach (array_unique($candidates) as $candidate) {
if (Storage::disk('public')->exists($candidate)) {
return asset('storage/'.$candidate);
}
}
if (Str::startsWith($logoPath, ['http://', 'https://']) && blank($this->legacy_portal)) {
return $logoPath;
}
return null;
}
}

View file

@ -0,0 +1,109 @@
<?php
namespace App\Models\Concerns;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
/**
* Generates a unique slug for an Eloquent model based on a source attribute.
*
* Models opt-in by overriding two methods:
* - {@see slugScopeAttributes()} returns the attribute names whose current
* model values (or provided overrides) should restrict slug uniqueness.
* Defaults to an empty array (global uniqueness on the slug column).
* - {@see slugFallback()} returns the fallback when the source attribute is
* blank (defaults to "item").
*
* The model is expected to expose `slug_column` (defaults to `slug`) and
* the source attribute name to slugify (defaults to `title` or `name`).
*
* Typical usage:
*
* $pr->slug = $pr->generateUniqueSlug($pr->title, [
* 'portal' => $pr->portal,
* 'language' => $pr->language,
* ]);
*
* @mixin Model
*/
trait HasUniqueSlug
{
/**
* Build a unique slug from `$source`. Optionally pass overrides for the
* scoping attributes (matched against {@see slugScopeAttributes()}).
*
* @param array<string, mixed> $scope
*/
public function generateUniqueSlug(string $source, array $scope = []): string
{
$base = Str::slug($source) ?: $this->slugFallback();
$slug = $base;
$suffix = 2;
while ($this->slugExists($slug, $scope)) {
$slug = $base.'-'.$suffix++;
}
return $slug;
}
/**
* @param array<string, mixed> $scope
*/
protected function slugExists(string $slug, array $scope): bool
{
/** @var Model $self */
$self = $this;
$query = $self::query()
->withoutGlobalScopes()
->where($this->slugColumn(), $slug);
if ($self->exists) {
$query->where($self->getKeyName(), '!=', $self->getKey());
}
foreach ($this->slugScopeAttributes() as $attribute) {
$value = array_key_exists($attribute, $scope)
? $scope[$attribute]
: $self->getAttribute($attribute);
$value !== null
? $query->where($attribute, $value)
: $query->whereNull($attribute);
}
$this->applySlugConstraints($query);
return $query->exists();
}
/**
* @return list<string>
*/
protected function slugScopeAttributes(): array
{
return [];
}
protected function slugColumn(): string
{
return 'slug';
}
protected function slugFallback(): string
{
return 'item';
}
/**
* Hook for models to add additional WHERE constraints (e.g. soft-deleted
* records). Default: no additional constraints.
*/
protected function applySlugConstraints(Builder $query): void
{
// no-op
}
}

62
app/Models/Contact.php Normal file
View file

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use App\Enums\Portal;
use App\Scopes\PortalScope;
use Database\Factories\ContactFactory;
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\SoftDeletes;
class Contact extends Model
{
/** @use HasFactory<ContactFactory> */
use HasFactory, SoftDeletes;
protected static function booted(): void
{
static::addGlobalScope(new PortalScope);
}
protected $fillable = [
'company_id',
'portal',
'salutation_key',
'title',
'first_name',
'last_name',
'responsibility',
'phone',
'fax',
'email',
'legacy_portal',
'legacy_id',
];
protected function casts(): array
{
return [
'portal' => Portal::class,
'deleted_at' => 'datetime',
];
}
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public function pressReleases(): BelongsToMany
{
return $this->belongsToMany(PressRelease::class, 'press_release_contact');
}
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class)
->withTimestamps();
}
}

43
app/Models/FooterCode.php Normal file
View file

@ -0,0 +1,43 @@
<?php
namespace App\Models;
use App\Enums\Portal;
use Database\Factories\FooterCodeFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class FooterCode extends Model
{
/** @use HasFactory<FooterCodeFactory> */
use HasFactory, SoftDeletes;
protected $fillable = [
'portal',
'language',
'title',
'content',
'is_global',
'is_active',
'priority',
'legacy_portal',
'legacy_id',
];
protected function casts(): array
{
return [
'portal' => Portal::class,
'is_global' => 'boolean',
'is_active' => 'boolean',
'priority' => 'integer',
];
}
public function categories(): BelongsToMany
{
return $this->belongsToMany(Category::class, 'category_footer_code');
}
}

62
app/Models/Invoice.php Normal file
View file

@ -0,0 +1,62 @@
<?php
namespace App\Models;
use App\Enums\InvoiceStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
class Invoice extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'user_id',
'user_payment_id',
'invoice_billing_address_id',
'number',
'status',
'amount_cents',
'tax_cents',
'total_cents',
'currency',
'is_netto',
'invoice_date',
'due_date',
'paid_at',
'stripe_invoice_id',
'pdf_path',
];
protected function casts(): array
{
return [
'status' => InvoiceStatus::class,
'amount_cents' => 'integer',
'tax_cents' => 'integer',
'total_cents' => 'integer',
'is_netto' => 'boolean',
'invoice_date' => 'date',
'due_date' => 'date',
'paid_at' => 'datetime',
'deleted_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function userPayment(): BelongsTo
{
return $this->belongsTo(UserPayment::class, 'user_payment_id');
}
public function invoiceBillingAddress(): BelongsTo
{
return $this->belongsTo(InvoiceBillingAddress::class);
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
class InvoiceBillingAddress extends Model
{
use HasFactory;
protected $fillable = [
'salutation_key',
'title',
'name',
'address1',
'address2',
'postal_code',
'city',
'country_code',
];
public function invoices(): HasMany
{
return $this->hasMany(Invoice::class);
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use App\Enums\Portal;
use Illuminate\Database\Eloquent\Model;
class LegacyImportMap extends Model
{
public $timestamps = false;
protected $table = 'legacy_import_map';
protected $fillable = [
'legacy_portal',
'legacy_table',
'legacy_id',
'target_table',
'target_id',
'imported_at',
];
protected function casts(): array
{
return [
'legacy_portal' => Portal::class,
'legacy_id' => 'integer',
'target_id' => 'integer',
'imported_at' => 'datetime',
];
}
}

View file

@ -0,0 +1,55 @@
<?php
namespace App\Models;
use App\Enums\Portal;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class LegacyInvoice extends Model
{
public $timestamps = false;
protected $fillable = [
'legacy_portal',
'legacy_id',
'user_id',
'legacy_user_id',
'number',
'amount_cents',
'tax_cents',
'total_cents',
'status',
'invoice_date',
'due_date',
'paid_at',
'payment_method',
'pdf_path',
'raw_snapshot',
'pdf_payload',
'pdf_generated_at',
'imported_at',
];
protected function casts(): array
{
return [
'legacy_portal' => Portal::class,
'amount_cents' => 'integer',
'tax_cents' => 'integer',
'total_cents' => 'integer',
'invoice_date' => 'date',
'due_date' => 'date',
'paid_at' => 'datetime',
'raw_snapshot' => 'array',
'pdf_payload' => 'array',
'pdf_generated_at' => 'datetime',
'imported_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

34
app/Models/MagicLink.php Normal file
View file

@ -0,0 +1,34 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MagicLink extends Model
{
protected $fillable = [
'user_id',
'token_hash',
'purpose',
'payload',
'expires_at',
'consumed_at',
'ip_requested',
'ip_consumed',
];
protected function casts(): array
{
return [
'payload' => 'array',
'expires_at' => 'datetime',
'consumed_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View file

@ -0,0 +1,47 @@
<?php
namespace App\Models;
use App\Enums\Portal;
use App\Scopes\PortalScope;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class NewsletterSubscription extends Model
{
protected static function booted(): void
{
static::addGlobalScope(new PortalScope);
}
protected $fillable = [
'portal',
'user_id',
'salutation_key',
'first_name',
'last_name',
'email',
'ip_address',
'is_confirmed',
'confirmation_token',
'subscribed_at',
'unsubscribed_at',
'legacy_portal',
'legacy_id',
];
protected function casts(): array
{
return [
'portal' => Portal::class,
'is_confirmed' => 'boolean',
'subscribed_at' => 'datetime',
'unsubscribed_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace App\Models;
use App\Enums\PaymentOptionType;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
class PaymentOption extends Model
{
use HasFactory, SoftDeletes;
protected $fillable = [
'article_number',
'type',
'price_cents',
'currency',
'interval',
'is_hidden',
'stripe_product_id',
'stripe_price_id',
];
protected function casts(): array
{
return [
'type' => PaymentOptionType::class,
'price_cents' => 'integer',
'is_hidden' => 'boolean',
'deleted_at' => 'datetime',
];
}
public function translations(): HasMany
{
return $this->hasMany(PaymentOptionTranslation::class);
}
public function userPaymentOptions(): HasMany
{
return $this->hasMany(UserPaymentOption::class);
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PaymentOptionTranslation extends Model
{
use HasFactory;
protected $fillable = [
'payment_option_id',
'locale',
'name',
'description',
];
public function paymentOption(): BelongsTo
{
return $this->belongsTo(PaymentOption::class);
}
}

105
app/Models/PressRelease.php Normal file
View file

@ -0,0 +1,105 @@
<?php
namespace App\Models;
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Models\Concerns\HasUniqueSlug;
use App\Scopes\PortalScope;
use Database\Factories\PressReleaseFactory;
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\SoftDeletes;
class PressRelease extends Model
{
/** @use HasFactory<PressReleaseFactory> */
use HasFactory, HasUniqueSlug, SoftDeletes;
/**
* @return list<string>
*/
protected function slugScopeAttributes(): array
{
return ['portal', 'language'];
}
protected function slugFallback(): string
{
return 'pressemitteilung';
}
protected static function booted(): void
{
static::addGlobalScope(new PortalScope);
}
protected $fillable = [
'uuid',
'portal',
'user_id',
'company_id',
'category_id',
'language',
'title',
'slug',
'text',
'backlink_url',
'keywords',
'status',
'hits',
'teaser_begin',
'teaser_end',
'no_export',
'published_at',
'legacy_portal',
'legacy_id',
];
protected function casts(): array
{
return [
'portal' => Portal::class,
'status' => PressReleaseStatus::class,
'hits' => 'integer',
'teaser_begin' => 'integer',
'teaser_end' => 'integer',
'no_export' => 'boolean',
'published_at' => 'datetime',
'deleted_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function company(): BelongsTo
{
return $this->belongsTo(Company::class);
}
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
}
public function images(): HasMany
{
return $this->hasMany(PressReleaseImage::class);
}
public function contacts(): BelongsToMany
{
return $this->belongsToMany(Contact::class, 'press_release_contact');
}
public function statusLogs(): HasMany
{
return $this->hasMany(PressReleaseStatusLog::class)->orderByDesc('created_at');
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Support\Facades\Storage;
class PressReleaseImage extends Model
{
use SoftDeletes;
protected $fillable = [
'press_release_id',
'disk',
'path',
'variants',
'title',
'description',
'copyright',
'is_preview',
'sort_order',
'width',
'height',
'mime',
'legacy_portal',
'legacy_id',
];
protected function casts(): array
{
return [
'variants' => 'array',
'is_preview' => 'boolean',
'sort_order' => 'integer',
'width' => 'integer',
'height' => 'integer',
'deleted_at' => 'datetime',
];
}
public function pressRelease(): BelongsTo
{
return $this->belongsTo(PressRelease::class);
}
public function url(): ?string
{
if (blank($this->path)) {
return null;
}
return $this->resolveDiskUrl($this->path);
}
public function variantUrl(string $key): ?string
{
$variantPath = $this->variants[$key] ?? null;
if (! is_string($variantPath) || blank($variantPath)) {
return null;
}
return $this->resolveDiskUrl($variantPath);
}
private function resolveDiskUrl(string $relativePath): ?string
{
if ($this->disk === 'public') {
return asset('storage/'.ltrim($relativePath, '/'));
}
try {
$disk = Storage::disk($this->disk);
if (method_exists($disk, 'url')) {
return $disk->url($relativePath);
}
return null;
} catch (\Throwable) {
return null;
}
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Models;
use App\Enums\PressReleaseStatus;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class PressReleaseStatusLog extends Model
{
public $timestamps = false;
protected $fillable = [
'press_release_id',
'changed_by_user_id',
'from_status',
'to_status',
'reason',
'source',
'created_at',
];
protected function casts(): array
{
return [
'created_at' => 'datetime',
'from_status' => PressReleaseStatus::class,
'to_status' => PressReleaseStatus::class,
];
}
public function pressRelease(): BelongsTo
{
return $this->belongsTo(PressRelease::class);
}
public function changedBy(): BelongsTo
{
return $this->belongsTo(User::class, 'changed_by_user_id');
}
}

52
app/Models/Profile.php Normal file
View file

@ -0,0 +1,52 @@
<?php
namespace App\Models;
use Database\Factories\ProfileFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Profile extends Model
{
/** @use HasFactory<ProfileFactory> */
use HasFactory;
protected $fillable = [
'user_id',
'salutation_key',
'title',
'first_name',
'last_name',
'phone',
'address',
'country_code',
'birthdate',
'backlink_url',
'show_stats',
'validation_date',
'contract_date',
'validate_token',
'tax_id_number',
'tax_exempt',
'tax_exempt_reason',
'disable_footer_code',
];
protected function casts(): array
{
return [
'birthdate' => 'date',
'show_stats' => 'boolean',
'validation_date' => 'datetime',
'contract_date' => 'datetime',
'tax_exempt' => 'boolean',
'disable_footer_code' => 'boolean',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View file

@ -3,17 +3,25 @@
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use App\Enums\Portal;
use App\Enums\RegistrationType;
use Database\Factories\UserFactory;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Illuminate\Support\Str;
use Laravel\Fortify\TwoFactorAuthenticatable;
use Laravel\Sanctum\HasApiTokens;
use Spatie\Permission\Traits\HasRoles;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasApiTokens, HasFactory, Notifiable, HasRoles, TwoFactorAuthenticatable;
/** @use HasFactory<UserFactory> */
use HasApiTokens, HasFactory, HasRoles, Notifiable, SoftDeletes, TwoFactorAuthenticatable;
/**
* The attributes that are mass assignable.
@ -23,6 +31,17 @@ class User extends Authenticatable
protected $fillable = [
'name',
'email',
'portal',
'registration_type',
'language',
'is_active',
'is_super_admin',
'last_login_at',
'last_login_ip',
'gdpr_consent_at',
'last_seen_at',
'legacy_portal',
'legacy_id',
'password',
];
@ -45,6 +64,14 @@ class User extends Authenticatable
{
return [
'email_verified_at' => 'datetime',
'portal' => Portal::class,
'registration_type' => RegistrationType::class,
'is_active' => 'boolean',
'is_super_admin' => 'boolean',
'last_login_at' => 'datetime',
'gdpr_consent_at' => 'datetime',
'last_seen_at' => 'datetime',
'deleted_at' => 'datetime',
'password' => 'hashed',
];
}
@ -59,4 +86,89 @@ class User extends Authenticatable
->map(fn (string $name) => Str::of($name)->substr(0, 1))
->implode('');
}
public function profile(): HasOne
{
return $this->hasOne(Profile::class);
}
public function magicLinks(): HasMany
{
return $this->hasMany(MagicLink::class);
}
public function ownedCompanies(): HasMany
{
return $this->hasMany(Company::class, 'owner_user_id');
}
public function companies(): BelongsToMany
{
return $this->belongsToMany(Company::class)
->withPivot('role')
->withTimestamps();
}
public function contacts(): BelongsToMany
{
return $this->belongsToMany(Contact::class)
->withTimestamps();
}
public function pressReleases(): HasMany
{
return $this->hasMany(PressRelease::class);
}
public function newsletterSubscriptions(): HasMany
{
return $this->hasMany(NewsletterSubscription::class);
}
public function billingAddress(): HasOne
{
return $this->hasOne(BillingAddress::class);
}
public function userPaymentOptions(): HasMany
{
return $this->hasMany(UserPaymentOption::class);
}
public function invoices(): HasMany
{
return $this->hasMany(Invoice::class);
}
public function legacyInvoices(): HasMany
{
return $this->hasMany(LegacyInvoice::class);
}
public function filterPresets(): HasMany
{
return $this->hasMany(UserFilterPreset::class);
}
public function canAccessAdmin(): bool
{
if (! $this->is_active) {
return false;
}
if ($this->is_super_admin) {
return true;
}
return $this->hasAnyRole(['admin', 'editor']);
}
public function canAccessCustomer(): bool
{
if (! $this->is_active) {
return false;
}
return $this->hasAnyRole(['admin', 'editor', 'customer']);
}
}

View file

@ -0,0 +1,32 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class UserFilterPreset extends Model
{
protected $fillable = [
'user_id',
'page',
'name',
'is_default',
'last_used_at',
'filters',
];
protected function casts(): array
{
return [
'is_default' => 'boolean',
'last_used_at' => 'datetime',
'filters' => 'array',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View file

@ -0,0 +1,41 @@
<?php
namespace App\Models;
use App\Enums\PaymentStatus;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class UserPayment extends Model
{
use HasFactory;
protected $fillable = [
'user_payment_option_id',
'amount_cents',
'currency',
'status',
'stripe_charge_id',
'stripe_invoice_id',
];
protected function casts(): array
{
return [
'amount_cents' => 'integer',
'status' => PaymentStatus::class,
];
}
public function userPaymentOption(): BelongsTo
{
return $this->belongsTo(UserPaymentOption::class);
}
public function invoices(): HasMany
{
return $this->hasMany(Invoice::class, 'user_payment_id');
}
}

View file

@ -0,0 +1,61 @@
<?php
namespace App\Models;
use App\Enums\UserPaymentOptionStatus;
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;
class UserPaymentOption extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'payment_option_id',
'status',
'grandfathered_until',
'legacy_conditions',
'current_period_start',
'current_period_end',
'stripe_subscription_id',
'cancelled_at',
];
protected function casts(): array
{
return [
'status' => UserPaymentOptionStatus::class,
'grandfathered_until' => 'date',
'legacy_conditions' => 'array',
'current_period_start' => 'date',
'current_period_end' => 'date',
'cancelled_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function paymentOption(): BelongsTo
{
return $this->belongsTo(PaymentOption::class);
}
public function companies(): BelongsToMany
{
return $this->belongsToMany(Company::class, 'user_payment_option_company')
->withPivot('is_active')
->withTimestamps();
}
public function payments(): HasMany
{
return $this->hasMany(UserPayment::class);
}
}