From 07bd177e325cfa7491f2385dc491ed1d7724d949 Mon Sep 17 00:00:00 2001 From: Samuel Olaegbe Date: Tue, 25 Feb 2025 21:27:29 +0100 Subject: [PATCH 1/2] Implement multi-tenancy using Spatie Laravel Multitenancy package --- app/Models/Plan.php | 70 +++++++++++ app/Models/School.php | 17 ++- app/Models/Scopes/TenantScope.php | 33 +++++ app/Models/Subscription.php | 75 +++++++++++ app/Models/Tenant.php | 88 +++++++++++++ app/Models/Traits/BelongsToTenant.php | 32 +++++ app/Models/User.php | 11 +- app/Multitenancy/DomainTenantFinder.php | 29 +++++ composer.json | 1 + composer.lock | 85 ++++++++++++- config/multitenancy.php | 117 ++++++++++++++++++ ...2025_02_25_201002_create_tenants_table.php | 45 +++++++ ..._201508_add_tenant_id_to_schools_table.php | 31 +++++ ...25_201551_add_tenant_id_to_users_table.php | 31 +++++ ..._25_201903_add_tenant_id_to_all_tables.php | 71 +++++++++++ ...2_25_202104_create_subscriptions_table.php | 41 ++++++ .../2025_02_25_202237_create_plans_table.php | 43 +++++++ ...25_202418_add_plan_id_to_tenants_table.php | 31 +++++ ...5_201321_create_landlord_tenants_table.php | 45 +++++++ database/seeders/DatabaseSeeder.php | 1 + database/seeders/PlanSeeder.php | 95 ++++++++++++++ 21 files changed, 987 insertions(+), 5 deletions(-) create mode 100644 app/Models/Plan.php create mode 100644 app/Models/Scopes/TenantScope.php create mode 100644 app/Models/Subscription.php create mode 100644 app/Models/Tenant.php create mode 100644 app/Models/Traits/BelongsToTenant.php create mode 100644 app/Multitenancy/DomainTenantFinder.php create mode 100644 config/multitenancy.php create mode 100644 database/migrations/2025_02_25_201002_create_tenants_table.php create mode 100644 database/migrations/2025_02_25_201508_add_tenant_id_to_schools_table.php create mode 100644 database/migrations/2025_02_25_201551_add_tenant_id_to_users_table.php create mode 100644 database/migrations/2025_02_25_201903_add_tenant_id_to_all_tables.php create mode 100644 database/migrations/2025_02_25_202104_create_subscriptions_table.php create mode 100644 database/migrations/2025_02_25_202237_create_plans_table.php create mode 100644 database/migrations/2025_02_25_202418_add_plan_id_to_tenants_table.php create mode 100644 database/migrations/landlord/2025_02_25_201321_create_landlord_tenants_table.php create mode 100644 database/seeders/PlanSeeder.php diff --git a/app/Models/Plan.php b/app/Models/Plan.php new file mode 100644 index 0000000..dee1de6 --- /dev/null +++ b/app/Models/Plan.php @@ -0,0 +1,70 @@ + 'boolean', + 'price_monthly' => 'decimal:2', + 'price_yearly' => 'decimal:2', + 'features' => 'array', + ]; + + /** + * Get the subscriptions for the plan. + */ + public function subscriptions(): HasMany + { + return $this->hasMany(Subscription::class); + } + + /** + * Get the monthly price formatted. + */ + public function getMonthlyPriceFormattedAttribute(): string + { + return '$' . number_format($this->price_monthly, 2); + } + + /** + * Get the yearly price formatted. + */ + public function getYearlyPriceFormattedAttribute(): string + { + return '$' . number_format($this->price_yearly, 2); + } + + /** + * Get active plans. + */ + public function scopeActive($query) + { + return $query->where('is_active', true); + } +} diff --git a/app/Models/School.php b/app/Models/School.php index dd909ad..3bd8486 100644 --- a/app/Models/School.php +++ b/app/Models/School.php @@ -2,14 +2,29 @@ namespace App\Models; +use App\Models\Traits\BelongsToTenant; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\BelongsTo; class School extends BaseModel { - use HasFactory; + use HasFactory, BelongsToTenant; + protected $fillable = [ + 'tenant_id', + 'name', + 'address', + 'phone', + 'email', + 'website', + 'logo', + ]; + + /** + * Get the users for the school. + */ public function users(): HasMany { return $this->hasMany(User::class); diff --git a/app/Models/Scopes/TenantScope.php b/app/Models/Scopes/TenantScope.php new file mode 100644 index 0000000..1b38735 --- /dev/null +++ b/app/Models/Scopes/TenantScope.php @@ -0,0 +1,33 @@ +where($model->getTable() . '.tenant_id', Tenant::current()->id); + } + + /** + * Extend the query builder with the needed functions. + */ + public function extend(Builder $builder): void + { + $builder->macro('withoutTenantScope', function (Builder $builder) { + return $builder->withoutGlobalScope($this); + }); + } +} diff --git a/app/Models/Subscription.php b/app/Models/Subscription.php new file mode 100644 index 0000000..d50fc8b --- /dev/null +++ b/app/Models/Subscription.php @@ -0,0 +1,75 @@ + 'datetime', + 'ends_at' => 'datetime', + ]; + + /** + * Get the tenant that owns the subscription. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } + + /** + * Get the plan that the subscription belongs to. + */ + public function plan(): BelongsTo + { + return $this->belongsTo(Plan::class); + } + + /** + * Determine if the subscription is active. + */ + public function active(): bool + { + return $this->stripe_status === 'active' || + $this->paystack_status === 'active' || + $this->onTrial(); + } + + /** + * Determine if the subscription is on trial. + */ + public function onTrial(): bool + { + return $this->trial_ends_at && $this->trial_ends_at->isFuture(); + } + + /** + * Determine if the subscription has ended. + */ + public function ended(): bool + { + return $this->ends_at && $this->ends_at->isPast(); + } +} diff --git a/app/Models/Tenant.php b/app/Models/Tenant.php new file mode 100644 index 0000000..ec371b2 --- /dev/null +++ b/app/Models/Tenant.php @@ -0,0 +1,88 @@ + 'boolean', + 'trial_ends_at' => 'datetime', + ]; + + /** + * Get the plan that the tenant belongs to. + */ + public function plan(): BelongsTo + { + return $this->belongsTo(Plan::class); + } + + /** + * Get the schools for the tenant. + */ + public function schools(): HasMany + { + return $this->hasMany(School::class); + } + + /** + * Get the users for the tenant. + */ + public function users(): HasMany + { + return $this->hasMany(User::class); + } + + /** + * Get the subscriptions for the tenant. + */ + public function subscriptions(): HasMany + { + return $this->hasMany(Subscription::class); + } + + /** + * Check if the tenant is on trial. + */ + public function onTrial(): bool + { + return $this->trial_ends_at && $this->trial_ends_at->isFuture(); + } + + /** + * Check if the tenant's trial has ended. + */ + public function trialEnded(): bool + { + return $this->trial_ends_at && $this->trial_ends_at->isPast(); + } +} diff --git a/app/Models/Traits/BelongsToTenant.php b/app/Models/Traits/BelongsToTenant.php new file mode 100644 index 0000000..c5a3fba --- /dev/null +++ b/app/Models/Traits/BelongsToTenant.php @@ -0,0 +1,32 @@ +tenant_id && \Spatie\Multitenancy\Models\Tenant::checkCurrent()) { + $model->tenant_id = \Spatie\Multitenancy\Models\Tenant::current()->id; + } + }); + } + + /** + * Get the tenant that owns the model. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 2b0a51a..a72cb1b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -22,11 +22,12 @@ use Illuminate\Database\Eloquent\SoftDeletes; use Illuminate\Database\Eloquent\Attributes\ScopedBy; use App\Events\StudentPromoted; +use App\Models\Traits\BelongsToTenant; #[ScopedBy([SchoolScope::class])] class User extends Authenticatable implements FilamentUser, HasName, CanResetPassword { - use HasApiTokens, HasFactory, Notifiable, HasSuperAdmin, SoftDeletes; + use HasApiTokens, HasFactory, Notifiable, HasSuperAdmin, SoftDeletes, BelongsToTenant; public static string $TEACHER_ROLE = 'Teacher'; public static string $STUDENT_ROLE = 'Student'; @@ -609,4 +610,12 @@ public function canBeImpersonated() User::$ADMIN_ROLE ]); } + + /** + * Get the tenant that owns the user. + */ + public function tenant(): BelongsTo + { + return $this->belongsTo(Tenant::class); + } } diff --git a/app/Multitenancy/DomainTenantFinder.php b/app/Multitenancy/DomainTenantFinder.php new file mode 100644 index 0000000..83d3421 --- /dev/null +++ b/app/Multitenancy/DomainTenantFinder.php @@ -0,0 +1,29 @@ +getHost(); + + // Check for a subdomain + $parts = explode('.', $host); + if (count($parts) > 2) { + $subdomain = $parts[0]; + return Tenant::query() + ->where('subdomain', $subdomain) + ->first(); + } + + // Check for a custom domain + return Tenant::query() + ->where('domain', $host) + ->first(); + } +} diff --git a/composer.json b/composer.json index f772fe4..46714ba 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "opcodesio/log-viewer": "^3.8", "saade/filament-fullcalendar": "^3.0", "spatie/laravel-db-snapshots": "^2.7", + "spatie/laravel-multitenancy": "^3.2", "stechstudio/filament-impersonate": "^3.14", "unicodeveloper/laravel-paystack": "^1.1", "z3d0x/filament-logger": "^0.7.2" diff --git a/composer.lock b/composer.lock index 4e7573e..7e30537 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "83aad5753cdd775ed4b964dc77aec50d", + "content-hash": "6f88dec1f454bcae2c7ded806297b2cb", "packages": [ { "name": "althinect/filament-spatie-roles-permissions", @@ -6752,6 +6752,85 @@ ], "time": "2024-06-12T15:01:18+00:00" }, + { + "name": "spatie/laravel-multitenancy", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-multitenancy.git", + "reference": "8f80fc78a28d3ab04eba1834eb8d33a5fb7401a4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-multitenancy/zipball/8f80fc78a28d3ab04eba1834eb8d33a5fb7401a4", + "reference": "8f80fc78a28d3ab04eba1834eb8d33a5fb7401a4", + "shasum": "" + }, + "require": { + "illuminate/support": "^10.0|^11.0", + "php": "^8.2", + "spatie/laravel-package-tools": "^1.9" + }, + "require-dev": { + "laravel/legacy-factories": "^1.0.4", + "laravel/octane": "^2.3", + "laravel/serializable-closure": "^1.1", + "mockery/mockery": "^1.4", + "orchestra/testbench": "^8.0|^9.0", + "pestphp/pest": "^2.34", + "spatie/valuestore": "^1.2" + }, + "type": "library", + "extra": { + "laravel": { + "aliases": { + "Multitenancy": "Spatie\\Multitenancy\\MultitenancyFacade" + }, + "providers": [ + "Spatie\\Multitenancy\\MultitenancyServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\Multitenancy\\": "src", + "Spatie\\Multitenancy\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "http://spatie.be", + "role": "Developer" + } + ], + "description": "Make your Laravel app usable by multiple tenants", + "homepage": "https://github.com/spatie/laravel-multitenancy", + "keywords": [ + "laravel-multitenancy", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-multitenancy/issues", + "source": "https://github.com/spatie/laravel-multitenancy/tree/3.2.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2024-03-12T15:53:43+00:00" + }, { "name": "spatie/laravel-package-tools", "version": "1.16.5", @@ -12152,12 +12231,12 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": true, "prefer-lowest": false, "platform": { "php": "^8.1" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/config/multitenancy.php b/config/multitenancy.php new file mode 100644 index 0000000..5fa7f3d --- /dev/null +++ b/config/multitenancy.php @@ -0,0 +1,117 @@ + \App\Multitenancy\DomainTenantFinder::class, + + /* + * These fields are used by tenant:artisan command to match one or more tenant. + */ + 'tenant_artisan_search_fields' => [ + 'id', + ], + + /* + * These tasks will be performed when switching tenants. + * + * A valid task is any class that implements Spatie\Multitenancy\Tasks\SwitchTenantTask + */ + 'switch_tenant_tasks' => [ + \Spatie\Multitenancy\Tasks\PrefixCacheTask::class, + \Spatie\Multitenancy\Tasks\SwitchTenantDatabaseTask::class, + \Spatie\Multitenancy\Tasks\SwitchRouteCacheTask::class, + ], + + /* + * This class is the model used for storing configuration on tenants. + * + * It must be or extend `Spatie\Multitenancy\Models\Tenant::class` + */ + 'tenant_model' => Tenant::class, + + /* + * If there is a current tenant when dispatching a job, the id of the current tenant + * will be automatically set on the job. When the job is executed, the set + * tenant on the job will be made current. + */ + 'queues_are_tenant_aware_by_default' => true, + + /* + * The connection name to reach the tenant database. + * + * Set to `null` to use the default connection. + */ + 'tenant_database_connection_name' => null, + + /* + * The connection name to reach the landlord database. + */ + 'landlord_database_connection_name' => null, + + /* + * This key will be used to bind the current tenant in the container. + */ + 'current_tenant_container_key' => 'currentTenant', + + /** + * Set it to `true` if you like to cache the tenant(s) routes + * in a shared file using the `SwitchRouteCacheTask`. + */ + 'shared_routes_cache' => false, + + /* + * You can customize some of the behavior of this package by using your own custom action. + * Your custom action should always extend the default one. + */ + 'actions' => [ + 'make_tenant_current_action' => MakeTenantCurrentAction::class, + 'forget_current_tenant_action' => ForgetCurrentTenantAction::class, + 'make_queue_tenant_aware_action' => MakeQueueTenantAwareAction::class, + 'migrate_tenant' => MigrateTenantAction::class, + ], + + /* + * You can customize the way in which the package resolves the queueable to a job. + * + * For example, using the package laravel-actions (by Loris Leiva), you can + * resolve JobDecorator to getAction() like so: JobDecorator::class => 'getAction' + */ + 'queueable_to_job' => [ + SendQueuedMailable::class => 'mailable', + SendQueuedNotifications::class => 'notification', + CallQueuedClosure::class => 'closure', + CallQueuedListener::class => 'class', + BroadcastEvent::class => 'event', + ], + + /* + * Jobs tenant aware even if these don't implement the TenantAware interface. + */ + 'tenant_aware_jobs' => [ + // ... + ], + + /* + * Jobs not tenant aware even if these don't implement the NotTenantAware interface. + */ + 'not_tenant_aware_jobs' => [ + // ... + ], +]; diff --git a/database/migrations/2025_02_25_201002_create_tenants_table.php b/database/migrations/2025_02_25_201002_create_tenants_table.php new file mode 100644 index 0000000..f4de695 --- /dev/null +++ b/database/migrations/2025_02_25_201002_create_tenants_table.php @@ -0,0 +1,45 @@ +id(); + $table->string('name'); + $table->string('domain')->unique()->nullable(); + $table->string('database')->unique()->nullable(); + $table->string('subdomain')->unique(); + $table->string('logo')->nullable(); + $table->string('email')->unique(); + $table->string('phone')->nullable(); + $table->text('address')->nullable(); + $table->string('city')->nullable(); + $table->string('state')->nullable(); + $table->string('country')->nullable(); + $table->string('zip_code')->nullable(); + $table->string('timezone')->default('UTC'); + $table->string('locale')->default('en'); + $table->string('currency')->default('USD'); + $table->boolean('active')->default(true); + $table->timestamp('trial_ends_at')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('tenants'); + } +}; diff --git a/database/migrations/2025_02_25_201508_add_tenant_id_to_schools_table.php b/database/migrations/2025_02_25_201508_add_tenant_id_to_schools_table.php new file mode 100644 index 0000000..a62082b --- /dev/null +++ b/database/migrations/2025_02_25_201508_add_tenant_id_to_schools_table.php @@ -0,0 +1,31 @@ +foreignId('tenant_id')->nullable()->after('id')->constrained('tenants')->onDelete('cascade'); + $table->index('tenant_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('schools', function (Blueprint $table) { + $table->dropForeign(['tenant_id']); + $table->dropIndex(['tenant_id']); + $table->dropColumn('tenant_id'); + }); + } +}; diff --git a/database/migrations/2025_02_25_201551_add_tenant_id_to_users_table.php b/database/migrations/2025_02_25_201551_add_tenant_id_to_users_table.php new file mode 100644 index 0000000..f0b04f2 --- /dev/null +++ b/database/migrations/2025_02_25_201551_add_tenant_id_to_users_table.php @@ -0,0 +1,31 @@ +foreignId('tenant_id')->nullable()->after('id')->constrained('tenants')->onDelete('cascade'); + $table->index('tenant_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropForeign(['tenant_id']); + $table->dropIndex(['tenant_id']); + $table->dropColumn('tenant_id'); + }); + } +}; diff --git a/database/migrations/2025_02_25_201903_add_tenant_id_to_all_tables.php b/database/migrations/2025_02_25_201903_add_tenant_id_to_all_tables.php new file mode 100644 index 0000000..d07423f --- /dev/null +++ b/database/migrations/2025_02_25_201903_add_tenant_id_to_all_tables.php @@ -0,0 +1,71 @@ +tables as $table) { + if (Schema::hasTable($table) && !Schema::hasColumn($table, 'tenant_id')) { + Schema::table($table, function (Blueprint $table) { + $table->foreignId('tenant_id')->nullable()->after('id')->constrained('tenants')->onDelete('cascade'); + $table->index('tenant_id'); + }); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + foreach ($this->tables as $table) { + if (Schema::hasTable($table) && Schema::hasColumn($table, 'tenant_id')) { + Schema::table($table, function (Blueprint $table) { + $table->dropForeign(['tenant_id']); + $table->dropIndex(['tenant_id']); + $table->dropColumn('tenant_id'); + }); + } + } + } +}; diff --git a/database/migrations/2025_02_25_202104_create_subscriptions_table.php b/database/migrations/2025_02_25_202104_create_subscriptions_table.php new file mode 100644 index 0000000..b5aeddd --- /dev/null +++ b/database/migrations/2025_02_25_202104_create_subscriptions_table.php @@ -0,0 +1,41 @@ +id(); + $table->foreignId('tenant_id')->constrained('tenants')->onDelete('cascade'); + $table->foreignId('plan_id')->constrained('plans')->onDelete('cascade'); + $table->string('name'); + $table->string('stripe_id')->nullable()->index(); + $table->string('stripe_status')->nullable(); + $table->string('stripe_price')->nullable(); + $table->string('paystack_id')->nullable()->index(); + $table->string('paystack_status')->nullable(); + $table->string('paystack_plan')->nullable(); + $table->integer('quantity')->default(1); + $table->timestamp('trial_ends_at')->nullable(); + $table->timestamp('ends_at')->nullable(); + $table->timestamps(); + + $table->index(['tenant_id', 'stripe_status']); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('subscriptions'); + } +}; diff --git a/database/migrations/2025_02_25_202237_create_plans_table.php b/database/migrations/2025_02_25_202237_create_plans_table.php new file mode 100644 index 0000000..cac9d2f --- /dev/null +++ b/database/migrations/2025_02_25_202237_create_plans_table.php @@ -0,0 +1,43 @@ +id(); + $table->string('name'); + $table->string('slug')->unique(); + $table->text('description')->nullable(); + $table->boolean('is_active')->default(true); + $table->decimal('price_monthly', 10, 2); + $table->decimal('price_yearly', 10, 2); + $table->string('stripe_monthly_plan_id')->nullable(); + $table->string('stripe_yearly_plan_id')->nullable(); + $table->string('paystack_monthly_plan_id')->nullable(); + $table->string('paystack_yearly_plan_id')->nullable(); + $table->integer('trial_days')->default(0); + $table->integer('max_schools')->default(1); + $table->integer('max_students')->default(100); + $table->integer('max_teachers')->default(10); + $table->integer('max_parents')->default(200); + $table->json('features')->nullable(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::dropIfExists('plans'); + } +}; diff --git a/database/migrations/2025_02_25_202418_add_plan_id_to_tenants_table.php b/database/migrations/2025_02_25_202418_add_plan_id_to_tenants_table.php new file mode 100644 index 0000000..38c69ce --- /dev/null +++ b/database/migrations/2025_02_25_202418_add_plan_id_to_tenants_table.php @@ -0,0 +1,31 @@ +foreignId('plan_id')->nullable()->after('id')->constrained('plans')->onDelete('set null'); + $table->index('plan_id'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('tenants', function (Blueprint $table) { + $table->dropForeign(['plan_id']); + $table->dropIndex(['plan_id']); + $table->dropColumn('plan_id'); + }); + } +}; diff --git a/database/migrations/landlord/2025_02_25_201321_create_landlord_tenants_table.php b/database/migrations/landlord/2025_02_25_201321_create_landlord_tenants_table.php new file mode 100644 index 0000000..ac9299f --- /dev/null +++ b/database/migrations/landlord/2025_02_25_201321_create_landlord_tenants_table.php @@ -0,0 +1,45 @@ +id(); + $table->string('name'); + $table->string('domain')->unique(); + $table->string('database')->unique(); + $table->string('subdomain')->unique(); + $table->string('logo')->nullable(); + $table->string('email')->unique(); + $table->string('phone')->nullable(); + $table->text('address')->nullable(); + $table->string('city')->nullable(); + $table->string('state')->nullable(); + $table->string('country')->nullable(); + $table->string('zip_code')->nullable(); + $table->string('timezone')->default('UTC'); + $table->string('locale')->default('en'); + $table->string('currency')->default('USD'); + $table->boolean('active')->default(true); + $table->timestamp('trial_ends_at')->nullable(); + $table->softDeletes(); + $table->timestamps(); + }); + } + + /** + * Reverse the migrations. + */ + public function down() + { + Schema::dropIfExists('tenants'); + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index 25a9225..2a65826 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -13,6 +13,7 @@ class DatabaseSeeder extends Seeder public function run(): void { $this->call([ + PlanSeeder::class, ClassSeeder::class, RoleSeeder::class ]); diff --git a/database/seeders/PlanSeeder.php b/database/seeders/PlanSeeder.php new file mode 100644 index 0000000..fce6399 --- /dev/null +++ b/database/seeders/PlanSeeder.php @@ -0,0 +1,95 @@ + 'Basic', + 'slug' => 'basic', + 'description' => 'Perfect for small schools with limited needs.', + 'is_active' => true, + 'price_monthly' => 49.99, + 'price_yearly' => 499.99, + 'trial_days' => 14, + 'max_schools' => 1, + 'max_students' => 100, + 'max_teachers' => 10, + 'max_parents' => 200, + 'features' => [ + 'Student Management', + 'Teacher Management', + 'Parent Portal', + 'Basic Reporting', + 'Email Support', + ], + ], + [ + 'name' => 'Standard', + 'slug' => 'standard', + 'description' => 'Ideal for medium-sized schools with growing needs.', + 'is_active' => true, + 'price_monthly' => 99.99, + 'price_yearly' => 999.99, + 'trial_days' => 14, + 'max_schools' => 2, + 'max_students' => 500, + 'max_teachers' => 50, + 'max_parents' => 1000, + 'features' => [ + 'Student Management', + 'Teacher Management', + 'Parent Portal', + 'Advanced Reporting', + 'Fee Management', + 'Library Management', + 'Email & Chat Support', + 'API Access', + ], + ], + [ + 'name' => 'Premium', + 'slug' => 'premium', + 'description' => 'Comprehensive solution for large schools and institutions.', + 'is_active' => true, + 'price_monthly' => 199.99, + 'price_yearly' => 1999.99, + 'trial_days' => 14, + 'max_schools' => 5, + 'max_students' => 2000, + 'max_teachers' => 200, + 'max_parents' => 4000, + 'features' => [ + 'Student Management', + 'Teacher Management', + 'Parent Portal', + 'Advanced Reporting', + 'Fee Management', + 'Library Management', + 'Timetable Management', + 'Attendance Tracking', + 'Exam Management', + 'Custom Branding', + 'Priority Support', + 'API Access', + 'Data Export', + 'Dedicated Account Manager', + ], + ], + ]; + + foreach ($plans as $plan) { + Plan::create($plan); + } + } +} From 30d399b67664e4091109ea64e70770c761ee9d2b Mon Sep 17 00:00:00 2001 From: Samuel Olaegbe Date: Tue, 25 Feb 2025 21:40:41 +0100 Subject: [PATCH 2/2] Fix migration dependencies --- ...e_plans_table.php => 2025_02_25_202037_create_plans_table.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename database/migrations/{2025_02_25_202237_create_plans_table.php => 2025_02_25_202037_create_plans_table.php} (100%) diff --git a/database/migrations/2025_02_25_202237_create_plans_table.php b/database/migrations/2025_02_25_202037_create_plans_table.php similarity index 100% rename from database/migrations/2025_02_25_202237_create_plans_table.php rename to database/migrations/2025_02_25_202037_create_plans_table.php