Implement package limit notification system
This commit is contained in:
128
tests/Unit/Services/PackageLimitEvaluatorTest.php
Normal file
128
tests/Unit/Services/PackageLimitEvaluatorTest.php
Normal file
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\EventPackage;
|
||||
use App\Models\Package;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Services\Packages\PackageLimitEvaluator;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PackageLimitEvaluatorTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private PackageLimitEvaluator $evaluator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->evaluator = $this->app->make(PackageLimitEvaluator::class);
|
||||
}
|
||||
|
||||
public function test_assess_event_creation_returns_null_when_allowance_available(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create(['event_credits_balance' => 2]);
|
||||
|
||||
$violation = $this->evaluator->assessEventCreation($tenant);
|
||||
|
||||
$this->assertNull($violation);
|
||||
}
|
||||
|
||||
public function test_assess_event_creation_returns_package_violation_when_quota_reached(): void
|
||||
{
|
||||
$package = Package::factory()->reseller()->create([
|
||||
'max_events_per_year' => 1,
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create(['event_credits_balance' => 0]);
|
||||
|
||||
TenantPackage::factory()->create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'used_events' => 1,
|
||||
'expires_at' => now()->addMonth(),
|
||||
'active' => true,
|
||||
]);
|
||||
|
||||
$tenant->refresh();
|
||||
|
||||
$violation = $this->evaluator->assessEventCreation($tenant);
|
||||
|
||||
$this->assertNotNull($violation);
|
||||
$this->assertSame('event_limit_exceeded', $violation['code']);
|
||||
$this->assertSame('events', $violation['meta']['scope']);
|
||||
$this->assertSame(0, $violation['meta']['remaining']);
|
||||
}
|
||||
|
||||
public function test_assess_event_creation_returns_credit_violation_when_no_credits(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create(['event_credits_balance' => 0]);
|
||||
|
||||
$violation = $this->evaluator->assessEventCreation($tenant);
|
||||
|
||||
$this->assertNotNull($violation);
|
||||
$this->assertSame('event_credits_exhausted', $violation['code']);
|
||||
$this->assertSame('credits', $violation['meta']['scope']);
|
||||
}
|
||||
|
||||
public function test_assess_photo_upload_returns_violation_when_photo_limit_reached(): void
|
||||
{
|
||||
$package = Package::factory()->endcustomer()->create([
|
||||
'max_photos' => 5,
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
$event = Event::factory()
|
||||
->for($tenant)
|
||||
->create();
|
||||
|
||||
EventPackage::create([
|
||||
'event_id' => $event->id,
|
||||
'package_id' => $package->id,
|
||||
'purchased_price' => $package->price,
|
||||
'purchased_at' => now(),
|
||||
'used_photos' => 5,
|
||||
'used_guests' => 0,
|
||||
'gallery_expires_at' => now()->addDays(14),
|
||||
]);
|
||||
|
||||
$violation = $this->evaluator->assessPhotoUpload($tenant->fresh(), $event->id);
|
||||
|
||||
$this->assertNotNull($violation);
|
||||
$this->assertSame('photo_limit_exceeded', $violation['code']);
|
||||
$this->assertSame('photos', $violation['meta']['scope']);
|
||||
$this->assertSame(0, $violation['meta']['remaining']);
|
||||
}
|
||||
|
||||
public function test_assess_photo_upload_returns_null_when_below_limit(): void
|
||||
{
|
||||
$package = Package::factory()->endcustomer()->create([
|
||||
'max_photos' => 10,
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
$event = Event::factory()
|
||||
->for($tenant)
|
||||
->create();
|
||||
|
||||
EventPackage::create([
|
||||
'event_id' => $event->id,
|
||||
'package_id' => $package->id,
|
||||
'purchased_price' => $package->price,
|
||||
'purchased_at' => now(),
|
||||
'used_photos' => 4,
|
||||
'used_guests' => 0,
|
||||
'gallery_expires_at' => now()->addDays(14),
|
||||
]);
|
||||
|
||||
$violation = $this->evaluator->assessPhotoUpload($tenant->fresh(), $event->id);
|
||||
|
||||
$this->assertNull($violation);
|
||||
}
|
||||
}
|
||||
148
tests/Unit/Services/PackageUsageTrackerTest.php
Normal file
148
tests/Unit/Services/PackageUsageTrackerTest.php
Normal file
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Events\Packages\EventPackageGuestLimitReached;
|
||||
use App\Events\Packages\EventPackageGuestThresholdReached;
|
||||
use App\Events\Packages\EventPackagePhotoLimitReached;
|
||||
use App\Events\Packages\EventPackagePhotoThresholdReached;
|
||||
use App\Models\Event;
|
||||
use App\Models\EventPackage;
|
||||
use App\Models\Package;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Packages\PackageUsageTracker;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Event as EventFacade;
|
||||
use Tests\TestCase;
|
||||
|
||||
class PackageUsageTrackerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_dispatches_threshold_event_when_crossed(): void
|
||||
{
|
||||
EventFacade::fake([
|
||||
EventPackagePhotoThresholdReached::class,
|
||||
EventPackagePhotoLimitReached::class,
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
$package = Package::factory()->endcustomer()->create([
|
||||
'max_photos' => 10,
|
||||
]);
|
||||
$event = Event::factory()->for($tenant)->create();
|
||||
|
||||
$eventPackage = EventPackage::create([
|
||||
'event_id' => $event->id,
|
||||
'package_id' => $package->id,
|
||||
'purchased_price' => $package->price,
|
||||
'purchased_at' => now(),
|
||||
'used_photos' => 8,
|
||||
'used_guests' => 0,
|
||||
'gallery_expires_at' => now()->addDays(7),
|
||||
])->fresh(['package']);
|
||||
|
||||
/** @var PackageUsageTracker $tracker */
|
||||
$tracker = app(PackageUsageTracker::class);
|
||||
|
||||
$tracker->recordPhotoUsage($eventPackage, 7, 1);
|
||||
|
||||
EventFacade::assertDispatched(EventPackagePhotoThresholdReached::class);
|
||||
EventFacade::assertNotDispatched(EventPackagePhotoLimitReached::class);
|
||||
}
|
||||
|
||||
public function test_dispatches_limit_event_when_reached(): void
|
||||
{
|
||||
EventFacade::fake([
|
||||
EventPackagePhotoThresholdReached::class,
|
||||
EventPackagePhotoLimitReached::class,
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
$package = Package::factory()->endcustomer()->create([
|
||||
'max_photos' => 2,
|
||||
'max_guests' => null,
|
||||
]);
|
||||
$event = Event::factory()->for($tenant)->create();
|
||||
|
||||
$eventPackage = EventPackage::create([
|
||||
'event_id' => $event->id,
|
||||
'package_id' => $package->id,
|
||||
'purchased_price' => $package->price,
|
||||
'purchased_at' => now(),
|
||||
'used_photos' => 2,
|
||||
'used_guests' => 0,
|
||||
'gallery_expires_at' => now()->addDays(7),
|
||||
])->fresh(['package']);
|
||||
|
||||
/** @var PackageUsageTracker $tracker */
|
||||
$tracker = app(PackageUsageTracker::class);
|
||||
|
||||
$tracker->recordPhotoUsage($eventPackage, 1, 1);
|
||||
|
||||
EventFacade::assertDispatched(EventPackagePhotoLimitReached::class);
|
||||
}
|
||||
|
||||
public function test_dispatches_guest_threshold_event_when_crossed(): void
|
||||
{
|
||||
EventFacade::fake([
|
||||
EventPackageGuestThresholdReached::class,
|
||||
EventPackageGuestLimitReached::class,
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
$package = Package::factory()->endcustomer()->create([
|
||||
'max_guests' => 10,
|
||||
]);
|
||||
$event = Event::factory()->for($tenant)->create();
|
||||
|
||||
$eventPackage = EventPackage::create([
|
||||
'event_id' => $event->id,
|
||||
'package_id' => $package->id,
|
||||
'purchased_price' => $package->price,
|
||||
'purchased_at' => now(),
|
||||
'used_photos' => 0,
|
||||
'used_guests' => 8,
|
||||
'gallery_expires_at' => now()->addDays(7),
|
||||
])->fresh(['package']);
|
||||
|
||||
/** @var PackageUsageTracker $tracker */
|
||||
$tracker = app(PackageUsageTracker::class);
|
||||
|
||||
$tracker->recordGuestUsage($eventPackage, 7, 1);
|
||||
|
||||
EventFacade::assertDispatched(EventPackageGuestThresholdReached::class);
|
||||
EventFacade::assertNotDispatched(EventPackageGuestLimitReached::class);
|
||||
}
|
||||
|
||||
public function test_dispatches_guest_limit_event_when_reached(): void
|
||||
{
|
||||
EventFacade::fake([
|
||||
EventPackageGuestThresholdReached::class,
|
||||
EventPackageGuestLimitReached::class,
|
||||
]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
$package = Package::factory()->endcustomer()->create([
|
||||
'max_guests' => 2,
|
||||
]);
|
||||
$event = Event::factory()->for($tenant)->create();
|
||||
|
||||
$eventPackage = EventPackage::create([
|
||||
'event_id' => $event->id,
|
||||
'package_id' => $package->id,
|
||||
'purchased_price' => $package->price,
|
||||
'purchased_at' => now(),
|
||||
'used_photos' => 0,
|
||||
'used_guests' => 2,
|
||||
'gallery_expires_at' => now()->addDays(7),
|
||||
])->fresh(['package']);
|
||||
|
||||
/** @var PackageUsageTracker $tracker */
|
||||
$tracker = app(PackageUsageTracker::class);
|
||||
|
||||
$tracker->recordGuestUsage($eventPackage, 1, 1);
|
||||
|
||||
EventFacade::assertDispatched(EventPackageGuestLimitReached::class);
|
||||
}
|
||||
}
|
||||
115
tests/Unit/Services/TenantUsageTrackerTest.php
Normal file
115
tests/Unit/Services/TenantUsageTrackerTest.php
Normal file
@@ -0,0 +1,115 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Events\Packages\TenantCreditsLow;
|
||||
use App\Events\Packages\TenantPackageEventLimitReached;
|
||||
use App\Events\Packages\TenantPackageEventThresholdReached;
|
||||
use App\Models\Package;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantPackage;
|
||||
use App\Services\Packages\TenantUsageTracker;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Event as EventFacade;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TenantUsageTrackerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_record_event_usage_dispatches_threshold_and_updates_columns(): void
|
||||
{
|
||||
EventFacade::fake([
|
||||
TenantPackageEventThresholdReached::class,
|
||||
TenantPackageEventLimitReached::class,
|
||||
]);
|
||||
|
||||
Config::set('package-limits.event_thresholds', [0.5]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
$package = Package::factory()->reseller()->create([
|
||||
'max_events_per_year' => 10,
|
||||
]);
|
||||
|
||||
$tenantPackage = TenantPackage::factory()
|
||||
->for($tenant)
|
||||
->for($package)
|
||||
->create([
|
||||
'used_events' => 6,
|
||||
])->fresh();
|
||||
|
||||
/** @var TenantUsageTracker $tracker */
|
||||
$tracker = app(TenantUsageTracker::class);
|
||||
|
||||
$tracker->recordEventUsage($tenantPackage, 4, 2);
|
||||
|
||||
EventFacade::assertDispatched(TenantPackageEventThresholdReached::class);
|
||||
EventFacade::assertNotDispatched(TenantPackageEventLimitReached::class);
|
||||
|
||||
$tenantPackage->refresh();
|
||||
|
||||
$this->assertNotNull($tenantPackage->event_warning_sent_at);
|
||||
$this->assertSame(0.5, (float) $tenantPackage->event_warning_threshold);
|
||||
}
|
||||
|
||||
public function test_record_event_usage_dispatches_limit_and_sets_timestamp(): void
|
||||
{
|
||||
EventFacade::fake([
|
||||
TenantPackageEventThresholdReached::class,
|
||||
TenantPackageEventLimitReached::class,
|
||||
]);
|
||||
|
||||
Config::set('package-limits.event_thresholds', [0.5]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
$package = Package::factory()->reseller()->create([
|
||||
'max_events_per_year' => 3,
|
||||
]);
|
||||
|
||||
$tenantPackage = TenantPackage::factory()
|
||||
->for($tenant)
|
||||
->for($package)
|
||||
->create([
|
||||
'used_events' => 3,
|
||||
])->fresh();
|
||||
|
||||
/** @var TenantUsageTracker $tracker */
|
||||
$tracker = app(TenantUsageTracker::class);
|
||||
|
||||
$tracker->recordEventUsage($tenantPackage, 2, 1);
|
||||
|
||||
EventFacade::assertDispatched(TenantPackageEventLimitReached::class);
|
||||
|
||||
$tenantPackage->refresh();
|
||||
|
||||
$this->assertNotNull($tenantPackage->event_limit_notified_at);
|
||||
}
|
||||
|
||||
public function test_record_credit_balance_dispatches_event_and_updates_tenant(): void
|
||||
{
|
||||
EventFacade::fake([TenantCreditsLow::class]);
|
||||
|
||||
Config::set('package-limits.credit_thresholds', [5, 1]);
|
||||
|
||||
$tenant = Tenant::factory()->create([
|
||||
'event_credits_balance' => 5,
|
||||
'credit_warning_sent_at' => null,
|
||||
'credit_warning_threshold' => null,
|
||||
]);
|
||||
|
||||
/** @var TenantUsageTracker $tracker */
|
||||
$tracker = app(TenantUsageTracker::class);
|
||||
|
||||
$tracker->recordCreditBalance($tenant, 6, 5);
|
||||
|
||||
EventFacade::assertDispatched(TenantCreditsLow::class, function ($event) use ($tenant) {
|
||||
return $event->tenant->is($tenant) && $event->threshold === 5;
|
||||
});
|
||||
|
||||
$tenant->refresh();
|
||||
|
||||
$this->assertNotNull($tenant->credit_warning_sent_at);
|
||||
$this->assertSame(5, $tenant->credit_warning_threshold);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Events\Packages\TenantPackageEventLimitReached;
|
||||
use App\Events\Packages\TenantPackageEventThresholdReached;
|
||||
use App\Models\Event;
|
||||
use App\Models\Package;
|
||||
use App\Models\PackagePurchase;
|
||||
@@ -9,13 +11,15 @@ use App\Models\Photo;
|
||||
use App\Models\Tenant;
|
||||
use App\Models\TenantPackage;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Config;
|
||||
use Illuminate\Support\Facades\Event as EventFacade;
|
||||
use Tests\TestCase;
|
||||
|
||||
class TenantModelTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function testTenantHasManyEvents(): void
|
||||
public function test_tenant_has_many_events(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
Event::factory()->count(3)->create(['tenant_id' => $tenant->id]);
|
||||
@@ -23,7 +27,7 @@ class TenantModelTest extends TestCase
|
||||
$this->assertCount(3, $tenant->events()->get());
|
||||
}
|
||||
|
||||
public function testTenantHasPhotosThroughEvents(): void
|
||||
public function test_tenant_has_photos_through_events(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
$event = Event::factory()->create(['tenant_id' => $tenant->id]);
|
||||
@@ -32,7 +36,7 @@ class TenantModelTest extends TestCase
|
||||
$this->assertCount(2, $tenant->photos()->get());
|
||||
}
|
||||
|
||||
public function testTenantHasManyPackagePurchases(): void
|
||||
public function test_tenant_has_many_package_purchases(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
$package = Package::factory()->create();
|
||||
@@ -44,7 +48,7 @@ class TenantModelTest extends TestCase
|
||||
$this->assertCount(2, $tenant->purchases()->get());
|
||||
}
|
||||
|
||||
public function testActiveSubscriptionAccessorReturnsTrueWhenActivePackageExists(): void
|
||||
public function test_active_subscription_accessor_returns_true_when_active_package_exists(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
$package = Package::factory()->create(['type' => 'reseller']);
|
||||
@@ -58,21 +62,21 @@ class TenantModelTest extends TestCase
|
||||
$this->assertTrue($tenant->fresh()->active_subscription);
|
||||
}
|
||||
|
||||
public function testActiveSubscriptionAccessorReturnsFalseWithoutActivePackage(): void
|
||||
public function test_active_subscription_accessor_returns_false_without_active_package(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
$this->assertFalse($tenant->fresh()->active_subscription);
|
||||
}
|
||||
|
||||
public function testIncrementUsedEventsReturnsFalseWithoutActivePackage(): void
|
||||
public function test_increment_used_events_returns_false_without_active_package(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
|
||||
$this->assertFalse($tenant->incrementUsedEvents());
|
||||
}
|
||||
|
||||
public function testIncrementUsedEventsUpdatesActivePackage(): void
|
||||
public function test_increment_used_events_updates_active_package(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create();
|
||||
$package = Package::factory()->create(['type' => 'reseller']);
|
||||
@@ -87,7 +91,41 @@ class TenantModelTest extends TestCase
|
||||
$this->assertEquals(3, $tenantPackage->fresh()->used_events);
|
||||
}
|
||||
|
||||
public function testSettingsCastToArray(): void
|
||||
public function test_consume_event_allowance_dispatches_notifications_and_updates_usage(): void
|
||||
{
|
||||
EventFacade::fake([
|
||||
TenantPackageEventThresholdReached::class,
|
||||
TenantPackageEventLimitReached::class,
|
||||
]);
|
||||
|
||||
Config::set('package-limits.event_thresholds', [0.5]);
|
||||
|
||||
$tenant = Tenant::factory()->create();
|
||||
$package = Package::factory()->create([
|
||||
'type' => 'reseller',
|
||||
'max_events_per_year' => 4,
|
||||
]);
|
||||
|
||||
$tenantPackage = TenantPackage::factory()->create([
|
||||
'tenant_id' => $tenant->id,
|
||||
'package_id' => $package->id,
|
||||
'active' => true,
|
||||
'used_events' => 1,
|
||||
]);
|
||||
|
||||
$this->assertTrue($tenant->consumeEventAllowance());
|
||||
|
||||
EventFacade::assertDispatched(TenantPackageEventThresholdReached::class);
|
||||
EventFacade::assertNotDispatched(TenantPackageEventLimitReached::class);
|
||||
|
||||
$tenantPackage->refresh();
|
||||
|
||||
$this->assertSame(2, $tenantPackage->used_events);
|
||||
$this->assertNotNull($tenantPackage->event_warning_sent_at);
|
||||
$this->assertSame(0.5, (float) $tenantPackage->event_warning_threshold);
|
||||
}
|
||||
|
||||
public function test_settings_cast_to_array(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create([
|
||||
'settings' => ['theme' => 'dark', 'logo' => 'logo.png'],
|
||||
@@ -97,7 +135,7 @@ class TenantModelTest extends TestCase
|
||||
$this->assertSame('dark', $tenant->settings['theme']);
|
||||
}
|
||||
|
||||
public function testFeaturesCastToArray(): void
|
||||
public function test_features_cast_to_array(): void
|
||||
{
|
||||
$tenant = Tenant::factory()->create([
|
||||
'features' => ['photo_likes' => true, 'analytics' => false],
|
||||
@@ -107,4 +145,23 @@ class TenantModelTest extends TestCase
|
||||
$this->assertTrue($tenant->features['photo_likes']);
|
||||
$this->assertFalse($tenant->features['analytics']);
|
||||
}
|
||||
|
||||
public function test_increment_credits_clears_warning_when_balance_above_threshold(): void
|
||||
{
|
||||
Config::set('package-limits.credit_thresholds', [5, 1]);
|
||||
|
||||
$tenant = Tenant::factory()->create([
|
||||
'event_credits_balance' => 1,
|
||||
'credit_warning_sent_at' => now()->subDay(),
|
||||
'credit_warning_threshold' => 1,
|
||||
]);
|
||||
|
||||
$tenant->incrementCredits(10);
|
||||
|
||||
$tenant->refresh();
|
||||
|
||||
$this->assertNull($tenant->credit_warning_sent_at);
|
||||
$this->assertNull($tenant->credit_warning_threshold);
|
||||
$this->assertSame(11, (int) $tenant->event_credits_balance);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user