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() { > -
+ {({ processing, errors }) => (
diff --git a/resources/js/pages/auth/forgot-password.tsx b/resources/js/pages/auth/forgot-password.tsx index c81458c..b08a9c6 100644 --- a/resources/js/pages/auth/forgot-password.tsx +++ b/resources/js/pages/auth/forgot-password.tsx @@ -1,5 +1,5 @@ // Components -import PasswordResetLinkController from '@/actions/App/Http/Controllers/Auth/PasswordResetLinkController'; +import { store } from '@/actions/App/Http/Controllers/Auth/PasswordResetLinkController'; import { login } from '@/routes'; import { Form, Head } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; @@ -19,7 +19,7 @@ export default function ForgotPassword({ status }: { status?: string }) { {status &&
{status}
}
- + {({ processing, errors }) => ( <>
diff --git a/resources/js/pages/auth/login.tsx b/resources/js/pages/auth/login.tsx index 802abcb..c86c8cb 100644 --- a/resources/js/pages/auth/login.tsx +++ b/resources/js/pages/auth/login.tsx @@ -1,4 +1,4 @@ -import AuthenticatedSessionController from '@/actions/App/Http/Controllers/Auth/AuthenticatedSessionController'; +import { useForm, router } from '@inertiajs/react'; import InputError from '@/components/input-error'; import TextLink from '@/components/text-link'; import { Button } from '@/components/ui/button'; @@ -8,7 +8,7 @@ import { Label } from '@/components/ui/label'; import AuthLayout from '@/layouts/auth-layout'; import { register } from '@/routes'; import { request } from '@/routes/password'; -import { Form, Head } from '@inertiajs/react'; +import { Head } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; interface LoginProps { @@ -17,70 +17,87 @@ interface LoginProps { } export default function Login({ status, canResetPassword }: LoginProps) { + const { data, setData, post, processing, errors } = useForm({ + email: '', + password: '', + remember: false, + }); + + const submit = (e: React.FormEvent) => { + e.preventDefault(); + post('/login'); + }; + return ( - - {({ processing, errors }) => ( - <> -
-
- - - -
+ +
+
+ + setData('email', e.target.value)} + /> + +
-
-
- - {canResetPassword && ( - - Forgot password? - - )} -
- - -
- -
- - -
- - +
+
+ + {canResetPassword && ( + + Forgot password? + + )}
+ setData('password', e.target.value)} + /> + +
-
- Don't have an account?{' '} - - Sign up - -
- - )} - +
+ setData('remember', Boolean(checked))} + /> + +
+ + +
+ +
+ Don't have an account?{' '} + + Sign up + +
+ {status &&
{status}
} diff --git a/resources/js/pages/auth/register.tsx b/resources/js/pages/auth/register.tsx index bcb16f1..eb0284e 100644 --- a/resources/js/pages/auth/register.tsx +++ b/resources/js/pages/auth/register.tsx @@ -1,100 +1,251 @@ -import RegisteredUserController from '@/actions/App/Http/Controllers/Auth/RegisteredUserController'; -import { login } from '@/routes'; -import { Form, Head } from '@inertiajs/react'; +import React from 'react'; +import { useForm, router } from '@inertiajs/react'; +import { Head } from '@inertiajs/react'; import { LoaderCircle } from 'lucide-react'; -import InputError from '@/components/input-error'; -import TextLink from '@/components/text-link'; -import { Button } from '@/components/ui/button'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import AuthLayout from '@/layouts/auth-layout'; +interface RegisterProps { + package?: { + id: number; + name: string; + description: string; + price: number; + } | null; +} + +export default function Register({ package: initialPackage }: RegisterProps) { + const { data, setData, post, processing, errors } = useForm({ + name: '', + username: '', + email: '', + password: '', + password_confirmation: '', + first_name: '', + last_name: '', + address: '', + phone: '', + privacy_consent: false, + package_id: initialPackage?.id || null, + }); + + const submit = (e: React.FormEvent) => { + e.preventDefault(); + router.post('/register'); + }; -export default function Register() { return ( - - -
- {({ processing, errors }) => ( - <> -
-
- - - -
- -
- - - -
- -
- - - -
- -
- - - -
- - +
+ +
+
+

+ Registrieren +

+ {initialPackage && ( +
+

{initialPackage.name}

+

{initialPackage.description}

+

+ {initialPackage.price === 0 ? 'Kostenlos' : `${initialPackage.price} €`} +

+
+ )} +
+ +
+
+ + setData('name', e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + placeholder="Vollständiger Name" + /> + {errors.name &&

{errors.name}

}
-
- Already have an account?{' '} - - Log in - +
+ + setData('username', e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + placeholder="Benutzername" + /> + {errors.username &&

{errors.username}

}
- - )} - - + +
+ + setData('email', e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + placeholder="email@example.com" + /> + {errors.email &&

{errors.email}

} +
+ +
+ + setData('first_name', e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + placeholder="Vorname" + /> + {errors.first_name &&

{errors.first_name}

} +
+ +
+ + setData('last_name', e.target.value)} + className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm" + placeholder="Nachname" + /> + {errors.last_name &&

{errors.last_name}

} +
+ +
+ +