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

@@ -5,6 +5,7 @@ namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class EventPackage extends Model
{
@@ -20,6 +21,10 @@ class EventPackage extends Model
'used_photos',
'used_guests',
'gallery_expires_at',
'limits_snapshot',
'extra_photos',
'extra_guests',
'extra_gallery_days',
];
protected $casts = [
@@ -30,6 +35,10 @@ class EventPackage extends Model
'gallery_expired_notified_at' => 'datetime',
'used_photos' => 'integer',
'used_guests' => 'integer',
'extra_photos' => 'integer',
'extra_guests' => 'integer',
'extra_gallery_days' => 'integer',
'limits_snapshot' => 'array',
];
public function event(): BelongsTo
@@ -42,6 +51,11 @@ class EventPackage extends Model
return $this->belongsTo(Package::class)->withTrashed();
}
public function addons(): HasMany
{
return $this->hasMany(EventPackageAddon::class);
}
public function isActive(): bool
{
return $this->gallery_expires_at && $this->gallery_expires_at->isFuture();
@@ -53,7 +67,11 @@ class EventPackage extends Model
return false;
}
$maxPhotos = $this->package->max_photos ?? 0;
$maxPhotos = $this->effectiveLimits()['max_photos'];
if ($maxPhotos === null) {
return true;
}
return $this->used_photos < $maxPhotos;
}
@@ -64,23 +82,84 @@ class EventPackage extends Model
return false;
}
$maxGuests = $this->package->max_guests ?? 0;
$maxGuests = $this->effectiveLimits()['max_guests'];
if ($maxGuests === null) {
return true;
}
return $this->used_guests < $maxGuests;
}
public function getRemainingPhotosAttribute(): int
{
$max = $this->package->max_photos ?? 0;
$limit = $this->effectiveLimits()['max_photos'] ?? 0;
return max(0, $max - $this->used_photos);
return max(0, (int) $limit - $this->used_photos);
}
public function getRemainingGuestsAttribute(): int
{
$max = $this->package->max_guests ?? 0;
$limit = $this->effectiveLimits()['max_guests'] ?? 0;
return max(0, $max - $this->used_guests);
return max(0, (int) $limit - $this->used_guests);
}
/**
* @return array{max_photos: ?int, max_guests: ?int, gallery_days: ?int, max_tasks: ?int, max_events_per_year: ?int}
*/
public function effectiveLimits(): array
{
$snapshot = is_array($this->limits_snapshot) ? $this->limits_snapshot : [];
$base = [
'max_photos' => array_key_exists('max_photos', $snapshot)
? $snapshot['max_photos']
: ($this->package->max_photos ?? null),
'max_guests' => array_key_exists('max_guests', $snapshot)
? $snapshot['max_guests']
: ($this->package->max_guests ?? null),
'gallery_days' => array_key_exists('gallery_days', $snapshot)
? $snapshot['gallery_days']
: ($this->package->gallery_days ?? null),
'max_tasks' => array_key_exists('max_tasks', $snapshot)
? $snapshot['max_tasks']
: ($this->package->max_tasks ?? null),
'max_events_per_year' => array_key_exists('max_events_per_year', $snapshot)
? $snapshot['max_events_per_year']
: ($this->package->max_events_per_year ?? null),
];
$applyExtra = static function (?int $limit, int $extra): ?int {
if ($limit === null) {
return null;
}
$safeExtra = max(0, $extra);
return max(0, $limit + $safeExtra);
};
$maxPhotos = $applyExtra($this->normalizeLimit($base['max_photos']), (int) ($this->extra_photos ?? 0));
$maxGuests = $applyExtra($this->normalizeLimit($base['max_guests']), (int) ($this->extra_guests ?? 0));
return [
'max_photos' => $maxPhotos,
'max_guests' => $maxGuests,
'gallery_days' => $this->normalizeLimit($base['gallery_days']),
'max_tasks' => $this->normalizeLimit($base['max_tasks']),
'max_events_per_year' => $this->normalizeLimit($base['max_events_per_year']),
];
}
public function effectivePhotoLimit(): ?int
{
return $this->effectiveLimits()['max_photos'];
}
public function effectiveGuestLimit(): ?int
{
return $this->effectiveLimits()['max_guests'];
}
protected static function boot()
@@ -95,6 +174,31 @@ class EventPackage extends Model
$days = $eventPackage->package->gallery_days ?? 30;
$eventPackage->gallery_expires_at = now()->addDays($days);
}
if (! $eventPackage->limits_snapshot) {
$package = $eventPackage->relationLoaded('package')
? $eventPackage->package
: Package::query()->find($eventPackage->package_id);
if ($package) {
$eventPackage->limits_snapshot = array_filter([
'max_photos' => $package->max_photos,
'max_guests' => $package->max_guests,
'gallery_days' => $package->gallery_days,
'max_tasks' => $package->max_tasks,
'max_events_per_year' => $package->max_events_per_year,
], static fn ($value) => $value !== null);
}
}
});
}
private function normalizeLimit($value): ?int
{
if ($value === null) {
return null;
}
return is_numeric($value) ? (int) $value : null;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class EventPackageAddon extends Model
{
use HasFactory;
protected $fillable = [
'event_package_id',
'event_id',
'tenant_id',
'addon_key',
'quantity',
'extra_photos',
'extra_guests',
'extra_gallery_days',
'price_id',
'checkout_id',
'transaction_id',
'status',
'amount',
'currency',
'metadata',
'receipt_payload',
'error',
'purchased_at',
];
protected $casts = [
'metadata' => 'array',
'receipt_payload' => 'array',
'purchased_at' => 'datetime',
];
protected function increments(): Attribute
{
return Attribute::make(
get: fn () => [
'extra_photos' => (int) ($this->extra_photos ?? 0),
'extra_guests' => (int) ($this->extra_guests ?? 0),
'extra_gallery_days' => (int) ($this->extra_gallery_days ?? 0),
],
);
}
public function eventPackage(): BelongsTo
{
return $this->belongsTo(EventPackage::class);
}
public function event(): BelongsTo
{
return $this->belongsTo(Event::class);
}
public function tenant(): BelongsTo
{
return $this->belongsTo(Tenant::class);
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Casts\Attribute;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class PackageAddon extends Model
{
use HasFactory;
protected $fillable = [
'key',
'label',
'price_id',
'extra_photos',
'extra_guests',
'extra_gallery_days',
'active',
'sort',
'metadata',
];
protected $casts = [
'active' => 'boolean',
'metadata' => 'array',
'extra_photos' => 'integer',
'extra_guests' => 'integer',
'extra_gallery_days' => 'integer',
'sort' => 'integer',
];
protected function increments(): Attribute
{
return Attribute::make(
get: fn () => [
'extra_photos' => (int) ($this->extra_photos ?? 0),
'extra_guests' => (int) ($this->extra_guests ?? 0),
'extra_gallery_days' => (int) ($this->extra_gallery_days ?? 0),
],
);
}
}