Enforce task limits and update event form
This commit is contained in:
@@ -110,6 +110,7 @@ class TaskCollectionController extends Controller
|
||||
),
|
||||
'created_task_ids' => $result['created_task_ids'],
|
||||
'attached_task_ids' => $result['attached_task_ids'],
|
||||
'skipped_task_ids' => $result['skipped_task_ids'],
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ use App\Models\Event;
|
||||
use App\Models\Task;
|
||||
use App\Models\TaskCollection;
|
||||
use App\Models\Tenant;
|
||||
use App\Services\Packages\PackageLimitEvaluator;
|
||||
use App\Support\ApiError;
|
||||
use App\Support\TenantMemberPermissions;
|
||||
use App\Support\TenantRequestResolver;
|
||||
@@ -20,6 +21,8 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class TaskController extends Controller
|
||||
{
|
||||
public function __construct(private readonly PackageLimitEvaluator $packageLimitEvaluator) {}
|
||||
|
||||
/**
|
||||
* Display a listing of the tenant's tasks.
|
||||
*/
|
||||
@@ -163,7 +166,8 @@ class TaskController extends Controller
|
||||
{
|
||||
TenantMemberPermissions::ensureEventPermission($request, $event, 'tasks:manage');
|
||||
|
||||
$tenantId = $this->currentTenant($request)->id;
|
||||
$tenant = $this->currentTenant($request);
|
||||
$tenantId = $tenant->id;
|
||||
|
||||
if (($task->tenant_id && $task->tenant_id !== $tenantId) || $event->tenant_id !== $tenantId) {
|
||||
abort(404);
|
||||
@@ -173,6 +177,11 @@ class TaskController extends Controller
|
||||
return response()->json(['message' => 'Task ist bereits diesem Event zugewiesen.'], 409);
|
||||
}
|
||||
|
||||
$limitStatus = $this->resolveTaskLimitStatus($event, $tenant);
|
||||
if ($limitStatus['remaining'] !== null && $limitStatus['remaining'] <= 0) {
|
||||
return $this->taskLimitExceededResponse($event, $limitStatus);
|
||||
}
|
||||
|
||||
$task->assignedEvents()->attach($event->id);
|
||||
|
||||
return response()->json([
|
||||
@@ -187,7 +196,8 @@ class TaskController extends Controller
|
||||
{
|
||||
TenantMemberPermissions::ensureEventPermission($request, $event, 'tasks:manage');
|
||||
|
||||
$tenantId = $this->currentTenant($request)->id;
|
||||
$tenant = $this->currentTenant($request);
|
||||
$tenantId = $tenant->id;
|
||||
|
||||
if ($event->tenant_id !== $tenantId) {
|
||||
abort(404);
|
||||
@@ -203,12 +213,27 @@ class TaskController extends Controller
|
||||
);
|
||||
}
|
||||
|
||||
$taskIds = array_values(array_unique(array_map('intval', $taskIds)));
|
||||
$tasks = Task::whereIn('id', $taskIds)
|
||||
->where(function ($query) use ($tenantId) {
|
||||
$query->whereNull('tenant_id')->orWhere('tenant_id', $tenantId);
|
||||
})
|
||||
->get();
|
||||
|
||||
$assignedIds = $event->tasks()
|
||||
->whereIn('tasks.id', $taskIds)
|
||||
->pluck('tasks.id')
|
||||
->all();
|
||||
$pendingIds = array_values(array_diff($taskIds, $assignedIds));
|
||||
$limitStatus = $this->resolveTaskLimitStatus($event, $tenant);
|
||||
if (
|
||||
$limitStatus['remaining'] !== null
|
||||
&& $pendingIds !== []
|
||||
&& $limitStatus['remaining'] < count($pendingIds)
|
||||
) {
|
||||
return $this->taskLimitExceededResponse($event, $limitStatus);
|
||||
}
|
||||
|
||||
$attached = 0;
|
||||
foreach ($tasks as $task) {
|
||||
if (! $task->assignedEvents()->where('event_id', $event->id)->exists()) {
|
||||
@@ -330,6 +355,52 @@ class TaskController extends Controller
|
||||
return TenantRequestResolver::resolve($request);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{limit: ?int, used: int, remaining: ?int, package_id: ?int}
|
||||
*/
|
||||
protected function resolveTaskLimitStatus(Event $event, Tenant $tenant): array
|
||||
{
|
||||
$event->loadMissing(['eventPackage.package', 'eventPackages.package']);
|
||||
|
||||
$eventPackage = $this->packageLimitEvaluator->resolveEventPackageForPhotoUpload(
|
||||
$tenant,
|
||||
$event->id,
|
||||
$event
|
||||
);
|
||||
|
||||
$limit = $eventPackage?->effectiveLimits()['max_tasks'] ?? null;
|
||||
$used = $event->tasks()->count();
|
||||
$remaining = $limit === null ? null : max(0, (int) $limit - $used);
|
||||
|
||||
return [
|
||||
'limit' => $limit === null ? null : (int) $limit,
|
||||
'used' => $used,
|
||||
'remaining' => $remaining,
|
||||
'package_id' => $eventPackage?->package_id,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{limit: ?int, used: int, remaining: ?int, package_id: ?int} $limitStatus
|
||||
*/
|
||||
protected function taskLimitExceededResponse(Event $event, array $limitStatus): JsonResponse
|
||||
{
|
||||
return ApiError::response(
|
||||
'task_limit_exceeded',
|
||||
__('api.packages.task_limit_exceeded.title'),
|
||||
__('api.packages.task_limit_exceeded.message'),
|
||||
Response::HTTP_PAYMENT_REQUIRED,
|
||||
[
|
||||
'scope' => 'tasks',
|
||||
'used' => $limitStatus['used'],
|
||||
'limit' => $limitStatus['limit'],
|
||||
'remaining' => $limitStatus['remaining'] ?? 0,
|
||||
'event_id' => $event->id,
|
||||
'package_id' => $limitStatus['package_id'],
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
protected function prepareTaskPayload(array $data, int $tenantId, ?Task $original = null): array
|
||||
{
|
||||
if (array_key_exists('title', $data)) {
|
||||
|
||||
@@ -97,13 +97,26 @@ class EventResource extends JsonResource
|
||||
'watermark_allowed' => (bool) optional($eventPackage->package)->watermark_allowed,
|
||||
] : null,
|
||||
'limits' => $eventPackage && $limitEvaluator
|
||||
? $limitEvaluator->summarizeEventPackage($eventPackage)
|
||||
? $limitEvaluator->summarizeEventPackage($eventPackage, $this->resolveTasksUsed())
|
||||
: null,
|
||||
'addons' => $eventPackage ? $this->formatAddons($eventPackage) : [],
|
||||
'member_permissions' => $memberPermissions,
|
||||
];
|
||||
}
|
||||
|
||||
protected function resolveTasksUsed(): ?int
|
||||
{
|
||||
if (isset($this->tasks_count)) {
|
||||
return (int) $this->tasks_count;
|
||||
}
|
||||
|
||||
if ($this->relationLoaded('tasks')) {
|
||||
return $this->tasks->count();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $settings
|
||||
* @return array<string, mixed>
|
||||
|
||||
@@ -223,7 +223,7 @@ class PackageLimitEvaluator
|
||||
return $eventPackage;
|
||||
}
|
||||
|
||||
public function summarizeEventPackage(EventPackage $eventPackage): array
|
||||
public function summarizeEventPackage(EventPackage $eventPackage, ?int $tasksUsed = null): array
|
||||
{
|
||||
$limits = $eventPackage->effectiveLimits();
|
||||
|
||||
@@ -244,12 +244,22 @@ class PackageLimitEvaluator
|
||||
config('package-limits.gallery_warning_days', [])
|
||||
);
|
||||
|
||||
$taskSummary = $tasksUsed === null
|
||||
? null
|
||||
: $this->buildUsageSummary(
|
||||
$tasksUsed,
|
||||
$limits['max_tasks'],
|
||||
[]
|
||||
);
|
||||
|
||||
return [
|
||||
'photos' => $photoSummary,
|
||||
'guests' => $guestSummary,
|
||||
'gallery' => $gallerySummary,
|
||||
'tasks' => $taskSummary,
|
||||
'can_upload_photos' => $photoSummary['state'] !== 'limit_reached' && $gallerySummary['state'] !== 'expired',
|
||||
'can_add_guests' => $guestSummary['state'] !== 'limit_reached',
|
||||
'can_add_tasks' => $taskSummary ? $taskSummary['state'] !== 'limit_reached' : null,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -11,12 +11,10 @@ use RuntimeException;
|
||||
|
||||
class TaskCollectionImportService
|
||||
{
|
||||
public function __construct(private readonly DatabaseManager $db)
|
||||
{
|
||||
}
|
||||
public function __construct(private readonly DatabaseManager $db) {}
|
||||
|
||||
/**
|
||||
* @return array{collection: TaskCollection, created_task_ids: array<int>, attached_task_ids: array<int>}
|
||||
* @return array{collection: TaskCollection, created_task_ids: array<int>, attached_task_ids: array<int>, skipped_task_ids: array<int>}
|
||||
*/
|
||||
public function import(TaskCollection $collection, Event $event): array
|
||||
{
|
||||
@@ -33,8 +31,28 @@ class TaskCollectionImportService
|
||||
|
||||
$createdTaskIds = [];
|
||||
$attachedTaskIds = [];
|
||||
$skippedTaskIds = [];
|
||||
|
||||
$event->loadMissing(['eventPackage.package', 'eventPackages.package']);
|
||||
$eventPackage = $event->eventPackage;
|
||||
if (! $eventPackage && method_exists($event, 'eventPackages')) {
|
||||
$eventPackage = $event->eventPackages()
|
||||
->with('package')
|
||||
->orderByDesc('purchased_at')
|
||||
->orderByDesc('created_at')
|
||||
->first();
|
||||
}
|
||||
|
||||
$taskLimit = $eventPackage?->effectiveLimits()['max_tasks'] ?? null;
|
||||
$remaining = $taskLimit === null ? null : max(0, (int) $taskLimit - $event->tasks()->count());
|
||||
|
||||
foreach ($collection->tasks as $task) {
|
||||
if ($remaining !== null && $remaining <= 0) {
|
||||
$skippedTaskIds[] = $task->id;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$tenantTask = $this->resolveTenantTask($task, $targetCollection, $tenantId);
|
||||
|
||||
if ($tenantTask->wasRecentlyCreated) {
|
||||
@@ -44,6 +62,9 @@ class TaskCollectionImportService
|
||||
if (! $tenantTask->assignedEvents()->where('event_id', $event->id)->exists()) {
|
||||
$tenantTask->assignedEvents()->attach($event->id);
|
||||
$attachedTaskIds[] = $tenantTask->id;
|
||||
if ($remaining !== null) {
|
||||
$remaining = max(0, $remaining - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,6 +76,7 @@ class TaskCollectionImportService
|
||||
'collection' => $targetCollection->fresh(),
|
||||
'created_task_ids' => $createdTaskIds,
|
||||
'attached_task_ids' => $attachedTaskIds,
|
||||
'skipped_task_ids' => $skippedTaskIds,
|
||||
];
|
||||
});
|
||||
}
|
||||
@@ -139,10 +161,10 @@ class TaskCollectionImportService
|
||||
|
||||
protected function buildCollectionSlug(?string $slug, int $tenantId): string
|
||||
{
|
||||
$base = Str::slug(($slug ?: 'collection') . '-' . $tenantId);
|
||||
$base = Str::slug(($slug ?: 'collection').'-'.$tenantId);
|
||||
|
||||
do {
|
||||
$candidate = $base . '-' . Str::random(4);
|
||||
$candidate = $base.'-'.Str::random(4);
|
||||
} while (TaskCollection::where('slug', $candidate)->exists());
|
||||
|
||||
return $candidate;
|
||||
@@ -153,7 +175,7 @@ class TaskCollectionImportService
|
||||
$slugBase = Str::slug($base) ?: 'task';
|
||||
|
||||
do {
|
||||
$candidate = $slugBase . '-' . Str::random(6);
|
||||
$candidate = $slugBase.'-'.Str::random(6);
|
||||
} while (Task::where('slug', $candidate)->exists());
|
||||
|
||||
return $candidate;
|
||||
|
||||
Reference in New Issue
Block a user