feat(billing): plan limits service, plan factory, seeder (free/pro/business/agency)
parent
f757f43926
commit
dd89ee395c
|
|
@ -2,17 +2,25 @@
|
|||
|
||||
namespace App\Domains\Subscription\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 $guarded = ['id'];
|
||||
|
||||
protected $casts = [
|
||||
'features' => 'array',
|
||||
];
|
||||
|
||||
protected static function newFactory(): \Database\Factories\PlanFactory
|
||||
{
|
||||
return \Database\Factories\PlanFactory::new();
|
||||
}
|
||||
|
||||
public function workspaces(): HasMany
|
||||
{
|
||||
return $this->hasMany(\App\Domains\Workspace\Models\Workspace::class);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
<?php
|
||||
|
||||
namespace App\Domains\Subscription\Services;
|
||||
|
||||
use App\Domains\Workspace\Models\Workspace;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
|
||||
class PlanLimitsService
|
||||
{
|
||||
public function canCreateLink(Workspace $workspace): bool
|
||||
{
|
||||
$plan = $workspace->plan;
|
||||
if (! $plan) {
|
||||
return true; // No plan = allow (trial or free)
|
||||
}
|
||||
$count = $workspace->links()->count();
|
||||
return $count < $plan->link_limit;
|
||||
}
|
||||
|
||||
public function monthlyClicksUsed(Workspace $workspace): int
|
||||
{
|
||||
$key = "clicks:{$workspace->id}:monthly:" . now()->format('Ym');
|
||||
return (int) Redis::get($key);
|
||||
}
|
||||
|
||||
public function isClickLimitApproaching(Workspace $workspace): bool
|
||||
{
|
||||
$plan = $workspace->plan;
|
||||
if (! $plan) return false;
|
||||
$used = $this->monthlyClicksUsed($workspace);
|
||||
return $used >= ($plan->click_limit_monthly * 0.9);
|
||||
}
|
||||
|
||||
public function isClickLimitExceeded(Workspace $workspace): bool
|
||||
{
|
||||
$plan = $workspace->plan;
|
||||
if (! $plan) return false;
|
||||
return $this->monthlyClicksUsed($workspace) >= $plan->click_limit_monthly;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Factories;
|
||||
|
||||
use App\Domains\Subscription\Models\Plan;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class PlanFactory extends Factory
|
||||
{
|
||||
protected $model = Plan::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'name' => fake()->unique()->word(),
|
||||
'monthly_price_cents' => 0,
|
||||
'workspace_limit' => 1,
|
||||
'member_limit' => 1,
|
||||
'custom_domain_limit' => 0,
|
||||
'link_limit' => 50,
|
||||
'click_limit_monthly' => 1000,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
namespace Database\Seeders;
|
||||
|
||||
use App\Domains\Subscription\Models\Plan;
|
||||
use Illuminate\Database\Seeder;
|
||||
|
||||
class PlanSeeder extends Seeder
|
||||
{
|
||||
public function run(): void
|
||||
{
|
||||
$plans = [
|
||||
['name' => 'free', 'monthly_price_cents' => 0, 'workspace_limit' => 1, 'member_limit' => 1, 'custom_domain_limit' => 0, 'link_limit' => 50, 'click_limit_monthly' => 1000],
|
||||
['name' => 'pro', 'monthly_price_cents' => 1900, 'workspace_limit' => 1, 'member_limit' => 3, 'custom_domain_limit' => 1, 'link_limit' => 1000, 'click_limit_monthly' => 25000],
|
||||
['name' => 'business', 'monthly_price_cents' => 7900, 'workspace_limit' => 3, 'member_limit' => 10, 'custom_domain_limit' => 5, 'link_limit' => 10000, 'click_limit_monthly' => 250000],
|
||||
['name' => 'agency', 'monthly_price_cents' => 19900, 'workspace_limit' => 999, 'member_limit' => 999, 'custom_domain_limit' => 999, 'link_limit' => 999999, 'click_limit_monthly' => 5000000],
|
||||
];
|
||||
|
||||
foreach ($plans as $plan) {
|
||||
Plan::updateOrCreate(['name' => $plan['name']], $plan);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
use App\Domains\Subscription\Services\PlanLimitsService;
|
||||
use App\Domains\Subscription\Models\Plan;
|
||||
use App\Domains\Workspace\Models\Workspace;
|
||||
use App\Domains\Link\Models\Link;
|
||||
|
||||
uses(Tests\TestCase::class);
|
||||
|
||||
it('allows link creation when under limit', function () {
|
||||
$plan = Plan::factory()->create(['link_limit' => 10]);
|
||||
$workspace = Workspace::factory()->create(['plan_id' => $plan->id]);
|
||||
|
||||
$service = new PlanLimitsService;
|
||||
expect($service->canCreateLink($workspace))->toBeTrue();
|
||||
});
|
||||
|
||||
it('denies link creation when at limit', function () {
|
||||
$plan = Plan::factory()->create(['link_limit' => 2]);
|
||||
$workspace = Workspace::factory()->create(['plan_id' => $plan->id]);
|
||||
Link::factory()->count(2)->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$service = new PlanLimitsService;
|
||||
expect($service->canCreateLink($workspace))->toBeFalse();
|
||||
});
|
||||
|
||||
it('returns true when workspace has no plan', function () {
|
||||
$workspace = Workspace::factory()->create(['plan_id' => null]);
|
||||
|
||||
$service = new PlanLimitsService;
|
||||
// No plan = free tier or trial = allow
|
||||
expect($service->canCreateLink($workspace))->toBeTrue();
|
||||
});
|
||||
Loading…
Reference in New Issue