Fix guest demo UX and enforce guest limits
This commit is contained in:
@@ -185,6 +185,57 @@ class EventPublicController extends BaseController
|
||||
);
|
||||
}
|
||||
|
||||
$deviceId = (string) $request->header('X-Device-Id', $request->input('device_id', ''));
|
||||
$deviceId = $deviceId !== '' ? $deviceId : null;
|
||||
|
||||
if ($event->id ?? null) {
|
||||
$eventModel = Event::with(['tenant', 'eventPackage.package', 'eventPackages.package'])->find($event->id);
|
||||
if ($eventModel && $eventModel->tenant) {
|
||||
$eventPackage = $this->packageLimitEvaluator->resolveEventPackageForPhotoUpload(
|
||||
$eventModel->tenant,
|
||||
$eventModel->id,
|
||||
$eventModel
|
||||
);
|
||||
$maxGuests = $eventPackage?->effectiveGuestLimit();
|
||||
|
||||
if ($eventPackage && $maxGuests !== null) {
|
||||
$grace = (int) config('package-limits.guest_grace', 10);
|
||||
$hardLimit = $maxGuests + max(0, $grace);
|
||||
$usedGuests = (int) $eventPackage->used_guests;
|
||||
$isReturningGuest = $this->joinTokenService->hasSeenGuest($eventModel->id, $deviceId, $request->ip());
|
||||
|
||||
if ($usedGuests >= $hardLimit && ! $isReturningGuest) {
|
||||
$this->recordTokenEvent(
|
||||
$joinToken,
|
||||
$request,
|
||||
'guest_limit_exceeded',
|
||||
[
|
||||
'event_id' => $eventModel->id,
|
||||
'used' => $usedGuests,
|
||||
'limit' => $maxGuests,
|
||||
'hard_limit' => $hardLimit,
|
||||
],
|
||||
$token,
|
||||
Response::HTTP_PAYMENT_REQUIRED
|
||||
);
|
||||
|
||||
return ApiError::response(
|
||||
'guest_limit_exceeded',
|
||||
__('api.packages.guest_limit_exceeded.title'),
|
||||
__('api.packages.guest_limit_exceeded.message'),
|
||||
Response::HTTP_PAYMENT_REQUIRED,
|
||||
[
|
||||
'event_id' => $eventModel->id,
|
||||
'used' => $usedGuests,
|
||||
'limit' => $maxGuests,
|
||||
'hard_limit' => $hardLimit,
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RateLimiter::clear($rateLimiterKey);
|
||||
|
||||
if (isset($event->status)) {
|
||||
@@ -1906,7 +1957,9 @@ class EventPublicController extends BaseController
|
||||
$policy = $this->guestPolicy();
|
||||
|
||||
if ($joinToken) {
|
||||
$this->joinTokenService->incrementUsage($joinToken);
|
||||
$deviceId = (string) $request->header('X-Device-Id', $request->input('device_id', ''));
|
||||
$deviceId = $deviceId !== '' ? $deviceId : null;
|
||||
$this->joinTokenService->incrementUsage($joinToken, $deviceId, $request->ip());
|
||||
}
|
||||
|
||||
$demoReadOnly = (bool) Arr::get($joinToken?->metadata ?? [], 'demo_read_only', false);
|
||||
|
||||
@@ -4,6 +4,7 @@ namespace App\Services;
|
||||
|
||||
use App\Models\Event;
|
||||
use App\Models\EventJoinToken;
|
||||
use App\Models\EventJoinTokenEvent;
|
||||
use App\Models\GuestPolicySetting;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Carbon;
|
||||
@@ -63,7 +64,7 @@ class EventJoinTokenService
|
||||
return $joinToken;
|
||||
}
|
||||
|
||||
public function incrementUsage(EventJoinToken $joinToken): void
|
||||
public function incrementUsage(EventJoinToken $joinToken, ?string $deviceId = null, ?string $ipAddress = null): void
|
||||
{
|
||||
$joinToken->increment('usage_count');
|
||||
|
||||
@@ -78,6 +79,12 @@ class EventJoinTokenService
|
||||
$eventPackage = $limitEvaluator->resolveEventPackageForPhotoUpload($event->tenant, $event->id, $event);
|
||||
|
||||
if ($eventPackage && $eventPackage->package?->max_guests !== null) {
|
||||
$normalizedDeviceId = $this->normalizeDeviceId($deviceId);
|
||||
$normalizedIp = is_string($ipAddress) && trim($ipAddress) !== '' ? $ipAddress : null;
|
||||
if (! $this->shouldCountGuest($event->id, $normalizedDeviceId, $normalizedIp)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$previous = (int) $eventPackage->used_guests;
|
||||
$eventPackage->increment('used_guests');
|
||||
$eventPackage->refresh();
|
||||
@@ -87,6 +94,28 @@ class EventJoinTokenService
|
||||
}
|
||||
}
|
||||
|
||||
public function hasSeenGuest(int $eventId, ?string $deviceId, ?string $ipAddress): bool
|
||||
{
|
||||
$normalizedDeviceId = $this->normalizeDeviceId($deviceId);
|
||||
$normalizedIp = is_string($ipAddress) && trim($ipAddress) !== '' ? $ipAddress : null;
|
||||
|
||||
if (! $normalizedDeviceId && ! $normalizedIp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = EventJoinTokenEvent::query()
|
||||
->where('event_id', $eventId)
|
||||
->where('event_type', 'access_granted');
|
||||
|
||||
if ($normalizedDeviceId) {
|
||||
$query->where('device_id', $normalizedDeviceId);
|
||||
} else {
|
||||
$query->where('ip_address', $normalizedIp);
|
||||
}
|
||||
|
||||
return $query->exists();
|
||||
}
|
||||
|
||||
public function findToken(string $token, bool $includeInactive = false): ?EventJoinToken
|
||||
{
|
||||
$hash = $this->hashToken($token);
|
||||
@@ -132,4 +161,35 @@ class EventJoinTokenService
|
||||
{
|
||||
return hash('sha256', $token);
|
||||
}
|
||||
|
||||
private function normalizeDeviceId(?string $deviceId): ?string
|
||||
{
|
||||
if (! is_string($deviceId) || trim($deviceId) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cleaned = preg_replace('/[^a-zA-Z0-9_-]/', '', $deviceId) ?? '';
|
||||
$cleaned = substr($cleaned, 0, 64);
|
||||
|
||||
return $cleaned !== '' ? $cleaned : null;
|
||||
}
|
||||
|
||||
private function shouldCountGuest(int $eventId, ?string $deviceId, ?string $ipAddress): bool
|
||||
{
|
||||
if (! $deviceId && ! $ipAddress) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = EventJoinTokenEvent::query()
|
||||
->where('event_id', $eventId)
|
||||
->where('event_type', 'access_granted');
|
||||
|
||||
if ($deviceId) {
|
||||
$query->where('device_id', $deviceId);
|
||||
} else {
|
||||
$query->where('ip_address', $ipAddress);
|
||||
}
|
||||
|
||||
return $query->count() <= 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -233,10 +233,13 @@ class PackageLimitEvaluator
|
||||
config('package-limits.photo_thresholds', [])
|
||||
);
|
||||
|
||||
$guestSummary = $this->buildUsageSummary(
|
||||
(int) $eventPackage->used_guests,
|
||||
$limits['max_guests'],
|
||||
config('package-limits.guest_thresholds', [])
|
||||
$guestSummary = $this->applyGuestGrace(
|
||||
$this->buildUsageSummary(
|
||||
(int) $eventPackage->used_guests,
|
||||
$limits['max_guests'],
|
||||
config('package-limits.guest_thresholds', [])
|
||||
),
|
||||
(int) $eventPackage->used_guests
|
||||
);
|
||||
|
||||
$gallerySummary = $this->buildGallerySummary(
|
||||
@@ -429,4 +432,37 @@ class PackageLimitEvaluator
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{limit: ?int, used: int, remaining: ?int, percentage: ?float, state: string, threshold_reached: ?float, next_threshold: ?float, thresholds: array} $summary
|
||||
* @return array{limit: ?int, used: int, remaining: ?int, percentage: ?float, state: string, threshold_reached: ?float, next_threshold: ?float, thresholds: array}
|
||||
*/
|
||||
private function applyGuestGrace(array $summary, int $used): array
|
||||
{
|
||||
$limit = $summary['limit'] ?? null;
|
||||
if ($limit === null || $limit <= 0) {
|
||||
return $summary;
|
||||
}
|
||||
|
||||
$grace = (int) config('package-limits.guest_grace', 10);
|
||||
$hardLimit = $limit + max(0, $grace);
|
||||
|
||||
if ($used >= $hardLimit) {
|
||||
$summary['state'] = 'limit_reached';
|
||||
$summary['threshold_reached'] = 1.0;
|
||||
$summary['next_threshold'] = null;
|
||||
$summary['remaining'] = 0;
|
||||
|
||||
return $summary;
|
||||
}
|
||||
|
||||
if ($used >= $limit) {
|
||||
$summary['state'] = 'warning';
|
||||
$summary['threshold_reached'] = 1.0;
|
||||
$summary['next_threshold'] = null;
|
||||
$summary['remaining'] = 0;
|
||||
}
|
||||
|
||||
return $summary;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,6 +58,8 @@ class PackageUsageTracker
|
||||
}
|
||||
|
||||
$newUsed = $eventPackage->used_guests;
|
||||
$grace = (int) config('package-limits.guest_grace', 10);
|
||||
$hardLimit = $limit + max(0, $grace);
|
||||
|
||||
$thresholds = collect(config('package-limits.guest_thresholds', []))
|
||||
->filter(fn (float $value) => $value > 0 && $value < 1)
|
||||
@@ -80,8 +82,8 @@ class PackageUsageTracker
|
||||
}
|
||||
}
|
||||
|
||||
if ($newUsed >= $limit && ($previousUsed < $limit)) {
|
||||
$this->dispatcher->dispatch(new EventPackageGuestLimitReached($eventPackage, $limit));
|
||||
if ($newUsed >= $hardLimit && ($previousUsed < $hardLimit)) {
|
||||
$this->dispatcher->dispatch(new EventPackageGuestLimitReached($eventPackage, $hardLimit));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user