feat(billing): plan limits service, plan factory, seeder (free/pro/business/agency)

main
boban 2026-05-16 01:06:01 +02:00
parent f757f43926
commit dd89ee395c
5 changed files with 128 additions and 0 deletions

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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,
];
}
}

View File

@ -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);
}
}
}

View File

@ -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();
});