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,30 @@
<?php
namespace Database\Factories;
use App\Models\AdminPreset;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<AdminPreset>
*/
class AdminPresetFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'key' => 'test.'.fake()->unique()->slug(),
'area' => 'test',
'type' => 'text',
'label' => fake()->sentence(3),
'value' => fake()->paragraph(),
'payload' => null,
'is_active' => true,
];
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Models\BillingAddress;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<BillingAddress>
*/
class BillingAddressFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'salutation_key' => fake()->randomElement(['mr', 'mrs', 'diverse']),
'title' => fake()->optional()->randomElement(['Dr.', 'Prof.']),
'name' => fake()->company(),
'address1' => fake()->streetAddress(),
'address2' => fake()->optional()->secondaryAddress(),
'postal_code' => fake()->postcode(),
'city' => fake()->city(),
'country_code' => 'DE',
];
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Enums\Portal;
use App\Models\Category;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Category>
*/
class CategoryFactory extends Factory
{
protected $model = Category::class;
public function definition(): array
{
return [
'portal' => fake()->randomElement([Portal::Presseecho->value, Portal::Businessportal24->value, Portal::Both->value]),
'is_active' => true,
];
}
public function withTranslations(): static
{
return $this->afterCreating(function (Category $category): void {
$category->translations()->createMany([
['locale' => 'de', 'name' => fake('de_DE')->words(2, true), 'slug' => 'de-'.fake()->unique()->slug(2)],
['locale' => 'en', 'name' => fake('en_US')->words(2, true), 'slug' => 'en-'.fake()->unique()->slug(2)],
]);
});
}
}

View file

@ -0,0 +1,50 @@
<?php
namespace Database\Factories;
use App\Enums\CompanyType;
use App\Enums\Portal;
use App\Models\Company;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends Factory<Company>
*/
class CompanyFactory extends Factory
{
protected $model = Company::class;
public function definition(): array
{
$name = fake()->company();
return [
'portal' => fake()->randomElement([Portal::Presseecho->value, Portal::Businessportal24->value]),
'type' => fake()->randomElement(CompanyType::cases())->value,
'name' => $name,
'slug' => Str::slug($name).'-'.fake()->unique()->numberBetween(1000, 9999),
'address' => fake()->streetAddress(),
'country_code' => 'DE',
'phone' => fake()->phoneNumber(),
'email' => fake()->companyEmail(),
'website' => fake()->url(),
'is_active' => true,
];
}
public function presseecho(): static
{
return $this->state(['portal' => Portal::Presseecho->value]);
}
public function businessportal24(): static
{
return $this->state(['portal' => Portal::Businessportal24->value]);
}
public function inactive(): static
{
return $this->state(['is_active' => false]);
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Database\Factories;
use App\Enums\Portal;
use App\Models\Company;
use App\Models\Contact;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Contact>
*/
class ContactFactory extends Factory
{
protected $model = Contact::class;
public function definition(): array
{
return [
'company_id' => Company::factory(),
'portal' => fake()->randomElement([Portal::Presseecho->value, Portal::Businessportal24->value]),
'salutation_key' => fake()->randomElement(['mr', 'mrs', null]),
'first_name' => fake()->firstName(),
'last_name' => fake()->lastName(),
'responsibility' => fake()->jobTitle(),
'phone' => fake()->phoneNumber(),
'email' => fake()->email(),
];
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Database\Factories;
use App\Enums\Portal;
use App\Models\FooterCode;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<FooterCode>
*/
class FooterCodeFactory extends Factory
{
protected $model = FooterCode::class;
public function definition(): array
{
return [
'portal' => fake()->randomElement([
Portal::Both->value,
Portal::Presseecho->value,
Portal::Businessportal24->value,
]),
'language' => fake()->randomElement([null, 'de', 'en']),
'title' => fake()->sentence(3),
'content' => fake()->paragraph(),
'is_global' => false,
'is_active' => true,
'priority' => fake()->numberBetween(0, 100),
];
}
public function global(): static
{
return $this->state(['is_global' => true]);
}
public function inactive(): static
{
return $this->state(['is_active' => false]);
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Database\Factories;
use App\Models\InvoiceBillingAddress;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<InvoiceBillingAddress>
*/
class InvoiceBillingAddressFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'salutation_key' => fake()->randomElement(['mr', 'mrs', 'diverse']),
'title' => fake()->optional()->randomElement(['Dr.', 'Prof.']),
'name' => fake()->company(),
'address1' => fake()->streetAddress(),
'address2' => fake()->optional()->secondaryAddress(),
'postal_code' => fake()->postcode(),
'city' => fake()->city(),
'country_code' => 'DE',
];
}
}

View file

@ -0,0 +1,45 @@
<?php
namespace Database\Factories;
use App\Enums\InvoiceStatus;
use App\Models\Invoice;
use App\Models\InvoiceBillingAddress;
use App\Models\User;
use App\Models\UserPayment;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Invoice>
*/
class InvoiceFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$amount = fake()->numberBetween(1000, 20000);
$tax = (int) round($amount * 0.19);
return [
'user_id' => User::factory(),
'user_payment_id' => UserPayment::factory(),
'invoice_billing_address_id' => InvoiceBillingAddress::factory(),
'number' => now()->format('Ym').'-'.fake()->unique()->numerify('####'),
'status' => fake()->randomElement(InvoiceStatus::cases())->value,
'amount_cents' => $amount,
'tax_cents' => $tax,
'total_cents' => $amount + $tax,
'currency' => 'EUR',
'is_netto' => false,
'invoice_date' => now()->toDateString(),
'due_date' => now()->addDays(14)->toDateString(),
'paid_at' => null,
'stripe_invoice_id' => fake()->optional()->regexify('in_[A-Za-z0-9]{14}'),
'pdf_path' => null,
];
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Database\Factories;
use App\Enums\PaymentOptionType;
use App\Models\PaymentOption;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<PaymentOption>
*/
class PaymentOptionFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
$type = fake()->randomElement(PaymentOptionType::cases())->value;
$interval = $type === PaymentOptionType::Onetime->value
? 'once'
: fake()->randomElement(['monthly', 'yearly']);
return [
'article_number' => 'ART-'.fake()->unique()->numerify('#####'),
'type' => $type,
'price_cents' => fake()->numberBetween(990, 49900),
'currency' => 'EUR',
'interval' => $interval,
'is_hidden' => false,
'stripe_product_id' => 'prod_'.fake()->regexify('[A-Za-z0-9]{14}'),
'stripe_price_id' => 'price_'.fake()->regexify('[A-Za-z0-9]{14}'),
];
}
}

View file

@ -0,0 +1,28 @@
<?php
namespace Database\Factories;
use App\Models\PaymentOption;
use App\Models\PaymentOptionTranslation;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<PaymentOptionTranslation>
*/
class PaymentOptionTranslationFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'payment_option_id' => PaymentOption::factory(),
'locale' => fake()->randomElement(['de', 'en']),
'name' => fake()->words(3, true),
'description' => fake()->sentence(),
];
}
}

View file

@ -0,0 +1,58 @@
<?php
namespace Database\Factories;
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use App\Models\Category;
use App\Models\Company;
use App\Models\PressRelease;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
/**
* @extends Factory<PressRelease>
*/
class PressReleaseFactory extends Factory
{
protected $model = PressRelease::class;
public function definition(): array
{
$title = fake()->sentence(6);
return [
'uuid' => (string) Str::uuid(),
'portal' => fake()->randomElement([Portal::Presseecho->value, Portal::Businessportal24->value]),
'user_id' => User::factory(),
'company_id' => Company::factory(),
'category_id' => Category::factory(),
'language' => 'de',
'title' => $title,
'slug' => Str::slug($title).'-'.fake()->unique()->numberBetween(1000, 9999),
'text' => fake()->paragraphs(3, true),
'status' => PressReleaseStatus::Draft->value,
'hits' => 0,
'no_export' => false,
];
}
public function published(): static
{
return $this->state([
'status' => PressReleaseStatus::Published->value,
'published_at' => now(),
]);
}
public function inReview(): static
{
return $this->state(['status' => PressReleaseStatus::Review->value]);
}
public function forPortal(Portal $portal): static
{
return $this->state(['portal' => $portal->value]);
}
}

View file

@ -0,0 +1,42 @@
<?php
namespace Database\Factories;
use App\Models\Profile;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<Profile>
*/
class ProfileFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'salutation_key' => fake()->randomElement(['mr', 'mrs', 'none']),
'title' => fake()->optional()->title(),
'first_name' => fake()->firstName(),
'last_name' => fake()->lastName(),
'phone' => fake()->optional()->phoneNumber(),
'address' => fake()->optional()->streetAddress(),
'country_code' => 'DE',
'birthdate' => fake()->optional()->date(),
'backlink_url' => fake()->optional()->url(),
'show_stats' => fake()->boolean(),
'validation_date' => fake()->optional()->dateTimeBetween('-5 years'),
'contract_date' => fake()->optional()->dateTimeBetween('-10 years'),
'validate_token' => fake()->optional()->regexify('[A-Za-z0-9]{17}'),
'tax_id_number' => fake()->optional()->numerify('DE#########'),
'tax_exempt' => false,
'tax_exempt_reason' => null,
'disable_footer_code' => false,
];
}
}

View file

@ -2,25 +2,20 @@
namespace Database\Factories;
use App\Enums\Portal;
use App\Enums\RegistrationType;
use App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/**
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\User>
* @extends Factory<User>
*/
class UserFactory extends Factory
{
/**
* The current password being used by the factory.
*/
protected static ?string $password;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
@ -29,16 +24,30 @@ class UserFactory extends Factory
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
// Felder aus expand_users_table_for_migration_2026 explizit setzen,
// da DB-Defaults nicht automatisch in Model-Attribute übernommen werden.
'portal' => Portal::Both->value,
'registration_type' => RegistrationType::ExistingLegacy->value,
'language' => 'de',
'is_active' => true,
'is_super_admin' => false,
];
}
/**
* Indicate that the model's email address should be unverified.
*/
public function unverified(): static
{
return $this->state(fn (array $attributes) => [
'email_verified_at' => null,
]);
}
public function inactive(): static
{
return $this->state(['is_active' => false]);
}
public function superAdmin(): static
{
return $this->state(['is_super_admin' => true]);
}
}

View file

@ -0,0 +1,31 @@
<?php
namespace Database\Factories;
use App\Enums\PaymentStatus;
use App\Models\UserPayment;
use App\Models\UserPaymentOption;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<UserPayment>
*/
class UserPaymentFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_payment_option_id' => UserPaymentOption::factory(),
'amount_cents' => fake()->numberBetween(990, 19900),
'currency' => 'EUR',
'status' => fake()->randomElement(PaymentStatus::cases())->value,
'stripe_charge_id' => fake()->optional()->regexify('ch_[A-Za-z0-9]{14}'),
'stripe_invoice_id' => fake()->optional()->regexify('in_[A-Za-z0-9]{14}'),
];
}
}

View file

@ -0,0 +1,37 @@
<?php
namespace Database\Factories;
use App\Enums\UserPaymentOptionStatus;
use App\Models\PaymentOption;
use App\Models\User;
use App\Models\UserPaymentOption;
use Illuminate\Database\Eloquent\Factories\Factory;
/**
* @extends Factory<UserPaymentOption>
*/
class UserPaymentOptionFactory extends Factory
{
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition(): array
{
return [
'user_id' => User::factory(),
'payment_option_id' => PaymentOption::factory(),
'status' => fake()->randomElement(UserPaymentOptionStatus::cases())->value,
'grandfathered_until' => fake()->optional()->dateTimeBetween('now', '+2 years'),
'legacy_conditions' => fake()->boolean(25)
? ['note' => fake()->sentence(), 'discount_percent' => fake()->numberBetween(5, 40)]
: null,
'current_period_start' => now()->startOfMonth(),
'current_period_end' => now()->endOfMonth(),
'stripe_subscription_id' => fake()->optional()->regexify('sub_[A-Za-z0-9]{14}'),
'cancelled_at' => null,
];
}
}

View file

@ -0,0 +1,75 @@
<?php
use App\Enums\Portal;
use App\Enums\RegistrationType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
$table->enum('portal', array_map(
static fn (Portal $portal): string => $portal->value,
Portal::cases()
))->default(Portal::Both->value)->after('email');
$table->enum('registration_type', array_map(
static fn (RegistrationType $type): string => $type->value,
RegistrationType::cases()
))->default(RegistrationType::ExistingLegacy->value)->after('portal');
$table->string('language', 2)->default('de')->after('registration_type');
$table->boolean('is_active')->default(true)->after('language');
$table->boolean('is_super_admin')->default(false)->after('is_active');
$table->timestamp('last_login_at')->nullable()->after('is_super_admin');
$table->string('last_login_ip', 45)->nullable()->after('last_login_at');
$table->timestamp('gdpr_consent_at')->nullable()->after('last_login_ip');
$table->timestamp('last_seen_at')->nullable()->after('gdpr_consent_at');
$table->enum('legacy_portal', [
Portal::Presseecho->value,
Portal::Businessportal24->value,
])->nullable()->after('last_seen_at');
$table->unsignedBigInteger('legacy_id')->nullable()->after('legacy_portal');
$table->softDeletes()->after('updated_at');
$table->unique(['legacy_portal', 'legacy_id'], 'users_legacy_portal_legacy_id_unique');
$table->string('password')->nullable()->change();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropUnique('users_legacy_portal_legacy_id_unique');
$table->dropColumn([
'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',
'deleted_at',
]);
$table->string('password')->nullable(false)->change();
});
}
};

View file

@ -0,0 +1,134 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
throw_if($teams && empty($columnNames['team_foreign_key'] ?? null), Exception::class, 'Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
Schema::create($tableNames['permissions'], static function (Blueprint $table) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // permission id
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
// $table->engine('InnoDB');
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
throw_if(empty($tableNames), Exception::class, 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
};

View file

@ -0,0 +1,42 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('magic_links', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->char('token_hash', 64)->unique();
$table->enum('purpose', [
'login',
'press_release_access',
'invoice_access',
'initial_setup',
])->default('login');
$table->json('payload')->nullable();
$table->timestamp('expires_at');
$table->timestamp('consumed_at')->nullable();
$table->string('ip_requested', 45)->nullable();
$table->string('ip_consumed', 45)->nullable();
$table->timestamps();
$table->index(['user_id', 'purpose', 'consumed_at'], 'magic_links_user_purpose_consumed_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('magic_links');
}
};

View file

@ -0,0 +1,45 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('profiles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
$table->string('salutation_key', 20)->nullable();
$table->string('title', 80)->nullable();
$table->string('first_name', 80)->nullable();
$table->string('last_name', 80)->nullable();
$table->string('phone', 40)->nullable();
$table->string('address', 1000)->nullable();
$table->string('country_code', 2)->nullable();
$table->date('birthdate')->nullable();
$table->string('backlink_url')->nullable();
$table->boolean('show_stats')->default(false);
$table->timestamp('validation_date')->nullable();
$table->timestamp('contract_date')->nullable();
$table->string('validate_token', 64)->nullable();
$table->string('tax_id_number')->nullable();
$table->boolean('tax_exempt')->default(false);
$table->string('tax_exempt_reason', 1000)->nullable();
$table->boolean('disable_footer_code')->default(false);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('profiles');
}
};

View file

@ -0,0 +1,60 @@
<?php
use App\Enums\CompanyType;
use App\Enums\Portal;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('companies', function (Blueprint $table) {
$table->id();
$table->enum('portal', array_map(
static fn (Portal $portal): string => $portal->value,
Portal::cases()
))->default(Portal::Both->value);
$table->foreignId('owner_user_id')->nullable()->constrained('users')->nullOnDelete();
$table->enum('type', array_map(
static fn (CompanyType $type): string => $type->value,
CompanyType::cases()
))->default(CompanyType::Company->value);
$table->string('name');
$table->string('slug');
$table->string('address', 1000)->nullable();
$table->string('country_code', 2)->nullable();
$table->string('phone', 40)->nullable();
$table->string('fax', 40)->nullable();
$table->string('email')->nullable();
$table->string('website')->nullable();
$table->string('logo_path')->nullable();
$table->json('logo_variants')->nullable();
$table->boolean('is_active')->default(true);
$table->boolean('disable_footer_code')->default(false);
$table->enum('legacy_portal', [
Portal::Presseecho->value,
Portal::Businessportal24->value,
])->nullable();
$table->unsignedBigInteger('legacy_id')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['portal', 'slug'], 'companies_portal_slug_unique');
$table->unique(['legacy_portal', 'legacy_id'], 'companies_legacy_portal_legacy_id_unique');
$table->index(['portal', 'is_active'], 'companies_portal_active_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('companies');
}
};

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('company_user', function (Blueprint $table) {
$table->foreignId('company_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->enum('role', ['member', 'responsible', 'owner'])->default('member');
$table->timestamps();
$table->primary(['company_id', 'user_id']);
$table->index(['user_id', 'role'], 'company_user_user_role_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('company_user');
}
};

View file

@ -0,0 +1,50 @@
<?php
use App\Enums\Portal;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('contacts', function (Blueprint $table) {
$table->id();
$table->foreignId('company_id')->constrained()->cascadeOnDelete();
$table->enum('portal', array_map(
static fn (Portal $portal): string => $portal->value,
Portal::cases()
))->default(Portal::Both->value);
$table->string('salutation_key', 20)->nullable();
$table->string('title', 80)->nullable();
$table->string('first_name', 80)->nullable();
$table->string('last_name', 80)->nullable();
$table->string('responsibility')->nullable();
$table->string('phone', 40)->nullable();
$table->string('fax', 40)->nullable();
$table->string('email')->nullable();
$table->enum('legacy_portal', [
Portal::Presseecho->value,
Portal::Businessportal24->value,
])->nullable();
$table->unsignedBigInteger('legacy_id')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['legacy_portal', 'legacy_id'], 'contacts_legacy_portal_legacy_id_unique');
$table->index(['company_id', 'portal'], 'contacts_company_portal_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('contacts');
}
};

View file

@ -0,0 +1,43 @@
<?php
use App\Enums\Portal;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('categories', function (Blueprint $table) {
$table->id();
$table->foreignId('parent_id')->nullable()->constrained('categories')->nullOnDelete();
$table->enum('portal', array_map(
static fn (Portal $portal): string => $portal->value,
Portal::cases()
))->default(Portal::Both->value);
$table->boolean('is_active')->default(true);
$table->enum('legacy_portal', [
Portal::Presseecho->value,
Portal::Businessportal24->value,
])->nullable();
$table->unsignedBigInteger('legacy_id')->nullable();
$table->timestamps();
$table->unique(['legacy_portal', 'legacy_id'], 'categories_legacy_portal_legacy_id_unique');
$table->index(['portal', 'is_active'], 'categories_portal_active_idx');
$table->index(['parent_id', 'portal'], 'categories_parent_portal_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('categories');
}
};

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('category_translations', function (Blueprint $table) {
$table->id();
$table->foreignId('category_id')->constrained()->cascadeOnDelete();
$table->string('locale', 2);
$table->string('name');
$table->string('slug');
$table->text('description')->nullable();
$table->timestamps();
$table->unique(['category_id', 'locale'], 'category_translations_category_locale_unique');
$table->index(['locale', 'slug'], 'category_translations_locale_slug_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('category_translations');
}
};

View file

@ -0,0 +1,63 @@
<?php
use App\Enums\Portal;
use App\Enums\PressReleaseStatus;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('press_releases', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->enum('portal', array_map(
static fn (Portal $portal): string => $portal->value,
Portal::cases()
))->default(Portal::Both->value);
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('company_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('category_id')->constrained()->restrictOnDelete();
$table->string('language', 2)->default('de');
$table->string('title');
$table->string('slug');
$table->mediumText('text');
$table->string('backlink_url')->nullable();
$table->string('keywords')->nullable();
$table->enum('status', array_map(
static fn (PressReleaseStatus $status): string => $status->value,
PressReleaseStatus::cases()
))->default(PressReleaseStatus::Draft->value);
$table->unsignedInteger('hits')->default(0);
$table->unsignedInteger('teaser_begin')->default(0);
$table->unsignedInteger('teaser_end')->default(0);
$table->boolean('no_export')->default(false);
$table->timestamp('published_at')->nullable();
$table->enum('legacy_portal', [
Portal::Presseecho->value,
Portal::Businessportal24->value,
])->nullable();
$table->unsignedBigInteger('legacy_id')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['portal', 'language', 'slug'], 'press_releases_portal_language_slug_unique');
$table->unique(['legacy_portal', 'legacy_id'], 'press_releases_legacy_portal_legacy_id_unique');
$table->index(['portal', 'status', 'published_at'], 'press_releases_portal_status_published_idx');
$table->index(['category_id', 'portal'], 'press_releases_category_portal_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('press_releases');
}
};

View file

@ -0,0 +1,49 @@
<?php
use App\Enums\Portal;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('press_release_images', function (Blueprint $table) {
$table->id();
$table->foreignId('press_release_id')->constrained()->cascadeOnDelete();
$table->string('disk', 30)->default('public');
$table->string('path', 512);
$table->json('variants')->nullable();
$table->string('title', 120)->nullable();
$table->string('description', 500)->nullable();
$table->string('copyright')->nullable();
$table->boolean('is_preview')->default(false);
$table->unsignedInteger('sort_order')->default(0);
$table->unsignedInteger('width')->nullable();
$table->unsignedInteger('height')->nullable();
$table->string('mime', 60)->nullable();
$table->enum('legacy_portal', [
Portal::Presseecho->value,
Portal::Businessportal24->value,
])->nullable();
$table->unsignedBigInteger('legacy_id')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['legacy_portal', 'legacy_id'], 'press_release_images_legacy_portal_legacy_id_unique');
$table->index(['press_release_id', 'sort_order'], 'press_release_images_release_sort_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('press_release_images');
}
};

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('press_release_contact', function (Blueprint $table) {
$table->foreignId('press_release_id')->constrained()->cascadeOnDelete();
$table->foreignId('contact_id')->constrained()->cascadeOnDelete();
$table->primary(['press_release_id', 'contact_id'], 'press_release_contact_primary');
$table->index(['contact_id', 'press_release_id'], 'press_release_contact_contact_release_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('press_release_contact');
}
};

View file

@ -0,0 +1,52 @@
<?php
use App\Enums\Portal;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('newsletter_subscriptions', function (Blueprint $table) {
$table->id();
$table->enum('portal', array_map(
static fn (Portal $portal): string => $portal->value,
Portal::cases()
))->default(Portal::Both->value);
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->string('salutation_key', 20)->nullable();
$table->string('first_name', 80)->nullable();
$table->string('last_name', 80)->nullable();
$table->string('email', 190);
$table->string('ip_address', 45)->nullable();
$table->boolean('is_confirmed')->default(false);
$table->string('confirmation_token', 32)->nullable();
$table->timestamp('subscribed_at')->nullable();
$table->timestamp('unsubscribed_at')->nullable();
$table->enum('legacy_portal', [
Portal::Presseecho->value,
Portal::Businessportal24->value,
])->nullable();
$table->unsignedBigInteger('legacy_id')->nullable();
$table->timestamps();
$table->unique(['portal', 'email'], 'newsletter_subscriptions_portal_email_unique');
$table->unique(['confirmation_token'], 'newsletter_subscriptions_confirmation_token_unique');
$table->unique(['legacy_portal', 'legacy_id'], 'newsletter_subscriptions_legacy_portal_legacy_id_unique');
$table->index(['portal', 'is_confirmed'], 'newsletter_subscriptions_portal_confirmed_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('newsletter_subscriptions');
}
};

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('billing_addresses', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->unique()->constrained()->cascadeOnDelete();
$table->string('salutation_key', 20)->nullable();
$table->string('title', 80)->nullable();
$table->string('name', 255);
$table->string('address1', 255);
$table->string('address2', 255)->nullable();
$table->string('postal_code', 20);
$table->string('city', 120);
$table->string('country_code', 2);
$table->timestamps();
$table->index(['country_code', 'postal_code'], 'billing_addresses_country_postal_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('billing_addresses');
}
};

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invoice_billing_addresses', function (Blueprint $table) {
$table->id();
$table->string('salutation_key', 20)->nullable();
$table->string('title', 80)->nullable();
$table->string('name', 255);
$table->string('address1', 255);
$table->string('address2', 255)->nullable();
$table->string('postal_code', 20);
$table->string('city', 120);
$table->string('country_code', 2);
$table->timestamps();
$table->index(['country_code', 'postal_code'], 'invoice_billing_country_postal_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('invoice_billing_addresses');
}
};

View file

@ -0,0 +1,43 @@
<?php
use App\Enums\PaymentOptionType;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('payment_options', function (Blueprint $table) {
$table->id();
$table->string('article_number', 60)->unique();
$table->enum('type', array_map(
static fn (PaymentOptionType $type): string => $type->value,
PaymentOptionType::cases()
))->default(PaymentOptionType::Recurring->value);
$table->unsignedInteger('price_cents');
$table->string('currency', 3)->default('EUR');
$table->enum('interval', ['monthly', 'yearly', 'once'])->nullable();
$table->boolean('is_hidden')->default(false);
$table->string('stripe_product_id', 60)->nullable();
$table->string('stripe_price_id', 60)->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['type', 'is_hidden'], 'payment_options_type_hidden_idx');
$table->index(['stripe_product_id', 'stripe_price_id'], 'payment_options_stripe_ids_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('payment_options');
}
};

View file

@ -0,0 +1,37 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('payment_option_translations', function (Blueprint $table) {
$table->id();
$table->foreignId('payment_option_id')->constrained()->cascadeOnDelete();
$table->string('locale', 2);
$table->string('name');
$table->text('description')->nullable();
$table->timestamps();
$table->unique(
['payment_option_id', 'locale'],
'payment_option_translations_option_locale_unique'
);
$table->index(['locale', 'name'], 'payment_option_translations_locale_name_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('payment_option_translations');
}
};

View file

@ -0,0 +1,44 @@
<?php
use App\Enums\UserPaymentOptionStatus;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_payment_options', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('payment_option_id')->constrained()->cascadeOnDelete();
$table->enum('status', array_map(
static fn (UserPaymentOptionStatus $status): string => $status->value,
UserPaymentOptionStatus::cases()
))->default(UserPaymentOptionStatus::Active->value);
$table->date('grandfathered_until')->nullable();
$table->json('legacy_conditions')->nullable();
$table->date('current_period_start');
$table->date('current_period_end');
$table->string('stripe_subscription_id', 60)->nullable();
$table->timestamp('cancelled_at')->nullable();
$table->timestamps();
$table->index(['user_id', 'status'], 'user_payment_options_user_status_idx');
$table->index(['payment_option_id', 'status'], 'user_payment_options_option_status_idx');
$table->index(['stripe_subscription_id'], 'user_payment_options_stripe_sub_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_payment_options');
}
};

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_payment_option_company', function (Blueprint $table) {
$table->foreignId('user_payment_option_id')->constrained()->cascadeOnDelete();
$table->foreignId('company_id')->constrained()->cascadeOnDelete();
$table->boolean('is_active')->default(true);
$table->timestamps();
$table->primary(
['user_payment_option_id', 'company_id'],
'user_payment_option_company_primary'
);
$table->index(
['company_id', 'user_payment_option_id'],
'user_payment_option_company_company_option_idx'
);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_payment_option_company');
}
};

View file

@ -0,0 +1,41 @@
<?php
use App\Enums\PaymentStatus;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('user_payments', function (Blueprint $table) {
$table->id();
$table->foreignId('user_payment_option_id')->nullable()->constrained()->nullOnDelete();
$table->unsignedInteger('amount_cents');
$table->string('currency', 3)->default('EUR');
$table->enum('status', array_map(
static fn (PaymentStatus $status): string => $status->value,
PaymentStatus::cases()
))->default(PaymentStatus::Pending->value);
$table->string('stripe_charge_id', 60)->nullable();
$table->string('stripe_invoice_id', 60)->nullable();
$table->timestamps();
$table->index(['user_payment_option_id', 'status'], 'user_payments_option_status_idx');
$table->index(['stripe_charge_id'], 'user_payments_stripe_charge_idx');
$table->index(['stripe_invoice_id'], 'user_payments_stripe_invoice_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('user_payments');
}
};

View file

@ -0,0 +1,51 @@
<?php
use App\Enums\InvoiceStatus;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('invoices', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_payment_id')->nullable()->constrained('user_payments')->nullOnDelete();
$table->foreignId('invoice_billing_address_id')->constrained()->restrictOnDelete();
$table->string('number', 40)->unique();
$table->enum('status', array_map(
static fn (InvoiceStatus $status): string => $status->value,
InvoiceStatus::cases()
))->default(InvoiceStatus::Open->value);
$table->unsignedInteger('amount_cents');
$table->unsignedInteger('tax_cents')->default(0);
$table->unsignedInteger('total_cents');
$table->string('currency', 3)->default('EUR');
$table->boolean('is_netto')->default(false);
$table->date('invoice_date');
$table->date('due_date')->nullable();
$table->timestamp('paid_at')->nullable();
$table->string('stripe_invoice_id', 60)->nullable();
$table->string('pdf_path', 512)->nullable();
$table->timestamps();
$table->softDeletes();
$table->index(['user_id', 'invoice_date'], 'invoices_user_invoice_date_idx');
$table->index(['status', 'invoice_date'], 'invoices_status_invoice_date_idx');
$table->index(['stripe_invoice_id'], 'invoices_stripe_invoice_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('invoices');
}
};

View file

@ -0,0 +1,49 @@
<?php
use App\Enums\Portal;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('legacy_invoices', function (Blueprint $table) {
$table->id();
$table->enum('legacy_portal', [
Portal::Presseecho->value,
Portal::Businessportal24->value,
]);
$table->unsignedBigInteger('legacy_id');
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->unsignedBigInteger('legacy_user_id')->nullable();
$table->string('number', 40)->nullable();
$table->unsignedInteger('amount_cents')->default(0);
$table->unsignedInteger('tax_cents')->default(0);
$table->unsignedInteger('total_cents')->default(0);
$table->string('status', 40)->nullable();
$table->date('invoice_date')->nullable();
$table->date('due_date')->nullable();
$table->timestamp('paid_at')->nullable();
$table->string('payment_method', 40)->nullable();
$table->string('pdf_path', 512)->nullable();
$table->json('raw_snapshot')->nullable();
$table->timestamp('imported_at')->nullable();
$table->unique(['legacy_portal', 'legacy_id'], 'legacy_invoices_portal_legacy_id_unique');
$table->index(['user_id', 'invoice_date'], 'legacy_invoices_user_invoice_date_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('legacy_invoices');
}
};

View file

@ -0,0 +1,42 @@
<?php
use App\Enums\Portal;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('legacy_import_map', function (Blueprint $table) {
$table->id();
$table->enum('legacy_portal', [
Portal::Presseecho->value,
Portal::Businessportal24->value,
]);
$table->string('legacy_table', 80);
$table->unsignedBigInteger('legacy_id');
$table->string('target_table', 80);
$table->unsignedBigInteger('target_id');
$table->timestamp('imported_at')->nullable();
$table->unique(
['legacy_portal', 'legacy_table', 'legacy_id'],
'legacy_import_map_portal_table_id_unique'
);
$table->index(['target_table', 'target_id'], 'legacy_import_map_target_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('legacy_import_map');
}
};

View file

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('user_filter_presets', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->string('page', 100);
$table->string('name', 120);
$table->boolean('is_default')->default(false);
$table->timestamp('last_used_at')->nullable();
$table->json('filters');
$table->timestamps();
$table->unique(['user_id', 'page', 'name'], 'ufp_user_page_name_unique');
$table->index(['user_id', 'page'], 'ufp_user_page_idx');
$table->index(['user_id', 'page', 'is_default'], 'ufp_user_page_default_idx');
$table->index(['user_id', 'page', 'last_used_at'], 'ufp_user_page_last_used_idx');
});
}
public function down(): void
{
Schema::dropIfExists('user_filter_presets');
}
};

View file

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('contact_user', function (Blueprint $table) {
$table->foreignId('contact_id')->constrained()->cascadeOnDelete();
$table->foreignId('user_id')->constrained()->cascadeOnDelete();
$table->timestamps();
$table->primary(['contact_id', 'user_id']);
$table->index(['user_id', 'contact_id'], 'contact_user_user_contact_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('contact_user');
}
};

View file

@ -0,0 +1,38 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
/**
* Legacy-Daten enthalten Telefonnummern mit Beschreibungstext (bis 93 Zeichen).
* VARCHAR(40) reicht nicht aus auf 255 erweitern.
*/
return new class extends Migration
{
public function up(): void
{
Schema::table('companies', function (Blueprint $table) {
$table->string('phone', 255)->nullable()->change();
$table->string('fax', 255)->nullable()->change();
});
Schema::table('contacts', function (Blueprint $table) {
$table->string('phone', 255)->nullable()->change();
$table->string('fax', 255)->nullable()->change();
});
}
public function down(): void
{
Schema::table('companies', function (Blueprint $table) {
$table->string('phone', 40)->nullable()->change();
$table->string('fax', 40)->nullable()->change();
});
Schema::table('contacts', function (Blueprint $table) {
$table->string('phone', 40)->nullable()->change();
$table->string('fax', 40)->nullable()->change();
});
}
};

View file

@ -0,0 +1,34 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('admin_presets', function (Blueprint $table) {
$table->id();
$table->string('key')->unique();
$table->string('area')->index();
$table->string('type')->default('text');
$table->string('label');
$table->longText('value')->nullable();
$table->json('payload')->nullable();
$table->boolean('is_active')->default(true);
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('admin_presets');
}
};

View file

@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('api_usage_logs', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->nullable()->constrained()->nullOnDelete();
$table->foreignId('personal_access_token_id')->nullable()->constrained()->nullOnDelete();
$table->string('method', 10);
$table->string('path', 512);
$table->string('route_name', 120)->nullable();
$table->unsignedSmallInteger('status_code');
$table->string('ip_address', 45)->nullable();
$table->string('user_agent', 512)->nullable();
$table->unsignedInteger('duration_ms')->nullable();
$table->timestamp('requested_at')->useCurrent();
$table->index(['user_id', 'requested_at'], 'api_usage_logs_user_requested_idx');
$table->index(['personal_access_token_id', 'requested_at'], 'api_usage_logs_token_requested_idx');
$table->index(['path', 'requested_at'], 'api_usage_logs_path_requested_idx');
$table->index(['status_code', 'requested_at'], 'api_usage_logs_status_requested_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('api_usage_logs');
}
};

View file

@ -0,0 +1,60 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('press_releases', function (Blueprint $table) {
$table->index(['created_at', 'id'], 'press_releases_created_at_id_idx');
$table->index(['status', 'created_at', 'id'], 'press_releases_status_created_at_id_idx');
});
Schema::table('companies', function (Blueprint $table) {
$table->index(['created_at', 'id'], 'companies_created_at_id_idx');
$table->index(['is_active', 'created_at', 'id'], 'companies_active_created_at_id_idx');
});
Schema::table('contacts', function (Blueprint $table) {
$table->index(['portal', 'created_at', 'id'], 'contacts_portal_created_at_id_idx');
$table->index(['created_at', 'id'], 'contacts_created_at_id_idx');
});
Schema::table('users', function (Blueprint $table) {
$table->index(['is_active', 'created_at', 'id'], 'users_active_created_at_id_idx');
$table->index(['portal', 'created_at', 'id'], 'users_portal_created_at_id_idx');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropIndex('users_portal_created_at_id_idx');
$table->dropIndex('users_active_created_at_id_idx');
});
Schema::table('contacts', function (Blueprint $table) {
$table->dropIndex('contacts_created_at_id_idx');
$table->dropIndex('contacts_portal_created_at_id_idx');
});
Schema::table('companies', function (Blueprint $table) {
$table->dropIndex('companies_active_created_at_id_idx');
$table->dropIndex('companies_created_at_id_idx');
});
Schema::table('press_releases', function (Blueprint $table) {
$table->dropIndex('press_releases_status_created_at_id_idx');
$table->dropIndex('press_releases_created_at_id_idx');
});
}
};

View file

@ -0,0 +1,74 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! $this->supportsFullTextIndexes()) {
return;
}
Schema::table('press_releases', function (Blueprint $table): void {
$table->fullText(['title', 'keywords'], 'press_releases_admin_search_fulltext');
});
Schema::table('companies', function (Blueprint $table): void {
$table->fullText(['name', 'email', 'slug'], 'companies_admin_search_fulltext');
});
Schema::table('contacts', function (Blueprint $table): void {
$table->fullText(['first_name', 'last_name', 'email', 'responsibility'], 'contacts_admin_search_fulltext');
});
Schema::table('users', function (Blueprint $table): void {
$table->fullText(['name', 'email'], 'users_admin_search_fulltext');
});
Schema::table('category_translations', function (Blueprint $table): void {
$table->fullText(['name', 'slug'], 'category_translations_admin_search_fulltext');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
if (! $this->supportsFullTextIndexes()) {
return;
}
Schema::table('category_translations', function (Blueprint $table): void {
$table->dropFullText('category_translations_admin_search_fulltext');
});
Schema::table('contacts', function (Blueprint $table): void {
$table->dropFullText('contacts_admin_search_fulltext');
});
Schema::table('users', function (Blueprint $table): void {
$table->dropFullText('users_admin_search_fulltext');
});
Schema::table('companies', function (Blueprint $table): void {
$table->dropFullText('companies_admin_search_fulltext');
});
Schema::table('press_releases', function (Blueprint $table): void {
$table->dropFullText('press_releases_admin_search_fulltext');
});
}
private function supportsFullTextIndexes(): bool
{
return in_array(DB::connection()->getDriverName(), ['mysql', 'pgsql'], true);
}
};

View file

@ -0,0 +1,51 @@
<?php
use App\Enums\Portal;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('footer_codes', function (Blueprint $table): void {
$table->id();
$table->enum('portal', array_map(
static fn (Portal $portal): string => $portal->value,
Portal::cases(),
))->default(Portal::Both->value);
$table->string('language', 2)->nullable();
$table->string('title', 255);
$table->text('content');
$table->boolean('is_global')->default(false);
$table->boolean('is_active')->default(true);
$table->unsignedInteger('priority')->default(0);
$table->enum('legacy_portal', [
Portal::Presseecho->value,
Portal::Businessportal24->value,
])->nullable();
$table->unsignedBigInteger('legacy_id')->nullable();
$table->timestamps();
$table->softDeletes();
$table->unique(['legacy_portal', 'legacy_id'], 'footer_codes_legacy_portal_legacy_id_unique');
$table->index(['portal', 'language', 'is_active'], 'footer_codes_portal_language_active_idx');
$table->index(['is_global', 'is_active'], 'footer_codes_global_active_idx');
});
Schema::create('category_footer_code', function (Blueprint $table): void {
$table->foreignId('footer_code_id')->constrained()->cascadeOnDelete();
$table->foreignId('category_id')->constrained()->cascadeOnDelete();
$table->primary(['footer_code_id', 'category_id'], 'category_footer_code_pk');
$table->index('category_id', 'category_footer_code_category_idx');
});
}
public function down(): void
{
Schema::dropIfExists('category_footer_code');
Schema::dropIfExists('footer_codes');
}
};

View file

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('press_release_status_logs', function (Blueprint $table): void {
$table->id();
$table->foreignId('press_release_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('changed_by_user_id')
->nullable()
->constrained('users')
->nullOnDelete();
$table->string('from_status', 32)->nullable();
$table->string('to_status', 32);
$table->text('reason')->nullable();
$table->string('source', 32)->default('admin');
$table->timestamp('created_at')->useCurrent();
$table->index(['press_release_id', 'created_at'], 'pr_status_logs_pr_created_idx');
$table->index('to_status', 'pr_status_logs_to_status_idx');
});
}
public function down(): void
{
Schema::dropIfExists('press_release_status_logs');
}
};

View file

@ -0,0 +1,29 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('legacy_invoices', function (Blueprint $table) {
$table->json('pdf_payload')->nullable()->after('raw_snapshot');
$table->timestamp('pdf_generated_at')->nullable()->after('pdf_payload');
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('legacy_invoices', function (Blueprint $table) {
$table->dropColumn(['pdf_payload', 'pdf_generated_at']);
});
}
};

View file

@ -0,0 +1,27 @@
<?php
namespace Database\Seeders;
use App\Models\AdminPreset;
use Illuminate\Database\Seeder;
class AdminPresetSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
AdminPreset::query()->updateOrCreate(
['key' => AdminPreset::PRESS_RELEASE_DELETED_PUBLISHED_TEXT],
[
'area' => 'press_releases',
'type' => 'text',
'label' => 'Ersatztext fuer geloeschte veroeffentlichte Pressemitteilungen',
'value' => "Diese Pressemitteilung wurde entfernt.\n\nDer Inhalt ist nicht mehr verfuegbar. Bitte wenden Sie sich bei Rueckfragen an den Betreiber des Presseportals.",
'payload' => null,
'is_active' => true,
],
);
}
}

View file

@ -0,0 +1,100 @@
<?php
namespace Database\Seeders;
use App\Enums\Portal;
use App\Models\Category;
use Illuminate\Database\Seeder;
class CategorySeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$categories = [
[
'key' => 'news-wirtschaft',
'portal' => Portal::Both,
'translations' => [
'de' => [
'name' => 'Wirtschaft',
'slug' => 'wirtschaft',
'description' => 'Pressemitteilungen aus Wirtschaft und Unternehmen.',
],
'en' => [
'name' => 'Business',
'slug' => 'business',
'description' => 'Press releases from business and companies.',
],
],
],
[
'key' => 'news-technologie',
'portal' => Portal::Both,
'translations' => [
'de' => [
'name' => 'Technologie',
'slug' => 'technologie',
'description' => 'News rund um Innovation, IT und digitale Themen.',
],
'en' => [
'name' => 'Technology',
'slug' => 'technology',
'description' => 'News around innovation, IT, and digital topics.',
],
],
],
[
'key' => 'news-finanzen',
'portal' => Portal::Both,
'translations' => [
'de' => [
'name' => 'Finanzen',
'slug' => 'finanzen',
'description' => 'Pressemitteilungen zu Banken, Boerse und Finanzthemen.',
],
'en' => [
'name' => 'Finance',
'slug' => 'finance',
'description' => 'Press releases about banks, stock markets, and finance topics.',
],
],
],
];
foreach ($categories as $categoryData) {
$deSlug = $categoryData['translations']['de']['slug'];
$category = Category::query()
->whereHas('translations', function ($query) use ($deSlug) {
$query->where('locale', 'de')->where('slug', $deSlug);
})
->first();
if (! $category) {
$category = Category::query()->create([
'portal' => $categoryData['portal']->value,
'is_active' => true,
]);
} else {
$category->update([
'portal' => $categoryData['portal']->value,
'is_active' => true,
]);
}
foreach ($categoryData['translations'] as $locale => $translation) {
$category->translations()->updateOrCreate(
['locale' => $locale],
[
'name' => $translation['name'],
'slug' => $translation['slug'],
'description' => $translation['description'],
]
);
}
}
}
}

View file

@ -2,22 +2,51 @@
namespace Database\Seeders;
use App\Enums\Portal;
use App\Enums\RegistrationType;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\Services\Auth\UserRolePermissionSyncService;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder
{
/**
* Seed the application's database.
*/
public function run(): void
public function run(UserRolePermissionSyncService $rolePermissionSync): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
$this->call([
RolesAndPermissionsSeeder::class,
AdminPresetSeeder::class,
PaymentOptionSeeder::class,
CategorySeeder::class,
]);
$adminUser = User::firstOrCreate([
'email' => 'admin@presseportale.test',
], [
'name' => 'Portal Admin',
'password' => Hash::make('password'),
'portal' => Portal::Both->value,
'registration_type' => RegistrationType::ExistingLegacy->value,
'language' => 'de',
'is_active' => true,
]);
$rolePermissionSync->assignRoleAndSyncPermissions($adminUser, 'admin');
$testUser = User::firstOrCreate([
'email' => 'test@example.com',
], [
'name' => 'Test User',
'password' => Hash::make('password'),
'portal' => Portal::Both->value,
'registration_type' => RegistrationType::ExistingLegacy->value,
'language' => 'de',
'is_active' => true,
]);
$rolePermissionSync->assignRoleAndSyncPermissions($testUser, 'customer');
}
}

View file

@ -0,0 +1,78 @@
<?php
namespace Database\Seeders;
use App\Enums\PaymentOptionType;
use App\Models\PaymentOption;
use Illuminate\Database\Seeder;
class PaymentOptionSeeder extends Seeder
{
/**
* Run the database seeds.
*/
public function run(): void
{
$options = [
[
'article_number' => 'PR-MONTHLY-001',
'type' => PaymentOptionType::Recurring->value,
'price_cents' => 4900,
'currency' => 'EUR',
'interval' => 'monthly',
'is_hidden' => false,
'stripe_product_id' => 'prod_placeholder_monthly',
'stripe_price_id' => 'price_placeholder_monthly',
'translations' => [
'de' => ['name' => 'Monatspaket', 'description' => 'Monatliche Veroeffentlichungen'],
'en' => ['name' => 'Monthly package', 'description' => 'Monthly publication package'],
],
],
[
'article_number' => 'PR-YEARLY-001',
'type' => PaymentOptionType::Recurring->value,
'price_cents' => 49900,
'currency' => 'EUR',
'interval' => 'yearly',
'is_hidden' => false,
'stripe_product_id' => 'prod_placeholder_yearly',
'stripe_price_id' => 'price_placeholder_yearly',
'translations' => [
'de' => ['name' => 'Jahrespaket', 'description' => 'Jaehrliche Veroeffentlichungen'],
'en' => ['name' => 'Yearly package', 'description' => 'Yearly publication package'],
],
],
[
'article_number' => 'PR-BOOST-ONCE-001',
'type' => PaymentOptionType::Onetime->value,
'price_cents' => 9900,
'currency' => 'EUR',
'interval' => 'once',
'is_hidden' => false,
'stripe_product_id' => 'prod_placeholder_onetime',
'stripe_price_id' => 'price_placeholder_onetime',
'translations' => [
'de' => ['name' => 'Reichweiten-Boost', 'description' => 'Einmaliger Promotion-Boost'],
'en' => ['name' => 'Reach boost', 'description' => 'One-time promotion boost'],
],
],
];
foreach ($options as $optionData) {
$translations = $optionData['translations'];
unset($optionData['translations']);
$option = PaymentOption::updateOrCreate(
['article_number' => $optionData['article_number']],
$optionData
);
foreach ($translations as $locale => $translation) {
$option->translations()->updateOrCreate(
['locale' => $locale],
$translation
);
}
}
}
}

View file

@ -2,11 +2,9 @@
namespace Database\Seeders;
use App\Models\User;
// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use Spatie\Permission\Models\Role;
class RolesAndPermissionsSeeder extends Seeder
{
@ -15,65 +13,54 @@ class RolesAndPermissionsSeeder extends Seeder
*/
public function run(): void
{
// Rollen
Role::create(['name' => 'superadmin']);
Role::create(['name' => 'admin']);
Role::create(['name' => 'trader']);
Role::create(['name' => 'customer']);
$roles = [
'admin',
'editor',
'customer',
'api-only',
];
// Beispiel-Permissions
//Trader
Permission::create(['name' => 'products_manage']);
foreach ($roles as $role) {
Role::firstOrCreate(['name' => $role]);
}
//Customer
Permission::create(['name' => 'orders_view']);
$permissions = [
'press-releases:read',
'press-releases:write',
'press-releases:publish',
'press-release-images:write',
'companies:read',
'newsletter:subscribe',
'users:manage',
'roles:manage',
];
foreach ($permissions as $permission) {
Permission::firstOrCreate(['name' => $permission]);
}
//Admin
//CMS
Permission::create(['name' => 'cms_manage']);
Permission::create(['name' => 'cms_view']);
Permission::create(['name' => 'cms_create']);
Permission::create(['name' => 'cms_delete']);
Permission::create(['name' => 'cms_list']);
//User
Permission::create(['name' => 'user_edit']);
Permission::create(['name' => 'user_view']);
Permission::create(['name' => 'user_create']);
Permission::create(['name' => 'user_delete']);
Permission::create(['name' => 'user_list']);
//Superadmin
//alles
/*Permission::create(['name' => 'products_manage']);
Permission::create(['name' => 'orders_view']);
Permission::create(['name' => 'user_settings']);
Permission::create(['name' => 'system_settings']);
Permission::create(['name' => 'user_manage']);
Permission::create(['name' => 'order_manage']);
Permission::create(['name' => 'product_manage']);
Permission::create(['name' => 'user_view']);
Permission::create(['name' => 'order_view']);
Permission::create(['name' => 'product_view']);
Permission::create(['name' => 'system_view']);
Permission::create(['name' => 'user_create']);
Permission::create(['name' => 'order_create']);
Permission::create(['name' => 'product_create']);
Permission::create(['name' => 'system_create']);
Permission::create(['name' => 'order_edit']);
Permission::create(['name' => 'product_edit']);
Permission::create(['name' => 'system_edit']);
Permission::create(['name' => 'user_delete']);
Permission::create(['name' => 'order_delete']);
Permission::create(['name' => 'product_delete']);
Permission::create(['name' => 'system_delete']);
Permission::create(['name' => 'user_list']);
Permission::create(['name' => 'order_list']);
Permission::create(['name' => 'product_list']);
Permission::create(['name' => 'system_list']);*/
Role::findByName('admin')->syncPermissions($permissions);
Role::findByName('editor')->syncPermissions([
'press-releases:read',
'press-releases:write',
'press-releases:publish',
'press-release-images:write',
'companies:read',
'newsletter:subscribe',
]);
Role::findByName('customer')->syncPermissions([
'press-releases:read',
'press-releases:write',
'press-release-images:write',
'companies:read',
'newsletter:subscribe',
]);
Role::findByName('api-only')->syncPermissions([
'press-releases:read',
'press-releases:write',
'press-release-images:write',
'companies:read',
'newsletter:subscribe',
]);
}
}