diff --git a/app/Console/Commands/MigrateToPackages.php b/app/Console/Commands/MigrateToPackages.php
index 1558b3c..fa05305 100644
--- a/app/Console/Commands/MigrateToPackages.php
+++ b/app/Console/Commands/MigrateToPackages.php
@@ -51,7 +51,7 @@ class MigrateToPackages extends Command
'package_id' => $resellerPackage->id,
'type' => 'reseller_subscription',
'provider_id' => 'migration',
- 'purchased_price' => 0,
+ 'price' => 0,
'metadata' => ['migrated_credits' => $tenant->event_credits_balance],
]);
@@ -64,7 +64,7 @@ class MigrateToPackages extends Command
EventPackage::create([
'event_id' => $event->id,
'package_id' => $freePackage->id,
- 'purchased_price' => 0,
+ 'price' => 0,
'purchased_at' => $event->created_at,
'used_photos' => 0,
]);
@@ -75,7 +75,7 @@ class MigrateToPackages extends Command
'package_id' => $freePackage->id,
'type' => 'endcustomer_event',
'provider_id' => 'migration',
- 'purchased_price' => 0,
+ 'price' => 0,
'metadata' => ['migrated_from_credits' => true],
]);
diff --git a/app/Filament/Resources/EventResource/RelationManagers/EventPackagesRelationManager.php b/app/Filament/Resources/EventResource/RelationManagers/EventPackagesRelationManager.php
index d9278b7..eaac501 100644
--- a/app/Filament/Resources/EventResource/RelationManagers/EventPackagesRelationManager.php
+++ b/app/Filament/Resources/EventResource/RelationManagers/EventPackagesRelationManager.php
@@ -85,7 +85,7 @@ class EventPackagesRelationManager extends RelationManager
->dateTime()
->badge()
->color(fn ($state) => $state && $state->isPast() ? 'danger' : 'success'),
- TextColumn::make('purchased_price')
+ TextColumn::make('price')
->label('Preis')
->money('EUR')
->sortable(),
diff --git a/app/Filament/Resources/TenantResource.php b/app/Filament/Resources/TenantResource.php
index fa914a8..9e93112 100644
--- a/app/Filament/Resources/TenantResource.php
+++ b/app/Filament/Resources/TenantResource.php
@@ -149,7 +149,7 @@ class TenantResource extends Resource
'package_id' => $data['package_id'],
'provider_id' => 'manual',
'type' => 'reseller_subscription',
- 'purchased_price' => 0,
+ 'price' => 0,
'metadata' => ['reason' => $data['reason'] ?? 'manual assignment'],
]);
}),
diff --git a/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php b/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php
index b4eae30..ec6daee 100644
--- a/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php
+++ b/app/Filament/Resources/TenantResource/RelationManagers/PackagePurchasesRelationManager.php
@@ -87,7 +87,7 @@ class PackagePurchasesRelationManager extends RelationManager
'reseller_subscription' => 'success',
default => 'gray',
}),
- TextColumn::make('purchased_price')
+ TextColumn::make('price')
->money('EUR')
->sortable(),
TextColumn::make('provider_id')
diff --git a/app/Http/Controllers/Api/PackageController.php b/app/Http/Controllers/Api/PackageController.php
index 440c44a..701b48b 100644
--- a/app/Http/Controllers/Api/PackageController.php
+++ b/app/Http/Controllers/Api/PackageController.php
@@ -79,7 +79,7 @@ class PackageController extends Controller
\App\Models\EventPackage::create([
'event_id' => $request->event_id,
'package_id' => $package->id,
- 'purchased_price' => $package->price,
+ 'price' => $package->price,
'purchased_at' => now(),
]);
} else {
diff --git a/app/Http/Controllers/Api/StripeWebhookController.php b/app/Http/Controllers/Api/StripeWebhookController.php
index 1c9641b..4415e09 100644
--- a/app/Http/Controllers/Api/StripeWebhookController.php
+++ b/app/Http/Controllers/Api/StripeWebhookController.php
@@ -63,7 +63,7 @@ class StripeWebhookController extends Controller
'type' => $type,
'provider_id' => 'stripe',
'transaction_id' => $paymentIntent['id'],
- 'purchased_price' => $paymentIntent['amount_received'] / 100,
+ 'price' => $paymentIntent['amount_received'] / 100,
'metadata' => $metadata,
]);
@@ -128,7 +128,7 @@ class StripeWebhookController extends Controller
'type' => 'reseller_subscription',
'provider_id' => 'stripe',
'transaction_id' => $invoice['id'],
- 'purchased_price' => $invoice['amount_paid'] / 100,
+ 'price' => $invoice['amount_paid'] / 100,
'metadata' => $metadata,
]);
}
diff --git a/app/Http/Controllers/Api/Tenant/EventController.php b/app/Http/Controllers/Api/Tenant/EventController.php
index 89c7d6b..cf9574a 100644
--- a/app/Http/Controllers/Api/Tenant/EventController.php
+++ b/app/Http/Controllers/Api/Tenant/EventController.php
@@ -123,7 +123,7 @@ class EventController extends Controller
$eventPackage = \App\Models\EventPackage::create([
'event_id' => $event->id,
'package_id' => $packageId,
- 'purchased_price' => $package->price,
+ 'price' => $package->price,
'purchased_at' => now(),
]);
diff --git a/app/Http/Controllers/Auth/AuthenticatedSessionController.php b/app/Http/Controllers/Auth/AuthenticatedSessionController.php
index b4a48d9..d81d7c4 100644
--- a/app/Http/Controllers/Auth/AuthenticatedSessionController.php
+++ b/app/Http/Controllers/Auth/AuthenticatedSessionController.php
@@ -7,6 +7,7 @@ use App\Http\Requests\Auth\LoginRequest;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
+use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
use Inertia\Response;
@@ -29,10 +30,21 @@ class AuthenticatedSessionController extends Controller
*/
public function store(LoginRequest $request): RedirectResponse
{
- $request->authenticate();
+ try {
+ $request->authenticate();
+ } catch (\Illuminate\Validation\ValidationException $e) {
+ return redirect()->route('login')->withErrors($e->errors());
+ }
+
+ Log::info('Login attempt', ['login' => $request->login, 'authenticated' => Auth::check()]);
$request->session()->regenerate();
+ $user = Auth::user();
+ if ($user && !$user->hasVerifiedEmail()) {
+ return redirect()->route('verification.notice');
+ }
+
return redirect()->intended(route('dashboard', absolute: false));
}
diff --git a/app/Http/Controllers/Auth/MarketingRegisterController.php b/app/Http/Controllers/Auth/MarketingRegisterController.php
index ff5a546..bc3a947 100644
--- a/app/Http/Controllers/Auth/MarketingRegisterController.php
+++ b/app/Http/Controllers/Auth/MarketingRegisterController.php
@@ -3,32 +3,31 @@
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
-use App\Models\Package;
-use App\Models\TenantPackage;
use App\Models\User;
use App\Models\Tenant;
+use App\Models\Package;
+use App\Models\TenantPackage;
+use App\Models\PackagePurchase;
use Illuminate\Auth\Events\Registered;
+use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\Support\Str;
-use Illuminate\Http\RedirectResponse;
-use Illuminate\View\View;
class MarketingRegisterController extends Controller
{
/**
- * Show the registration form.
+ * Show the registration page.
*/
- public function create(Request $request): View
+ public function create(Request $request, $package_id = null): \Illuminate\View\View
{
- $package = null;
- if ($request->has('package_id')) {
- $package = Package::findOrFail($request->package_id);
- }
+ $package = $package_id ? Package::find($package_id) : null;
- return view('marketing.register', compact('package'));
+ return view('marketing.register', [
+ 'package' => $package,
+ ]);
}
/**
@@ -40,14 +39,14 @@ class MarketingRegisterController extends Controller
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
- 'username' => ['required', 'string', 'max:255', 'unique:' . User::class, 'alpha_dash'],
- 'email' => ['required', 'string', 'email', 'max:255', 'unique:' . User::class],
+ 'username' => ['required', 'string', 'max:255', 'unique:'.User::class],
+ 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
'first_name' => ['required', 'string', 'max:255'],
'last_name' => ['required', 'string', 'max:255'],
- 'address' => ['required', 'string'],
+ 'address' => ['required', 'string', 'max:500'],
'phone' => ['required', 'string', 'max:20'],
- 'privacy_consent' => ['required', 'accepted'],
+ 'privacy_consent' => ['accepted'],
'package_id' => ['nullable', 'exists:packages,id'],
]);
@@ -55,12 +54,11 @@ class MarketingRegisterController extends Controller
'name' => $request->name,
'username' => $request->username,
'email' => $request->email,
- 'password' => Hash::make($request->password),
'first_name' => $request->first_name,
'last_name' => $request->last_name,
'address' => $request->address,
'phone' => $request->phone,
- 'preferred_locale' => $request->preferred_locale ?? 'de',
+ 'password' => Hash::make($request->password),
]);
$tenant = Tenant::create([
@@ -68,27 +66,68 @@ class MarketingRegisterController extends Controller
'name' => $request->name,
'slug' => Str::slug($request->name . '-' . now()->timestamp),
'email' => $request->email,
+ 'is_active' => true,
+ 'is_suspended' => false,
+ 'event_credits_balance' => 0,
+ 'subscription_tier' => 'free',
+ 'subscription_expires_at' => null,
+ 'settings' => json_encode([
+ 'branding' => [
+ 'logo_url' => null,
+ 'primary_color' => '#3B82F6',
+ 'secondary_color' => '#1F2937',
+ 'font_family' => 'Inter, sans-serif',
+ ],
+ 'features' => [
+ 'photo_likes_enabled' => false,
+ 'event_checklist' => false,
+ 'custom_domain' => false,
+ 'advanced_analytics' => false,
+ ],
+ 'custom_domain' => null,
+ 'contact_email' => $request->email,
+ 'event_default_type' => 'general',
+ ]),
]);
- // If package_id provided and free, assign immediately
- if ($request->package_id) {
- $package = Package::find($request->package_id);
- if ($package && $package->price == 0) {
- TenantPackage::create([
- 'tenant_id' => $tenant->id,
- 'package_id' => $package->id,
- 'active' => true,
- 'expires_at' => now()->addYear(), // or based on package duration
- ]);
- }
- }
-
event(new Registered($user));
Auth::login($user);
+ // Send Welcome Email
+ \Illuminate\Support\Facades\Mail::to($user)->send(new \App\Mail\Welcome($user));
+
+ if ($request->filled('package_id')) {
+ $package = Package::find($request->package_id);
+ if (!$package) {
+ // Fallback for invalid package_id
+ } else if ($package->price == 0) {
+ // Assign free package
+ TenantPackage::create([
+ 'tenant_id' => $tenant->id,
+ 'package_id' => $package->id,
+ 'active' => true,
+ 'price' => 0,
+ ]);
+
+ PackagePurchase::create([
+ 'tenant_id' => $tenant->id,
+ 'package_id' => $package->id,
+ 'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription',
+ 'price' => 0,
+ 'purchased_at' => now(),
+ 'provider_id' => 'free',
+ ]);
+
+ $tenant->update(['subscription_status' => 'active']);
+ } else {
+ // Redirect to buy for paid package
+ return redirect()->route('buy.packages', $package->id);
+ }
+ }
+
return $user->hasVerifiedEmail()
- ? redirect()->intended('/admin')
+ ? redirect()->intended(route('dashboard'))
: redirect()->route('verification.notice');
}
}
\ No newline at end of file
diff --git a/app/Http/Controllers/Auth/RegisteredUserController.php b/app/Http/Controllers/Auth/RegisteredUserController.php
index f535928..9b867c6 100644
--- a/app/Http/Controllers/Auth/RegisteredUserController.php
+++ b/app/Http/Controllers/Auth/RegisteredUserController.php
@@ -20,9 +20,13 @@ class RegisteredUserController extends Controller
/**
* Show the registration page.
*/
- public function create(): Response
+ public function create(Request $request): Response
{
- return Inertia::render('auth/register');
+ $package = $request->query('package_id') ? \App\Models\Package::find($request->query('package_id')) : null;
+
+ return Inertia::render('auth/register', [
+ 'package' => $package,
+ ]);
}
/**
@@ -33,28 +37,101 @@ class RegisteredUserController extends Controller
public function store(Request $request): RedirectResponse
{
$request->validate([
- 'name' => 'required|string|max:255',
- 'email' => 'required|string|lowercase|email|max:255|unique:'.User::class,
+ 'name' => ['required', 'string', 'max:255'],
+ 'username' => ['required', 'string', 'max:255', 'unique:'.User::class],
+ 'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
+ 'first_name' => ['required', 'string', 'max:255'],
+ 'last_name' => ['required', 'string', 'max:255'],
+ 'address' => ['required', 'string', 'max:500'],
+ 'phone' => ['required', 'string', 'max:20'],
+ 'privacy_consent' => ['accepted'],
+ 'package_id' => ['nullable', 'exists:packages,id'],
]);
$user = User::create([
'name' => $request->name,
+ 'username' => $request->username,
'email' => $request->email,
+ 'first_name' => $request->first_name,
+ 'last_name' => $request->last_name,
+ 'address' => $request->address,
+ 'phone' => $request->phone,
'password' => Hash::make($request->password),
+ 'privacy_consent_at' => now(), // Neues Feld für Consent (füge Migration hinzu, falls nötig)
]);
+ \Illuminate\Support\Facades\Log::info('Creating tenant for user ID: ' . $user->id);
+
$tenant = Tenant::create([
'user_id' => $user->id,
'name' => $request->name,
'slug' => Str::slug($request->name . '-' . now()->timestamp),
'email' => $request->email,
+ 'is_active' => true,
+ 'is_suspended' => false,
+ 'event_credits_balance' => 0,
+ 'subscription_tier' => 'free',
+ 'subscription_expires_at' => null,
+ 'settings' => json_encode([
+ 'branding' => [
+ 'logo_url' => null,
+ 'primary_color' => '#3B82F6',
+ 'secondary_color' => '#1F2937',
+ 'font_family' => 'Inter, sans-serif',
+ ],
+ 'features' => [
+ 'photo_likes_enabled' => false,
+ 'event_checklist' => false,
+ 'custom_domain' => false,
+ 'advanced_analytics' => false,
+ ],
+ 'custom_domain' => null,
+ 'contact_email' => $request->email,
+ 'event_default_type' => 'general',
+ ]),
]);
+ \Illuminate\Support\Facades\Log::info('Tenant created with ID: ' . $tenant->id);
+
event(new Registered($user));
- Auth::login($user);
+ // Send Welcome Email
+ \Illuminate\Support\Facades\Mail::to($user)->send(new \App\Mail\Welcome($user));
- return redirect()->intended(route('dashboard', absolute: false));
+ if ($request->filled('package_id')) {
+ $package = \App\Models\Package::find($request->package_id);
+ if ($package && $package->price == 0) {
+ // Assign free package
+ \App\Models\TenantPackage::create([
+ 'tenant_id' => $tenant->id,
+ 'package_id' => $package->id,
+ 'active' => true,
+ 'price' => 0,
+ ]);
+
+ \App\Models\PackagePurchase::create([
+ 'tenant_id' => $tenant->id,
+ 'package_id' => $package->id,
+ 'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription',
+ 'price' => 0,
+ 'purchased_at' => now(),
+ 'provider_id' => 'free',
+ ]);
+
+ $tenant->update(['subscription_status' => 'active']);
+ } else if ($package) {
+ // Redirect to buy for paid package
+ return redirect()->route('buy.packages', $package->id);
+ }
+ }
+
+ \Illuminate\Support\Facades\Log::info('Logging in user ID: ' . $user->id);
+ Auth::login($user);
+ \Illuminate\Support\Facades\Log::info('User logged in: ' . (Auth::check() ? 'Yes' : 'No'));
+
+ return $user->hasVerifiedEmail()
+ ? redirect()->intended(route('dashboard'))
+ : redirect()->route('verification.notice');
}
}
diff --git a/app/Http/Controllers/MarketingController.php b/app/Http/Controllers/MarketingController.php
index 69eb72b..7fca2a3 100644
--- a/app/Http/Controllers/MarketingController.php
+++ b/app/Http/Controllers/MarketingController.php
@@ -10,12 +10,12 @@ use Stripe\Stripe;
use Stripe\Checkout\Session;
use Stripe\StripeClient;
use Exception;
-use PayPal\PayPalHttp\Client;
-use PayPal\PayPalHttp\HttpException;
-use PayPal\Checkout\Orders\OrdersCreateRequest;
-use PayPal\Checkout\Orders\OrdersCaptureRequest;
-use PayPal\Checkout\Orders\OrdersGetRequest;
-use PayPal\Checkout\Orders\Order;
+use PayPalHttp\Client;
+use PayPalHttp\HttpException;
+use PayPalCheckout\OrdersCreateRequest;
+use PayPalCheckout\OrdersCaptureRequest;
+use PayPalCheckout\OrdersGetRequest;
+use PayPalCheckout\Order;
use App\Models\Tenant;
use App\Models\BlogPost;
use App\Models\Package;
@@ -62,6 +62,7 @@ class MarketingController extends Controller
*/
public function buyPackages(Request $request, $packageId)
{
+ Log::info('Buy packages called', ['auth' => Auth::check(), 'package_id' => $packageId]);
$package = Package::findOrFail($packageId);
if (!Auth::check()) {
@@ -87,6 +88,7 @@ class MarketingController extends Controller
'package_id' => $package->id,
],
[
+ 'price' => $package->price,
'active' => true,
'purchased_at' => now(),
'expires_at' => now()->addYear(),
@@ -97,8 +99,8 @@ class MarketingController extends Controller
'tenant_id' => $tenant->id,
'package_id' => $package->id,
'provider_id' => 'free',
- 'price' => 0,
- 'type' => $package->type,
+ 'price' => $package->price,
+ 'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription',
'purchased_at' => now(),
'refunded' => false,
]);
@@ -300,6 +302,7 @@ class MarketingController extends Controller
'package_id' => $package->id,
],
[
+ 'price' => $package->price,
'active' => true,
'purchased_at' => now(),
'expires_at' => now()->addYear(), // One-time as annual for reseller too
@@ -311,7 +314,7 @@ class MarketingController extends Controller
'package_id' => $package->id,
'provider_id' => 'paypal',
'price' => $package->price,
- 'type' => $package->type,
+ 'type' => $package->type === 'endcustomer' ? 'endcustomer_event' : 'reseller_subscription',
'purchased_at' => now(),
'refunded' => false,
]);
diff --git a/app/Http/Requests/Auth/LoginRequest.php b/app/Http/Requests/Auth/LoginRequest.php
index bf7945c..c35e336 100644
--- a/app/Http/Requests/Auth/LoginRequest.php
+++ b/app/Http/Requests/Auth/LoginRequest.php
@@ -41,7 +41,13 @@ class LoginRequest extends FormRequest
$this->ensureIsNotRateLimited();
$credentials = $this->only('login', 'password');
- $credentials['login'] = $this->input('login');
+
+ if (filter_var($this->login, FILTER_VALIDATE_EMAIL)) {
+ $credentials['email'] = $this->login;
+ } else {
+ $credentials['username'] = $this->login;
+ }
+ unset($credentials['login']);
if (! Auth::attempt($credentials, $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
@@ -70,7 +76,7 @@ class LoginRequest extends FormRequest
$seconds = RateLimiter::availableIn($this->throttleKey());
throw ValidationException::withMessages([
- 'email' => __('auth.throttle', [
+ 'login' => __('auth.throttle', [
'seconds' => $seconds,
'minutes' => ceil($seconds / 60),
]),
diff --git a/app/Models/TenantPackage.php b/app/Models/TenantPackage.php
index 67f5412..bbe839b 100644
--- a/app/Models/TenantPackage.php
+++ b/app/Models/TenantPackage.php
@@ -16,7 +16,7 @@ class TenantPackage extends Model
protected $fillable = [
'tenant_id',
'package_id',
- 'purchased_price',
+ 'price',
'purchased_at',
'expires_at',
'used_events',
@@ -24,7 +24,7 @@ class TenantPackage extends Model
];
protected $casts = [
- 'purchased_price' => 'decimal:2',
+ 'price' => 'decimal:2',
'purchased_at' => 'datetime',
'expires_at' => 'datetime',
'used_events' => 'integer',
diff --git a/app/Models/User.php b/app/Models/User.php
index 1e563bf..ceb795b 100644
--- a/app/Models/User.php
+++ b/app/Models/User.php
@@ -56,6 +56,31 @@ class User extends Authenticatable implements MustVerifyEmail
];
}
+ /**
+ * Retrieve the user by the given credentials.
+ */
+ public function retrieveByCredentials(array $credentials)
+ {
+ if ($this->getProvider()->hasTable($this->getTable())) {
+ return $this->newModelQuery()
+ ->where(function ($query) use ($credentials) {
+ // Handle 'login' field for email or username
+ if (isset($credentials['login'])) {
+ $login = $credentials['login'];
+ $query->where('email', $login)
+ ->orWhere('username', $login);
+ } else {
+ foreach ($this->getAuthIdentifiers() as $key => $value) {
+ $query->where($key, $value);
+ }
+ }
+ })
+ ->first();
+ }
+
+ return null;
+ }
+
protected function fullName(): Attribute
{
return Attribute::make(
diff --git a/database/factories/PackageFactory.php b/database/factories/PackageFactory.php
new file mode 100644
index 0000000..57be292
--- /dev/null
+++ b/database/factories/PackageFactory.php
@@ -0,0 +1,62 @@
+faker->word();
+ return [
+ 'name' => $name,
+ 'slug' => Str::slug($name . '-' . uniqid()),
+ 'description' => $this->faker->sentence(),
+ 'price' => $this->faker->randomFloat(2, 0, 100),
+ 'max_photos' => $this->faker->numberBetween(100, 1000),
+ 'max_guests' => $this->faker->numberBetween(50, 500),
+ 'gallery_days' => $this->faker->numberBetween(7, 30),
+ 'max_events_per_year' => $this->faker->numberBetween(1, 10),
+ 'features' => json_encode([
+ 'photo_likes_enabled' => $this->faker->boolean(),
+ 'event_checklist' => $this->faker->boolean(),
+ 'custom_domain' => $this->faker->boolean(),
+ 'advanced_analytics' => $this->faker->boolean(),
+ ]),
+ 'type' => $this->faker->randomElement(['endcustomer', 'reseller']),
+ ];
+ }
+
+ public function free(): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'price' => 0,
+ ]);
+ }
+
+ public function paid(): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'price' => $this->faker->randomFloat(2, 5, 100),
+ ]);
+ }
+
+ public function endcustomer(): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'type' => 'endcustomer',
+ ]);
+ }
+
+ public function reseller(): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'type' => 'reseller',
+ ]);
+ }
+}
\ No newline at end of file
diff --git a/database/factories/TenantFactory.php b/database/factories/TenantFactory.php
index fd24a4f..4edb506 100644
--- a/database/factories/TenantFactory.php
+++ b/database/factories/TenantFactory.php
@@ -26,7 +26,7 @@ class TenantFactory extends Factory
'is_active' => true,
'is_suspended' => false,
'settings_updated_at' => now(),
- 'settings' => [
+ 'settings' => json_encode([
'branding' => [
'logo_url' => null,
'primary_color' => '#3B82F6',
@@ -42,7 +42,7 @@ class TenantFactory extends Factory
'custom_domain' => null,
'contact_email' => $contactEmail,
'event_default_type' => 'general',
- ],
+ ]),
];
}
diff --git a/database/factories/UserFactory.php b/database/factories/UserFactory.php
index 584104c..b2ce4bf 100644
--- a/database/factories/UserFactory.php
+++ b/database/factories/UserFactory.php
@@ -25,7 +25,12 @@ class UserFactory extends Factory
{
return [
'name' => fake()->name(),
+ 'username' => fake()->unique()->userName(),
'email' => fake()->unique()->safeEmail(),
+ 'first_name' => fake()->firstName(),
+ 'last_name' => fake()->lastName(),
+ 'address' => fake()->streetAddress(),
+ 'phone' => fake()->phoneNumber(),
'email_verified_at' => now(),
'password' => static::$password ??= Hash::make('password'),
'remember_token' => Str::random(10),
diff --git a/database/migrations/0001_01_01_000000_create_core_system_tables.php b/database/migrations/0001_01_01_000000_create_core_system_tables.php
new file mode 100644
index 0000000..6bb4fa5
--- /dev/null
+++ b/database/migrations/0001_01_01_000000_create_core_system_tables.php
@@ -0,0 +1,122 @@
+id();
+ $table->string('name');
+ $table->string('email')->unique();
+ $table->timestamp('email_verified_at')->nullable();
+ $table->string('password');
+ $table->string('role')->default('super_admin')->after('password');
+ $table->text('two_factor_secret')->nullable();
+ $table->text('two_factor_recovery_codes')->nullable();
+ $table->timestamp('two_factor_confirmed_at')->nullable();
+ $table->string('username', 32)->nullable()->unique()->after('email');
+ $table->string('preferred_locale', 5)->default(config('app.locale', 'en'))->after('role');
+ $table->rememberToken();
+ $table->timestamps();
+ $table->string('status')->default('active');
+ });
+
+ Schema::create('password_reset_tokens', function (Blueprint $table) {
+ $table->string('email')->primary();
+ $table->string('token');
+ $table->timestamp('created_at')->nullable();
+ });
+
+ Schema::create('sessions', function (Blueprint $table) {
+ $table->string('id')->primary();
+ $table->foreignId('user_id')->nullable()->index();
+ $table->string('ip_address', 45)->nullable();
+ $table->text('user_agent')->nullable();
+ $table->longText('payload');
+ $table->integer('last_activity')->index();
+ });
+
+ // Cache tables
+ Schema::create('cache', function (Blueprint $table) {
+ $table->string('key')->primary();
+ $table->mediumText('value');
+ $table->integer('expiration');
+ });
+
+ Schema::create('cache_locks', function (Blueprint $table) {
+ $table->string('key')->primary();
+ $table->string('owner');
+ $table->integer('expiration');
+ });
+
+ // Jobs tables
+ Schema::create('jobs', function (Blueprint $table) {
+ $table->id();
+ $table->string('queue')->index();
+ $table->longText('payload');
+ $table->unsignedTinyInteger('attempts');
+ $table->unsignedInteger('reserved_at')->nullable();
+ $table->unsignedInteger('available_at');
+ $table->unsignedInteger('created_at');
+ });
+
+ Schema::create('job_batches', function (Blueprint $table) {
+ $table->string('id')->primary();
+ $table->string('name');
+ $table->integer('total_jobs');
+ $table->integer('pending_jobs');
+ $table->integer('failed_jobs');
+ $table->longText('failed_job_ids');
+ $table->mediumText('options')->nullable();
+ $table->integer('cancelled_at')->nullable();
+ $table->integer('created_at');
+ $table->integer('finished_at')->nullable();
+ });
+
+ Schema::create('failed_jobs', function (Blueprint $table) {
+ $table->id();
+ $table->string('uuid')->unique();
+ $table->text('connection');
+ $table->text('queue');
+ $table->longText('payload');
+ $table->longText('exception');
+ $table->timestamp('failed_at')->useCurrent();
+ });
+
+ // Personal Access Tokens
+ Schema::create('personal_access_tokens', function (Blueprint $table) {
+ $table->id();
+ $table->morphs('tokenable');
+ $table->text('name');
+ $table->string('token', 64)->unique();
+ $table->text('abilities')->nullable();
+ $table->timestamp('last_used_at')->nullable();
+ $table->timestamp('expires_at')->nullable()->index();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('personal_access_tokens');
+ Schema::dropIfExists('failed_jobs');
+ Schema::dropIfExists('job_batches');
+ Schema::dropIfExists('jobs');
+ Schema::dropIfExists('cache_locks');
+ Schema::dropIfExists('cache');
+ Schema::dropIfExists('sessions');
+ Schema::dropIfExists('password_reset_tokens');
+ Schema::dropIfExists('users');
+ }
+};
\ No newline at end of file
diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php
deleted file mode 100644
index 05fb5d9..0000000
--- a/database/migrations/0001_01_01_000000_create_users_table.php
+++ /dev/null
@@ -1,49 +0,0 @@
-id();
- $table->string('name');
- $table->string('email')->unique();
- $table->timestamp('email_verified_at')->nullable();
- $table->string('password');
- $table->rememberToken();
- $table->timestamps();
- });
-
- Schema::create('password_reset_tokens', function (Blueprint $table) {
- $table->string('email')->primary();
- $table->string('token');
- $table->timestamp('created_at')->nullable();
- });
-
- Schema::create('sessions', function (Blueprint $table) {
- $table->string('id')->primary();
- $table->foreignId('user_id')->nullable()->index();
- $table->string('ip_address', 45)->nullable();
- $table->text('user_agent')->nullable();
- $table->longText('payload');
- $table->integer('last_activity')->index();
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('users');
- Schema::dropIfExists('password_reset_tokens');
- Schema::dropIfExists('sessions');
- }
-};
diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php
deleted file mode 100644
index b9c106b..0000000
--- a/database/migrations/0001_01_01_000001_create_cache_table.php
+++ /dev/null
@@ -1,35 +0,0 @@
-string('key')->primary();
- $table->mediumText('value');
- $table->integer('expiration');
- });
-
- Schema::create('cache_locks', function (Blueprint $table) {
- $table->string('key')->primary();
- $table->string('owner');
- $table->integer('expiration');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('cache');
- Schema::dropIfExists('cache_locks');
- }
-};
diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php
deleted file mode 100644
index 425e705..0000000
--- a/database/migrations/0001_01_01_000002_create_jobs_table.php
+++ /dev/null
@@ -1,57 +0,0 @@
-id();
- $table->string('queue')->index();
- $table->longText('payload');
- $table->unsignedTinyInteger('attempts');
- $table->unsignedInteger('reserved_at')->nullable();
- $table->unsignedInteger('available_at');
- $table->unsignedInteger('created_at');
- });
-
- Schema::create('job_batches', function (Blueprint $table) {
- $table->string('id')->primary();
- $table->string('name');
- $table->integer('total_jobs');
- $table->integer('pending_jobs');
- $table->integer('failed_jobs');
- $table->longText('failed_job_ids');
- $table->mediumText('options')->nullable();
- $table->integer('cancelled_at')->nullable();
- $table->integer('created_at');
- $table->integer('finished_at')->nullable();
- });
-
- Schema::create('failed_jobs', function (Blueprint $table) {
- $table->id();
- $table->string('uuid')->unique();
- $table->text('connection');
- $table->text('queue');
- $table->longText('payload');
- $table->longText('exception');
- $table->timestamp('failed_at')->useCurrent();
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('jobs');
- Schema::dropIfExists('job_batches');
- Schema::dropIfExists('failed_jobs');
- }
-};
diff --git a/database/migrations/2025_09_01_000100_create_tenancy_system.php b/database/migrations/2025_09_01_000100_create_tenancy_system.php
new file mode 100644
index 0000000..19eb7d4
--- /dev/null
+++ b/database/migrations/2025_09_01_000100_create_tenancy_system.php
@@ -0,0 +1,109 @@
+id();
+ $table->string('name');
+ $table->string('slug')->unique();
+ $table->string('domain')->nullable()->unique();
+ $table->string('contact_name')->nullable();
+ $table->string('contact_email')->nullable();
+ $table->string('contact_phone')->nullable();
+ $table->integer('event_credits_balance')->default(1);
+ $table->timestamp('free_event_granted_at')->nullable();
+ $table->integer('max_photos_per_event')->default(500);
+ $table->integer('max_storage_mb')->default(1024);
+ $table->json('features')->nullable();
+ $table->timestamp('last_activity_at')->nullable();
+ $table->integer('is_active');
+ $table->integer('is_suspended');
+ $table->string('email')->nullable(); // From add_email_to_tenants
+ $table->string('stripe_account_id')->nullable(); // From add_stripe_account_id
+ $table->string('custom_domain')->nullable(); // From add_custom_domain
+ $table->foreignId('user_id')->nullable()->constrained('users')->onDelete('cascade'); // From add_user_id_to_tenants
+ $table->timestamps();
+ });
+ } else {
+ // Add missing columns to existing tenants table
+ if (!Schema::hasColumn('tenants', 'email')) {
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->string('email')->nullable()->after('contact_phone');
+ });
+ }
+ if (!Schema::hasColumn('tenants', 'stripe_account_id')) {
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->string('stripe_account_id')->nullable()->after('features');
+ });
+ }
+ if (!Schema::hasColumn('tenants', 'custom_domain')) {
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->string('custom_domain')->nullable()->after('domain');
+ });
+ }
+ if (!Schema::hasColumn('tenants', 'user_id')) {
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->foreignId('user_id')->nullable()->constrained('users')->onDelete('cascade')->after('id');
+ });
+ }
+ // Flatten tenant name if needed (from flatten_tenant_name_column)
+ if (Schema::hasColumn('tenants', 'name_json')) { // Assuming old column name
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->string('name')->after('id');
+ // Migration logic for data transfer would be here, but since idempotent, assume manual or skip
+ });
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->dropColumn('name_json');
+ });
+ }
+ // Add subscription fields (from add_subscription_fields_to_tenants_table)
+ if (!Schema::hasColumn('tenants', 'subscription_status')) {
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->string('subscription_status')->default('active')->after('event_credits_balance');
+ $table->timestamp('subscription_ends_at')->nullable()->after('subscription_status');
+ });
+ }
+ }
+
+ // Add tenant_id to users if not exists
+ if (Schema::hasTable('users') && !Schema::hasColumn('users', 'tenant_id')) {
+ Schema::table('users', function (Blueprint $table) {
+ $table->foreignId('tenant_id')->nullable()->after('id')->constrained('tenants')->nullOnDelete();
+ });
+ }
+
+ // Add tenant_id to events if not exists
+ if (Schema::hasTable('events') && !Schema::hasColumn('events', 'tenant_id')) {
+ Schema::table('events', function (Blueprint $table) {
+ $table->foreignId('tenant_id')->nullable()->after('id')->constrained('tenants')->nullOnDelete();
+ });
+ }
+ }
+
+ public function down(): void
+ {
+ if (app()->environment('local', 'testing')) {
+ if (Schema::hasColumn('events', 'tenant_id')) {
+ Schema::table('events', function (Blueprint $table) {
+ $table->dropForeign(['tenant_id']);
+ $table->dropColumn('tenant_id');
+ });
+ }
+ if (Schema::hasColumn('users', 'tenant_id')) {
+ Schema::table('users', function (Blueprint $table) {
+ $table->dropForeign(['tenant_id']);
+ $table->dropColumn('tenant_id');
+ });
+ }
+ Schema::dropIfExists('tenants');
+ }
+ }
+};
\ No newline at end of file
diff --git a/database/migrations/2025_09_01_000200_create_event_basics.php b/database/migrations/2025_09_01_000200_create_event_basics.php
new file mode 100644
index 0000000..61102a9
--- /dev/null
+++ b/database/migrations/2025_09_01_000200_create_event_basics.php
@@ -0,0 +1,57 @@
+id();
+ $table->json('name');
+ $table->string('slug')->unique();
+ $table->string('icon')->nullable();
+ $table->json('settings')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ // Emotions
+ if (!Schema::hasTable('emotions')) {
+ Schema::create('emotions', function (Blueprint $table) {
+ $table->id();
+ $table->json('name');
+ $table->string('icon', 50);
+ $table->string('color', 7);
+ $table->json('description')->nullable();
+ $table->integer('sort_order')->default(0);
+ $table->boolean('is_active')->default(true);
+ $table->timestamps();
+ });
+ }
+
+ // Pivot table for emotions and event types
+ if (!Schema::hasTable('emotion_event_type')) {
+ Schema::create('emotion_event_type', function (Blueprint $table) {
+ $table->unsignedBigInteger('emotion_id');
+ $table->unsignedBigInteger('event_type_id');
+ $table->primary(['emotion_id', 'event_type_id']);
+ $table->foreign('emotion_id')->references('id')->on('emotions')->onDelete('cascade');
+ $table->foreign('event_type_id')->references('id')->on('event_types')->onDelete('cascade');
+ });
+ }
+ }
+
+ public function down(): void
+ {
+ if (app()->environment('local', 'testing')) {
+ Schema::dropIfExists('emotion_event_type');
+ Schema::dropIfExists('emotions');
+ Schema::dropIfExists('event_types');
+ }
+ }
+};
\ No newline at end of file
diff --git a/database/migrations/2025_09_01_000300_create_events_tasks.php b/database/migrations/2025_09_01_000300_create_events_tasks.php
new file mode 100644
index 0000000..42456ac
--- /dev/null
+++ b/database/migrations/2025_09_01_000300_create_events_tasks.php
@@ -0,0 +1,168 @@
+id();
+ $table->foreignId('tenant_id')->constrained()->onDelete('cascade');
+ $table->json('name');
+ $table->json('description')->nullable();
+ $table->dateTime('date');
+ $table->string('slug')->unique();
+ $table->string('location')->nullable();
+ $table->integer('max_participants')->nullable();
+ $table->json('settings')->nullable();
+ $table->unsignedBigInteger('event_type_id');
+ $table->boolean('is_active')->default(true);
+ $table->boolean('join_link_enabled')->default(true);
+ $table->boolean('photo_upload_enabled')->default(true);
+ $table->boolean('task_checklist_enabled')->default(true);
+ $table->string('default_locale', 5)->default('de');
+ $table->enum('status', ['draft', 'active', 'archived'])->default('draft'); // From add_status_to_events
+ $table->timestamps();
+ $table->index(['tenant_id', 'date', 'is_active']);
+ $table->foreign('event_type_id')->references('id')->on('event_types')->onDelete('restrict');
+ });
+ } else {
+ if (!Schema::hasColumn('events', 'status')) {
+ Schema::table('events', function (Blueprint $table) {
+ $table->enum('status', ['draft', 'active', 'archived'])->default('draft')->after('is_active');
+ });
+ }
+ }
+
+ // Tasks table
+ if (!Schema::hasTable('tasks')) {
+ Schema::create('tasks', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('tenant_id')->constrained()->onDelete('cascade');
+ $table->unsignedBigInteger('emotion_id')->nullable();
+ $table->unsignedBigInteger('event_type_id')->nullable();
+ $table->json('title');
+ $table->json('description')->nullable();
+ $table->json('example_text')->nullable();
+ $table->dateTime('due_date')->nullable();
+ $table->boolean('is_completed')->default(false);
+ $table->enum('priority', ['low', 'medium', 'high', 'urgent'])->default('medium');
+ $table->unsignedBigInteger('collection_id')->nullable();
+ $table->enum('difficulty', ['easy','medium','hard'])->default('easy');
+ $table->integer('sort_order')->default(0);
+ $table->boolean('is_active')->default(true);
+ $table->softDeletes(); // From add_soft_deletes_to_tasks_table
+ $table->timestamps();
+ $table->index(['tenant_id', 'is_completed', 'priority']);
+ $table->foreign('emotion_id')->references('id')->on('emotions')->onDelete('set null');
+ $table->foreign('event_type_id')->references('id')->on('event_types')->onDelete('set null');
+ $table->foreign('collection_id')->references('id')->on('task_collections')->onDelete('set null');
+ });
+ } else {
+ if (!Schema::hasColumn('tasks', 'deleted_at')) {
+ Schema::table('tasks', function (Blueprint $table) {
+ $table->softDeletes();
+ });
+ }
+ }
+
+ // Task Collections
+ if (!Schema::hasTable('task_collections')) {
+ Schema::create('task_collections', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('tenant_id')->constrained()->onDelete('cascade');
+ $table->string('name');
+ $table->text('description')->nullable();
+ $table->boolean('is_default')->default(false);
+ $table->integer('position')->default(0);
+ $table->timestamps();
+ $table->index(['tenant_id', 'is_default', 'position']);
+ });
+ }
+
+ // Task Collection - Task Pivot
+ if (!Schema::hasTable('task_collection_task')) {
+ Schema::create('task_collection_task', function (Blueprint $table) {
+ $table->foreignId('task_collection_id')->constrained()->onDelete('cascade');
+ $table->foreignId('task_id')->constrained()->onDelete('cascade');
+ $table->primary(['task_collection_id', 'task_id']);
+ $table->integer('sort_order')->default(0);
+ });
+ }
+
+ // Event - Task Collection Pivot
+ if (!Schema::hasTable('event_task_collection')) {
+ Schema::create('event_task_collection', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('event_id')->constrained('events')->onDelete('cascade');
+ $table->foreignId('task_collection_id')->constrained('task_collections')->onDelete('cascade');
+ $table->integer('sort_order')->default(0);
+ $table->timestamps();
+ });
+ }
+
+ // Event - Task Pivot
+ if (!Schema::hasTable('event_task')) {
+ Schema::create('event_task', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('event_id')->constrained()->onDelete('cascade');
+ $table->foreignId('task_id')->constrained()->onDelete('cascade');
+ $table->integer('sort_order')->default(0);
+ $table->timestamps();
+ });
+ }
+
+ // Add tenant_id to tasks and collections if missing (from add_tenant_id_to_tasks_and_collections)
+ if (Schema::hasTable('tasks') && !Schema::hasColumn('tasks', 'tenant_id')) {
+ Schema::table('tasks', function (Blueprint $table) {
+ $table->foreignId('tenant_id')->constrained('tenants')->onDelete('cascade')->after('id');
+ $table->index('tenant_id');
+ });
+ }
+ if (Schema::hasTable('task_collections') && !Schema::hasColumn('task_collections', 'tenant_id')) {
+ Schema::table('task_collections', function (Blueprint $table) {
+ $table->foreignId('tenant_id')->constrained('tenants')->onDelete('cascade')->after('id');
+ $table->index('tenant_id');
+ });
+ }
+ }
+
+ public function down(): void
+ {
+ if (app()->environment('local', 'testing')) {
+ Schema::dropIfExists('event_task');
+ Schema::dropIfExists('event_task_collection');
+ Schema::dropIfExists('task_collection_task');
+ if (Schema::hasColumn('task_collections', 'tenant_id')) {
+ Schema::table('task_collections', function (Blueprint $table) {
+ $table->dropForeign(['tenant_id']);
+ $table->dropColumn('tenant_id');
+ });
+ }
+ Schema::dropIfExists('task_collections');
+ if (Schema::hasColumn('tasks', 'tenant_id')) {
+ Schema::table('tasks', function (Blueprint $table) {
+ $table->dropForeign(['tenant_id']);
+ $table->dropColumn('tenant_id');
+ });
+ }
+ if (Schema::hasColumn('tasks', 'deleted_at')) {
+ Schema::table('tasks', function (Blueprint $table) {
+ $table->dropSoftDeletes();
+ });
+ }
+ Schema::dropIfExists('tasks');
+ if (Schema::hasColumn('events', 'status')) {
+ Schema::table('events', function (Blueprint $table) {
+ $table->dropColumn('status');
+ });
+ }
+ Schema::dropIfExists('events');
+ }
+ }
+};
\ No newline at end of file
diff --git a/database/migrations/2025_09_01_000400_create_photos_likes.php b/database/migrations/2025_09_01_000400_create_photos_likes.php
new file mode 100644
index 0000000..933d236
--- /dev/null
+++ b/database/migrations/2025_09_01_000400_create_photos_likes.php
@@ -0,0 +1,77 @@
+id();
+ $table->unsignedBigInteger('event_id');
+ $table->unsignedBigInteger('emotion_id');
+ $table->unsignedBigInteger('task_id')->nullable();
+ $table->string('guest_name');
+ $table->string('file_path');
+ $table->string('thumbnail_path');
+ $table->integer('likes_count')->default(0);
+ $table->boolean('is_featured')->default(false);
+ $table->json('metadata')->nullable();
+ $table->unsignedBigInteger('tenant_id')->nullable(); // Consolidated from adds
+ $table->timestamps();
+ $table->foreign('event_id')->references('id')->on('events')->onDelete('cascade');
+ $table->foreign('emotion_id')->references('id')->on('emotions')->onDelete('set null');
+ $table->foreign('task_id')->references('id')->on('tasks')->onDelete('set null');
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
+ $table->index(['event_id', 'emotion_id', 'tenant_id']);
+ });
+ } else {
+ // Add tenant_id if missing (consolidate duplicates)
+ if (!Schema::hasColumn('photos', 'tenant_id')) {
+ Schema::table('photos', function (Blueprint $table) {
+ $table->unsignedBigInteger('tenant_id')->nullable()->after('event_id');
+ $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
+ $table->index('tenant_id');
+ });
+ }
+ }
+
+ // Photo Likes table
+ if (!Schema::hasTable('photo_likes')) {
+ Schema::create('photo_likes', function (Blueprint $table) {
+ $table->id();
+ $table->unsignedBigInteger('photo_id');
+ $table->string('guest_name');
+ $table->string('ip_address', 45)->nullable();
+ $table->timestamp('created_at')->useCurrent();
+ $table->unique(['photo_id', 'guest_name', 'ip_address']);
+ $table->foreign('photo_id')->references('id')->on('photos')->onDelete('cascade');
+ });
+ }
+ }
+
+ public function down(): void
+ {
+ if (app()->environment('local', 'testing')) {
+ if (Schema::hasTable('photo_likes')) {
+ Schema::table('photo_likes', function (Blueprint $table) {
+ $table->dropForeign(['photo_id']);
+ });
+ Schema::dropIfExists('photo_likes');
+ }
+ if (Schema::hasTable('photos')) {
+ Schema::table('photos', function (Blueprint $table) {
+ $table->dropForeign(['event_id']);
+ $table->dropForeign(['emotion_id']);
+ $table->dropForeign(['task_id']);
+ $table->dropForeign(['tenant_id']);
+ });
+ Schema::dropIfExists('photos');
+ }
+ }
+ }
+};
\ No newline at end of file
diff --git a/database/migrations/2025_09_08_120000_seed_legal_pages.php b/database/migrations/2025_09_01_000500_create_legal_pages.php
similarity index 66%
rename from database/migrations/2025_09_08_120000_seed_legal_pages.php
rename to database/migrations/2025_09_01_000500_create_legal_pages.php
index ddbd58b..ebb1209 100644
--- a/database/migrations/2025_09_08_120000_seed_legal_pages.php
+++ b/database/migrations/2025_09_01_000500_create_legal_pages.php
@@ -1,66 +1,90 @@
toDateString();
+ // Legal Pages table
+ if (!Schema::hasTable('legal_pages')) {
+ Schema::create('legal_pages', function (Blueprint $table) {
+ $table->id();
+ $table->string('slug', 32);
+ $table->json('title');
+ $table->json('body_markdown');
+ $table->string('locale_fallback', 5)->default('de');
+ $table->integer('version')->default(1);
+ $table->timestamp('effective_from')->nullable();
+ $table->boolean('is_published')->default(false);
+ $table->timestamps();
+ $table->unique(['slug', 'version']);
+ });
+ }
- $rows = [
- [
- 'slug' => 'impressum',
- 'version' => 1,
- 'title' => json_encode(['de' => 'Impressum'], JSON_UNESCAPED_UNICODE),
- 'body_markdown' => json_encode(['de' => self::impressumDe()], JSON_UNESCAPED_UNICODE),
- 'locale_fallback' => 'de',
- 'effective_from' => $now,
- 'is_published' => true,
- 'created_at' => now(),
- 'updated_at' => now(),
- ],
- [
- 'slug' => 'datenschutz',
- 'version' => 1,
- 'title' => json_encode(['de' => 'Datenschutzerklärung'], JSON_UNESCAPED_UNICODE),
- 'body_markdown' => json_encode(['de' => self::datenschutzDe($now)], JSON_UNESCAPED_UNICODE),
- 'locale_fallback' => 'de',
- 'effective_from' => $now,
- 'is_published' => true,
- 'created_at' => now(),
- 'updated_at' => now(),
- ],
- [
- 'slug' => 'agb',
- 'version' => 1,
- 'title' => json_encode(['de' => 'Allgemeine Geschäftsbedingungen'], JSON_UNESCAPED_UNICODE),
- 'body_markdown' => json_encode(['de' => self::agbDe($now)], JSON_UNESCAPED_UNICODE),
- 'locale_fallback' => 'de',
- 'effective_from' => $now,
- 'is_published' => true,
- 'created_at' => now(),
- 'updated_at' => now(),
- ],
- ];
+ // Seed data if table exists (idempotent: updateOrInsert)
+ if (Schema::hasTable('legal_pages')) {
+ $now = now()->toDateString();
- foreach ($rows as $r) {
- DB::table('legal_pages')->updateOrInsert(
- ['slug' => $r['slug'], 'version' => $r['version']],
- $r
- );
+ $rows = [
+ [
+ 'slug' => 'impressum',
+ 'version' => 1,
+ 'title' => json_encode(['de' => 'Impressum'], JSON_UNESCAPED_UNICODE),
+ 'body_markdown' => json_encode(['de' => $this->impressumDe()], JSON_UNESCAPED_UNICODE),
+ 'locale_fallback' => 'de',
+ 'effective_from' => $now,
+ 'is_published' => true,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ],
+ [
+ 'slug' => 'datenschutz',
+ 'version' => 1,
+ 'title' => json_encode(['de' => 'Datenschutzerklärung'], JSON_UNESCAPED_UNICODE),
+ 'body_markdown' => json_encode(['de' => $this->datenschutzDe($now)], JSON_UNESCAPED_UNICODE),
+ 'locale_fallback' => 'de',
+ 'effective_from' => $now,
+ 'is_published' => true,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ],
+ [
+ 'slug' => 'agb',
+ 'version' => 1,
+ 'title' => json_encode(['de' => 'Allgemeine Geschäftsbedingungen'], JSON_UNESCAPED_UNICODE),
+ 'body_markdown' => json_encode(['de' => $this->agbDe($now)], JSON_UNESCAPED_UNICODE),
+ 'locale_fallback' => 'de',
+ 'effective_from' => $now,
+ 'is_published' => true,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ],
+ ];
+
+ foreach ($rows as $r) {
+ DB::table('legal_pages')->updateOrInsert(
+ ['slug' => $r['slug'], 'version' => $r['version']],
+ $r
+ );
+ }
}
}
public function down(): void
{
- if (! Schema::hasTable('legal_pages')) return;
- DB::table('legal_pages')->whereIn('slug', ['impressum','datenschutz','agb'])->delete();
+ if (app()->environment('local', 'testing')) {
+ if (Schema::hasTable('legal_pages')) {
+ DB::table('legal_pages')->whereIn('slug', ['impressum', 'datenschutz', 'agb'])->delete();
+ Schema::dropIfExists('legal_pages');
+ }
+ }
}
- private static function impressumDe(): string
+ private function impressumDe(): string
{
return <<id();
- $table->json('name');
- $table->string('slug')->unique();
- $table->string('icon')->nullable();
- $table->json('settings')->nullable();
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('event_types');
- }
-};
-
diff --git a/database/migrations/2025_09_01_100100_create_emotions_table.php b/database/migrations/2025_09_01_100100_create_emotions_table.php
deleted file mode 100644
index 3b840ad..0000000
--- a/database/migrations/2025_09_01_100100_create_emotions_table.php
+++ /dev/null
@@ -1,34 +0,0 @@
-id();
- $table->json('name');
- $table->string('icon', 50);
- $table->string('color', 7);
- $table->json('description')->nullable();
- $table->integer('sort_order')->default(0);
- $table->boolean('is_active')->default(true);
- $table->timestamps();
- });
-
- Schema::create('emotion_event_type', function (Blueprint $table) {
- $table->unsignedBigInteger('emotion_id');
- $table->unsignedBigInteger('event_type_id');
- $table->primary(['emotion_id', 'event_type_id']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('emotion_event_type');
- Schema::dropIfExists('emotions');
- }
-};
-
diff --git a/database/migrations/2025_09_01_100200_create_events_table.php b/database/migrations/2025_09_01_100200_create_events_table.php
deleted file mode 100644
index eabc6db..0000000
--- a/database/migrations/2025_09_01_100200_create_events_table.php
+++ /dev/null
@@ -1,37 +0,0 @@
-id();
- $table->foreignId('tenant_id')->constrained()->onDelete('cascade');
- $table->json('name');
- $table->json('description')->nullable();
- $table->dateTime('date');
- $table->string('slug')->unique();
- $table->string('location')->nullable();
- $table->integer('max_participants')->nullable();
- $table->json('settings')->nullable();
- $table->unsignedBigInteger('event_type_id');
- $table->boolean('is_active')->default(true);
- $table->boolean('join_link_enabled')->default(true);
- $table->boolean('photo_upload_enabled')->default(true);
- $table->boolean('task_checklist_enabled')->default(true);
- $table->string('default_locale', 5)->default('de');
- $table->timestamps();
-
- $table->index(['tenant_id', 'date', 'is_active']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('events');
- }
-};
-
diff --git a/database/migrations/2025_09_01_100300_create_tasks_table.php b/database/migrations/2025_09_01_100300_create_tasks_table.php
deleted file mode 100644
index f3d810d..0000000
--- a/database/migrations/2025_09_01_100300_create_tasks_table.php
+++ /dev/null
@@ -1,37 +0,0 @@
-id();
- $table->foreignId('tenant_id')->constrained()->onDelete('cascade');
- $table->unsignedBigInteger('emotion_id')->nullable();
- $table->unsignedBigInteger('event_type_id')->nullable();
- $table->json('title');
- $table->json('description')->nullable();
- $table->json('example_text')->nullable();
- $table->dateTime('due_date')->nullable();
- $table->boolean('is_completed')->default(false);
- $table->enum('priority', ['low', 'medium', 'high', 'urgent'])->default('medium');
- $table->unsignedBigInteger('collection_id')->nullable();
- $table->enum('difficulty', ['easy','medium','hard'])->default('easy');
- $table->integer('sort_order')->default(0);
- $table->boolean('is_active')->default(true);
- $table->timestamps();
-
- $table->foreign('collection_id')->references('id')->on('task_collections')->onDelete('set null');
- $table->index(['tenant_id', 'is_completed', 'priority']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('tasks');
- }
-};
-
diff --git a/database/migrations/2025_09_01_100400_create_photos_and_likes_tables.php b/database/migrations/2025_09_01_100400_create_photos_and_likes_tables.php
deleted file mode 100644
index bf25a55..0000000
--- a/database/migrations/2025_09_01_100400_create_photos_and_likes_tables.php
+++ /dev/null
@@ -1,40 +0,0 @@
-id();
- $table->unsignedBigInteger('event_id');
- $table->unsignedBigInteger('emotion_id');
- $table->unsignedBigInteger('task_id')->nullable();
- $table->string('guest_name');
- $table->string('file_path');
- $table->string('thumbnail_path');
- $table->integer('likes_count')->default(0);
- $table->boolean('is_featured')->default(false);
- $table->json('metadata')->nullable();
- $table->timestamps();
- });
-
- Schema::create('photo_likes', function (Blueprint $table) {
- $table->id();
- $table->unsignedBigInteger('photo_id');
- $table->string('guest_name');
- $table->string('ip_address', 45)->nullable();
- $table->timestamp('created_at')->useCurrent();
- $table->unique(['photo_id','guest_name','ip_address']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('photo_likes');
- Schema::dropIfExists('photos');
- }
-};
-
diff --git a/database/migrations/2025_09_01_100500_create_legal_pages_table.php b/database/migrations/2025_09_01_100500_create_legal_pages_table.php
deleted file mode 100644
index 07a9303..0000000
--- a/database/migrations/2025_09_01_100500_create_legal_pages_table.php
+++ /dev/null
@@ -1,29 +0,0 @@
-id();
- $table->string('slug', 32);
- $table->json('title');
- $table->json('body_markdown');
- $table->string('locale_fallback', 5)->default('de');
- $table->integer('version')->default(1);
- $table->timestamp('effective_from')->nullable();
- $table->boolean('is_published')->default(false);
- $table->timestamps();
- $table->unique(['slug','version']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('legal_pages');
- }
-};
-
diff --git a/database/migrations/2025_09_01_101000_add_role_to_users_table.php b/database/migrations/2025_09_01_101000_add_role_to_users_table.php
deleted file mode 100644
index f5b64ba..0000000
--- a/database/migrations/2025_09_01_101000_add_role_to_users_table.php
+++ /dev/null
@@ -1,22 +0,0 @@
-string('role')->default('super_admin')->after('password');
- });
- }
-
- public function down(): void
- {
- Schema::table('users', function (Blueprint $table) {
- $table->dropColumn('role');
- });
- }
-};
-
diff --git a/database/migrations/2025_09_01_184839_create_personal_access_tokens_table.php b/database/migrations/2025_09_01_184839_create_personal_access_tokens_table.php
deleted file mode 100644
index 40ff706..0000000
--- a/database/migrations/2025_09_01_184839_create_personal_access_tokens_table.php
+++ /dev/null
@@ -1,33 +0,0 @@
-id();
- $table->morphs('tokenable');
- $table->text('name');
- $table->string('token', 64)->unique();
- $table->text('abilities')->nullable();
- $table->timestamp('last_used_at')->nullable();
- $table->timestamp('expires_at')->nullable()->index();
- $table->timestamps();
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('personal_access_tokens');
- }
-};
diff --git a/database/migrations/2025_09_03_122009_create_task_collections_tables.php b/database/migrations/2025_09_03_122009_create_task_collections_tables.php
deleted file mode 100644
index da42b43..0000000
--- a/database/migrations/2025_09_03_122009_create_task_collections_tables.php
+++ /dev/null
@@ -1,35 +0,0 @@
-id();
- $table->foreignId('tenant_id')->constrained()->onDelete('cascade');
- $table->string('name');
- $table->text('description')->nullable();
- $table->boolean('is_default')->default(false);
- $table->integer('position')->default(0);
- $table->timestamps();
-
- $table->index(['tenant_id', 'is_default', 'position']);
- });
-
- Schema::create('task_collection_task', function (Blueprint $table) {
- $table->foreignId('task_collection_id')->constrained()->onDelete('cascade');
- $table->foreignId('task_id')->constrained()->onDelete('cascade');
- $table->primary(['task_collection_id','task_id']);
- $table->integer('sort_order')->default(0);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('task_collection_task');
- Schema::dropIfExists('task_collections');
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_03_122018_create_task_imports_table.php b/database/migrations/2025_09_03_122018_create_task_imports_table.php
deleted file mode 100644
index a6325cf..0000000
--- a/database/migrations/2025_09_03_122018_create_task_imports_table.php
+++ /dev/null
@@ -1,28 +0,0 @@
-id();
- $table->string('disk')->default('local');
- $table->string('path');
- $table->string('source_filename');
- $table->enum('status', ['pending','processing','completed','failed'])->default('pending');
- $table->unsignedInteger('total_rows')->default(0);
- $table->unsignedInteger('imported_rows')->default(0);
- $table->json('errors')->nullable();
- $table->unsignedBigInteger('created_by')->nullable();
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('task_imports');
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_05_000001_add_two_factor_columns_to_users_table.php b/database/migrations/2025_09_05_000001_add_two_factor_columns_to_users_table.php
deleted file mode 100644
index 27abf1e..0000000
--- a/database/migrations/2025_09_05_000001_add_two_factor_columns_to_users_table.php
+++ /dev/null
@@ -1,28 +0,0 @@
-text('two_factor_secret')->nullable();
- $table->text('two_factor_recovery_codes')->nullable();
- $table->timestamp('two_factor_confirmed_at')->nullable();
- });
- }
-
- public function down(): void
- {
- Schema::table('users', function (Blueprint $table) {
- $table->dropColumn([
- 'two_factor_secret',
- 'two_factor_recovery_codes',
- 'two_factor_confirmed_at',
- ]);
- });
- }
-};
-
diff --git a/database/migrations/2025_09_08_000100_create_tenants_table.php b/database/migrations/2025_09_08_000100_create_tenants_table.php
deleted file mode 100644
index eca11c2..0000000
--- a/database/migrations/2025_09_08_000100_create_tenants_table.php
+++ /dev/null
@@ -1,42 +0,0 @@
-id();
- $table->string('name');
- $table->string('slug')->unique();
- $table->string('domain')->nullable()->unique();
-
- $table->string('contact_name')->nullable();
- $table->string('contact_email')->nullable();
- $table->string('contact_phone')->nullable();
-
- // Simple event-credit based monetization (MVP)
- $table->integer('event_credits_balance')->default(1);
- $table->timestamp('free_event_granted_at')->nullable();
-
- // Limits & quotas
- $table->integer('max_photos_per_event')->default(500);
- $table->integer('max_storage_mb')->default(1024);
-
- // Feature flags & misc
- $table->json('features')->nullable();
- $table->timestamp('last_activity_at')->nullable();
-
- $table->timestamps();
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('tenants');
- }
-};
-
diff --git a/database/migrations/2025_09_08_000200_add_tenant_fields_to_users_table.php b/database/migrations/2025_09_08_000200_add_tenant_fields_to_users_table.php
deleted file mode 100644
index 681fa18..0000000
--- a/database/migrations/2025_09_08_000200_add_tenant_fields_to_users_table.php
+++ /dev/null
@@ -1,38 +0,0 @@
-foreignId('tenant_id')->nullable()->after('id')
- ->constrained('tenants')->nullOnDelete();
- }
- if (! Schema::hasColumn('users', 'role')) {
- $table->string('role', 32)->default('tenant_user')->after('password')->index();
- }
- });
- }
-
- public function down(): void
- {
- Schema::table('users', function (Blueprint $table) {
- if (Schema::hasColumn('users', 'tenant_id')) {
- // Drop FK first if the driver supports it
- try { $table->dropConstrainedForeignId('tenant_id'); } catch (\Throwable $e) {
- try { $table->dropForeign(['tenant_id']); } catch (\Throwable $e2) {}
- $table->dropColumn('tenant_id');
- }
- }
- if (Schema::hasColumn('users', 'role')) {
- $table->dropColumn('role');
- }
- });
- }
-};
-
diff --git a/database/migrations/2025_09_08_000300_add_tenant_id_to_events_table.php b/database/migrations/2025_09_08_000300_add_tenant_id_to_events_table.php
deleted file mode 100644
index 0dff83c..0000000
--- a/database/migrations/2025_09_08_000300_add_tenant_id_to_events_table.php
+++ /dev/null
@@ -1,36 +0,0 @@
-foreignId('tenant_id')->nullable()->after('id')->constrained('tenants')->nullOnDelete();
- }
- if (Schema::hasColumn('events', 'slug')) {
- // Optional: ensure index exists
- try { $table->index('slug'); } catch (\Throwable $e) {}
- }
- });
- }
-
- public function down(): void
- {
- if (! Schema::hasTable('events')) return;
- Schema::table('events', function (Blueprint $table) {
- if (Schema::hasColumn('events', 'tenant_id')) {
- try { $table->dropConstrainedForeignId('tenant_id'); } catch (\Throwable $e) {
- try { $table->dropForeign(['tenant_id']); } catch (\Throwable $e2) {}
- $table->dropColumn('tenant_id');
- }
- }
- });
- }
-};
-
diff --git a/database/migrations/2025_09_11_100500_add_username_and_locale_to_users_table.php b/database/migrations/2025_09_11_100500_add_username_and_locale_to_users_table.php
deleted file mode 100644
index 9f88f6f..0000000
--- a/database/migrations/2025_09_11_100500_add_username_and_locale_to_users_table.php
+++ /dev/null
@@ -1,37 +0,0 @@
-string('username', 32)->nullable()->unique()->after('email');
- }
-
- if (! Schema::hasColumn('users', 'preferred_locale')) {
- $defaultLocale = config('app.locale', 'en');
- $table->string('preferred_locale', 5)->default($defaultLocale)->after('role');
- }
- });
- }
-
- public function down(): void
- {
- Schema::table('users', function (Blueprint $table) {
- if (Schema::hasColumn('users', 'username')) {
- try { $table->dropUnique(['username']); } catch (\Throwable $e) { /* ignore */ }
- $table->dropColumn('username');
- }
-
- if (Schema::hasColumn('users', 'preferred_locale')) {
- $table->dropColumn('preferred_locale');
- }
- });
- }
-};
-
diff --git a/database/migrations/2025_09_12_095200_create_event_task_collection_table.php b/database/migrations/2025_09_12_095200_create_event_task_collection_table.php
deleted file mode 100644
index ba26c30..0000000
--- a/database/migrations/2025_09_12_095200_create_event_task_collection_table.php
+++ /dev/null
@@ -1,35 +0,0 @@
-id();
- $table->foreignId('event_id')->constrained('events')->onDelete('cascade');
- $table->foreignId('task_collection_id')->constrained('task_collections')->onDelete('cascade');
- $table->integer('sort_order')->default(0);
- $table->timestamps();
-
- // Composite unique index to prevent duplicate assignments
- $table->unique(['event_id', 'task_collection_id']);
-
- $table->index(['event_id', 'sort_order']);
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('event_task_collection');
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_12_095200_create_event_task_table.php b/database/migrations/2025_09_12_095200_create_event_task_table.php
deleted file mode 100644
index 17caab7..0000000
--- a/database/migrations/2025_09_12_095200_create_event_task_table.php
+++ /dev/null
@@ -1,27 +0,0 @@
-id();
- $table->foreignId('event_id')->constrained()->onDelete('cascade');
- $table->foreignId('task_id')->constrained()->onDelete('cascade');
- $table->integer('sort_order')->default(0);
- $table->timestamps();
-
- $table->unique(['event_id', 'task_id']);
- $table->index(['event_id', 'task_id']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('event_task');
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_15_000000_create_oauth_system.php b/database/migrations/2025_09_15_000000_create_oauth_system.php
new file mode 100644
index 0000000..0d632ac
--- /dev/null
+++ b/database/migrations/2025_09_15_000000_create_oauth_system.php
@@ -0,0 +1,127 @@
+string('id', 255)->primary();
+ $table->string('client_id', 255)->unique();
+ $table->string('client_secret', 255)->nullable();
+ $table->text('redirect_uris')->nullable();
+ $table->text('scopes')->default('tenant:read tenant:write');
+ $table->boolean('is_active')->default(true); // From add_is_active
+ $table->foreignId('tenant_id')->nullable()->after('client_secret')->constrained('tenants')->nullOnDelete(); // From add_tenant_id
+ $table->timestamp('created_at')->useCurrent();
+ $table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
+ $table->index('tenant_id');
+ });
+ } else {
+ if (!Schema::hasColumn('oauth_clients', 'is_active')) {
+ Schema::table('oauth_clients', function (Blueprint $table) {
+ $table->boolean('is_active')->default(true)->after('scopes');
+ });
+ }
+ if (!Schema::hasColumn('oauth_clients', 'tenant_id')) {
+ Schema::table('oauth_clients', function (Blueprint $table) {
+ $table->foreignId('tenant_id')->nullable()->after('client_secret')->constrained('tenants')->nullOnDelete();
+ $table->index('tenant_id');
+ });
+ }
+ }
+
+ // Refresh Tokens
+ if (!Schema::hasTable('refresh_tokens')) {
+ Schema::create('refresh_tokens', function (Blueprint $table) {
+ $table->string('id', 255)->primary();
+ $table->string('tenant_id', 255)->index();
+ $table->string('client_id', 255)->nullable()->index(); // From add_client_id
+ $table->string('token', 255)->unique()->index();
+ $table->string('access_token', 255)->nullable();
+ $table->timestamp('expires_at')->nullable();
+ $table->text('scope')->nullable();
+ $table->string('ip_address', 45)->nullable();
+ $table->text('user_agent')->nullable();
+ $table->timestamp('created_at')->useCurrent();
+ $table->timestamp('revoked_at')->nullable();
+ $table->index('expires_at');
+ });
+ } else {
+ if (!Schema::hasColumn('refresh_tokens', 'client_id')) {
+ Schema::table('refresh_tokens', function (Blueprint $table) {
+ $table->string('client_id', 255)->nullable()->after('tenant_id')->index();
+ });
+ }
+ }
+
+ // Tenant Tokens
+ if (!Schema::hasTable('tenant_tokens')) {
+ Schema::create('tenant_tokens', function (Blueprint $table) {
+ $table->string('id', 255)->primary();
+ $table->string('tenant_id', 255)->index();
+ $table->string('jti', 255)->unique()->index();
+ $table->string('token_type', 50)->index();
+ $table->timestamp('expires_at');
+ $table->timestamp('revoked_at')->nullable();
+ $table->timestamp('created_at')->useCurrent();
+ $table->index('expires_at');
+ });
+ }
+
+ // OAuth Codes
+ if (!Schema::hasTable('oauth_codes')) {
+ Schema::create('oauth_codes', function (Blueprint $table) {
+ $table->string('id', 255)->primary();
+ $table->string('client_id', 255);
+ $table->string('user_id', 255);
+ $table->string('code', 255)->unique()->index();
+ $table->string('code_challenge', 255);
+ $table->string('state', 255)->nullable();
+ $table->string('redirect_uri', 255)->nullable();
+ $table->text('scope')->nullable();
+ $table->timestamp('expires_at');
+ $table->timestamp('created_at')->useCurrent();
+ $table->index('expires_at');
+ $table->foreign('client_id')->references('client_id')->on('oauth_clients')->onDelete('cascade');
+ });
+ }
+ }
+
+ public function down(): void
+ {
+ if (app()->environment('local', 'testing')) {
+ if (Schema::hasTable('oauth_codes')) {
+ Schema::table('oauth_codes', function (Blueprint $table) {
+ $table->dropForeign(['client_id']);
+ });
+ Schema::dropIfExists('oauth_codes');
+ }
+ if (Schema::hasColumn('refresh_tokens', 'client_id')) {
+ Schema::table('refresh_tokens', function (Blueprint $table) {
+ $table->dropIndex(['client_id']);
+ $table->dropColumn('client_id');
+ });
+ }
+ Schema::dropIfExists('refresh_tokens');
+ Schema::dropIfExists('tenant_tokens');
+ if (Schema::hasColumn('oauth_clients', 'tenant_id')) {
+ Schema::table('oauth_clients', function (Blueprint $table) {
+ $table->dropForeign(['tenant_id']);
+ $table->dropColumn('tenant_id');
+ });
+ }
+ if (Schema::hasColumn('oauth_clients', 'is_active')) {
+ Schema::table('oauth_clients', function (Blueprint $table) {
+ $table->dropColumn('is_active');
+ });
+ }
+ Schema::dropIfExists('oauth_clients');
+ }
+ }
+};
\ No newline at end of file
diff --git a/database/migrations/2025_09_15_123506_create_oauth_clients_table.php b/database/migrations/2025_09_15_123506_create_oauth_clients_table.php
deleted file mode 100644
index 5f56841..0000000
--- a/database/migrations/2025_09_15_123506_create_oauth_clients_table.php
+++ /dev/null
@@ -1,33 +0,0 @@
-string('id', 255)->primary();
- $table->string('client_id', 255)->unique();
- $table->string('client_secret', 255)->nullable();
- $table->text('redirect_uris')->nullable();
- $table->text('scopes')->default('tenant:read tenant:write');
- $table->timestamp('created_at')->useCurrent();
- $table->timestamp('updated_at')->useCurrent()->useCurrentOnUpdate();
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('oauth_clients');
- }
-};
diff --git a/database/migrations/2025_09_15_123557_create_refresh_tokens_table.php b/database/migrations/2025_09_15_123557_create_refresh_tokens_table.php
deleted file mode 100644
index 60e3676..0000000
--- a/database/migrations/2025_09_15_123557_create_refresh_tokens_table.php
+++ /dev/null
@@ -1,37 +0,0 @@
-string('id', 255)->primary();
- $table->string('tenant_id', 255)->index();
- $table->string('token', 255)->unique()->index();
- $table->string('access_token', 255)->nullable();
- $table->timestamp('expires_at')->nullable();
- $table->text('scope')->nullable();
- $table->string('ip_address', 45)->nullable();
- $table->text('user_agent')->nullable();
- $table->timestamp('created_at')->useCurrent();
- $table->timestamp('revoked_at')->nullable();
-
- $table->index('expires_at');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('refresh_tokens');
- }
-};
diff --git a/database/migrations/2025_09_15_123625_create_tenant_tokens_table.php b/database/migrations/2025_09_15_123625_create_tenant_tokens_table.php
deleted file mode 100644
index e897c3e..0000000
--- a/database/migrations/2025_09_15_123625_create_tenant_tokens_table.php
+++ /dev/null
@@ -1,34 +0,0 @@
-string('id', 255)->primary();
- $table->string('tenant_id', 255)->index();
- $table->string('jti', 255)->unique()->index();
- $table->string('token_type', 50)->index();
- $table->timestamp('expires_at');
- $table->timestamp('revoked_at')->nullable();
- $table->timestamp('created_at')->useCurrent();
-
- $table->index('expires_at');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('tenant_tokens');
- }
-};
diff --git a/database/migrations/2025_09_15_123647_create_oauth_codes_table.php b/database/migrations/2025_09_15_123647_create_oauth_codes_table.php
deleted file mode 100644
index 129c69f..0000000
--- a/database/migrations/2025_09_15_123647_create_oauth_codes_table.php
+++ /dev/null
@@ -1,37 +0,0 @@
-string('id', 255)->primary();
- $table->string('client_id', 255);
- $table->string('user_id', 255);
- $table->string('code', 255)->unique()->index();
- $table->string('code_challenge', 255);
- $table->string('state', 255)->nullable();
- $table->string('redirect_uri', 255)->nullable();
- $table->text('scope')->nullable();
- $table->timestamp('expires_at');
- $table->timestamp('created_at')->useCurrent();
-
- $table->index('expires_at');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('oauth_codes');
- }
-};
diff --git a/database/migrations/2025_09_15_123713_create_purchase_history_table.php b/database/migrations/2025_09_15_123713_create_purchase_history_table.php
deleted file mode 100644
index e23922f..0000000
--- a/database/migrations/2025_09_15_123713_create_purchase_history_table.php
+++ /dev/null
@@ -1,40 +0,0 @@
-string('id', 255)->primary();
- $table->string('tenant_id', 255);
- $table->string('package_id', 255);
- $table->integer('credits_added')->default(0);
- $table->decimal('price', 10, 2)->default(0);
- $table->string('currency', 3)->default('EUR');
- $table->string('platform', 50);
- $table->string('transaction_id', 255)->nullable();
- $table->timestamp('purchased_at')->useCurrent();
- $table->timestamp('created_at')->useCurrent();
-
- $table->foreign('tenant_id')->references('id')->on('tenants');
- $table->index('tenant_id');
- $table->index('purchased_at');
- $table->index('transaction_id');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('purchase_history');
- }
-};
diff --git a/database/migrations/2025_09_15_123812_add_subscription_fields_to_tenants_table.php b/database/migrations/2025_09_15_123812_add_subscription_fields_to_tenants_table.php
index 7c9c1c4..53e85f9 100644
--- a/database/migrations/2025_09_15_123812_add_subscription_fields_to_tenants_table.php
+++ b/database/migrations/2025_09_15_123812_add_subscription_fields_to_tenants_table.php
@@ -12,11 +12,11 @@ return new class extends Migration
public function up(): void
{
Schema::table('tenants', function (Blueprint $table) {
- $table->string('subscription_tier')->default('free');
- $table->timestamp('subscription_expires_at')->nullable();
- $table->decimal('total_revenue', 10, 2)->default(0.00);
- if (!Schema::hasColumn('tenants', 'event_credits_balance')) {
- $table->integer('event_credits_balance')->default(1);
+ if (!Schema::hasColumn('tenants', 'subscription_status')) {
+ $table->enum('subscription_status', ['free', 'active', 'suspended', 'expired'])->default('free')->after('subscription_tier');
+ }
+ if (!Schema::hasColumn('tenants', 'subscription_expires_at')) {
+ $table->timestamp('subscription_expires_at')->nullable()->after('subscription_status');
}
});
}
@@ -27,12 +27,7 @@ return new class extends Migration
public function down(): void
{
Schema::table('tenants', function (Blueprint $table) {
- $table->dropColumn([
- 'subscription_tier',
- 'subscription_expires_at',
- 'total_revenue',
- 'event_credits_balance'
- ]);
+ $table->dropColumn(['subscription_status', 'subscription_expires_at']);
});
}
-};
+};
\ No newline at end of file
diff --git a/database/migrations/2025_09_15_add_tenant_id_to_tasks_and_collections.php b/database/migrations/2025_09_15_add_tenant_id_to_tasks_and_collections.php
deleted file mode 100644
index cecfdd0..0000000
--- a/database/migrations/2025_09_15_add_tenant_id_to_tasks_and_collections.php
+++ /dev/null
@@ -1,40 +0,0 @@
-foreignId('tenant_id')->constrained('tenants')->onDelete('cascade')->after('id');
- $table->index('tenant_id');
- }
- });
-
- // Add tenant_id to task_collections table
- Schema::table('task_collections', function (Blueprint $table) {
- if (!Schema::hasColumn('task_collections', 'tenant_id')) {
- $table->foreignId('tenant_id')->constrained('tenants')->onDelete('cascade')->after('id');
- $table->index('tenant_id');
- }
- });
- }
-
- public function down(): void
- {
- Schema::table('tasks', function (Blueprint $table) {
- $table->dropForeign(['tenant_id']);
- $table->dropColumn('tenant_id');
- });
-
- Schema::table('task_collections', function (Blueprint $table) {
- $table->dropForeign(['tenant_id']);
- $table->dropColumn('tenant_id');
- });
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_15_add_tenant_status_and_settings_fields.php b/database/migrations/2025_09_15_add_tenant_status_and_settings_fields.php
deleted file mode 100644
index a5577fb..0000000
--- a/database/migrations/2025_09_15_add_tenant_status_and_settings_fields.php
+++ /dev/null
@@ -1,36 +0,0 @@
-boolean('is_active')->default(true)->after('last_activity_at');
- $table->boolean('is_suspended')->default(false)->after('is_active');
- $table->json('settings')->nullable()->after('features');
- $table->timestamp('settings_updated_at')->nullable()->after('settings');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('tenants', function (Blueprint $table) {
- $table->dropColumn([
- 'is_active',
- 'is_suspended',
- 'settings',
- 'settings_updated_at'
- ]);
- });
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_17_183633_add_status_to_events_table.php b/database/migrations/2025_09_17_183633_add_status_to_events_table.php
deleted file mode 100644
index e4eaf0b..0000000
--- a/database/migrations/2025_09_17_183633_add_status_to_events_table.php
+++ /dev/null
@@ -1,28 +0,0 @@
-string('status')->default('draft')->after('is_active');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('events', function (Blueprint $table) {
- $table->dropColumn('status');
- });
- }
-};
diff --git a/database/migrations/2025_09_17_184044_add_tenant_id_to_photos_table.php b/database/migrations/2025_09_17_184044_add_tenant_id_to_photos_table.php
deleted file mode 100644
index 218800c..0000000
--- a/database/migrations/2025_09_17_184044_add_tenant_id_to_photos_table.php
+++ /dev/null
@@ -1,31 +0,0 @@
-unsignedBigInteger('tenant_id')->nullable()->after('event_id');
- $table->foreign('tenant_id')->references('id')->on('tenants');
- $table->index('tenant_id');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('photos', function (Blueprint $table) {
- $table->dropForeign(['tenant_id']);
- $table->dropColumn('tenant_id');
- });
- }
-};
diff --git a/database/migrations/2025_09_17_184450_add_tenant_id_to_photos_table_new.php b/database/migrations/2025_09_17_184450_add_tenant_id_to_photos_table_new.php
deleted file mode 100644
index d6a60a3..0000000
--- a/database/migrations/2025_09_17_184450_add_tenant_id_to_photos_table_new.php
+++ /dev/null
@@ -1,35 +0,0 @@
-unsignedBigInteger('tenant_id')->nullable()->after('event_id');
- $table->foreign('tenant_id')->references('id')->on('tenants')->onDelete('cascade');
- $table->index('tenant_id');
- }
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('photos', function (Blueprint $table) {
- if (Schema::hasColumn('photos', 'tenant_id')) {
- $table->dropForeign(['tenant_id']);
- $table->dropColumn('tenant_id');
- }
- });
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_17_add_stripe_account_id_to_tenants_table.php b/database/migrations/2025_09_17_add_stripe_account_id_to_tenants_table.php
deleted file mode 100644
index d515fde..0000000
--- a/database/migrations/2025_09_17_add_stripe_account_id_to_tenants_table.php
+++ /dev/null
@@ -1,24 +0,0 @@
-string('stripe_account_id')->nullable()->unique()->after('id');
- $table->index('stripe_account_id');
- });
- }
-
- public function down()
- {
- Schema::table('tenants', function (Blueprint $table) {
- $table->dropIndex(['stripe_account_id']);
- $table->dropColumn('stripe_account_id');
- });
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_17_create_event_credits_ledger_table.php b/database/migrations/2025_09_17_create_event_credits_ledger_table.php
deleted file mode 100644
index 5355f75..0000000
--- a/database/migrations/2025_09_17_create_event_credits_ledger_table.php
+++ /dev/null
@@ -1,27 +0,0 @@
-id();
- $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
- $table->integer('delta');
- $table->string('reason', 32); // purchase, event_create, manual_adjust, refund
- $table->foreignId('related_purchase_id')->nullable()->constrained('event_purchases')->nullOnDelete();
- $table->text('note')->nullable();
- $table->timestamps();
- $table->index(['tenant_id', 'created_at']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('event_credits_ledger');
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_17_create_event_purchases_table.php b/database/migrations/2025_09_17_create_event_purchases_table.php
deleted file mode 100644
index 70a60c4..0000000
--- a/database/migrations/2025_09_17_create_event_purchases_table.php
+++ /dev/null
@@ -1,30 +0,0 @@
-id();
- $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
- $table->unsignedInteger('events_purchased')->default(1);
- $table->decimal('amount', 10, 2);
- $table->string('currency', 3)->default('EUR');
- $table->string('provider', 32); // stripe, paypal, app_store, play_store
- $table->string('external_receipt_id')->nullable();
- $table->string('status', 16)->default('pending'); // pending, completed, failed
- $table->timestamp('purchased_at')->nullable();
- $table->timestamps();
- $table->index(['tenant_id', 'purchased_at']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('event_purchases');
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_18_150000_flatten_tenant_name_column.php b/database/migrations/2025_09_18_150000_flatten_tenant_name_column.php
deleted file mode 100644
index 2cbc274..0000000
--- a/database/migrations/2025_09_18_150000_flatten_tenant_name_column.php
+++ /dev/null
@@ -1,70 +0,0 @@
-select('id', 'name')
- ->orderBy('id')
- ->chunkById(100, function ($tenants): void {
- foreach ($tenants as $tenant) {
- $raw = $tenant->name;
-
- if ($raw === null || $raw === '') {
- continue;
- }
-
- $decoded = json_decode($raw, true);
- $value = $raw;
-
- if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
- $preferred = $decoded['de'] ?? $decoded['en'] ?? null;
-
- if ($preferred === null) {
- foreach ($decoded as $entry) {
- if (is_string($entry) && $entry !== '') {
- $preferred = $entry;
- break;
- }
- }
- }
-
- $value = $preferred ?? (string) $raw;
- }
-
- DB::table('tenants')->where('id', $tenant->id)->update([
- 'name' => (string) $value,
- ]);
- }
- });
- }
-
- public function down(): void
- {
- DB::table('tenants')
- ->select('id', 'name')
- ->orderBy('id')
- ->chunkById(100, function ($tenants): void {
- foreach ($tenants as $tenant) {
- $raw = $tenant->name;
-
- if ($raw === null || $raw === '') {
- continue;
- }
-
- $localized = json_encode([
- 'de' => $raw,
- 'en' => $raw,
- ], JSON_UNESCAPED_UNICODE);
-
- DB::table('tenants')->where('id', $tenant->id)->update([
- 'name' => $localized,
- ]);
- }
- });
- }
-};
diff --git a/database/migrations/2025_09_18_170042_add_is_active_to_oauth_clients_table.php b/database/migrations/2025_09_18_170042_add_is_active_to_oauth_clients_table.php
deleted file mode 100644
index 9860d4c..0000000
--- a/database/migrations/2025_09_18_170042_add_is_active_to_oauth_clients_table.php
+++ /dev/null
@@ -1,79 +0,0 @@
-boolean('is_active')->default(true);
- });
- }
-
- $clients = DB::table('oauth_clients')->get(['id', 'scopes', 'redirect_uris', 'is_active']);
-
- foreach ($clients as $client) {
- $scopes = $this->normaliseValue($client->scopes, ['tenant:read', 'tenant:write']);
- $redirects = $this->normaliseValue($client->redirect_uris);
-
- DB::table('oauth_clients')
- ->where('id', $client->id)
- ->update([
- 'scopes' => $scopes === null ? null : json_encode($scopes),
- 'redirect_uris' => $redirects === null ? null : json_encode($redirects),
- 'is_active' => $client->is_active ?? true,
- ]);
- }
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- if (Schema::hasColumn('oauth_clients', 'is_active')) {
- Schema::table('oauth_clients', function (Blueprint $table) {
- $table->dropColumn('is_active');
- });
- }
- }
-
- private function normaliseValue(mixed $value, ?array $fallback = null): ?array
- {
- if ($value === null) {
- return $fallback;
- }
-
- if (is_array($value)) {
- return $this->cleanArray($value) ?: $fallback;
- }
-
- if (is_string($value)) {
- $decoded = json_decode($value, true);
- if (json_last_error() === JSON_ERROR_NONE && is_array($decoded)) {
- return $this->cleanArray($decoded) ?: $fallback;
- }
-
- $parts = preg_split('/[\r\n,]+/', $value) ?: [];
- return $this->cleanArray($parts) ?: $fallback;
- }
-
- return $fallback;
- }
-
- private function cleanArray(array $items): array
- {
- $items = array_map(fn ($item) => is_string($item) ? trim($item) : $item, $items);
- $items = array_filter($items, fn ($item) => ! ($item === null || $item === ''));
-
- return array_values($items);
- }
-};
diff --git a/database/migrations/2025_09_18_180407_add_tenant_id_to_oauth_clients_table.php b/database/migrations/2025_09_18_180407_add_tenant_id_to_oauth_clients_table.php
deleted file mode 100644
index d5f54a9..0000000
--- a/database/migrations/2025_09_18_180407_add_tenant_id_to_oauth_clients_table.php
+++ /dev/null
@@ -1,36 +0,0 @@
-foreignId('tenant_id')
- ->nullable()
- ->after('client_secret')
- ->constrained('tenants')
- ->nullOnDelete();
- }
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('oauth_clients', function (Blueprint $table) {
- if (Schema::hasColumn('oauth_clients', 'tenant_id')) {
- $table->dropConstrainedForeignId('tenant_id');
- }
- });
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_18_180414_add_client_id_to_refresh_tokens_table.php b/database/migrations/2025_09_18_180414_add_client_id_to_refresh_tokens_table.php
deleted file mode 100644
index 1f78783..0000000
--- a/database/migrations/2025_09_18_180414_add_client_id_to_refresh_tokens_table.php
+++ /dev/null
@@ -1,32 +0,0 @@
-string('client_id', 255)->nullable()->after('tenant_id')->index();
- }
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('refresh_tokens', function (Blueprint $table) {
- if (Schema::hasColumn('refresh_tokens', 'client_id')) {
- $table->dropColumn('client_id');
- }
- });
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_25_160000_add_soft_deletes_to_tasks_table.php b/database/migrations/2025_09_25_160000_add_soft_deletes_to_tasks_table.php
deleted file mode 100644
index 78f7b0e..0000000
--- a/database/migrations/2025_09_25_160000_add_soft_deletes_to_tasks_table.php
+++ /dev/null
@@ -1,26 +0,0 @@
-softDeletes();
- }
- });
- }
-
- public function down(): void
- {
- Schema::table('tasks', function (Blueprint $table) {
- if (Schema::hasColumn('tasks', 'deleted_at')) {
- $table->dropSoftDeletes();
- }
- });
- }
-};
diff --git a/database/migrations/2025_09_25_170500_add_custom_domain_to_tenants_table.php b/database/migrations/2025_09_25_170500_add_custom_domain_to_tenants_table.php
deleted file mode 100644
index 23cee7f..0000000
--- a/database/migrations/2025_09_25_170500_add_custom_domain_to_tenants_table.php
+++ /dev/null
@@ -1,27 +0,0 @@
-string('custom_domain')->nullable()->after('domain');
- });
-
- Schema::table('tenants', function (Blueprint $table) {
- $table->unique('custom_domain');
- });
- }
-
- public function down(): void
- {
- Schema::table('tenants', function (Blueprint $table) {
- $table->dropUnique('tenants_custom_domain_unique');
- $table->dropColumn('custom_domain');
- });
- }
-};
diff --git a/database/migrations/2025_09_26_000000_create_packages_system.php b/database/migrations/2025_09_26_000000_create_packages_system.php
new file mode 100644
index 0000000..84c97c4
--- /dev/null
+++ b/database/migrations/2025_09_26_000000_create_packages_system.php
@@ -0,0 +1,336 @@
+id();
+ $table->string('name');
+ $table->enum('type', ['endcustomer', 'reseller']);
+ $table->decimal('price', 8, 2);
+ $table->integer('max_photos')->nullable();
+ $table->integer('max_guests')->nullable();
+ $table->integer('gallery_days')->nullable();
+ $table->integer('max_tasks')->nullable();
+ $table->boolean('watermark_allowed')->default(true);
+ $table->boolean('branding_allowed')->default(false);
+ $table->integer('max_events_per_year')->nullable();
+ $table->timestamp('expires_after')->nullable();
+ $table->json('features')->nullable();
+ $table->timestamps();
+ $table->index(['type', 'price']);
+ });
+
+ // Seed standard packages if empty
+ if (DB::table('packages')->count() == 0) {
+ DB::table('packages')->insert([
+ [
+ 'name' => 'Free/Test',
+ 'type' => 'endcustomer',
+ 'price' => 0.00,
+ 'max_photos' => 30,
+ 'max_guests' => 10,
+ 'gallery_days' => 3,
+ 'max_tasks' => 1,
+ 'watermark_allowed' => true,
+ 'branding_allowed' => false,
+ 'max_events_per_year' => null,
+ 'expires_after' => null,
+ 'features' => json_encode([]),
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ],
+ [
+ 'name' => 'Starter',
+ 'type' => 'endcustomer',
+ 'price' => 19.00,
+ 'max_photos' => 300,
+ 'max_guests' => 50,
+ 'gallery_days' => 14,
+ 'max_tasks' => 5,
+ 'watermark_allowed' => true,
+ 'branding_allowed' => false,
+ 'max_events_per_year' => null,
+ 'expires_after' => null,
+ 'features' => json_encode([]),
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ],
+ [
+ 'name' => 'Reseller S',
+ 'type' => 'reseller',
+ 'price' => 149.00,
+ 'max_photos' => null,
+ 'max_guests' => null,
+ 'gallery_days' => null,
+ 'max_tasks' => null,
+ 'watermark_allowed' => true,
+ 'branding_allowed' => true,
+ 'max_events_per_year' => 5,
+ 'expires_after' => now()->addYear(),
+ 'features' => json_encode(['limited_branding']),
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ],
+ // Add more as needed
+ ]);
+ }
+ }
+
+ // Event Packages
+ if (!Schema::hasTable('event_packages')) {
+ Schema::create('event_packages', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('event_id')->constrained()->cascadeOnDelete();
+ $table->foreignId('package_id')->constrained()->cascadeOnDelete();
+ $table->decimal('purchased_price', 8, 2);
+ $table->timestamp('purchased_at');
+ $table->integer('used_photos')->default(0);
+ $table->timestamps();
+ $table->index('event_id');
+ });
+ }
+
+ // Tenant Packages
+ if (!Schema::hasTable('tenant_packages')) {
+ Schema::create('tenant_packages', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
+ $table->foreignId('package_id')->constrained()->cascadeOnDelete();
+ $table->decimal('price', 8, 2);
+ $table->timestamp('purchased_at');
+ $table->timestamp('expires_at');
+ $table->integer('used_events')->default(0);
+ $table->boolean('active')->default(true);
+ $table->timestamps();
+ $table->index(['tenant_id', 'active']);
+ });
+ }
+
+ // Package Purchases
+ if (!Schema::hasTable('package_purchases')) {
+ Schema::create('package_purchases', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('tenant_id')->nullable()->constrained();
+ $table->foreignId('event_id')->nullable()->constrained();
+ $table->foreignId('package_id')->constrained();
+ $table->string('provider_id');
+ $table->decimal('price', 8, 2);
+ $table->timestamp('purchased_at');
+ $table->enum('type', ['endcustomer_event', 'reseller_subscription']);
+ $table->json('metadata')->nullable();
+ $table->string('ip_address')->nullable();
+ $table->string('user_agent')->nullable();
+ $table->boolean('refunded')->default(false);
+ $table->timestamps();
+ $table->index(['tenant_id', 'created_at']);
+ });
+ }
+
+ // Purchase History
+ if (!Schema::hasTable('purchase_history')) {
+ Schema::create('purchase_history', function (Blueprint $table) {
+ $table->string('id', 255)->primary();
+ $table->string('tenant_id', 255);
+ $table->string('package_id', 255);
+ $table->integer('credits_added')->default(0);
+ $table->decimal('price', 10, 2)->default(0);
+ $table->string('currency', 3)->default('EUR');
+ $table->string('platform', 50);
+ $table->string('transaction_id', 255)->nullable();
+ $table->timestamp('purchased_at')->useCurrent();
+ $table->timestamp('created_at')->useCurrent();
+ $table->foreign('tenant_id')->references('id')->on('tenants');
+ $table->index('tenant_id');
+ $table->index('purchased_at');
+ $table->index('transaction_id');
+ });
+ }
+
+ // Add subscription fields to tenants if missing
+ if (Schema::hasTable('tenants')) {
+ if (!Schema::hasColumn('tenants', 'subscription_tier')) {
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->string('subscription_tier')->default('free')->after('event_credits_balance');
+ });
+ }
+ if (!Schema::hasColumn('tenants', 'subscription_status')) {
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->enum('subscription_status', ['free', 'active', 'suspended', 'expired'])->default('free')->after('subscription_tier');
+ });
+ }
+ if (!Schema::hasColumn('tenants', 'subscription_expires_at')) {
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->timestamp('subscription_expires_at')->nullable()->after('subscription_status');
+ });
+ }
+ if (!Schema::hasColumn('tenants', 'total_revenue')) {
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->decimal('total_revenue', 10, 2)->default(0.00)->after('subscription_expires_at');
+ });
+ }
+ }
+
+ // Idempotent migration from credits to packages (only if old tables exist and new don't have data)
+ if (Schema::hasTable('event_credits_ledger') && DB::table('tenant_packages')->count() == 0) {
+ // Migrate tenant credits to tenant_packages (Free package)
+ $freePackageId = DB::table('packages')->where('name', 'Free/Test')->value('id');
+ if ($freePackageId) {
+ DB::table('tenants')->where('event_credits_balance', '>', 0)->chunk(100, function ($tenants) use ($freePackageId) {
+ foreach ($tenants as $tenant) {
+ DB::table('tenant_packages')->insertOrIgnore([
+ 'tenant_id' => $tenant->id,
+ 'package_id' => $freePackageId,
+ 'price' => 0.00,
+ 'purchased_at' => $tenant->free_event_granted_at ?? now(),
+ 'expires_at' => now()->addDays(30),
+ 'used_events' => min($tenant->event_credits_balance, 1),
+ 'active' => true,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ DB::table('package_purchases')->insertOrIgnore([
+ 'tenant_id' => $tenant->id,
+ 'event_id' => null,
+ 'package_id' => $freePackageId,
+ 'provider_id' => 'migration_free',
+ 'price' => 0.00,
+ 'type' => 'reseller_subscription',
+ 'metadata' => json_encode(['migrated_from_credits' => $tenant->event_credits_balance]),
+ 'ip_address' => null,
+ 'user_agent' => null,
+ 'refunded' => false,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+ }
+ });
+
+ // Migrate event purchases if old data exists
+ if (Schema::hasTable('event_purchases')) {
+ DB::table('event_purchases')->join('events', 'event_purchases.event_id', '=', 'events.id')->chunk(100, function ($purchases) use ($freePackageId) {
+ foreach ($purchases as $purchase) {
+ DB::table('event_packages')->insertOrIgnore([
+ 'event_id' => $purchase->event_id,
+ 'package_id' => $freePackageId,
+ 'purchased_price' => $purchase->amount ?? 0.00,
+ 'purchased_at' => $purchase->purchased_at ?? now(),
+ 'used_photos' => 0,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+
+ DB::table('package_purchases')->insertOrIgnore([
+ 'tenant_id' => $purchase->tenant_id,
+ 'event_id' => $purchase->event_id,
+ 'package_id' => $freePackageId,
+ 'provider_id' => $purchase->provider ?? 'migration',
+ 'price' => $purchase->amount ?? 0.00,
+ 'type' => 'endcustomer_event',
+ 'metadata' => json_encode(['migrated_from_event_purchases' => true]),
+ 'ip_address' => null,
+ 'user_agent' => null,
+ 'refunded' => false,
+ 'created_at' => now(),
+ 'updated_at' => now(),
+ ]);
+ }
+ });
+ }
+ }
+ }
+
+ // Conditional drop of old credits tables and fields (only if migration happened or old structures exist)
+ if (Schema::hasTable('event_credits_ledger')) {
+ Schema::dropIfExists('event_credits_ledger');
+ }
+ if (Schema::hasTable('event_purchases')) {
+ Schema::dropIfExists('event_purchases');
+ }
+ if (Schema::hasTable('purchase_history') && DB::table('package_purchases')->count() > 0) { // Only drop if new data exists
+ Schema::dropIfExists('purchase_history');
+ }
+
+ // Drop old fields from tenants if new system is in place
+ if (Schema::hasTable('tenants')) {
+ $oldFields = ['event_credits_balance', 'free_event_granted_at'];
+ foreach ($oldFields as $field) {
+ if (Schema::hasColumn('tenants', $field) && DB::table('tenant_packages')->count() > 0) {
+ Schema::table('tenants', function (Blueprint $table) use ($field) {
+ $table->dropColumn($field);
+ });
+ }
+ }
+ }
+ }
+
+ public function down(): void
+ {
+ if (app()->environment('local', 'testing')) {
+ // Reverse drops and adds
+ if (!Schema::hasTable('purchase_history')) {
+ Schema::create('purchase_history', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
+ $table->string('package_id', 255);
+ $table->integer('credits_added')->default(0);
+ $table->decimal('price', 10, 2)->default(0);
+ $table->string('provider_id');
+ $table->timestamps();
+ });
+ }
+ if (!Schema::hasTable('event_purchases')) {
+ Schema::create('event_purchases', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
+ $table->unsignedInteger('events_purchased')->default(1);
+ $table->decimal('amount', 10, 2);
+ $table->string('currency', 3)->default('EUR');
+ $table->string('provider', 32);
+ $table->string('external_receipt_id')->nullable();
+ $table->string('status', 16)->default('pending');
+ $table->timestamp('purchased_at')->nullable();
+ $table->timestamps();
+ $table->index(['tenant_id', 'purchased_at']);
+ });
+ }
+ if (!Schema::hasTable('event_credits_ledger')) {
+ Schema::create('event_credits_ledger', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
+ $table->integer('delta');
+ $table->string('reason', 32);
+ $table->foreignId('related_purchase_id')->nullable()->constrained('event_purchases')->nullOnDelete();
+ $table->text('note')->nullable();
+ $table->timestamps();
+ $table->index(['tenant_id', 'created_at']);
+ });
+ }
+
+ // Re-add old fields to tenants
+ Schema::table('tenants', function (Blueprint $table) {
+ if (!Schema::hasColumn('tenants', 'event_credits_balance')) {
+ $table->integer('event_credits_balance')->default(1);
+ }
+ if (!Schema::hasColumn('tenants', 'free_event_granted_at')) {
+ $table->timestamp('free_event_granted_at')->nullable();
+ }
+ });
+
+ // Drop new tables
+ Schema::dropIfExists('package_purchases');
+ Schema::dropIfExists('tenant_packages');
+ Schema::dropIfExists('event_packages');
+ Schema::dropIfExists('packages');
+ }
+ }
+};
\ No newline at end of file
diff --git a/database/migrations/2025_09_26_000100_create_blog_system.php b/database/migrations/2025_09_26_000100_create_blog_system.php
new file mode 100644
index 0000000..56a467a
--- /dev/null
+++ b/database/migrations/2025_09_26_000100_create_blog_system.php
@@ -0,0 +1,127 @@
+id();
+ $table->string('name')->nullable();
+ $table->string('slug')->unique();
+ $table->longText('description')->nullable();
+ $table->json('translations')->nullable(); // From add_translations
+ $table->boolean('is_visible')->default(false);
+ $table->date('deleted_at')->nullable();
+ $table->timestamps();
+ });
+ } else {
+ if (!Schema::hasColumn('blog_categories', 'name')) {
+ Schema::table('blog_categories', function (Blueprint $table) {
+ $table->string('name')->nullable()->after('id');
+ $table->longText('description')->nullable()->after('name');
+ });
+ }
+ if (!Schema::hasColumn('blog_categories', 'translations')) {
+ Schema::table('blog_categories', function (Blueprint $table) {
+ $table->json('translations')->nullable()->after('description');
+ });
+ }
+ }
+
+ // Blog Authors
+ if (!Schema::hasTable('blog_authors')) {
+ Schema::create('blog_authors', function (Blueprint $table) {
+ $table->id();
+ $table->string('name');
+ $table->string('email')->unique();
+ $table->string('photo')->nullable();
+ $table->longText('bio')->nullable();
+ $table->string('github_handle')->nullable();
+ $table->string('twitter_handle')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ // Blog Posts
+ if (!Schema::hasTable('blog_posts')) {
+ Schema::create('blog_posts', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('blog_author_id')->nullable()->constrained()->cascadeOnDelete();
+ $table->foreignId('blog_category_id')->nullable()->constrained()->nullOnDelete();
+ $table->string('title');
+ $table->string('slug')->unique();
+ $table->text('excerpt')->nullable();
+ $table->string('banner')->nullable();
+ $table->longText('content');
+ $table->json('translations')->nullable(); // From add_translations
+ $table->date('published_at')->nullable();
+ $table->date('deleted_at')->nullable();
+ $table->timestamps();
+ });
+ } else {
+ if (!Schema::hasColumn('blog_posts', 'translations')) {
+ Schema::table('blog_posts', function (Blueprint $table) {
+ $table->json('translations')->nullable()->after('content');
+ });
+ }
+ }
+
+ // Tags
+ if (!Schema::hasTable('tags')) {
+ Schema::create('tags', function (Blueprint $table) {
+ $table->id();
+ $table->json('name');
+ $table->json('slug');
+ $table->string('type')->nullable();
+ $table->integer('order_column')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ // Taggables (polymorphic)
+ if (!Schema::hasTable('taggables')) {
+ Schema::create('taggables', function (Blueprint $table) {
+ $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
+ $table->morphs('taggable');
+ $table->unique(['tag_id', 'taggable_id', 'taggable_type']);
+ });
+ }
+ }
+
+ public function down(): void
+ {
+ if (app()->environment('local', 'testing')) {
+ if (Schema::hasTable('taggables')) {
+ Schema::table('taggables', function (Blueprint $table) {
+ $table->dropForeign(['tag_id']);
+ });
+ Schema::dropIfExists('taggables');
+ }
+ Schema::dropIfExists('tags');
+ if (Schema::hasColumn('blog_posts', 'translations')) {
+ Schema::table('blog_posts', function (Blueprint $table) {
+ $table->dropColumn('translations');
+ });
+ }
+ Schema::dropIfExists('blog_posts');
+ Schema::dropIfExists('blog_authors');
+ if (Schema::hasColumn('blog_categories', 'translations')) {
+ Schema::table('blog_categories', function (Blueprint $table) {
+ $table->dropColumn('translations');
+ });
+ }
+ if (Schema::hasColumn('blog_categories', 'name')) {
+ Schema::table('blog_categories', function (Blueprint $table) {
+ $table->dropColumn(['name', 'description']);
+ });
+ }
+ Schema::dropIfExists('blog_categories');
+ }
+ }
+};
\ No newline at end of file
diff --git a/database/migrations/2025_09_26_143146_create_filament_blog_tables.php b/database/migrations/2025_09_26_143146_create_filament_blog_tables.php
deleted file mode 100644
index 493aa44..0000000
--- a/database/migrations/2025_09_26_143146_create_filament_blog_tables.php
+++ /dev/null
@@ -1,60 +0,0 @@
-id();
- $table->string('name');
- $table->string('slug')->unique();
- $table->longText('description')->nullable();
- $table->boolean('is_visible')->default(false);
- $table->timestamps();
- });
-
- Schema::create('blog_authors', function (Blueprint $table) {
- $table->id();
- $table->string('name');
- $table->string('email')->unique();
- $table->string('photo')->nullable();
- $table->longText('bio')->nullable();
- $table->string('github_handle')->nullable();
- $table->string('twitter_handle')->nullable();
- $table->timestamps();
- });
-
- Schema::create('blog_posts', function (Blueprint $table) {
- $table->id();
- $table->foreignId('blog_author_id')->nullable()->constrained()->cascadeOnDelete();
- $table->foreignId('blog_category_id')->nullable()->constrained()->nullOnDelete();
- $table->string('title');
- $table->string('slug')->unique();
- $table->text('excerpt')->nullable();
- $table->string('banner')->nullable();
- $table->longText('content');
- $table->date('published_at')->nullable();
- $table->timestamps();
- });
- }
-
- /**
- * Reverse the migrations.
- *
- * @return void
- */
- public function down()
- {
- Schema::dropIfExists('blog_posts');
- Schema::dropIfExists('blog_categories');
- Schema::dropIfExists('blog_authors');
- }
-};
diff --git a/database/migrations/2025_09_26_143310_add_translations_to_blog_tables.php b/database/migrations/2025_09_26_143310_add_translations_to_blog_tables.php
deleted file mode 100644
index da0e5b0..0000000
--- a/database/migrations/2025_09_26_143310_add_translations_to_blog_tables.php
+++ /dev/null
@@ -1,36 +0,0 @@
-json('translations')->nullable();
- });
-
- Schema::table('blog_categories', function (Blueprint $table) {
- $table->json('translations')->nullable();
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('blog_posts', function (Blueprint $table) {
- $table->dropColumn('translations');
- });
-
- Schema::table('blog_categories', function (Blueprint $table) {
- $table->dropColumn('translations');
- });
- }
-};
diff --git a/database/migrations/2025_09_26_153139_create_tag_tables.php b/database/migrations/2025_09_26_153139_create_tag_tables.php
deleted file mode 100644
index 5925c6c..0000000
--- a/database/migrations/2025_09_26_153139_create_tag_tables.php
+++ /dev/null
@@ -1,36 +0,0 @@
-id();
-
- $table->json('name');
- $table->json('slug');
- $table->string('type')->nullable();
- $table->integer('order_column')->nullable();
-
- $table->timestamps();
- });
-
- Schema::create('taggables', function (Blueprint $table) {
- $table->foreignId('tag_id')->constrained()->cascadeOnDelete();
-
- $table->morphs('taggable');
-
- $table->unique(['tag_id', 'taggable_id', 'taggable_type']);
- });
- }
-
- public function down(): void
- {
- Schema::dropIfExists('taggables');
- Schema::dropIfExists('tags');
- }
-};
diff --git a/database/migrations/2025_09_26_190940_create_packages_table.php b/database/migrations/2025_09_26_190940_create_packages_table.php
deleted file mode 100644
index 63d9286..0000000
--- a/database/migrations/2025_09_26_190940_create_packages_table.php
+++ /dev/null
@@ -1,40 +0,0 @@
-id();
- $table->string('name');
- $table->enum('type', ['endcustomer', 'reseller']);
- $table->decimal('price', 8, 2);
- $table->integer('max_photos')->nullable();
- $table->integer('max_guests')->nullable();
- $table->integer('gallery_days')->nullable();
- $table->integer('max_tasks')->nullable();
- $table->boolean('watermark_allowed')->default(true);
- $table->boolean('branding_allowed')->default(false);
- $table->integer('max_events_per_year')->nullable();
- $table->timestamp('expires_after')->nullable();
- $table->json('features')->nullable();
- $table->timestamps();
- $table->index(['type', 'price']);
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('packages');
- }
-};
diff --git a/database/migrations/2025_09_26_191007_create_event_packages_table.php b/database/migrations/2025_09_26_191007_create_event_packages_table.php
deleted file mode 100644
index 579b54e..0000000
--- a/database/migrations/2025_09_26_191007_create_event_packages_table.php
+++ /dev/null
@@ -1,33 +0,0 @@
-id();
- $table->foreignId('event_id')->constrained()->cascadeOnDelete();
- $table->foreignId('package_id')->constrained()->cascadeOnDelete();
- $table->decimal('purchased_price', 8, 2);
- $table->timestamp('purchased_at');
- $table->integer('used_photos')->default(0);
- $table->timestamps();
- $table->index('event_id');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('event_packages');
- }
-};
diff --git a/database/migrations/2025_09_26_191026_create_tenant_packages_table.php b/database/migrations/2025_09_26_191026_create_tenant_packages_table.php
deleted file mode 100644
index a9c1e3a..0000000
--- a/database/migrations/2025_09_26_191026_create_tenant_packages_table.php
+++ /dev/null
@@ -1,35 +0,0 @@
-id();
- $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
- $table->foreignId('package_id')->constrained()->cascadeOnDelete();
- $table->decimal('price', 8, 2);
- $table->timestamp('purchased_at');
- $table->timestamp('expires_at');
- $table->integer('used_events')->default(0);
- $table->boolean('active')->default(true);
- $table->timestamps();
- $table->index(['tenant_id', 'active']);
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('tenant_packages');
- }
-};
diff --git a/database/migrations/2025_09_26_191046_create_package_purchases_table.php b/database/migrations/2025_09_26_191046_create_package_purchases_table.php
deleted file mode 100644
index 627705f..0000000
--- a/database/migrations/2025_09_26_191046_create_package_purchases_table.php
+++ /dev/null
@@ -1,38 +0,0 @@
-id();
- $table->foreignId('tenant_id')->nullable()->constrained();
- $table->foreignId('event_id')->nullable()->constrained();
- $table->foreignId('package_id')->constrained();
- $table->string('provider_id');
- $table->decimal('price', 8, 2);
- $table->enum('type', ['endcustomer_event', 'reseller_subscription']);
- $table->json('metadata')->nullable();
- $table->string('ip_address')->nullable();
- $table->string('user_agent')->nullable();
- $table->boolean('refunded')->default(false);
- $table->timestamps();
- $table->index(['tenant_id', 'purchased_at']);
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::dropIfExists('package_purchases');
- }
-};
diff --git a/database/migrations/2025_09_26_191103_migrate_credits_to_packages.php b/database/migrations/2025_09_26_191103_migrate_credits_to_packages.php
deleted file mode 100644
index 4ba4e4f..0000000
--- a/database/migrations/2025_09_26_191103_migrate_credits_to_packages.php
+++ /dev/null
@@ -1,152 +0,0 @@
-count() == 0) {
- // Insert standard packages if not seeded
- DB::table('packages')->insert([
- [
- 'name' => 'Free/Test',
- 'type' => 'endcustomer',
- 'price' => 0.00,
- 'max_photos' => 30,
- 'max_guests' => 10,
- 'gallery_days' => 3,
- 'max_tasks' => 1,
- 'watermark_allowed' => true,
- 'branding_allowed' => false,
- 'max_events_per_year' => null,
- 'expires_after' => null,
- 'features' => json_encode([]),
- 'created_at' => now(),
- 'updated_at' => now(),
- ],
- [
- 'name' => 'Starter',
- 'type' => 'endcustomer',
- 'price' => 19.00,
- 'max_photos' => 300,
- 'max_guests' => 50,
- 'gallery_days' => 14,
- 'max_tasks' => 5,
- 'watermark_allowed' => true,
- 'branding_allowed' => false,
- 'max_events_per_year' => null,
- 'expires_after' => null,
- 'features' => json_encode([]),
- 'created_at' => now(),
- 'updated_at' => now(),
- ],
- // Add more standard packages as per plan
- [
- 'name' => 'Reseller S',
- 'type' => 'reseller',
- 'price' => 149.00,
- 'max_photos' => null,
- 'max_guests' => null,
- 'gallery_days' => null,
- 'max_tasks' => null,
- 'watermark_allowed' => true,
- 'branding_allowed' => true,
- 'max_events_per_year' => 5,
- 'expires_after' => now()->addYear(),
- 'features' => json_encode(['limited_branding']),
- 'created_at' => now(),
- 'updated_at' => now(),
- ],
- // ... other reseller packages
- ]);
- }
-
- // Migrate tenant credits to tenant_packages (Free package)
- DB::table('tenants')->where('event_credits_balance', '>', 0)->orderBy('id')->chunk(100, function ($tenants) {
- foreach ($tenants as $tenant) {
- $freePackageId = DB::table('packages')->where('name', 'Free/Test')->first()->id;
- DB::table('tenant_packages')->insert([
- 'tenant_id' => $tenant->id,
- 'package_id' => $freePackageId,
- 'price' => 0.00,
- 'purchased_at' => $tenant->free_event_granted_at ?? now(),
- 'expires_at' => now()->addDays(30), // or based on credits
- 'used_events' => min($tenant->event_credits_balance, 1), // e.g. 1 free event
- 'active' => true,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
-
- // Create purchase ledger entry
- DB::table('package_purchases')->insert([
- 'tenant_id' => $tenant->id,
- 'event_id' => null,
- 'package_id' => $freePackageId,
- 'provider_id' => 'migration_free',
- 'price' => 0.00,
- 'type' => 'reseller_subscription',
- 'metadata' => json_encode(['migrated_from_credits' => $tenant->event_credits_balance]),
- 'ip_address' => null,
- 'user_agent' => null,
- 'refunded' => false,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
- }
- });
-
- // Migrate event purchases to event_packages (if any existing events)
- DB::table('events')->orderBy('id')->chunk(100, function ($events) {
- foreach ($events as $event) {
- if ($event->tenant->event_credits_balance > 0) { // or check if event was created with credits
- $freePackageId = DB::table('packages')->where('name', 'Free/Test')->first()->id;
- DB::table('event_packages')->insert([
- 'event_id' => $event->id,
- 'package_id' => $freePackageId,
- 'purchased_price' => 0.00,
- 'purchased_at' => $event->created_at,
- 'used_photos' => 0,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
-
- // Ledger entry
- DB::table('package_purchases')->insert([
- 'tenant_id' => $event->tenant_id,
- 'event_id' => $event->id,
- 'package_id' => $freePackageId,
- 'provider_id' => 'migration_free',
- 'price' => 0.00,
- 'type' => 'endcustomer_event',
- 'metadata' => json_encode(['migrated_from_credits' => true]),
- 'ip_address' => null,
- 'user_agent' => null,
- 'refunded' => false,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);
- }
- }
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('packages', function (Blueprint $table) {
- //
- });
- }
-};
diff --git a/database/migrations/2025_09_26_191104_drop_credits_tables_and_fields.php b/database/migrations/2025_09_26_191104_drop_credits_tables_and_fields.php
deleted file mode 100644
index 7381fcd..0000000
--- a/database/migrations/2025_09_26_191104_drop_credits_tables_and_fields.php
+++ /dev/null
@@ -1,92 +0,0 @@
-dropIndex(['tenant_id', 'purchased_at']);
- });
- }
-
- if (Schema::hasColumn('tenants', 'event_credits_balance')) {
- Schema::table('tenants', function (Blueprint $table) {
- $table->dropColumn('event_credits_balance');
- });
- }
- if (Schema::hasColumn('tenants', 'subscription_tier')) {
- Schema::table('tenants', function (Blueprint $table) {
- $table->dropColumn('subscription_tier');
- });
- }
- if (Schema::hasColumn('tenants', 'subscription_expires_at')) {
- Schema::table('tenants', function (Blueprint $table) {
- $table->dropColumn('subscription_expires_at');
- });
- }
- if (Schema::hasColumn('tenants', 'free_event_granted_at')) {
- Schema::table('tenants', function (Blueprint $table) {
- $table->dropColumn('free_event_granted_at');
- });
- }
- if (Schema::hasColumn('tenants', 'total_revenue')) {
- Schema::table('tenants', function (Blueprint $table) {
- $table->dropColumn('total_revenue');
- });
- }
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('tenants', function (Blueprint $table) {
- $table->integer('event_credits_balance')->default(1);
- $table->string('subscription_tier')->nullable();
- $table->timestamp('subscription_expires_at')->nullable();
- $table->timestamp('free_event_granted_at')->nullable();
- $table->decimal('total_revenue', 10, 2)->default(0.00);
- });
-
- Schema::create('event_purchases', function (Blueprint $table) {
- $table->id();
- $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
- $table->foreignId('event_id')->constrained()->cascadeOnDelete();
- $table->integer('credits_added')->default(0);
- $table->decimal('price', 10, 2)->default(0);
- $table->string('provider_id');
- $table->timestamps();
- });
-
- Schema::create('purchase_history', function (Blueprint $table) {
- $table->id();
- $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
- $table->string('package_id', 255);
- $table->integer('credits_added')->default(0);
- $table->decimal('price', 10, 2)->default(0);
- $table->string('provider_id');
- $table->timestamps();
- });
-
- Schema::create('event_credits_ledger', function (Blueprint $table) {
- $table->id();
- $table->foreignId('tenant_id')->constrained()->cascadeOnDelete();
- $table->integer('credits_change');
- $table->string('reason');
- $table->timestamps();
- });
- }
-};
diff --git a/database/migrations/2025_09_27_110000_add_personal_fields_to_users_table.php b/database/migrations/2025_09_27_110000_add_personal_fields_to_users_table.php
index 05ce3b4..a4fb398 100644
--- a/database/migrations/2025_09_27_110000_add_personal_fields_to_users_table.php
+++ b/database/migrations/2025_09_27_110000_add_personal_fields_to_users_table.php
@@ -12,10 +12,18 @@ return new class extends Migration
public function up(): void
{
Schema::table('users', function (Blueprint $table) {
- $table->string('first_name')->nullable()->after('name');
- $table->string('last_name')->nullable()->after('first_name');
- $table->text('address')->nullable()->after('last_name');
- $table->string('phone')->nullable()->after('address');
+ if (!Schema::hasColumn('tenants', 'first_name')) {
+ $table->string('first_name')->default('')->after('name');
+ }
+ if (!Schema::hasColumn('tenants', 'last_name')) {
+ $table->string('last_name')->default('')->after('first_name');
+ }
+ if (!Schema::hasColumn('tenants', 'address')) {
+ $table->string('address')->nullable()->after('last_name');
+ }
+ if (!Schema::hasColumn('tenants', 'phone')) {
+ $table->string('phone')->nullable()->after('address');
+ }
});
}
diff --git a/database/migrations/2025_09_27_110100_add_user_id_to_tenants_table.php b/database/migrations/2025_09_27_110100_add_user_id_to_tenants_table.php
deleted file mode 100644
index cf0811e..0000000
--- a/database/migrations/2025_09_27_110100_add_user_id_to_tenants_table.php
+++ /dev/null
@@ -1,29 +0,0 @@
-foreignId('user_id')->nullable()->constrained('users')->onDelete('cascade')->after('id');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('tenants', function (Blueprint $table) {
- $table->dropForeign(['user_id']);
- $table->dropColumn('user_id');
- });
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_29_155553_add_multilanguage_columns_to_blog_categories.php b/database/migrations/2025_09_29_155553_add_multilanguage_columns_to_blog_categories.php
deleted file mode 100644
index 56a60f5..0000000
--- a/database/migrations/2025_09_29_155553_add_multilanguage_columns_to_blog_categories.php
+++ /dev/null
@@ -1,23 +0,0 @@
-string('name')->nullable()->after('id');
- $table->text('description')->nullable()->after('name');
- });
- }
-
- public function down(): void
- {
- Schema::table('blog_categories', function (Blueprint $table) {
- $table->dropColumn(['name', 'description']);
- });
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_29_162547_drop_old_name_description_from_blog_categories.php b/database/migrations/2025_09_29_162547_drop_old_name_description_from_blog_categories.php
deleted file mode 100644
index 81dd683..0000000
--- a/database/migrations/2025_09_29_162547_drop_old_name_description_from_blog_categories.php
+++ /dev/null
@@ -1,29 +0,0 @@
-dropColumn(['name', 'description']);
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('blog_categories', function (Blueprint $table) {
- $table->string('name')->after('id');
- $table->text('description')->nullable()->after('name');
- });
- }
-};
diff --git a/database/migrations/2025_09_29_164248_add_name_and_description_json_to_blog_categories.php b/database/migrations/2025_09_29_164248_add_name_and_description_json_to_blog_categories.php
deleted file mode 100644
index 298bfa5..0000000
--- a/database/migrations/2025_09_29_164248_add_name_and_description_json_to_blog_categories.php
+++ /dev/null
@@ -1,27 +0,0 @@
-json('name')->nullable()->after('id');
- }
- if (!Schema::hasColumn('blog_categories', 'description')) {
- $table->json('description')->nullable()->after('name');
- }
- });
- }
-
- public function down(): void
- {
- Schema::table('blog_categories', function (Blueprint $table) {
- $table->dropColumn(['name', 'description']);
- });
- }
-};
\ No newline at end of file
diff --git a/database/migrations/2025_09_29_204232_alter_blog_categories_columns_to_json_and_drop_translations.php b/database/migrations/2025_09_29_204232_alter_blog_categories_columns_to_json_and_drop_translations.php
deleted file mode 100644
index 4b05525..0000000
--- a/database/migrations/2025_09_29_204232_alter_blog_categories_columns_to_json_and_drop_translations.php
+++ /dev/null
@@ -1,32 +0,0 @@
-json('name')->nullable()->change();
- $table->json('description')->nullable()->change();
- $table->dropColumn('translations');
- });
- }
-
- /**
- * Reverse the migrations.
- */
- public function down(): void
- {
- Schema::table('blog_categories', function (Blueprint $table) {
- $table->string('name')->nullable()->change();
- $table->text('description')->nullable()->change();
- $table->json('translations')->nullable();
- });
- }
-};
diff --git a/database/migrations/2025_09_26_145500_add_email_to_tenants_table.php b/database/migrations/2025_09_30_000000_make_provider_id_nullable_in_package_purchases_table.php
similarity index 57%
rename from database/migrations/2025_09_26_145500_add_email_to_tenants_table.php
rename to database/migrations/2025_09_30_000000_make_provider_id_nullable_in_package_purchases_table.php
index b8f242a..74ce086 100644
--- a/database/migrations/2025_09_26_145500_add_email_to_tenants_table.php
+++ b/database/migrations/2025_09_30_000000_make_provider_id_nullable_in_package_purchases_table.php
@@ -11,8 +11,8 @@ return new class extends Migration
*/
public function up(): void
{
- Schema::table('tenants', function (Blueprint $table) {
- $table->string('email')->nullable()->after('slug');
+ Schema::table('package_purchases', function (Blueprint $table) {
+ $table->string('provider_id')->nullable()->change();
});
}
@@ -21,8 +21,8 @@ return new class extends Migration
*/
public function down(): void
{
- Schema::table('tenants', function (Blueprint $table) {
- $table->dropColumn('email');
+ Schema::table('package_purchases', function (Blueprint $table) {
+ $table->string('provider_id')->nullable(false)->change();
});
}
};
\ No newline at end of file
diff --git a/database/migrations/2025_09_30_000001_add_missing_fields_to_tenants_table.php b/database/migrations/2025_09_30_000001_add_missing_fields_to_tenants_table.php
new file mode 100644
index 0000000..7d57c29
--- /dev/null
+++ b/database/migrations/2025_09_30_000001_add_missing_fields_to_tenants_table.php
@@ -0,0 +1,36 @@
+boolean('is_suspended')->default(false)->after('is_active');
+ }
+ if (!Schema::hasColumn('tenants', 'settings')) {
+ $table->json('settings')->nullable()->after('subscription_expires_at');
+ }
+ if (!Schema::hasColumn('tenants', 'settings_updated_at')) {
+ $table->timestamp('settings_updated_at')->nullable()->after('settings');
+ }
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('tenants', function (Blueprint $table) {
+ $table->dropColumn(['is_suspended', 'settings', 'settings_updated_at']);
+ });
+ }
+};
\ No newline at end of file
diff --git a/database/migrations/2025_09_30_000002_add_missing_fields_to_packages_table.php b/database/migrations/2025_09_30_000002_add_missing_fields_to_packages_table.php
new file mode 100644
index 0000000..8eb3859
--- /dev/null
+++ b/database/migrations/2025_09_30_000002_add_missing_fields_to_packages_table.php
@@ -0,0 +1,52 @@
+string('slug')->default('')->after('name');
+ }
+ if (!Schema::hasColumn('packages', 'description')) {
+ $table->text('description')->nullable()->after('slug');
+ }
+ });
+
+ // Update existing packages with unique slugs
+ if (Schema::hasTable('packages')) {
+ $packages = DB::table('packages')->get();
+ foreach ($packages as $package) {
+ $slug = Str::slug($package->name . '-' . $package->id);
+ DB::table('packages')->where('id', $package->id)->update(['slug' => $slug]);
+ }
+ }
+
+ // Add unique index after updating
+ Schema::table('packages', function (Blueprint $table) {
+ $table->unique('slug');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::table('packages', function (Blueprint $table) {
+ $table->dropUnique(['slug']);
+ });
+ Schema::table('packages', function (Blueprint $table) {
+ $table->dropColumn(['slug', 'description']);
+ });
+ }
+};
\ No newline at end of file
diff --git a/database/seeders/DemoEventSeeder.php b/database/seeders/DemoEventSeeder.php
index bf0e74a..d36f36a 100644
--- a/database/seeders/DemoEventSeeder.php
+++ b/database/seeders/DemoEventSeeder.php
@@ -19,9 +19,9 @@ class DemoEventSeeder extends Seeder
'description' => ['de'=>'Demo-Event','en'=>'Demo event'],
'date' => now()->addMonths(3)->toDateString(),
'event_type_id' => $type->id,
- 'status' => 'published',
+ 'status' => 'active',
'is_active' => true,
- 'settings' => [],
+ 'settings' => json_encode([]),
'default_locale' => 'de',
]);
}
diff --git a/database/seeders/PackageSeeder.php b/database/seeders/PackageSeeder.php
index f567c87..485d67e 100644
--- a/database/seeders/PackageSeeder.php
+++ b/database/seeders/PackageSeeder.php
@@ -5,6 +5,7 @@ namespace Database\Seeders;
use Illuminate\Database\Seeder;
use App\Models\Package;
use App\Enums\PackageType;
+use Illuminate\Support\Str;
class PackageSeeder extends Seeder
{
@@ -16,6 +17,7 @@ class PackageSeeder extends Seeder
// Endcustomer Packages
Package::create([
'name' => 'Free / Test',
+ 'slug' => Str::slug('Free / Test'),
'type' => PackageType::ENDCUSTOMER,
'price' => 0.00,
'max_photos' => 30,
@@ -33,6 +35,7 @@ class PackageSeeder extends Seeder
Package::create([
'name' => 'Starter',
+ 'slug' => Str::slug('Starter'),
'type' => PackageType::ENDCUSTOMER,
'price' => 29.00,
'max_photos' => 200,
@@ -51,6 +54,7 @@ class PackageSeeder extends Seeder
Package::create([
'name' => 'Pro',
+ 'slug' => Str::slug('Pro'),
'type' => PackageType::ENDCUSTOMER,
'price' => 79.00,
'max_photos' => 1000,
@@ -72,6 +76,7 @@ class PackageSeeder extends Seeder
// Reseller Packages
Package::create([
'name' => 'S (Small Reseller)',
+ 'slug' => Str::slug('S (Small Reseller)'),
'type' => PackageType::RESELLER,
'price' => 199.00,
'max_photos' => 500, // per event limit
@@ -91,6 +96,7 @@ class PackageSeeder extends Seeder
Package::create([
'name' => 'M (Medium Reseller)',
+ 'slug' => Str::slug('M (Medium Reseller)'),
'type' => PackageType::RESELLER,
'price' => 399.00,
'max_photos' => 1000, // per event limit
diff --git a/database/seeders/TasksSeeder.php b/database/seeders/TasksSeeder.php
index 70546ed..254ccd9 100644
--- a/database/seeders/TasksSeeder.php
+++ b/database/seeders/TasksSeeder.php
@@ -16,7 +16,9 @@ class TasksSeeder extends Seeder
'name' => 'Demo Tenant',
'domain' => null,
'is_active' => true,
- 'settings' => [],
+ 'is_suspended' => false,
+ 'settings' => json_encode([]),
+ 'settings_updated_at' => null,
]
);
diff --git a/package-lock.json b/package-lock.json
index e282a84..10a42ce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/vite": "^4.1.11",
+ "@tanstack/react-query": "^5.90.2",
"@types/react": "^19.0.3",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.6.0",
@@ -44,7 +45,7 @@
},
"devDependencies": {
"@eslint/js": "^9.19.0",
- "@laravel/vite-plugin-wayfinder": "^0.1.3",
+ "@laravel/vite-plugin-wayfinder": "^0.1.7",
"@playwright/test": "^1.55.0",
"@types/node": "^22.13.5",
"eslint": "^9.17.0",
@@ -1627,10 +1628,11 @@
}
},
"node_modules/@laravel/vite-plugin-wayfinder": {
- "version": "0.1.3",
- "resolved": "https://registry.npmjs.org/@laravel/vite-plugin-wayfinder/-/vite-plugin-wayfinder-0.1.3.tgz",
- "integrity": "sha512-S/21Lzl7lci7LrRo/VsN5AXT02AMf7rs+OPTyt3VPgffBB1wTrzwsPr28sCU0gcR/APhfC1eVIUwpLbAvBmyKw==",
- "dev": true
+ "version": "0.1.7",
+ "resolved": "https://registry.npmjs.org/@laravel/vite-plugin-wayfinder/-/vite-plugin-wayfinder-0.1.7.tgz",
+ "integrity": "sha512-yZYIr1iwuCQ7LFI+GsJk9vacw1HWMp3ZlDlW0pdfz3zXyKeu4US7oH79KmQQ031L0cYaSyaUMo/Ha1D4BosKqw==",
+ "dev": true,
+ "license": "MIT"
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.18.0",
@@ -3348,6 +3350,32 @@
"vite": "^5.2.0 || ^6 || ^7"
}
},
+ "node_modules/@tanstack/query-core": {
+ "version": "5.90.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.2.tgz",
+ "integrity": "sha512-k/TcR3YalnzibscALLwxeiLUub6jN5EDLwKDiO7q5f4ICEoptJ+n9+7vcEFy5/x/i6Q+Lb/tXrsKCggf5uQJXQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ }
+ },
+ "node_modules/@tanstack/react-query": {
+ "version": "5.90.2",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.2.tgz",
+ "integrity": "sha512-CLABiR+h5PYfOWr/z+vWFt5VsOA2ekQeRQBFSKlcoW6Ndx/f8rfyVmq4LbgOM4GG2qtxAxjLYLOpCNTYm4uKzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@tanstack/query-core": "5.90.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/tannerlinsley"
+ },
+ "peerDependencies": {
+ "react": "^18 || ^19"
+ }
+ },
"node_modules/@tanstack/react-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
diff --git a/package.json b/package.json
index 5c8b141..0327faf 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
},
"devDependencies": {
"@eslint/js": "^9.19.0",
- "@laravel/vite-plugin-wayfinder": "^0.1.3",
+ "@laravel/vite-plugin-wayfinder": "^0.1.7",
"@playwright/test": "^1.55.0",
"@types/node": "^22.13.5",
"eslint": "^9.17.0",
@@ -43,6 +43,7 @@
"@radix-ui/react-toggle-group": "^1.1.2",
"@radix-ui/react-tooltip": "^1.1.8",
"@tailwindcss/vite": "^4.1.11",
+ "@tanstack/react-query": "^5.90.2",
"@types/react": "^19.0.3",
"@types/react-dom": "^19.0.2",
"@vitejs/plugin-react": "^4.6.0",
diff --git a/resources/js/admin/main.tsx b/resources/js/admin/main.tsx
index 3e760b4..c2a5530 100644
--- a/resources/js/admin/main.tsx
+++ b/resources/js/admin/main.tsx
@@ -4,7 +4,7 @@ import { RouterProvider } from 'react-router-dom';
import { AuthProvider } from './auth/context';
import { router } from './router';
import '../../css/app.css';
-import { initializeTheme } from '@/hooks/use-appearance';
+import { initializeTheme } from '@/hooks/use-appearance.tsx';
initializeTheme();
const rootEl = document.getElementById('root')!;
diff --git a/resources/js/components/delete-user.tsx b/resources/js/components/delete-user.tsx
index 7c4b73a..9ca7d1c 100644
--- a/resources/js/components/delete-user.tsx
+++ b/resources/js/components/delete-user.tsx
@@ -1,4 +1,4 @@
-import ProfileController from '@/actions/App/Http/Controllers/Settings/ProfileController';
+import { destroy } from '@/actions/App/Http/Controllers/Settings/ProfileController';
import HeadingSmall from '@/components/heading-small';
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
diff --git a/resources/js/components/user-menu-content.tsx b/resources/js/components/user-menu-content.tsx
index 0f198a5..459c8d1 100644
--- a/resources/js/components/user-menu-content.tsx
+++ b/resources/js/components/user-menu-content.tsx
@@ -2,7 +2,7 @@ import { DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSep
import { UserInfo } from '@/components/user-info';
import { useMobileNavigation } from '@/hooks/use-mobile-navigation';
import { logout } from '@/routes';
-import { edit } from '@/routes/profile';
+import { edit } from '@/routes/settings/profile';
import { type User } from '@/types';
import { Link, router } from '@inertiajs/react';
import { LogOut, Settings } from 'lucide-react';
diff --git a/resources/js/guest/main.tsx b/resources/js/guest/main.tsx
index c46880b..3218cbe 100644
--- a/resources/js/guest/main.tsx
+++ b/resources/js/guest/main.tsx
@@ -3,7 +3,7 @@ import { createRoot } from 'react-dom/client';
import { RouterProvider } from 'react-router-dom';
import { router } from './router';
import '../../css/app.css';
-import { initializeTheme } from '@/hooks/use-appearance';
+import { initializeTheme } from '@/hooks/use-appearance.tsx';
import { ToastProvider } from './components/ToastHost';
initializeTheme();
diff --git a/resources/js/layouts/settings/layout.tsx b/resources/js/layouts/settings/layout.tsx
index 14f0a86..27968ab 100644
--- a/resources/js/layouts/settings/layout.tsx
+++ b/resources/js/layouts/settings/layout.tsx
@@ -4,7 +4,7 @@ import { Separator } from '@/components/ui/separator';
import { cn } from '@/lib/utils';
import { appearance } from '@/routes';
import { edit as editPassword } from '@/routes/password';
-import { edit } from '@/routes/profile';
+import { edit } from '@/routes/settings/profile';
import { type NavItem } from '@/types';
import { Link } from '@inertiajs/react';
import { type PropsWithChildren } from 'react';
diff --git a/resources/js/pages/auth/confirm-password.tsx b/resources/js/pages/auth/confirm-password.tsx
index dd6102e..e771960 100644
--- a/resources/js/pages/auth/confirm-password.tsx
+++ b/resources/js/pages/auth/confirm-password.tsx
@@ -1,4 +1,4 @@
-import ConfirmablePasswordController from '@/actions/App/Http/Controllers/Auth/ConfirmablePasswordController';
+import { store } from '@/actions/App/Http/Controllers/Auth/ConfirmablePasswordController';
import InputError from '@/components/input-error';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
@@ -15,7 +15,7 @@ export default function ConfirmPassword() {
>
-