- Add UserManagementController@store for creating users - Add POST /api/admin/users endpoint - Support user types: Free, Pro, Admin - Auto-create 100-year subscription for Pro/Admin users - Add user creation modal to Users.jsx - Complete SaaS limit testing: - Free user limits: 1 account, 10 categories, 3 budgets, 100 tx - Middleware blocks correctly at limits - Error messages are user-friendly - Usage stats API working correctly - Update SAAS_STATUS.md with test results - Bump version to 1.51.0
163 lines
3.8 KiB
PHP
163 lines
3.8 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
|
|
|
class Plan extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
protected $fillable = [
|
|
'name',
|
|
'slug',
|
|
'description',
|
|
'price',
|
|
'currency',
|
|
'billing_period',
|
|
'trial_days',
|
|
'features',
|
|
'limits',
|
|
'is_active',
|
|
'is_featured',
|
|
'is_free',
|
|
'sort_order',
|
|
'paypal_plan_id',
|
|
];
|
|
|
|
protected $casts = [
|
|
'price' => 'decimal:2',
|
|
'features' => 'array',
|
|
'limits' => 'array',
|
|
'is_active' => 'boolean',
|
|
'is_featured' => 'boolean',
|
|
'is_free' => 'boolean',
|
|
'trial_days' => 'integer',
|
|
'sort_order' => 'integer',
|
|
];
|
|
|
|
// ==================== RELATIONSHIPS ====================
|
|
|
|
public function subscriptions(): HasMany
|
|
{
|
|
return $this->hasMany(Subscription::class);
|
|
}
|
|
|
|
// ==================== SCOPES ====================
|
|
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
|
|
public function scopeOrdered($query)
|
|
{
|
|
return $query->orderBy('sort_order')->orderBy('price');
|
|
}
|
|
|
|
public function scopeFree($query)
|
|
{
|
|
return $query->where('billing_period', 'free');
|
|
}
|
|
|
|
public function scopePaid($query)
|
|
{
|
|
return $query->where('billing_period', '!=', 'free');
|
|
}
|
|
|
|
// ==================== ACCESSORS ====================
|
|
|
|
public function getIsFreeAttribute(): bool
|
|
{
|
|
return $this->billing_period === 'free' || $this->price == 0;
|
|
}
|
|
|
|
public function getHasTrialAttribute(): bool
|
|
{
|
|
return $this->trial_days > 0;
|
|
}
|
|
|
|
public function getFormattedPriceAttribute(): string
|
|
{
|
|
if ($this->is_free) {
|
|
return 'Gratis';
|
|
}
|
|
|
|
$symbols = [
|
|
'EUR' => '€',
|
|
'USD' => '$',
|
|
'BRL' => 'R$',
|
|
];
|
|
|
|
$symbol = $symbols[$this->currency] ?? $this->currency;
|
|
$period = match ($this->billing_period) {
|
|
'monthly' => '/mes',
|
|
'annual' => '/año',
|
|
'lifetime' => ' (único)',
|
|
default => '',
|
|
};
|
|
|
|
return $symbol . number_format($this->price, 2, ',', '.') . $period;
|
|
}
|
|
|
|
public function getMonthlyPriceAttribute(): float
|
|
{
|
|
if ($this->billing_period === 'annual') {
|
|
return round($this->price / 12, 2);
|
|
}
|
|
return $this->price;
|
|
}
|
|
|
|
public function getSavingsPercentAttribute(): ?int
|
|
{
|
|
if ($this->billing_period !== 'annual') {
|
|
return null;
|
|
}
|
|
|
|
// Find monthly equivalent
|
|
$monthlyPlan = static::where('billing_period', 'monthly')
|
|
->where('is_active', true)
|
|
->where('price', '>', 0)
|
|
->first();
|
|
|
|
if (!$monthlyPlan) {
|
|
return null;
|
|
}
|
|
|
|
$annualEquivalent = $monthlyPlan->price * 12;
|
|
$savings = (($annualEquivalent - $this->price) / $annualEquivalent) * 100;
|
|
|
|
return (int) round($savings);
|
|
}
|
|
|
|
// ==================== METHODS ====================
|
|
|
|
public function hasFeature(string $feature): bool
|
|
{
|
|
return in_array($feature, $this->features ?? []);
|
|
}
|
|
|
|
public function getLimit(string $key, $default = null)
|
|
{
|
|
return $this->limits[$key] ?? $default;
|
|
}
|
|
|
|
/**
|
|
* Get the default free plan
|
|
*/
|
|
public static function getFreePlan(): ?self
|
|
{
|
|
return static::where('slug', 'free')->first();
|
|
}
|
|
|
|
/**
|
|
* Get plans for pricing page
|
|
*/
|
|
public static function getForPricing(): \Illuminate\Database\Eloquent\Collection
|
|
{
|
|
return static::active()->ordered()->get();
|
|
}
|
|
}
|