Implement multi-tenancy support with OAuth2 authentication for tenant admins, Stripe integration for event purchases and credits ledger, new Filament resources for event purchases, updated API routes and middleware for tenant isolation and token guarding, added factories/seeders/migrations for new models (Tenant, EventPurchase, OAuth entities, etc.), enhanced tests, and documentation updates. Removed outdated DemoAchievementsSeeder.
This commit is contained in:
@@ -2,16 +2,20 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\{BelongsTo, HasMany, BelongsToMany};
|
||||
|
||||
class Event extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'events';
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
'date' => 'date',
|
||||
'name' => 'array',
|
||||
'description' => 'array',
|
||||
'settings' => 'array',
|
||||
'is_active' => 'boolean',
|
||||
];
|
||||
@@ -25,5 +29,15 @@ class Event extends Model
|
||||
{
|
||||
return $this->hasMany(Photo::class);
|
||||
}
|
||||
|
||||
public function taskCollections(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
TaskCollection::class,
|
||||
'event_task_collection',
|
||||
'event_id',
|
||||
'task_collection_id'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
29
app/Models/EventCreditsLedger.php
Normal file
29
app/Models/EventCreditsLedger.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EventCreditsLedger extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'event_credits_ledger';
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
'created_at' => 'datetime',
|
||||
'delta' => 'integer',
|
||||
];
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
public function relatedPurchase(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(EventPurchase::class, 'related_purchase_id');
|
||||
}
|
||||
}
|
||||
24
app/Models/EventPurchase.php
Normal file
24
app/Models/EventPurchase.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class EventPurchase extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'event_purchases';
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
'purchased_at' => 'datetime',
|
||||
'amount' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
}
|
||||
27
app/Models/OAuthClient.php
Normal file
27
app/Models/OAuthClient.php
Normal file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class OAuthClient extends Model
|
||||
{
|
||||
protected $table = 'oauth_clients';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'client_id',
|
||||
'client_secret',
|
||||
'redirect_uris',
|
||||
'scopes',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'scopes' => 'array',
|
||||
'redirect_uris' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
45
app/Models/OAuthCode.php
Normal file
45
app/Models/OAuthCode.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class OAuthCode extends Model
|
||||
{
|
||||
protected $table = 'oauth_codes';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'client_id',
|
||||
'user_id',
|
||||
'code',
|
||||
'code_challenge',
|
||||
'state',
|
||||
'redirect_uri',
|
||||
'scope',
|
||||
'expires_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function client(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(OAuthClient::class, 'client_id', 'client_id');
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function isExpired(): bool
|
||||
{
|
||||
return $this->expires_at < now();
|
||||
}
|
||||
}
|
||||
37
app/Models/PurchaseHistory.php
Normal file
37
app/Models/PurchaseHistory.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class PurchaseHistory extends Model
|
||||
{
|
||||
protected $table = 'purchase_history';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'tenant_id',
|
||||
'package_id',
|
||||
'credits_added',
|
||||
'price',
|
||||
'currency',
|
||||
'platform',
|
||||
'transaction_id',
|
||||
'purchased_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'credits_added' => 'integer',
|
||||
'price' => 'decimal:2',
|
||||
'purchased_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
}
|
||||
56
app/Models/RefreshToken.php
Normal file
56
app/Models/RefreshToken.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
|
||||
class RefreshToken extends Model
|
||||
{
|
||||
protected $table = 'refresh_tokens';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'tenant_id',
|
||||
'token',
|
||||
'access_token',
|
||||
'expires_at',
|
||||
'scope',
|
||||
'ip_address',
|
||||
'user_agent',
|
||||
'revoked_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
'revoked_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function tenant(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Tenant::class);
|
||||
}
|
||||
|
||||
public function revoke(): bool
|
||||
{
|
||||
return $this->update(['revoked_at' => now()]);
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->revoked_at === null && $this->expires_at > now();
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->whereNull('revoked_at')->where('expires_at', '>', now());
|
||||
}
|
||||
|
||||
public function scopeForTenant($query, string $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,15 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class Task extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'tasks';
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
@@ -24,4 +28,15 @@ class Task extends Model
|
||||
{
|
||||
return $this->belongsTo(EventType::class, 'event_type_id');
|
||||
}
|
||||
|
||||
public function taskCollection(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(TaskCollection::class, 'collection_id');
|
||||
}
|
||||
|
||||
public function assignedEvents(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(Event::class, 'event_task', 'task_id', 'event_id')
|
||||
->withTimestamps();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,14 +2,18 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||
|
||||
class TaskCollection extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'task_collections';
|
||||
|
||||
protected $fillable = [
|
||||
'tenant_id',
|
||||
'name',
|
||||
'description',
|
||||
];
|
||||
@@ -25,11 +29,11 @@ class TaskCollection extends Model
|
||||
public function tasks(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
Task::class,
|
||||
'task_collection_task',
|
||||
'task_collection_id',
|
||||
Task::class,
|
||||
'task_collection_task',
|
||||
'task_collection_id',
|
||||
'task_id'
|
||||
)->withTimestamps();
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,11 +42,11 @@ class TaskCollection extends Model
|
||||
public function events(): BelongsToMany
|
||||
{
|
||||
return $this->belongsToMany(
|
||||
Event::class,
|
||||
'event_task_collection',
|
||||
'task_collection_id',
|
||||
Event::class,
|
||||
'event_task_collection',
|
||||
'task_collection_id',
|
||||
'event_id'
|
||||
)->withTimestamps();
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -2,17 +2,30 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
|
||||
use Illuminate\Database\Eloquent\Casts\Attribute;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Models\EventPurchase;
|
||||
use App\Models\EventCreditsLedger;
|
||||
|
||||
class Tenant extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'tenants';
|
||||
protected $guarded = [];
|
||||
protected $casts = [
|
||||
'name' => 'array',
|
||||
'settings' => 'array',
|
||||
'features' => 'array',
|
||||
'last_activity_at' => 'datetime',
|
||||
'event_credits_balance' => 'integer',
|
||||
'subscription_tier' => 'string',
|
||||
'subscription_expires_at' => 'datetime',
|
||||
'total_revenue' => 'decimal:2',
|
||||
];
|
||||
|
||||
public function events(): HasMany
|
||||
@@ -31,4 +44,60 @@ class Tenant extends Model
|
||||
'id' // Local key on events table...
|
||||
);
|
||||
}
|
||||
|
||||
public function purchases(): HasMany
|
||||
{
|
||||
return $this->hasMany(PurchaseHistory::class);
|
||||
}
|
||||
|
||||
public function eventPurchases(): HasMany
|
||||
{
|
||||
return $this->hasMany(EventPurchase::class);
|
||||
}
|
||||
|
||||
public function creditsLedger(): HasMany
|
||||
{
|
||||
return $this->hasMany(EventCreditsLedger::class);
|
||||
}
|
||||
|
||||
public function activeSubscription(): Attribute
|
||||
{
|
||||
return Attribute::make(
|
||||
get: fn () => $this->subscription_expires_at && $this->subscription_expires_at->isFuture(),
|
||||
);
|
||||
}
|
||||
|
||||
public function decrementCredits(int $amount, string $reason = 'event_create', ?string $note = null, ?int $relatedPurchaseId = null): bool
|
||||
{
|
||||
if ($this->event_credits_balance < $amount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($amount, $reason, $note, $relatedPurchaseId) {
|
||||
EventCreditsLedger::create([
|
||||
'tenant_id' => $this->id,
|
||||
'delta' => -$amount,
|
||||
'reason' => $reason,
|
||||
'related_purchase_id' => $relatedPurchaseId,
|
||||
'note' => $note,
|
||||
]);
|
||||
|
||||
return $this->decrement('event_credits_balance', $amount);
|
||||
});
|
||||
}
|
||||
|
||||
public function incrementCredits(int $amount, string $reason = 'manual_adjust', ?string $note = null, ?int $relatedPurchaseId = null): bool
|
||||
{
|
||||
return DB::transaction(function () use ($amount, $reason, $note, $relatedPurchaseId) {
|
||||
EventCreditsLedger::create([
|
||||
'tenant_id' => $this->id,
|
||||
'delta' => $amount,
|
||||
'reason' => $reason,
|
||||
'related_purchase_id' => $relatedPurchaseId,
|
||||
'note' => $note,
|
||||
]);
|
||||
|
||||
return $this->increment('event_credits_balance', $amount);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
47
app/Models/TenantToken.php
Normal file
47
app/Models/TenantToken.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class TenantToken extends Model
|
||||
{
|
||||
protected $table = 'tenant_tokens';
|
||||
|
||||
protected $guarded = [];
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'tenant_id',
|
||||
'jti',
|
||||
'token_type',
|
||||
'expires_at',
|
||||
'revoked_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'expires_at' => 'datetime',
|
||||
'revoked_at' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
];
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->whereNull('revoked_at')->where('expires_at', '>', now());
|
||||
}
|
||||
|
||||
public function scopeForTenant($query, string $tenantId)
|
||||
{
|
||||
return $query->where('tenant_id', $tenantId);
|
||||
}
|
||||
|
||||
public function revoke(): bool
|
||||
{
|
||||
return $this->update(['revoked_at' => now()]);
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->revoked_at === null && $this->expires_at > now();
|
||||
}
|
||||
}
|
||||
@@ -7,11 +7,12 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Laravel\Sanctum\HasApiTokens;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
use HasApiTokens, HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
|
||||
Reference in New Issue
Block a user