implemented event package addons with filament resource, event-admin purchase path and notifications, showing up in purchase history

This commit is contained in:
Codex Agent
2025-11-21 11:25:45 +01:00
parent 07fe049b8a
commit 7a8d22a238
58 changed files with 3339 additions and 60 deletions

View File

@@ -0,0 +1,42 @@
<?php
namespace Tests\Unit\Jobs;
use App\Jobs\SyncPackageAddonToPaddle;
use App\Models\PackageAddon;
use App\Services\Paddle\PaddleAddonCatalogService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Mockery;
use Tests\TestCase;
class SyncPackageAddonToPaddleTest extends TestCase
{
use RefreshDatabase;
public function test_creates_and_updates_price_and_product(): void
{
$addon = PackageAddon::create([
'key' => 'extra_photos_500',
'label' => '+500 Fotos',
'extra_photos' => 500,
'metadata' => ['price_eur' => 5],
]);
$service = Mockery::mock(PaddleAddonCatalogService::class);
$service->shouldReceive('createProduct')
->once()
->andReturn(['id' => 'pro_addon_1']);
$service->shouldReceive('createPrice')
->once()
->andReturn(['id' => 'pri_addon_1']);
$job = new SyncPackageAddonToPaddle($addon->id);
$job->handle($service);
$addon->refresh();
$this->assertSame('pri_addon_1', $addon->price_id);
$this->assertEquals('pro_addon_1', $addon->metadata['paddle_product_id']);
$this->assertEquals('synced', $addon->metadata['paddle_sync_status']);
}
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Tests\Unit\Services;
use App\Models\PackageAddon;
use App\Services\Addons\EventAddonCatalog;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Config;
use Tests\TestCase;
class EventAddonCatalogTest extends TestCase
{
use RefreshDatabase;
public function test_prefers_database_addons_over_config(): void
{
Config::set('package-addons', [
'extra_photos_small' => [
'label' => 'Config Photos',
'price_id' => 'pri_config',
'increments' => ['extra_photos' => 100],
],
]);
PackageAddon::create([
'key' => 'extra_photos_small',
'label' => 'DB Photos',
'price_id' => 'pri_db',
'extra_photos' => 200,
'active' => true,
'sort' => 1,
]);
$catalog = $this->app->make(EventAddonCatalog::class);
$addon = $catalog->find('extra_photos_small');
$this->assertNotNull($addon);
$this->assertSame('DB Photos', $addon['label']);
$this->assertSame('pri_db', $addon['price_id']);
$this->assertSame(200, $addon['increments']['extra_photos']);
}
}

View File

@@ -165,4 +165,40 @@ class PackageLimitEvaluatorTest extends TestCase
$this->assertTrue($summary['can_upload_photos']);
$this->assertTrue($summary['can_add_guests']);
}
public function test_assess_photo_upload_respects_extra_limits(): void
{
$package = Package::factory()->endcustomer()->create([
'max_photos' => 5,
]);
$tenant = Tenant::factory()->create();
$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' => 5,
'used_guests' => 0,
'gallery_expires_at' => now()->addDays(14),
'extra_photos' => 5,
])->fresh(['package']);
$violation = $this->evaluator->assessPhotoUpload($tenant->fresh(), $event->id);
$this->assertNull($violation, 'Upload should be allowed within extra photo allowance');
$eventPackage->update(['used_photos' => 10]);
$violation = $this->evaluator->assessPhotoUpload($tenant->fresh(), $event->id);
$this->assertNotNull($violation, 'Upload should be blocked after exceeding base + extras');
$this->assertSame('photo_limit_exceeded', $violation['code']);
$this->assertSame(0, $violation['meta']['remaining']);
}
}

View File

@@ -145,4 +145,41 @@ class PackageUsageTrackerTest extends TestCase
EventFacade::assertDispatched(EventPackageGuestLimitReached::class);
}
public function test_effective_limits_include_extras(): void
{
EventFacade::fake([
EventPackagePhotoLimitReached::class,
]);
$tenant = Tenant::factory()->create();
$package = Package::factory()->endcustomer()->create([
'max_photos' => 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' => 2,
'used_guests' => 0,
'gallery_expires_at' => now()->addDays(7),
'extra_photos' => 2,
])->fresh(['package']);
/** @var PackageUsageTracker $tracker */
$tracker = app(PackageUsageTracker::class);
// Base limit reached but extras still available; no limit event expected yet.
$tracker->recordPhotoUsage($eventPackage, 1, 1);
EventFacade::assertNotDispatched(EventPackagePhotoLimitReached::class);
// Now consume extras and hit the effective limit.
$eventPackage->used_photos = 4;
$tracker->recordPhotoUsage($eventPackage, 3, 1);
EventFacade::assertDispatched(EventPackagePhotoLimitReached::class);
}
}